import type Database from "better-sqlite3"; import type { NotificationPrefs } from "../../schemas/notification-prefs.schema"; /** * Push registration and delivery state (C7). * * `POST /api/push/register` was a no-op returning `{ ok: true }`. Mobile * registered, received success, and nothing was stored — so no notification * could ever be delivered, no failure could be observed, and the client had no * way to discover that its successful registration meant nothing. * * This records tokens and what happened to them, which is the prerequisite for * every other C7 requirement: token health, last success/failure, retries, * revocation confirmation, and a test-notification endpoint all need somewhere * to read state from. */ /** * Consecutive failures after which a token is treated as dead. * * A provider rejecting a token repeatedly means the app was uninstalled or the * token rotated. Retrying forever wastes work and, worse, makes the health * report read "failing" indefinitely instead of "this device is gone". */ export declare const FAILURE_STREAK_LIMIT = 5; /** * Token kinds. A device supplies three non-interchangeable types, and * conflating them fails only at send time with no signal at registration: * * - `expo` — Expo relay token, for ordinary push notifications. * - `liveactivity_start` — ActivityKit push-to-start token. App-wide, one per * device, long-lived. Starts an activity when none exists. * - `liveactivity_update` — ActivityKit per-activity update token, issued by * iOS after an activity starts, scoped to that one activity, short-lived. */ export declare const PUSH_TOKEN_KINDS: readonly ["expo", "liveactivity_start", "liveactivity_update"]; export type PushTokenKind = (typeof PUSH_TOKEN_KINDS)[number]; /** * The kind assumed when a client does not send one. * * tb-mobile is released and cannot be force-updated, so an older client posting * `{ token, platform }` must keep working. Every such client is registering an * Expo relay token, because that is the only kind that existed then. */ export declare const DEFAULT_PUSH_TOKEN_KIND: PushTokenKind; export declare function isPushTokenKind(value: unknown): value is PushTokenKind; export interface PushTokenRow { token: string; platform: string; device_id: string | null; registered_at: number; last_success_at: number | null; last_failure_at: number | null; last_failure_code: string | null; failure_streak: number; revoked_at: number | null; kind: PushTokenKind; activity_id: string | null; session_id: string | null; expires_at: number | null; stale_date: number | null; started_at: number | null; renewed_at: number | null; /** The id the registering client files this server under; null for older clients. */ client_server_id: string | null; /** The app's display language (BCP 47); null for older clients. */ locale: string | null; /** JSON `NotificationPrefs`; null = the client never sent any, i.e. everything on. */ notification_prefs: string | null; } /** * Health as reported to a client. Deliberately omits the token itself — a push * token is a delivery credential, and a health endpoint has no reason to echo * one back. */ export interface PushTokenHealth { platform: string; deviceId: string | null; registeredAt: number; lastSuccessAt: number | null; lastFailureAt: number | null; lastFailureCode: string | null; failureStreak: number; revokedAt: number | null; /** * Never delivered vs delivering vs failing vs revoked vs expired. The * distinction the user actually needs: "not yet" and "broken" look identical * without it. */ state: "never-delivered" | "healthy" | "failing" | "dead" | "revoked" | "expired"; kind: PushTokenKind; /** Present only for per-activity Live Activity tokens. */ activityId: string | null; sessionId: string | null; expiresAt: number | null; } export declare function tokenState(row: PushTokenRow, now?: number): PushTokenHealth["state"]; export declare function toHealth(row: PushTokenRow, now?: number): PushTokenHealth; export declare class PushRepository { private upsertStmt; private getStmt; private listActiveStmt; private listAllStmt; private successStmt; private failureStmt; private revokeStmt; private deleteByTokenStmt; private deleteTokenForDeviceStmt; private setPrefsStmt; private setPrefsAnyDeviceStmt; private deleteByDeviceStmt; private claimEventStmt; private markDeliveredStmt; private listByKindSessionStmt; private listByKindStmt; private listRenewableStmt; private claimRenewalStmt; private expireStmt; private expireSessionActivitiesStmt; constructor(db: Database.Database); /** * Register or refresh a token. * * `kind` defaults to Expo so a released client posting `{ token, platform }` * keeps working — tb-mobile cannot be force-updated, and every client * predating Live Activities is registering an Expo relay token. * * Several rows per device is normal and intended: a device runs one activity * per live session, each with its own update token. The token itself is the * primary key, so distinct activities never collide. */ register(args: { token: string; platform: string; deviceId?: string | null; kind?: PushTokenKind; activityId?: string | null; sessionId?: string | null; expiresAt?: number | null; staleDate?: number | null; startedAt?: number | null; clientServerId?: string | null; locale?: string | null; notificationPrefs?: NotificationPrefs | null; now?: number; }): void; /** * Store new notification preferences for one token, touching nothing else. * * `deviceId` scopes the write to this device's own tokens (or an unattributed * one), so a `notifications`-holding device cannot mute another's. Pass * `null` for the shared api key, which names no device and may set any token. * Returns whether a row matched. */ setPrefs(token: string, prefs: NotificationPrefs, deviceId: string | null): boolean; get(token: string): PushTokenRow | null; /** * Expo tokens eligible for delivery — not revoked, not past the failure limit. * * Deliberately Expo-only. ActivityKit tokens go over direct APNs with a * different topic and are rejected by Expo's relay, so the ordinary * notification fan-out must not see them. */ listDeliverable(): PushTokenRow[]; /** Live-activity tokens for one session, eligible for delivery. */ listForSession(kind: PushTokenKind, sessionId: string, now?: number): PushTokenRow[]; /** * Every deliverable token of one kind. * * Used for push-to-start, which is app-wide rather than session-scoped: the * activity does not exist yet, so there is no per-activity token to look up. */ listByKind(kind: PushTokenKind, now?: number): PushTokenRow[]; /** Unrenewed activities with a renewal deadline, soonest first. */ listRenewable(): PushTokenRow[]; /** * Claim a row for renewal. * * Returns true exactly once per row. A restart re-arms timers from the * persisted deadline, so the same renewal can be attempted twice; the loser * gets false and must not send. Doing this as a conditional UPDATE rather * than read-then-write avoids the race where both attempts observe * "not yet renewed". */ claimRenewal(token: string, now?: number): boolean; /** Mark one token expired, so it stops being a delivery target. */ expire(token: string, now?: number): void; /** * Expire every live activity for a session. * * Called when the session ends. Without this, a per-activity token outlives * its session and a later renewal sweep would resurrect an activity for a * session that is already gone. */ expireSessionActivities(sessionId: string, now?: number): void; /** Every token, including dead and revoked ones, for the health report. */ listHealth(now?: number): PushTokenHealth[]; recordSuccess(token: string, now?: number): void; recordFailure(token: string, code: string, now?: number): void; revoke(token: string, now?: number): boolean; /** * Erase one token. * * Delete, not revoke: a retained row is still a stored delivery credential, * and a client unregistering is asking for the credential to be gone, not for * it to be marked dead and kept for the health report. */ deleteToken(token: string): boolean; /** * Erase one token, but only if it belongs to this device (or to no device). * * What keeps a `notifications`-holding device from retiring another device's * token: the token value is the only thing the route is given, so without the * ownership term any caller that learns a token can delete it. */ deleteTokenForDevice(token: string, deviceId: string): boolean; /** * Erase every token attributed to a device. * * Cross-database by necessity — devices live in runtime.db and tokens in * cache.db — so this is called from the device routes and the CLI, never * joined in SQL. */ deleteForDevice(deviceId: string): number; /** * Claim an event id for delivery. * * Returns true exactly once per event id. A retry, a reconnect * reconciliation, or two triggers firing for the same underlying event all * get false and must not notify — the user should never be told twice about * one thing. */ claimEvent(eventId: string, sessionId: string | null, now?: number): boolean; markDelivered(eventId: string, now?: number): void; } //# sourceMappingURL=push.repository.d.ts.map