Mint 100 per-local-day delegated notification JWTs for notify-api without touching the native background pool.

Each token is signed with the existing Endorser path and bounded by the user’s timezone midnight, so the next phase can submit the batch without changing prefetch JWTs or notification scheduling.
This commit is contained in:
Jose Olarte III
2026-08-26 16:05:51 +08:00
parent 46a2e0aaf5
commit d5bcdbae3f
5 changed files with 153 additions and 5 deletions
@@ -0,0 +1,5 @@
/**
* Delegated authorization JWTs for notification-wakeup-service via notify-api.
* Distinct from the native background prefetch pool in backgroundJwt.ts.
*/
export const DELEGATED_NOTIFICATION_JWT_COUNT = 100;
@@ -0,0 +1,29 @@
/**
* Batch of per-local-day delegated JWTs for notify-api / wakeup-service.
*
* Not authentication JWTs, not alertSearch cursor ULIDs, and not the native
* background prefetch pool (`mintBackgroundJwtTokenPool`).
*/
export interface DelegatedNotificationJwtWindow {
/** 1-based; 1 is today in the user's timezone. */
sequence: number;
/** Calendar date of this slot, YYYY-MM-DD in the user's timezone. */
localDate: string;
/** Unix seconds at 00:00:00 of this local day. */
nbf: number;
/** Unix seconds at 00:00:00 of the following local day. */
exp: number;
}
export interface DelegatedNotificationJwtSlot
extends DelegatedNotificationJwtWindow {
jwt: string;
}
export interface DelegatedNotificationJwtBatch {
did: string;
timeZone: string;
mintedAtEpoch: number;
tokens: DelegatedNotificationJwtSlot[];
}
+1
View File
@@ -1,5 +1,6 @@
export * from "./alertSearch";
export * from "./claims";
export * from "./delegatedNotificationJwt";
export * from "./claims-result";
export * from "./common";
export * from "./deepLinks";
+5 -5
View File
@@ -8,14 +8,14 @@
* - Authentication JWT: short-lived access token (`iss`/`iat`/`exp`) sent as
* `Authorization: Bearer` for interactive API calls (`accessToken` /
* `getHeaders`). Identifies the requester DID.
* - Delegated notification JWT: tokens in the native background pool (~100
* strings, `jti` + long `exp`) passed to daily-notification-plugin /
* notify-api so background workers can call Endorser. That pool is unchanged
* in this phase and is not an alertSearch cursor.
* - Delegated notification JWT: 100 per-local-day tokens from
* `mintDelegatedNotificationJwtBatch` for notify-api / wakeup-service.
* - Native background pool: `mintBackgroundJwtTokenPool` for daily-notification
* plugin prefetch. Unrelated to alertSearch and to the delegated batch.
* - alertSearch cursor ULID: server-issued record/JWT primary id (26-char
* ULID). `afterId` means ids strictly greater than that ULID; `beforeId`
* means strictly less. First daily run omits afterId. `beforeId` is for
* pagination within a run. These are not auth JWTs and not pool tokens.
* pagination within a run. These are not auth JWTs and not signed tokens.
*
* Truncation: each endorser bucket query uses a server hit-limit (typically
* 50). That flag is not currently returned on the alertSearch JSON envelope.
+113
View File
@@ -0,0 +1,113 @@
/**
* Mint 100 delegated notification JWTs (one local day each) for notify-api.
*
* JWT kinds (do not mix):
* - Authentication JWT: short-lived `accessToken` / `getHeaders` Bearer for the
* setup request itself (not generated here).
* - Delegated notification JWT: this module. Signed like other Endorser JWTs
* (`createEndorserJwtForDid`). `nbf`/`exp` are that local day's bounds.
* Sequence is array order: index 0 / sequence 1 = today.
* - Native background pool: `mintBackgroundJwtTokenPool` — unchanged, unused here.
* - alertSearch cursor ULID: server record id, not a signed token.
*
* Timezone: device IANA zone via Luxon `DateTime.local().zoneName` (same source
* as project create/edit). Not persisted in settings. Pass `timeZone` to override.
*
* Passkey (JWANT) identities cannot carry per-day nbf/exp; minting throws.
*/
import { DateTime } from "luxon";
import { DELEGATED_NOTIFICATION_JWT_COUNT } from "@/constants/delegatedNotificationJwt";
import type {
DelegatedNotificationJwtBatch,
DelegatedNotificationJwtSlot,
DelegatedNotificationJwtWindow,
} from "@/interfaces/delegatedNotificationJwt";
import { isFromPasskey } from "@/libs/crypto/vc";
import { createEndorserJwtForDid } from "@/libs/endorserServer";
import { retrieveAccountMetadata } from "@/libs/util";
export function resolveUserTimeZone(timeZone?: string): string {
const zone = timeZone ?? DateTime.local().zoneName ?? undefined;
if (!zone) {
throw new Error("Could not determine the user's timezone.");
}
const probe = DateTime.now().setZone(zone);
if (!probe.isValid) {
throw new Error(
"Invalid timezone for delegated notification JWTs: " + zone,
);
}
return zone;
}
/**
* Local-day [nbf, exp) windows for sequence 1..count.
* DST is handled by Luxon startOf('day') in the given IANA zone.
*/
export function buildDelegatedNotificationJwtWindows(
count: number = DELEGATED_NOTIFICATION_JWT_COUNT,
timeZone?: string,
now: Date = new Date(),
): DelegatedNotificationJwtWindow[] {
const zone = resolveUserTimeZone(timeZone);
const todayStart = DateTime.fromJSDate(now, { zone }).startOf("day");
const windows: DelegatedNotificationJwtWindow[] = [];
for (let i = 0; i < count; i++) {
const dayStart = todayStart.plus({ days: i }).startOf("day");
const nextStart = dayStart.plus({ days: 1 }).startOf("day");
windows.push({
sequence: i + 1,
localDate: dayStart.toFormat("yyyy-LL-dd"),
nbf: Math.floor(dayStart.toSeconds()),
exp: Math.floor(nextStart.toSeconds()),
});
}
return windows;
}
export function delegatedNotificationJwtStrings(
batch: DelegatedNotificationJwtBatch,
): string[] {
return batch.tokens.map((slot) => slot.jwt);
}
export async function mintDelegatedNotificationJwtBatch(
did: string,
options?: { timeZone?: string; now?: Date },
): Promise<DelegatedNotificationJwtBatch> {
if (!did) {
throw new Error("A DID is required to mint delegated notification JWTs.");
}
const account = await retrieveAccountMetadata(did);
if (isFromPasskey(account)) {
throw new Error(
"Delegated notification JWTs with per-day nbf/exp require a local signing key. Passkey JWANT tokens cannot carry those claims.",
);
}
const timeZone = resolveUserTimeZone(options?.timeZone);
const now = options?.now ?? new Date();
const mintedAtEpoch = Math.floor(now.getTime() / 1000);
const windows = buildDelegatedNotificationJwtWindows(
DELEGATED_NOTIFICATION_JWT_COUNT,
timeZone,
now,
);
const tokens: DelegatedNotificationJwtSlot[] = [];
for (const window of windows) {
const jwt = await createEndorserJwtForDid(did, {
iss: did,
iat: mintedAtEpoch,
nbf: window.nbf,
exp: window.exp,
jti: `${did}#delegated-notify#${window.localDate}`,
});
tokens.push({ ...window, jwt });
}
return { did, timeZone, mintedAtEpoch, tokens };
}