import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { apnsProviderTokenCacheKey, makeApnsJwt, type ApnsJwtError, type ApnsJwtSigningInput, } from "./apnsJwt.ts"; // APNs requires REUSING the provider token: refreshing it more than roughly // once per 20 minutes returns 429 TooManyProviderTokenUpdates and drops the // push (observed live: bursty Live Activity updates got 429'd, leaving stale // lock-screen state). Reuse each signed JWT for most of its 60-minute // validity. export const APNS_JWT_REUSE_SECONDS = 45 * 60; export class ApnsProviderTokens extends Context.Service< ApnsProviderTokens, { readonly getJwt: (input: ApnsJwtSigningInput) => Effect.Effect; } >()("t3code-relay/agentActivity/ApnsProviderTokens") {} interface CachedProviderToken { readonly jwt: string; readonly issuedAtUnixSeconds: number; } // Signing is deterministic (RFC 6979) and iat is quantized below, so every // isolate independently derives the byte-identical token for a window; this // map only avoids re-signing on every push. No shared storage is needed, and // no provider token is ever written anywhere. const isolateTokenCache = new Map(); export function __resetApnsProviderTokenCacheForTest(): void { isolateTokenCache.clear(); } // Quantize iat to the reuse window so all isolates agree on it. The token's // age stays under APNs' 60-minute limit, and the whole fleet rolls to the // next token at the same instant — one provider-token update per window. export function quantizedApnsJwtIssuedAt(nowUnixSeconds: number): number { return Math.floor(nowUnixSeconds / APNS_JWT_REUSE_SECONDS) * APNS_JWT_REUSE_SECONDS; } export const make = () => ApnsProviderTokens.of({ getJwt: Effect.fnUntraced(function* (input) { const issuedAtUnixSeconds = quantizedApnsJwtIssuedAt(input.issuedAtUnixSeconds); const cacheKey = apnsProviderTokenCacheKey(input); const cached = isolateTokenCache.get(cacheKey); if (cached && cached.issuedAtUnixSeconds === issuedAtUnixSeconds) { return cached.jwt; } const jwt = yield* makeApnsJwt({ ...input, issuedAtUnixSeconds }); isolateTokenCache.set(cacheKey, { jwt, issuedAtUnixSeconds }); return jwt; }), }); export const layer = Layer.succeed(ApnsProviderTokens, make());