Open History to see the last commit
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4
5import {
6 apnsProviderTokenCacheKey,
7 makeApnsJwt,
8 type ApnsJwtError,
9 type ApnsJwtSigningInput,
10} from "./apnsJwt.ts";
11
12// Reuse JWTs within their 60-minute validity; frequent refreshes trigger APNs TooManyProviderTokenUpdates.
13export const APNS_JWT_REUSE_SECONDS = 45 * 60;
14
15export class ApnsProviderTokens extends Context.Service<
16 ApnsProviderTokens,
17 {
18 readonly getJwt: (input: ApnsJwtSigningInput) => Effect.Effect<string, ApnsJwtError>;
19 }
20>()("t3code-relay/agentActivity/ApnsProviderTokens") {}
21
22interface CachedProviderToken {
23 readonly jwt: string;
24 readonly issuedAtUnixSeconds: number;
25}
26
27// Deterministic signing plus quantized iat gives isolates identical tokens; cache only to avoid repeated signing.
28const isolateTokenCache = new Map<string, CachedProviderToken>();
29
30export function __resetApnsProviderTokenCacheForTest(): void {
31 isolateTokenCache.clear();
32}
33
34function quantizedApnsJwtIssuedAt(nowUnixSeconds: number): number {
35 return Math.floor(nowUnixSeconds / APNS_JWT_REUSE_SECONDS) * APNS_JWT_REUSE_SECONDS;
36}
37
38const make = () =>
39 ApnsProviderTokens.of({
40 getJwt: Effect.fnUntraced(function* (input) {
41 const issuedAtUnixSeconds = quantizedApnsJwtIssuedAt(input.issuedAtUnixSeconds);
42 const cacheKey = apnsProviderTokenCacheKey(input);
43 const cached = isolateTokenCache.get(cacheKey);
44 if (cached && cached.issuedAtUnixSeconds === issuedAtUnixSeconds) {
45 return cached.jwt;
46 }
47 const jwt = yield* makeApnsJwt({ ...input, issuedAtUnixSeconds });
48 isolateTokenCache.set(cacheKey, { jwt, issuedAtUnixSeconds });
49 return jwt;
50 }),
51 });
52
53export const layer = Layer.succeed(ApnsProviderTokens, make());