@mpgd/platform 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,529 @@
1
+ import { type CancelNotificationReminderInput, type LocalNotificationEntry, type LocalNotificationRequest, type NotificationCapabilities, type NotificationConsentRecord, type NotificationDeliveryContent, type NotificationDeliveryOutcome, type NotificationDeliveryProviderEntry, type NotificationDeployment, type NotificationEndpointRegistration, type NotificationNotice, type NotificationNoticeIdentity, type NotificationNoticeSettings, type NotificationPermissionState, type NotificationProvider, type NotificationRuntime, type NotificationTemplateData, type NotificationTopicDeclaration, type NotificationTopicMapping, type SetNotificationReminderInput } from './notifications.js';
2
+ import { notificationConformanceDefaultLocale, type NotificationConformanceDeliverInput, type NotificationConformanceEndpointFixture, type NotificationConformanceHostFixture, type NotificationConformanceInstallationFixture, type NotificationConformanceProfile, type NotificationConformanceProfileLimits, type NotificationConformancePromptResult, type NotificationConformanceRejectionReason, type NotificationConformanceTapInput, type NotificationConformanceVector } from './notifications-vectors.js';
3
+ /**
4
+ * Notifications v2 conformance runner (RFC "Conformance vectors") with the
5
+ * in-memory reference pieces every vector needs: a fake clock; a fake host
6
+ * with its fixture `NotificationProvider`; a fake backend that keeps sessions,
7
+ * consent records, endpoints, server reminders, notices, and an idempotency
8
+ * ledger, and applies the pure policy gate before every provider call; and the
9
+ * reference notifications runtime the runner drives as the `game` actor. Pure
10
+ * in-process code: no Phaser, DOM, network, or SDK imports. Nothing here sends
11
+ * a real notification or grants anything.
12
+ *
13
+ * A P-phase provider runs the same vectors by supplying `createProvider`; the
14
+ * runner reads the provider's capabilities and skips, with a reason, every
15
+ * vector whose `appliesTo` they do not satisfy.
16
+ */
17
+ export { notificationConformanceDefaultLocale };
18
+ /** The evidence schema the fake host attaches to a host-rendered agreement. */
19
+ export declare const notificationConformanceAgreementSchema = "fixture.host-agreement.v1";
20
+ /** The fake session's installation id. */
21
+ export declare const notificationConformanceInstallationId = "installation-1";
22
+ /** Seconds a first `rate-limited` provider answer defers the send by when it names no delay. */
23
+ export declare const notificationConformanceRetryAfterSeconds = 60;
24
+ /** Seconds the fake ledger's claim lease lasts; an `ambiguous` answer keeps the claim this long. */
25
+ export declare const notificationConformanceLeaseSeconds = 300;
26
+ /** An operation the vector did not script; it fails the vector with its message. */
27
+ export declare class NotificationConformanceScriptError extends Error {
28
+ readonly name = "NotificationConformanceScriptError";
29
+ }
30
+ export interface NotificationConformanceClock {
31
+ /** RFC 3339 timestamp of the fake "now", in the canonical form. */
32
+ now(): string;
33
+ set(at: string): void;
34
+ advance(seconds: number): void;
35
+ }
36
+ export declare function createNotificationConformanceClock(initial: string): NotificationConformanceClock;
37
+ export interface NotificationConformanceLocalOperation {
38
+ readonly op: 'schedule' | 'replace' | 'cancel';
39
+ readonly key: string;
40
+ readonly topic: string;
41
+ readonly fireAt?: string;
42
+ /** Local reminders are always inexact. */
43
+ readonly exact?: boolean;
44
+ }
45
+ /** The `host` actor: what the vector scripts on the device or host side. */
46
+ export interface NotificationConformanceHostActor {
47
+ queuePrompt(result: NotificationConformancePromptResult): void;
48
+ setPermission(state: NotificationPermissionState): void;
49
+ issueEndpoint(endpoint: NotificationConformanceEndpointFixture): void;
50
+ /** The player completes the action the host waited for, such as logging in. */
51
+ completeAction(): void;
52
+ tap(input: NotificationConformanceTapInput): void;
53
+ /** A process restart: provider listeners and buffered events go, host state stays. */
54
+ restart?(): void;
55
+ /** Calls that reached the host prompt API. */
56
+ readonly prompts: number;
57
+ /** Native local-notification calls, in order. */
58
+ readonly localOperations: readonly NotificationConformanceLocalOperation[];
59
+ /** Pending local notifications, read when the provider has no `listLocal`. */
60
+ readonly localPending?: readonly LocalNotificationEntry[];
61
+ /** The backend's send-time read for a `permission.serverReadable` provider (Devvit `isOptedIn`). */
62
+ readServerPermission?(input: {
63
+ readonly player: string;
64
+ readonly topic: string;
65
+ }): NotificationPermissionState | Promise<NotificationPermissionState>;
66
+ }
67
+ export interface NotificationConformanceHost extends NotificationConformanceHostActor {
68
+ readonly provider: NotificationProvider;
69
+ readonly localPending: readonly LocalNotificationEntry[];
70
+ /** Every `scheduleLocal` request the provider received, in order. */
71
+ readonly localRequests: readonly LocalNotificationRequest[];
72
+ restart(): void;
73
+ readServerPermission(input: {
74
+ readonly player: string;
75
+ readonly topic: string;
76
+ }): NotificationPermissionState;
77
+ }
78
+ export interface CreateNotificationConformanceHostInput {
79
+ readonly profile: NotificationConformanceProfile;
80
+ readonly catalog: readonly NotificationTopicDeclaration[];
81
+ readonly host?: NotificationConformanceHostFixture;
82
+ /** The device zone a registration reports. */
83
+ readonly timeZone?: string;
84
+ }
85
+ /**
86
+ * The fake host and its fixture provider (RFC "Vector format", fake defaults).
87
+ *
88
+ * - States start at `prompt` for `os` and `game` prompts, unreadable `host`
89
+ * prompts read `unknown`, an unsupported profile answers `unsupported`, and a
90
+ * topic whose mapping needs no agreement reads `granted` and never prompts. A
91
+ * game-rendered consent is one boolean, as Devvit's `isOptedIn`: opted in
92
+ * reads `granted`, otherwise `prompt`.
93
+ * - `actionRequired` holds the provider in `availability: 'action-required'`
94
+ * with `requiredAction: 'login'` until `completeAction`.
95
+ * - A prompt consumes the next `queuePrompt` result and fails the vector when
96
+ * none is queued. A `denied` answer reports `prompt-with-rationale` while
97
+ * denials remain under `permission.promptLimit`, `blocked` once they are used
98
+ * up, and `denied` without a limit; `dismissed` leaves the state and counts
99
+ * no denial; `host-terms-declined` reaches the prompt and answers
100
+ * `action-required` with `requiredAction: 'host-settings'`. An unreadable
101
+ * host still reports the state its prompt just decided.
102
+ * - Registrations carry the device permission and zone, never an installation
103
+ * id; `issueEndpoint` after the provider's first subscriber emits
104
+ * `endpoint-changed`. Events before the first subscriber are buffered for
105
+ * it, and a tap is `cold` before that subscriber and `warm` after. A
106
+ * listener's error fails the host call that delivered the event, after every
107
+ * listener ran.
108
+ */
109
+ export declare function createNotificationConformanceHost(input: CreateNotificationConformanceHostInput): NotificationConformanceHost;
110
+ /**
111
+ * The registry entry the fake backend derives for a provider (RFC "Vector
112
+ * format"): purposes, addressing, content, and permission fields from its
113
+ * capabilities; `expiry` and `actions` for a free-form device provider; `link`
114
+ * for `host-link` opens; per-recipient limits from `perUserPerDay` and
115
+ * `perUserPerMinute`, the daily one first; and no `rate`. `undefined` for a
116
+ * provider without remote delivery.
117
+ */
118
+ export declare function notificationConformanceRegistryEntry(input: {
119
+ readonly provider: string;
120
+ readonly capabilities: NotificationCapabilities;
121
+ readonly limits?: NotificationConformanceProfileLimits;
122
+ }): NotificationDeliveryProviderEntry | undefined;
123
+ /** An authenticated game-services session; its installation id is the session's, never a request's. */
124
+ export interface NotificationConformanceSession {
125
+ readonly id: string;
126
+ readonly player: string;
127
+ readonly installationId: string;
128
+ }
129
+ /** What setting a server reminder answers: stored, or the gate step 3 to 5 refusal. */
130
+ export type NotificationConformanceReminderSetStatus = 'scheduled' | 'replaced' | 'permission-required' | 'request-missing' | 'preference-off' | 'advertising-consent-missing';
131
+ export type NotificationConformanceNoticeChannel = 'in-game' | 'push' | 'other';
132
+ /** The backend surface the runtime calls for the authenticated session. */
133
+ export interface NotificationConformanceBackendClient {
134
+ /** The authenticated session; `undefined` before authentication and after the session ends. */
135
+ session(): NotificationConformanceSession | undefined;
136
+ /** Calls `listener` whenever a session starts; returns the unsubscribe. */
137
+ onAuthenticated(listener: (session: NotificationConformanceSession) => void): () => void;
138
+ /** Appends a record for the session's player; the backend stamps `at` with its own clock. */
139
+ recordConsent(record: NotificationConsentRecord): Promise<void>;
140
+ /** The session player's consent records, oldest first. */
141
+ listConsent(): Promise<readonly NotificationConsentRecord[]>;
142
+ /**
143
+ * Binds a device registration to the session's installation and player, or
144
+ * keeps a platform-user registration's locale and zone for the player.
145
+ */
146
+ registerEndpoint(registration: NotificationEndpointRegistration): Promise<void>;
147
+ /**
148
+ * Unbinds the session's installation from `player`, the player whose
149
+ * session sent the unbind; an installation another player took over since
150
+ * stays bound.
151
+ */
152
+ unregisterEndpoint(input: {
153
+ readonly provider: string;
154
+ readonly player: string;
155
+ }): Promise<void>;
156
+ /** Checks gate steps 3 to 5, then stores or replaces the session player's reminder with this key. */
157
+ setReminder(input: SetNotificationReminderInput): Promise<NotificationConformanceReminderSetStatus>;
158
+ /** Whether a pending reminder with this key, or a waiting send of one, existed. */
159
+ cancelReminder(input: CancelNotificationReminderInput): Promise<boolean>;
160
+ /** The session player's listed notices; none unless the deployment applies the Korean rules. */
161
+ listNotices(): Promise<readonly NotificationNotice[]>;
162
+ /**
163
+ * Records a notice given to the session's player, named by its identity
164
+ * fields, while that notice is listed; recording one that is not listed
165
+ * changes nothing.
166
+ */
167
+ recordNotice(input: NotificationNoticeIdentity & {
168
+ readonly channel: NotificationConformanceNoticeChannel;
169
+ }): Promise<void>;
170
+ }
171
+ export type NotificationConformanceStoredConsent = NotificationConsentRecord & {
172
+ readonly player: string;
173
+ };
174
+ export interface NotificationConformanceEndpoint {
175
+ readonly provider: string;
176
+ readonly kind: 'fcm-token' | 'fcm-installation';
177
+ readonly value: string;
178
+ readonly environment?: 'production' | 'sandbox';
179
+ readonly installationId: string;
180
+ /** The device permission the latest registration reported. */
181
+ readonly permission: NotificationPermissionState;
182
+ readonly player: string;
183
+ readonly status: 'valid' | 'invalid';
184
+ readonly locale?: string;
185
+ readonly timeZone?: string;
186
+ readonly lastRegisteredAt: string;
187
+ /** The session that registered the endpoint; its end unbinds it. */
188
+ readonly session?: string;
189
+ }
190
+ /** One outcome answered or stored for a send key, in order. */
191
+ export type NotificationConformanceDelivery = NotificationDeliveryOutcome & {
192
+ readonly idempotencyKey: string;
193
+ readonly topic: string;
194
+ readonly player: string;
195
+ readonly at: string;
196
+ };
197
+ export interface NotificationConformanceProviderCall {
198
+ /** The ledger key: the send key, or `<key>:<installation id>` for a device endpoint. */
199
+ readonly idempotencyKey: string;
200
+ readonly sendKey: string;
201
+ readonly provider: string;
202
+ readonly topic: string;
203
+ readonly player: string;
204
+ /** The device address value, for device addressing. */
205
+ readonly address?: string;
206
+ readonly mapping?: NotificationTopicMapping;
207
+ /** The send's link, else the mapping's. */
208
+ readonly link?: string;
209
+ readonly content: NotificationDeliveryContent;
210
+ readonly expiresAt?: string;
211
+ readonly templateData?: NotificationTemplateData;
212
+ readonly data?: Readonly<Record<string, string>>;
213
+ readonly at: string;
214
+ }
215
+ export interface NotificationConformanceServerReminder {
216
+ readonly player: string;
217
+ readonly key: string;
218
+ readonly topic: string;
219
+ readonly fireAt: string;
220
+ readonly templateData?: NotificationTemplateData;
221
+ readonly link?: string;
222
+ }
223
+ /** A notice given to a player: which notice, how, and when. */
224
+ export interface NotificationConformanceNoticeRecord extends NotificationNoticeIdentity {
225
+ readonly player: string;
226
+ readonly channel: NotificationConformanceNoticeChannel;
227
+ /** When it was given. */
228
+ readonly at: string;
229
+ }
230
+ /** A queued answer of the server-side delivery provider (RFC "Provider answers"). */
231
+ export type NotificationConformanceProviderResult = {
232
+ readonly status: 'accepted';
233
+ } | {
234
+ readonly status: 'unregistered';
235
+ } | {
236
+ readonly status: 'rate-limited';
237
+ readonly retryAfterSeconds?: number;
238
+ } | {
239
+ readonly status: 'rejected';
240
+ readonly reason: NotificationConformanceRejectionReason;
241
+ } | {
242
+ readonly status: 'ambiguous';
243
+ };
244
+ export interface NotificationConformanceBackend extends NotificationConformanceBackendClient {
245
+ /** Starts a new session for `player` on the fake installation and tells the listeners. */
246
+ authenticate(player: string): NotificationConformanceSession;
247
+ /** Ends the current session; the endpoints it registered are unbound. */
248
+ revokeSession(): void;
249
+ /** Binds another installation of the default player (the `backend.registerEndpoint` action). */
250
+ registerInstallation(input: NotificationConformanceInstallationFixture): void;
251
+ /** One send through the idempotency ledger, the policy gate, and the delivery provider. */
252
+ deliver(input: NotificationConformanceDeliverInput): Promise<NotificationDeliveryOutcome>;
253
+ queueProviderResult(result: NotificationConformanceProviderResult): void;
254
+ /** Processes every due server reminder, stored send, and notice, earliest first. */
255
+ runScheduler(): Promise<void>;
256
+ /** The provider's registry entry; `undefined` when the provider has no remote delivery. */
257
+ readonly entry: NotificationDeliveryProviderEntry | undefined;
258
+ readonly consent: readonly NotificationConformanceStoredConsent[];
259
+ readonly endpoints: readonly NotificationConformanceEndpoint[];
260
+ readonly deliveries: readonly NotificationConformanceDelivery[];
261
+ readonly providerCalls: readonly NotificationConformanceProviderCall[];
262
+ readonly reminders: readonly NotificationConformanceServerReminder[];
263
+ /** Every player's listed notices, ordered by `dueBy` and then `kind`. */
264
+ readonly notices: readonly (NotificationNotice & {
265
+ readonly player: string;
266
+ })[];
267
+ readonly noticesGiven: readonly NotificationConformanceNoticeRecord[];
268
+ }
269
+ export interface CreateNotificationConformanceBackendInput {
270
+ /** The delivery provider's registry entry; `undefined` for a provider without remote delivery. */
271
+ readonly entry: NotificationDeliveryProviderEntry | undefined;
272
+ readonly catalog: readonly NotificationTopicDeclaration[];
273
+ readonly clock: Pick<NotificationConformanceClock, 'now'>;
274
+ readonly deployment?: NotificationDeployment;
275
+ /**
276
+ * The catalog's `notices` block as is: the topic consent notice pushes go
277
+ * out through, and their copy, which follows the catalog's rules
278
+ * (`notificationNoticeCopyIssues` with `defaultLocale`). Without it the
279
+ * scheduler sends no notice.
280
+ */
281
+ readonly notices?: NotificationNoticeSettings;
282
+ readonly defaultLocale: string;
283
+ /** The recipient of a `deliver` without a `player`, and the owner of `registerInstallation`. */
284
+ readonly defaultPlayer: string;
285
+ /** The host's answer at send time; required for a server-readable provider. */
286
+ readonly readServerPermission?: (input: {
287
+ readonly player: string;
288
+ readonly topic: string;
289
+ }) => NotificationPermissionState | Promise<NotificationPermissionState>;
290
+ }
291
+ /**
292
+ * The fake backend (RFC "Server").
293
+ *
294
+ * - Sessions: each `authenticate` starts a new session on the fake
295
+ * installation, and `revokeSession` ends it and unbinds the endpoints it
296
+ * registered. Consent records, registrations, reminders, and notices belong
297
+ * to the session's player. The backend stamps each record's `at` with its
298
+ * own clock, strictly after the player's previous record, so a record's
299
+ * kind and time name it, and so does each notice that reports on it. It
300
+ * records a notice only while that notice is listed, so a retried or early
301
+ * call changes nothing, and a pushed notice the game gave while the push
302
+ * waited is not sent again. Notices go out through `notices.topic` only on
303
+ * a target that declares it; a target that delivers advertising under the
304
+ * Korean rules must declare it, as the catalog validator requires, and
305
+ * `notices.copy` must follow the catalog's rules.
306
+ * - Endpoints: a device endpoint is keyed by provider and installation, keeps
307
+ * the latest address, permission, and `lastRegisteredAt`, and moves with the
308
+ * installation to a newly authenticated player. An address is unique per
309
+ * provider, a dead address stays dead until the installation reports a new
310
+ * one, and an endpoint not refreshed for 60 days counts as missing. A
311
+ * platform-user provider resolves the authenticated identity and keeps the
312
+ * player's locale and zone.
313
+ * - Sends: a send is validated (input bounds, reserved prefixes), bound to
314
+ * its fingerprint (`NOTIFICATIONS_IDEMPOTENCY_CONFLICT` for another request),
315
+ * and answered from its key state: `delivered` with `alreadyProcessed`, the
316
+ * stored `deferred` result, or `in-flight`. A new key runs the gate for each
317
+ * recipient (every valid endpoint in installation-id order, each under
318
+ * `<key>:<installation id>`, or the platform identity) and calls the
319
+ * provider for the allowed ones. The send is `delivered` when any recipient
320
+ * accepted, else `in-flight`, `deferred` (the earliest), a provider's
321
+ * refusal, a policy refusal, and last an unresolved recipient. Only
322
+ * delivered, deferred, and in-flight rows are stored.
323
+ * - Provider answers come from `queueProviderResult`, `accepted` when none is
324
+ * queued: `unregistered` invalidates the endpoint; `rate-limited` defers by
325
+ * `retryAfterSeconds` (60 when absent), then by doubling delays without
326
+ * jitter, never past 24 hours after the first, and then gives up;
327
+ * `rejected` is not stored, and with
328
+ * `agreement` appends a `host-agreement` record of `denied` from the
329
+ * provider; `ambiguous` keeps the claim for the 300-second lease.
330
+ * - The scheduler processes due server reminders (as `reminder:` sends), due
331
+ * stored rows (deferred ones, and in-flight ones of scheduled sends or
332
+ * device endpoints after the lease, replayed through the gate without the
333
+ * key lookup), and, under the Korean rules, every unrecorded notice from the
334
+ * Asia/Seoul date seven days before it is due (as a `notice:` send of the
335
+ * notice topic in the variant of `notices.copy` it is listed with, filled
336
+ * with `{{consentDate}}`, and recorded with channel `push` once delivered),
337
+ * earliest first and within the registry entry's `rate`. Only a provider
338
+ * that can carry a notice (`notificationProviderCarriesNotices`) sends one,
339
+ * and never with its mapping's own text; a notice that cannot render is
340
+ * not tried again. A direct caller replays its own in-flight platform send
341
+ * after the lease. Keys are kept for 30 days after their last change.
342
+ * - The notice topic is the scheduler's: `deliver` and `setReminder` through
343
+ * it throw `NOTIFICATIONS_INVALID_INPUT`.
344
+ * - Setting or cancelling a reminder drops its sends' rows that still wait
345
+ * (deferred) or are in flight, so neither is replayed; a send the provider
346
+ * may already have accepted cannot be recalled. A send's limits count the
347
+ * player's other sends, never an earlier delivery of the same send to
348
+ * another endpoint. A send the gate cannot evaluate as asked (such as missing
349
+ * template data) answers `NOTIFICATIONS_INVALID_INPUT` directly and fails
350
+ * once when scheduled; any other failure, such as a host read that throws,
351
+ * is retried on the next run, and one failing item never stops the others.
352
+ */
353
+ export declare function createNotificationConformanceBackend(input: CreateNotificationConformanceBackendInput): NotificationConformanceBackend;
354
+ export interface NotificationConformanceRuntime extends NotificationRuntime {
355
+ /**
356
+ * Subscribes to the provider and to authentication, compares the provider
357
+ * with its registry entry, and reconciles pending local reminders. Provider
358
+ * events are buffered for the game's first listener.
359
+ */
360
+ start(): Promise<void>;
361
+ /** Drops listeners, buffered events, and both subscriptions. */
362
+ stop(): void;
363
+ /** Waits for event-driven work (records, registrations, local reminders) and rethrows its first failure. */
364
+ runUntilIdle(): Promise<void>;
365
+ }
366
+ /** What the runtime keeps across restarts. */
367
+ export interface NotificationConformanceRuntimeStorage {
368
+ /** The last registration the backend acknowledged. */
369
+ registration?: NotificationConformancePersistedRegistration;
370
+ /** Set by `unregister()`; `register()` or the next authentication clears it. */
371
+ unbound?: boolean;
372
+ }
373
+ export interface NotificationConformancePersistedRegistration {
374
+ readonly registration: NotificationEndpointRegistration;
375
+ readonly player: string;
376
+ readonly session: string;
377
+ readonly at: string;
378
+ }
379
+ export interface CreateNotificationConformanceRuntimeInput {
380
+ readonly provider: NotificationProvider;
381
+ readonly backend: NotificationConformanceBackendClient;
382
+ /** The provider's topics, each with this target's mapping. */
383
+ readonly catalog: readonly NotificationTopicDeclaration[];
384
+ readonly clock: Pick<NotificationConformanceClock, 'now'>;
385
+ /** The deployment's zone, for local quiet hours while the device has registered none. */
386
+ readonly timeZone: string;
387
+ readonly defaultLocale: string;
388
+ /** The device locale for local reminder copy; defaults to `defaultLocale`. */
389
+ readonly locale?: string;
390
+ /** Survives restarts; a fresh object when absent. */
391
+ readonly storage?: NotificationConformanceRuntimeStorage;
392
+ /**
393
+ * The backend's registry entry for this provider. When the provider's
394
+ * capabilities disagree with it at start, every call throws
395
+ * `NOTIFICATIONS_CONFIGURATION_REQUIRED`.
396
+ */
397
+ readonly registryEntry?: NotificationDeliveryProviderEntry;
398
+ }
399
+ /**
400
+ * The reference notifications runtime (RFC "Runtime"): the provider composed
401
+ * with the backend for the authenticated session.
402
+ *
403
+ * - Reads return the provider snapshot. A topic the host reads `unknown`
404
+ * takes the latest record, `sources` says which answered, and a
405
+ * game-rendered consent that is not opted in reads `prompt` until a
406
+ * decision is recorded and `denied` after it. A topic whose mapping needs no
407
+ * agreement reads `granted`. A read that observes a changed decided state
408
+ * records it, and every read still answers when the backend cannot.
409
+ * - Reads, requests, and revocations take each topic once, however often it
410
+ * is listed.
411
+ * - Requests throw `NOTIFICATIONS_USER_GESTURE_REQUIRED` without a gesture on
412
+ * a gesture-only provider. An unsupported provider answers `unavailable`, a
413
+ * provider in `action-required` answers that with its `requiredAction`, an
414
+ * undeclared or unmapped topic answers `unavailable`, a topic that needs no
415
+ * agreement answers `already-granted`, and a topic the host reads `blocked`
416
+ * answers `blocked`, all without reaching the host; every other topic
417
+ * reaches the prompt.
418
+ * - Records: decided states that differ from the latest record of their kind
419
+ * and scope, never `dismissed` or `action-required` answers. Device
420
+ * providers write none (the endpoint holds the permission), other app-scope
421
+ * providers write `delivery-permission`, and topic-scope providers write
422
+ * `host-agreement` with the host evidence, plus a `topic-preference` of
423
+ * `granted` when the player accepts a `requested` topic's agreement.
424
+ * Per-send acceptances are appended every time. A record belongs to the
425
+ * player signed in when the call or event began: one observed before
426
+ * authentication waits for it (only the latest per kind and scope), and one
427
+ * observed for a player waits for that player's session. A player action
428
+ * (`setTopicPreference`, `setAdvertisingConsent`) needs an authenticated
429
+ * player and throws `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE` without one, as
430
+ * does every other backend call (a server reminder, `reminders.cancel`'s
431
+ * server part, and `notices`). A backend call belongs to the player signed
432
+ * in when it began and is refused when another player signed in since. A
433
+ * record the backend would refuse is dropped and its error surfaces.
434
+ * - Registrations: the provider's registration goes to the backend at
435
+ * authentication, on `register()`, on `endpoint-changed` (a rotated device
436
+ * address, or a platform user's changed locale or zone), and when a read
437
+ * or request, or a revocation, changes the device's decided permission; and
438
+ * at start and on reads once the last one the backend acknowledged is older
439
+ * than 7 days. An identical one for the same player and session is skipped
440
+ * for 7 days, except on `register()`. `unregister()` unbinds the
441
+ * installation from the player signed in when it was called and cancels the
442
+ * pending local reminders, even when the unbind fails, and nothing binds it
443
+ * again, across restarts, until `register()` or the next authentication.
444
+ * - Reminders: one key across both routes, and `cancel` removes it from each
445
+ * route that has it. Local ones pass the client gate steps (permission,
446
+ * then the topic preference from the backend, then quiet hours, which move
447
+ * them or refuse a topic that is not deferrable, then the topic's daily
448
+ * cap); server ones are checked and stored by the backend. Turning a topic off cancels its local
449
+ * reminders, a known permission other than granted cancels them, and start,
450
+ * authentication, and every backend read reconcile them with the catalog
451
+ * (a topic that no longer takes the local route loses its reminders), the
452
+ * latest preference, and permission.
453
+ * - Game listeners run after the runtime's own handling of an event; a
454
+ * listener that throws stops neither the others nor the runtime, and its
455
+ * error surfaces from `runUntilIdle`, as do failures of background work:
456
+ * the first one, or an `AggregateError` that counts them all. `stop()` and
457
+ * a later `start()` buffer events for the next first subscriber again.
458
+ */
459
+ export declare function createNotificationConformanceRuntime(input: CreateNotificationConformanceRuntimeInput): NotificationConformanceRuntime;
460
+ export interface NotificationConformanceHarness {
461
+ readonly provider: NotificationProvider;
462
+ /**
463
+ * Handles `host` steps and counts prompts and local operations. A vector
464
+ * that scripts the host, or expects `prompts` or `local.operations` (or
465
+ * `local.pending` from a provider without `listLocal`), fails without it.
466
+ */
467
+ readonly host?: NotificationConformanceHostActor;
468
+ /**
469
+ * The evidence schema this provider attaches to a host agreement. Vector
470
+ * expectations naming the fixture schema `fixture.host-agreement.v1` match
471
+ * it instead.
472
+ */
473
+ readonly hostAgreementEvidenceSchema?: string;
474
+ }
475
+ export interface NotificationConformanceContext {
476
+ readonly vector: NotificationConformanceVector;
477
+ readonly profile: NotificationConformanceProfile;
478
+ readonly catalog: readonly NotificationTopicDeclaration[];
479
+ readonly clock: NotificationConformanceClock;
480
+ readonly player: string;
481
+ /** The device zone, which the provider registers. */
482
+ readonly timeZone: string;
483
+ }
484
+ export interface NotificationConformanceRunOptions {
485
+ /** Supplies a provider over the caller's fake SDK; defaults to the fake host's fixture provider. */
486
+ readonly createProvider?: (context: NotificationConformanceContext) => NotificationConformanceHarness | Promise<NotificationConformanceHarness>;
487
+ /**
488
+ * The backend's and the runtime's default locale;
489
+ * `notificationConformanceDefaultLocale` when absent. A vector's
490
+ * `notices.copy` must include it, or the vector fails at `backend`.
491
+ */
492
+ readonly defaultLocale?: string;
493
+ }
494
+ export interface NotificationConformanceFailure {
495
+ readonly path: string;
496
+ readonly expected: unknown;
497
+ readonly actual: unknown;
498
+ }
499
+ export type NotificationConformanceVectorStatus = 'passed' | 'failed' | 'skipped';
500
+ export interface NotificationConformanceVectorResult {
501
+ readonly vector: string;
502
+ /** `skipped` vectors did not run; `passed` is false and `reason` says why. */
503
+ readonly status: NotificationConformanceVectorStatus;
504
+ readonly passed: boolean;
505
+ readonly failures: readonly NotificationConformanceFailure[];
506
+ readonly reason?: string;
507
+ }
508
+ export declare function createNotificationConformanceHarness(context: NotificationConformanceContext): NotificationConformanceHarness;
509
+ /** Why `vector` does not apply to a provider with `capabilities`, or `undefined` when it runs. */
510
+ export declare function notificationConformanceSkipReason(vector: NotificationConformanceVector, capabilities: NotificationCapabilities): string | undefined;
511
+ /**
512
+ * Runs one vector: reads the provider's capabilities and skips the vector when
513
+ * they do not satisfy `appliesTo`, executes every step (a step's `returns` is
514
+ * matched as a subset, its `throws` by `code`), lets the runtime settle after
515
+ * each, then matches `expect`. Lists match exactly in length and order with
516
+ * only the listed fields of each item asserted, a field set to `null` asserts
517
+ * that it is absent, and timestamps compare as instants. Any unscripted
518
+ * operation fails the vector at the step that needed it.
519
+ *
520
+ * The backend's registry entry is derived from the provider's own
521
+ * capabilities and the profile's limits. A `setReminder` step that expects a
522
+ * local `permission-required` also calls the provider's `scheduleLocal`
523
+ * directly: it must answer `permission-required` without prompting or
524
+ * scheduling. With `options.createProvider`, the fixture provider id and the
525
+ * fixture agreement evidence schema in `returns` and `expect` match the
526
+ * provider under test's own id and `hostAgreementEvidenceSchema`.
527
+ */
528
+ export declare function runNotificationConformanceVector(vector: NotificationConformanceVector, options?: NotificationConformanceRunOptions): Promise<NotificationConformanceVectorResult>;
529
+ export declare function runNotificationConformanceVectors(vectors: readonly NotificationConformanceVector[], options?: NotificationConformanceRunOptions): Promise<readonly NotificationConformanceVectorResult[]>;