Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | 38x 38x 38x 24x 24x 46x 11x 8x 9x 9x 9x 9x 15x 15x 6x 6x 6x 6x 6x 1x | /**
* Short-TTL, in-memory store for in-flight EUDI Wallet OpenID4VP login
* transactions, keyed by an opaque server-issued session id (`sid`).
*
* The `sid` is the only handle the browser ever sees — the verifier-side
* transaction id, the nonce, and (once verified) the resolved patient identity
* never leave the server. The browser polls `GET /api/auth/eudi/status?sid=…`
* and, on completion, calls `signIn("eudi-wallet", { sid })`; the Credentials
* provider re-reads the pinned, server-verified result for that `sid`.
*
* Persistence: an in-memory Map is acceptable because the Azure Container Apps
* UI runs a single replica (min=max=1, see scripts/azure/05-cfm-ui.sh). It does
* NOT survive a revision rollover/restart — in-flight logins would drop. A
* durable store (Neo4j / Vault / Postgres) is the production path. Flagged in
* ADR-028.
*/
import { randomUUID, randomBytes } from "crypto";
export type EudiTxStatus = "pending" | "completed" | "error";
export interface EudiVerifiedPatient {
/** username key matching DEMO_PERSONAS / PATIENT_RESOURCE_MAP (e.g. "patient1") */
username: string;
/** display name — the verified wallet holder's name when available */
displayName: string;
roles: string[];
}
export interface EudiTransaction {
sid: string;
/** verifier-side transaction / presentation id */
transactionId: string;
/** anti-replay nonce echoed by the wallet's presentation */
nonce: string;
status: EudiTxStatus;
createdAt: number;
/** pinned only after the verifier confirms a valid presentation */
verifiedPatient?: EudiVerifiedPatient;
/** true once a session has been minted from this sid (single-use) */
consumed?: boolean;
error?: string;
}
const TTL_MS = 5 * 60 * 1000;
const store = new Map<string, EudiTransaction>();
/** Remove expired transactions so the Map can't grow unbounded. */
function sweep(): void {
const cutoff = Date.now() - TTL_MS;
for (const [sid, tx] of store) {
Iif (tx.createdAt < cutoff) store.delete(sid);
}
}
/** Opaque browser-facing id. */
export function newSid(): string {
return randomUUID();
}
/** Anti-replay nonce for the OpenID4VP request. */
export function newNonce(): string {
return randomBytes(24).toString("base64url");
}
export function putTransaction(
tx: Omit<EudiTransaction, "status" | "createdAt"> &
Partial<Pick<EudiTransaction, "status" | "createdAt">>,
): EudiTransaction {
sweep();
const full: EudiTransaction = {
status: "pending",
createdAt: Date.now(),
...tx,
};
store.set(full.sid, full);
return full;
}
export function getTransaction(sid: string): EudiTransaction | undefined {
sweep();
return store.get(sid);
}
export function updateTransaction(
sid: string,
patch: Partial<EudiTransaction>,
): EudiTransaction | undefined {
const tx = store.get(sid);
Iif (!tx) return undefined;
const next = { ...tx, ...patch };
store.set(sid, next);
return next;
}
export function deleteTransaction(sid: string): void {
store.delete(sid);
}
|