import * as plugins from '../../plugins.js'; import { DcRouterDb } from '../classes.dcrouter-db.js'; import type { IWebPushEncryptedEnvelope } from '../../webpush/classes.webpush-crypto.js'; export interface IWebPushStoredCredential { id: string; status: plugins.servezoneInterfaces.data.TWebPushCredentialStatus; hmacKeyId: string; secretHash?: string; createdAt: number; updatedAt: number; lastRotatedAt: number | null; validUntil: number | null; } export interface IWebPushStoredVapidKey { id: string; publicKey: string; status: plugins.servezoneInterfaces.data.TWebPushVapidKeyStatus; createdAt: number; activatedAt: number | null; retireAfter: number | null; retiredAt: number | null; privateKeyEnvelope?: IWebPushEncryptedEnvelope; } export interface IWebPushCredentialRotationRecovery { requestedCredentialId: string; resultingCredentialId: string; lifecycleGeneration: number; secretEnvelope: IWebPushEncryptedEnvelope; createdAt: number; expiresAt: number; } export type TWebPushControllerIntent = 'enabled' | 'disabled' | 'deleted'; export interface IWebPushStoredControllerFence { id: string; epoch: string; generation: number; operationId: string; intent: TWebPushControllerIntent; requestDigest: string; appliedAt: number; } const getDb = () => DcRouterDb.getInstance().getDb(); @plugins.smartdata.Collection(() => getDb()) export class WebPushBindingDoc extends plugins.smartdata.SmartDataDbDoc< WebPushBindingDoc, WebPushBindingDoc > { @plugins.smartdata.svDb() public id!: string; @plugins.smartdata.svDb() public ownerGatewayClientType!: plugins.servezoneInterfaces.data.TGatewayClientType; @plugins.smartdata.svDb() public ownerGatewayClientId!: string; @plugins.smartdata.svDb() public ownerAppInstanceId!: string; @plugins.smartdata.svDb() public enabled!: boolean; @plugins.smartdata.svDb() public status!: plugins.servezoneInterfaces.data.TWebPushBindingStatus; @plugins.smartdata.svDb() public credential!: IWebPushStoredCredential; /** At most one still-valid overlap credential; expired entries are scrubbed. */ @plugins.smartdata.svDb() public previousCredentials: IWebPushStoredCredential[] = []; /** MUST BE ABSENT outside the bounded idempotent credential-rotation replay window. */ @plugins.smartdata.svDb() public credentialRotationRecovery?: IWebPushCredentialRotationRecovery; @plugins.smartdata.svDb() public vapidKeys: IWebPushStoredVapidKey[] = []; @plugins.smartdata.svDb() public createdAt!: number; @plugins.smartdata.svDb() public updatedAt!: number; @plugins.smartdata.svDb() public createdBy!: string; @plugins.smartdata.svDb() public mutationRevision!: number; @plugins.smartdata.svDb() public lifecycleGeneration!: number; /** Highest durably applied controller mutation for this owner-reserving binding. */ @plugins.smartdata.svDb() public controllerFence?: IWebPushStoredControllerFence; /** MUST BE ABSENT unless this owner-reserving binding is a deletion tombstone. */ @plugins.smartdata.svDb() public deletedAt?: number; public static getNativeCollection(): plugins.smartdata.SmartdataCollection['mongoDbCollection'] { return getDb().mongoDb.collection('WebPushBindingDoc'); } public override async createSavableObject(): Promise { const savable = await super.createSavableObject(); if (savable.credential.secretHash === undefined) { delete savable.credential.secretHash; } savable.previousCredentials = (savable.previousCredentials || []).map((credentialArg) => ({ ...credentialArg, lastRotatedAt: credentialArg.lastRotatedAt ?? null, validUntil: credentialArg.validUntil ?? null, })).map((credentialArg) => { if (credentialArg.secretHash === undefined) delete credentialArg.secretHash; return credentialArg; }); if (savable.credentialRotationRecovery === undefined) { delete savable.credentialRotationRecovery; } if (savable.controllerFence === undefined) delete savable.controllerFence; savable.vapidKeys = (savable.vapidKeys || []).map((keyArg) => { const normalized = { ...keyArg, activatedAt: keyArg.activatedAt ?? null, retireAfter: keyArg.retireAfter ?? null, retiredAt: keyArg.retiredAt ?? null, }; if (normalized.privateKeyEnvelope === undefined) { delete normalized.privateKeyEnvelope; } return normalized; }); if (savable.deletedAt === undefined) delete savable.deletedAt; return savable; } }