| 1 | // @effect-diagnostics-next-line nodeBuiltinImport:off -- Effect's Crypto has no generateKeyPairSync. |
| 2 | import * as NodeCrypto from "node:crypto"; |
| 3 | |
| 4 | import { describe, expect, it } from "@effect/vitest"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Redacted from "effect/Redacted"; |
| 7 | import * as Schema from "effect/Schema"; |
| 8 | |
| 9 | import * as ApnsProviderTokens from "./ApnsProviderTokens.ts"; |
| 10 | |
| 11 | const { privateKey } = NodeCrypto.generateKeyPairSync("ec", { |
| 12 | namedCurve: "prime256v1", |
| 13 | privateKeyEncoding: { type: "pkcs8", format: "pem" }, |
| 14 | publicKeyEncoding: { type: "spki", format: "pem" }, |
| 15 | }); |
| 16 | |
| 17 | const signingInput = { |
| 18 | teamId: "team-1", |
| 19 | keyId: "key-1", |
| 20 | privateKey: Redacted.make(privateKey), |
| 21 | }; |
| 22 | |
| 23 | const WINDOW = ApnsProviderTokens.APNS_JWT_REUSE_SECONDS; |
| 24 | |
| 25 | const decodeJwtPayload = Schema.decodeEffect( |
| 26 | Schema.fromJsonString(Schema.Struct({ iat: Schema.Number })), |
| 27 | ); |
| 28 | |
| 29 | describe("ApnsProviderTokens", () => { |
| 30 | it.effect("derives the byte-identical token across isolates within a window", () => { |
| 31 | ApnsProviderTokens.__resetApnsProviderTokenCacheForTest(); |
| 32 | return Effect.gen(function* () { |
| 33 | const tokens = yield* ApnsProviderTokens.ApnsProviderTokens; |
| 34 | const first = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW + 10 }); |
| 35 | |
| 36 | // A fresh isolate has no cache; deterministic signing plus quantized |
| 37 | // iat must still reproduce the exact same JWT for the same window. |
| 38 | ApnsProviderTokens.__resetApnsProviderTokenCacheForTest(); |
| 39 | const second = yield* tokens.getJwt({ |
| 40 | ...signingInput, |
| 41 | issuedAtUnixSeconds: WINDOW * 2 - 1, |
| 42 | }); |
| 43 | expect(second).toBe(first); |
| 44 | |
| 45 | const payload = yield* decodeJwtPayload( |
| 46 | Buffer.from(first.split(".")[1]!, "base64url").toString("utf8"), |
| 47 | ); |
| 48 | expect(payload.iat).toBe(WINDOW); |
| 49 | ApnsProviderTokens.__resetApnsProviderTokenCacheForTest(); |
| 50 | }).pipe(Effect.provide(ApnsProviderTokens.layer)); |
| 51 | }); |
| 52 | |
| 53 | it.effect("rolls to a new token at the window boundary", () => { |
| 54 | ApnsProviderTokens.__resetApnsProviderTokenCacheForTest(); |
| 55 | return Effect.gen(function* () { |
| 56 | const tokens = yield* ApnsProviderTokens.ApnsProviderTokens; |
| 57 | const first = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW + 10 }); |
| 58 | const next = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW * 2 }); |
| 59 | expect(next).not.toBe(first); |
| 60 | ApnsProviderTokens.__resetApnsProviderTokenCacheForTest(); |
| 61 | }).pipe(Effect.provide(ApnsProviderTokens.layer)); |
| 62 | }); |
| 63 | |
| 64 | it.effect("serves repeat pushes from the isolate cache without re-signing", () => { |
| 65 | ApnsProviderTokens.__resetApnsProviderTokenCacheForTest(); |
| 66 | return Effect.gen(function* () { |
| 67 | const tokens = yield* ApnsProviderTokens.ApnsProviderTokens; |
| 68 | const first = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW + 10 }); |
| 69 | const again = yield* tokens.getJwt({ ...signingInput, issuedAtUnixSeconds: WINDOW + 500 }); |
| 70 | // Deterministic signing makes equality hold either way; toBe on the |
| 71 | // exact string documents the cache contract. |
| 72 | expect(again).toBe(first); |
| 73 | ApnsProviderTokens.__resetApnsProviderTokenCacheForTest(); |
| 74 | }).pipe(Effect.provide(ApnsProviderTokens.layer)); |
| 75 | }); |
| 76 | }); |