@mpgd/platform 0.22.0 → 0.23.0

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,885 @@
1
+ import type { tags } from 'typia';
2
+ import type { PlatformEvidenceEnvelope } from './index.js';
3
+ import type { NotificationCapabilities, NotificationChannel, NotificationPermissionAllowance, NotificationPermissionScope, NotificationPermissionSnapshot, NotificationPermissionState, NotificationProviderId, NotificationPurpose, NotificationRemoteContent, NotificationTopicId } from './notifications-contract.js';
4
+ /**
5
+ * Notifications v2 pure policy (RFC "Topic catalog", "Reminders", "Server",
6
+ * and "Advertising rules"): topic declarations and their rules, consent
7
+ * records, deployment settings, provider registry entries, the delivery gate
8
+ * `evaluateNotificationDelivery` with the advertising rendering, input bounds
9
+ * and idempotency fingerprints, reminder routing and the client-evaluable
10
+ * reminder checks, the Asia/Seoul night window and quiet hours, and the
11
+ * consent notices. No Phaser, DOM, network, or SDK imports, and no state
12
+ * beyond a bounded cache of `Intl` formatters: every function decides from its
13
+ * arguments. `./notifications` re-exports it.
14
+ */
15
+ type NonEmptyString = string & tags.MinLength<1>;
16
+ type PositiveInteger = number & tags.Type<'uint32'> & tags.Minimum<1>;
17
+ type IsoDateTime = string & tags.Format<'date-time'>;
18
+ export interface NotificationTopicCopy {
19
+ readonly title: NonEmptyString;
20
+ readonly body: NonEmptyString;
21
+ }
22
+ export interface NotificationTopicFrequency {
23
+ /** Deliveries of the topic to one player within any 24 hours. */
24
+ readonly maxPerDay: PositiveInteger;
25
+ }
26
+ /** A wall-clock time `HH:MM`, 00:00 to 23:59. */
27
+ export type NotificationClockTime = string & tags.Pattern<'^(?:[01][0-9]|2[0-3]):[0-5][0-9]$'>;
28
+ /** Holds sends from `start` (inclusive) to `end` (exclusive); `start` after `end` spans midnight. */
29
+ export interface NotificationQuietHours {
30
+ readonly start: NotificationClockTime;
31
+ readonly end: NotificationClockTime;
32
+ }
33
+ /** One target's mapping fields, for example `templateSetCode` or approved `title` and `body`. */
34
+ export type NotificationTopicMapping = Readonly<Record<string, string | number | boolean>>;
35
+ /** A topic as the catalog declares it, independent of any target. */
36
+ export interface NotificationTopicDefinition {
37
+ readonly id: NonEmptyString;
38
+ readonly purpose: NotificationPurpose;
39
+ readonly channels: readonly NotificationChannel[] & tags.MinItems<1>;
40
+ /** The route the open leads to; it must pass `isRootRelativeNotificationRoute`. */
41
+ readonly route: NonEmptyString;
42
+ /** Required for `advertising`: the screen where the player withdraws advertising consent. */
43
+ readonly optOutRoute?: NonEmptyString;
44
+ /** Copy per locale; placeholders use `{{name}}`. */
45
+ readonly copy?: Readonly<Record<string, NotificationTopicCopy>>;
46
+ readonly frequency?: NotificationTopicFrequency;
47
+ readonly quietHours?: NotificationQuietHours;
48
+ /**
49
+ * `false`: a send held by the night window or the topic's quiet hours is
50
+ * suppressed instead of deferred. Default `true`.
51
+ */
52
+ readonly deferrable?: boolean;
53
+ }
54
+ /** A topic resolved for one provider: the definition plus that target's mapping. */
55
+ export interface NotificationTopicDeclaration extends NotificationTopicDefinition {
56
+ readonly mapping?: NotificationTopicMapping;
57
+ }
58
+ /** Template data and routing context of a send or reminder (RFC input bounds). */
59
+ export type NotificationTemplateData = Readonly<Record<string, string | number | boolean>>;
60
+ /** A broken topic rule, for example `{ path: 'topics[2].optOutRoute', message: '…' }`. */
61
+ export interface NotificationTopicIssue {
62
+ readonly path: string;
63
+ readonly message: string;
64
+ }
65
+ /**
66
+ * Whether a topic's mapping declares that the host needs no agreement for it
67
+ * (an Apps in Toss `service` template without an agreement statement): it
68
+ * reads `granted`, never prompts, is never recorded, and passes gate step 3.
69
+ * A provider's answer that the recipient has not agreed is still recorded,
70
+ * but it does not stop sends, because nothing could ask the player again:
71
+ * the host decides each send.
72
+ */
73
+ export declare function notificationTopicNeedsNoAgreement(topic: Pick<NotificationTopicDeclaration, 'mapping'>): boolean;
74
+ /**
75
+ * Whether `route` is a root-relative route on the game's origin, at most 2048
76
+ * characters, and already in the form `normalizeRootRelativeRoute` from
77
+ * `@mpgd/platform/routes` returns. That is the rule share links and v1
78
+ * notification deep links follow, so neither a URL parser nor a client that
79
+ * percent-decodes the path once reads the route as another host
80
+ * (`//evil.example/x`, `/%2e//evil.example/x`, `/%2F%2Fevil.example/x`).
81
+ * A route with another normalized form is refused rather than rewritten, so
82
+ * the route a topic declares is the route a player opens: dot segments
83
+ * (`/a/../b`), a backslash in the path, an empty `?` or `#`, and characters a
84
+ * URL serializes percent-encoded (`/é`, `/a b`) all change the form.
85
+ */
86
+ export declare function isRootRelativeNotificationRoute(route: string): boolean;
87
+ /**
88
+ * The error for a route `isRootRelativeNotificationRoute` refuses, about
89
+ * `subject` (`The local reminder route`), or `undefined` when it passes. A
90
+ * route with another normalized form names that form, so the caller can
91
+ * write it instead.
92
+ */
93
+ export declare function notificationRouteError(subject: string, route: string): string | undefined;
94
+ /**
95
+ * The rules a topic definition must meet on its own (RFC "Topic catalog"):
96
+ * a trimmed id that is not `__proto__`, channels that do not repeat, no
97
+ * `local` channel for advertising, root-relative `route` and `optOutRoute`, an
98
+ * `optOutRoute` for advertising, and quiet hours that do not start and end at
99
+ * the same time. `path` prefixes each issue's path.
100
+ */
101
+ export declare function notificationTopicDefinitionIssues(topic: NotificationTopicDefinition, path?: string): NotificationTopicIssue[];
102
+ export type NotificationConsentKind = 'delivery-permission' | 'host-agreement' | 'topic-preference' | 'advertising' | 'night';
103
+ /** Permission states the runtime records: decided ones, never `prompt`, `unknown`, `action-required`, or `unsupported`. */
104
+ export type NotificationRecordablePermissionState = Exclude<NotificationPermissionState, 'prompt' | 'unknown' | 'action-required' | 'unsupported'>;
105
+ export type NotificationConsentState = NotificationRecordablePermissionState | 'withdrawn';
106
+ /** `provider`: a delivery provider's answer that the recipient has not agreed. */
107
+ export type NotificationConsentSource = 'client-report' | 'player' | 'provider';
108
+ /**
109
+ * One consent record. The per-kind shapes encode the RFC rules: permission
110
+ * kinds carry a permission state, a `delivery-permission` record comes from a
111
+ * client report or a player revocation, a `host-agreement` record from a
112
+ * client report or a provider answer, `host-agreement` and `topic-preference`
113
+ * name their topic, the player-authored kinds carry `granted` or `withdrawn`,
114
+ * and advertising and night consent carry the consent text version. The
115
+ * backend stamps `at` with its own clock.
116
+ */
117
+ export type NotificationConsentRecord = {
118
+ readonly kind: 'delivery-permission';
119
+ readonly topic?: never;
120
+ readonly state: NotificationRecordablePermissionState;
121
+ readonly source: 'client-report' | 'player';
122
+ readonly at: IsoDateTime;
123
+ readonly evidence?: PlatformEvidenceEnvelope;
124
+ readonly textVersion?: never;
125
+ } | {
126
+ readonly kind: 'host-agreement';
127
+ readonly topic: NonEmptyString;
128
+ readonly state: NotificationRecordablePermissionState;
129
+ readonly source: 'client-report' | 'provider';
130
+ readonly at: IsoDateTime;
131
+ readonly evidence?: PlatformEvidenceEnvelope;
132
+ readonly textVersion?: never;
133
+ } | {
134
+ readonly kind: 'topic-preference';
135
+ readonly topic: NonEmptyString;
136
+ readonly state: 'granted' | 'withdrawn';
137
+ readonly source: 'player';
138
+ readonly at: IsoDateTime;
139
+ readonly evidence?: never;
140
+ readonly textVersion?: never;
141
+ } | {
142
+ readonly kind: 'advertising' | 'night';
143
+ readonly topic?: never;
144
+ readonly state: 'granted' | 'withdrawn';
145
+ readonly source: 'player';
146
+ readonly at: IsoDateTime;
147
+ readonly evidence?: never;
148
+ /** Consent text version the player agreed to or withdrew from. */
149
+ readonly textVersion: NonEmptyString;
150
+ };
151
+ export declare const recordableNotificationPermissionStates: readonly ["granted", "prompt-with-rationale", "denied", "blocked"];
152
+ /** Whether the runtime records this state: a decided state, never prompt, unknown, action-required, or unsupported. */
153
+ export declare function isRecordableNotificationPermissionState(state: NotificationPermissionState): state is NotificationRecordablePermissionState;
154
+ /** The consent kind that holds delivery permission for a permission scope. */
155
+ export declare function notificationPermissionConsentKind(scope: NotificationPermissionScope): 'delivery-permission' | 'host-agreement';
156
+ /** A topic's state in a snapshot: its own entry, else the app state, else `unknown`. */
157
+ export declare function notificationPermissionStateOf(snapshot: NotificationPermissionSnapshot, topic: NotificationTopicId): NotificationPermissionState;
158
+ /**
159
+ * The latest record of `kind` for exactly `topic` in `records` (oldest first):
160
+ * with `topic`, only records naming that topic match; without it, only
161
+ * records without a topic (app-scope permission, advertising, night) match.
162
+ */
163
+ export declare function latestNotificationConsent(records: readonly NotificationConsentRecord[], kind: NotificationConsentKind, topic?: NotificationTopicId): NotificationConsentRecord | undefined;
164
+ /** `kr` applies the Korean advertising rules to every recipient; `none` keeps consent and the opt-out route only. */
165
+ export type NotificationAdvertisingRules = 'kr' | 'none';
166
+ /** The deployment's advertising sender, which every advertising send carries under `kr`. */
167
+ export interface NotificationAdvertisingSender {
168
+ /** The sender's name; leave it out only when it equals the app's name. */
169
+ readonly name?: NonEmptyString;
170
+ /** The phone number or address the law requires. */
171
+ readonly contact: NonEmptyString;
172
+ }
173
+ export interface NotificationDeployment {
174
+ /**
175
+ * Required when the catalog has an `advertising` topic: the gate refuses to
176
+ * evaluate an advertising send without it.
177
+ */
178
+ readonly advertisingRules?: NotificationAdvertisingRules;
179
+ readonly advertisingSender?: NotificationAdvertisingSender;
180
+ /** The free-of-charge opt-out line; `무료 수신거부: 수신거부 버튼` when absent or blank. */
181
+ readonly advertisingOptOutLine?: NonEmptyString;
182
+ /** IANA zone for recipients that registered none; Asia/Seoul when absent. */
183
+ readonly timeZone?: NonEmptyString;
184
+ }
185
+ export declare const notificationDefaultOptOutLine = "\uBB34\uB8CC \uC218\uC2E0\uAC70\uBD80: \uC218\uC2E0\uAC70\uBD80 \uBC84\uD2BC";
186
+ export declare const notificationDefaultDeploymentTimeZone = "Asia/Seoul";
187
+ /** The deployment's advertising settings as gate step 8 reads them under `kr`. */
188
+ export interface NotificationAdvertisingSettings {
189
+ /** The sender, trimmed, without a blank name; absent when there is no contact to send. */
190
+ readonly sender?: NotificationAdvertisingSender;
191
+ /** The opt-out line, trimmed; the default when it is absent or blank. */
192
+ readonly optOutLine: string;
193
+ /** Each field the gate cannot use as written. */
194
+ readonly issues: readonly NotificationTopicIssue[];
195
+ }
196
+ /**
197
+ * How gate step 8 reads the deployment under `kr`; the catalog validator
198
+ * reports its `issues`, so both read the settings the same way. The sender's
199
+ * contact and name and the opt-out line are trimmed. A missing sender or a
200
+ * blank contact leaves `sender` absent, which suppresses every advertisement
201
+ * as `advertising-sender-missing`; a blank name is left out of the sender
202
+ * line, and a blank opt-out line gives way to the default. `path` prefixes
203
+ * each issue's path.
204
+ */
205
+ export declare function resolveNotificationAdvertisingSettings(deployment: NotificationDeployment | undefined, path?: string): NotificationAdvertisingSettings;
206
+ /** A per-recipient or app-wide count within a rolling window. */
207
+ export interface NotificationProviderLimit {
208
+ readonly count: PositiveInteger;
209
+ readonly windowSeconds: PositiveInteger;
210
+ }
211
+ /**
212
+ * A delivery provider's server-side registry entry, the gate's only
213
+ * capability input. The runtime compares its provider's capabilities with it
214
+ * at start.
215
+ */
216
+ export interface NotificationDeliveryProviderEntry {
217
+ readonly provider: NotificationProviderId;
218
+ readonly purposes: readonly NotificationPurpose[];
219
+ readonly addressing: 'device' | 'platform-user';
220
+ readonly content: NotificationRemoteContent;
221
+ readonly permission: {
222
+ readonly scope: NotificationPermissionScope;
223
+ readonly serverReadable: boolean;
224
+ readonly allowance: NotificationPermissionAllowance;
225
+ };
226
+ /** Per-recipient limits; a window of a day or longer suppresses, a shorter one defers. */
227
+ readonly limits?: readonly NotificationProviderLimit[];
228
+ /** App-wide dispatch rate the scheduler keeps to. */
229
+ readonly rate?: NotificationProviderLimit;
230
+ /** Whether a send can carry an expiry, which advertising needs under the Korean rules. */
231
+ readonly expiry: boolean;
232
+ /** Whether a send can carry the advertising opt-out action. */
233
+ readonly actions: boolean;
234
+ /** Whether each send needs a post or comment link (Devvit). */
235
+ readonly link: boolean;
236
+ /** The mapping fields this provider needs in each catalog topic. */
237
+ readonly mappingFields: readonly string[];
238
+ }
239
+ /** How the catalog validator checks one mapping field of a provider. */
240
+ export interface NotificationMappingField {
241
+ readonly required: boolean;
242
+ /** Longest accepted string in Unicode code points. */
243
+ readonly maxLength?: PositiveInteger;
244
+ /** The accepted string values, when the field is an enumeration. */
245
+ readonly values?: readonly string[];
246
+ /** A pattern every string value must match, such as Devvit's `t3_`/`t1_` ids. */
247
+ readonly pattern?: string;
248
+ }
249
+ /** A registry entry with the mapping-field rules the catalog validator applies. */
250
+ export interface NotificationProviderRegistryEntry extends NotificationDeliveryProviderEntry {
251
+ readonly mapping: Readonly<Record<string, NotificationMappingField>>;
252
+ }
253
+ /**
254
+ * Reference registry entries for the RFC provider profiles. They are reference
255
+ * values: a project registers its own entries under its own provider ids, and
256
+ * the P-phase providers own the final values.
257
+ */
258
+ export declare const referenceNotificationProviders: Readonly<Record<'fcm' | 'apps-in-toss' | 'devvit', NotificationProviderRegistryEntry>>;
259
+ /** Reference target names and the provider each one enables. */
260
+ export declare const referenceNotificationTargetProviders: Readonly<Record<string, keyof typeof referenceNotificationProviders>>;
261
+ /**
262
+ * Where a client provider's capabilities disagree with the provider's registry
263
+ * entry, or `undefined` when they agree. The runtime compares them at start,
264
+ * and a mismatch makes the provider `configuration-required`.
265
+ */
266
+ export declare function notificationRegistryMismatch(capabilities: NotificationCapabilities, entry: NotificationDeliveryProviderEntry): string | undefined;
267
+ export type NotificationDeliveryStatus = 'delivered' | 'suppressed' | 'deferred' | 'unavailable' | 'in-flight';
268
+ export type NotificationSuppressionReason = 'topic-unknown' | 'channel-unsupported' | 'purpose-unsupported' | 'permission-denied' | 'agreement-missing' | 'allowance-exhausted' | 'request-missing' | 'preference-off' | 'advertising-consent-missing' | 'night-consent-missing' | 'quiet-hours' | 'frequency-capped' | 'provider-limit' | 'advertising-sender-missing';
269
+ /** Why a send waits: a time window, a short provider limit, or a provider's rate-limit answer. */
270
+ export type NotificationDeferralReason = 'night-window' | 'quiet-hours' | 'provider-limit' | 'provider-rate-limited';
271
+ export type NotificationUnavailableReason = 'no-endpoint' | 'link-missing' | 'endpoint-invalid' | 'provider-rejected' | 'provider-rate-limited';
272
+ export declare const notificationSuppressionReasons: readonly ["topic-unknown", "channel-unsupported", "purpose-unsupported", "permission-denied", "agreement-missing", "allowance-exhausted", "request-missing", "preference-off", "advertising-consent-missing", "night-consent-missing", "quiet-hours", "frequency-capped", "provider-limit", "advertising-sender-missing"];
273
+ export type NotificationDeliveryRefusal = {
274
+ readonly status: 'suppressed';
275
+ readonly reason: NotificationSuppressionReason;
276
+ } | {
277
+ readonly status: 'deferred';
278
+ readonly reason: NotificationDeferralReason;
279
+ readonly until: IsoDateTime;
280
+ } | {
281
+ readonly status: 'unavailable';
282
+ readonly reason: NotificationUnavailableReason;
283
+ };
284
+ /**
285
+ * The recorded result of one send. `alreadyProcessed` marks a key that had
286
+ * already delivered; `in-flight` answers a key whose delivery a claim holds.
287
+ */
288
+ export type NotificationDeliveryOutcome = {
289
+ readonly status: 'delivered';
290
+ readonly alreadyProcessed: boolean;
291
+ } | {
292
+ readonly status: 'in-flight';
293
+ } | NotificationDeliveryRefusal;
294
+ /** An action a notification carries; under `kr`, advertising carries the opt-out action. */
295
+ export interface NotificationAction {
296
+ readonly id: NonEmptyString;
297
+ readonly title: NonEmptyString;
298
+ /** A root-relative route the action opens. */
299
+ readonly route: NonEmptyString;
300
+ }
301
+ /** The advertising opt-out action under the Korean rules. */
302
+ export declare const notificationAdvertisingOptOutActionId = "advertising-opt-out";
303
+ /** What a provider sends; `title` and `body` are absent when the host template carries the copy. */
304
+ export interface NotificationDeliveryContent {
305
+ readonly title?: string;
306
+ readonly body?: string;
307
+ readonly route: string;
308
+ /** Under `kr`, advertising carries the opt-out action. */
309
+ readonly actions?: readonly NotificationAction[];
310
+ readonly source: 'mapping' | 'host-template' | 'catalog';
311
+ }
312
+ /**
313
+ * The gate's answer: `allowed` with the content to send and, for advertising
314
+ * without night consent under `kr`, the expiry at the next 21:00 KST; or the
315
+ * refusal to record.
316
+ */
317
+ export type NotificationDeliveryDecision = {
318
+ readonly status: 'allowed';
319
+ readonly content: NotificationDeliveryContent;
320
+ readonly expiresAt?: IsoDateTime;
321
+ } | NotificationDeliveryRefusal;
322
+ /** A send already delivered to the recipient. */
323
+ export interface NotificationDeliveryHistoryEntry {
324
+ readonly provider: NotificationProviderId;
325
+ readonly topic: NotificationTopicId;
326
+ readonly at: string;
327
+ }
328
+ export interface EvaluateNotificationDeliveryInput {
329
+ /** The provider's registry entry; the gate never reads the client's declaration. */
330
+ readonly entry: NotificationDeliveryProviderEntry;
331
+ readonly deployment?: NotificationDeployment;
332
+ /** The send's topic as declared for this provider; absent when the catalog does not declare it. */
333
+ readonly topic: NotificationTopicDeclaration | undefined;
334
+ /**
335
+ * Gate step 2: this recipient resolved, a valid endpoint for
336
+ * `addressing: 'device'` or an authenticated platform identity for
337
+ * `'platform-user'`.
338
+ */
339
+ readonly recipientResolved: boolean;
340
+ /** The send's post or comment link, for providers whose entry sets `link`. */
341
+ readonly link?: string;
342
+ /** The recipient's consent records, oldest first. */
343
+ readonly consent: readonly NotificationConsentRecord[];
344
+ /** The host's answer at send time; required when the entry is server-readable. */
345
+ readonly serverPermission?: NotificationPermissionState;
346
+ /**
347
+ * For `addressing: 'device'`: the permission this endpoint's latest
348
+ * registration reported. Anything but `granted`, or none, does not deliver.
349
+ */
350
+ readonly endpointPermission?: NotificationPermissionState;
351
+ /**
352
+ * Sends delivered to the recipient, oldest first (the gate sorts a copy of
353
+ * a history out of order, so the order never changes the answer): at least
354
+ * those within the longest limit window and, for a per-send provider, every
355
+ * send of the topic since the first recorded acceptance. A backend that
356
+ * reserves acceptances atomically with the delivery claim also passes its
357
+ * in-flight sends.
358
+ */
359
+ readonly history: readonly NotificationDeliveryHistoryEntry[];
360
+ /** The time of the provider call, RFC 3339 with an offset. */
361
+ readonly at: string;
362
+ /** The recipient's registered zone; the deployment's zone when absent. */
363
+ readonly timeZone?: string;
364
+ /** The delivery's locale, else the recipient's registered locale. */
365
+ readonly locale?: string;
366
+ readonly defaultLocale: string;
367
+ readonly templateData?: NotificationTemplateData;
368
+ }
369
+ /**
370
+ * The policy gate, evaluated per recipient immediately before the provider
371
+ * call: after any queue, and for a scheduled reminder or a deferred send when
372
+ * it comes due (the caller looks up the idempotency key first). The steps run
373
+ * in order and the first failure decides:
374
+ *
375
+ * 1. the topic is declared (`topic-unknown`), has the `remote` channel
376
+ * (`channel-unsupported`), and the entry delivers its purpose, advertising
377
+ * only as free-form content and, under `kr`, with an expiry and the opt-out
378
+ * action (`purpose-unsupported`);
379
+ * 2. the recipient resolves (`unavailable: no-endpoint`), with a link where
380
+ * the entry needs one (`unavailable: link-missing`);
381
+ * 3. delivery permission is positively `granted`: the send-time host answer on
382
+ * a server-readable provider, the endpoint's registered permission for
383
+ * device addressing, or a latest `delivery-permission` record of `granted`
384
+ * for an app-scope platform-user provider; a topic-scope provider also
385
+ * needs a latest `host-agreement` of `granted` unless the topic needs no
386
+ * agreement (then the host decides each send, after a provider's denial
387
+ * too), and a per-send provider an unreserved acceptance
388
+ * (`permission-denied`, `agreement-missing`, `allowance-exhausted`);
389
+ * 4. the topic preference, only `topic-preference` records, the latest winning
390
+ * (`request-missing`, `preference-off`);
391
+ * 5. advertising consent (`advertising-consent-missing`);
392
+ * 6. the night window under `kr` and the topic's quiet hours in the
393
+ * recipient's zone (`deferred` until the holding window ends, `suppressed`
394
+ * for a topic that is not deferrable, or `night-consent-missing` when the
395
+ * windows leave no time to send);
396
+ * 7. the topic frequency cap and the entry's limits over rolling windows
397
+ * (`frequency-capped`, `provider-limit`; a limit window shorter than a day
398
+ * defers instead);
399
+ * 8. under `kr`, advertising needs the deployment's sender with a contact
400
+ * that is not blank (`advertising-sender-missing`) and is rendered with the
401
+ * label, the sender line, the opt-out line, and the opt-out action, the
402
+ * settings read by `resolveNotificationAdvertisingSettings`.
403
+ *
404
+ * Caller errors throw a `TypeError` instead of deciding: a missing
405
+ * `serverPermission`, an advertising topic without the deployment's
406
+ * `advertisingRules` (the gate fails closed rather than guess them), an
407
+ * unknown time zone, missing copy or template data, an advertising topic
408
+ * without `optOutRoute`, and a route or opt-out route that is not
409
+ * root-relative.
410
+ */
411
+ export declare function evaluateNotificationDelivery(input: EvaluateNotificationDeliveryInput): NotificationDeliveryDecision;
412
+ /**
413
+ * A text's `{{name}}` placeholders, as `fillNotificationTemplate` reads them,
414
+ * and whether a brace is left once they are taken out. `{name}`, `{{name}`,
415
+ * `{{a.b}}`, and full-width braces fill nothing and would reach the player as
416
+ * written.
417
+ */
418
+ export declare function readNotificationTemplate(text: string): {
419
+ readonly placeholders: readonly string[];
420
+ readonly strayBrace: boolean;
421
+ };
422
+ /** Fills `{{name}}` placeholders from `data`; a placeholder without a value is a caller error. */
423
+ export declare function fillNotificationTemplate(text: string, data?: NotificationTemplateData): string;
424
+ /**
425
+ * The topic's copy for `locale`: that locale, then its language (`en` for
426
+ * `en-US` or `en_US`), then `defaultLocale`, each matched without regard to
427
+ * case as locale tags are; `undefined` when the topic has none of them.
428
+ */
429
+ export declare function findNotificationCopy(topic: Pick<NotificationTopicDefinition, 'copy'>, locale: string | undefined, defaultLocale: string): NotificationTopicCopy | undefined;
430
+ export interface ResolveNotificationContentInput {
431
+ readonly topic: NotificationTopicDeclaration;
432
+ readonly content: NotificationRemoteContent;
433
+ /** The delivery's locale, else the recipient's registered locale. */
434
+ readonly locale?: string;
435
+ readonly defaultLocale: string;
436
+ readonly templateData?: NotificationTemplateData;
437
+ }
438
+ /**
439
+ * Content precedence (RFC "Topic catalog"): the target mapping when it
440
+ * carries its own `title` and `body`, the host template when
441
+ * `remote.content` is `host-template`, and otherwise the catalog copy for the
442
+ * locale (`findNotificationCopy`). No copy, a missing template value, and a
443
+ * route that `isRootRelativeNotificationRoute` refuses are caller errors, so
444
+ * a catalog that skipped validation still never sends another host's link.
445
+ */
446
+ export declare function resolveNotificationContent(input: ResolveNotificationContentInput): NotificationDeliveryContent;
447
+ /** The advertising label every advertising title and body starts with under `kr`. */
448
+ export declare const notificationAdvertisingPrefix = "(\uAD11\uACE0)";
449
+ /**
450
+ * `(광고)`, one space, and the text without leading whitespace. Text that
451
+ * already starts with the label keeps it once, followed by one space.
452
+ */
453
+ export declare function applyNotificationAdvertisingLabel(text: string): string;
454
+ /**
455
+ * Gate step 8 under `kr`: the title starts with `(광고) `; the body starts with
456
+ * `(광고) ` and the sender line (`name contact`, or `contact` alone), then the
457
+ * copy on the next line, then the opt-out line on the last line; and the
458
+ * message carries the opt-out action that opens the topic's `optOutRoute`.
459
+ * The sender and the opt-out line are read as
460
+ * `resolveNotificationAdvertisingSettings` reads them; a blank contact is a
461
+ * caller error.
462
+ */
463
+ export declare function renderNotificationAdvertisement(topic: NotificationTopicDefinition, content: NotificationDeliveryContent, sender: NotificationAdvertisingSender, optOutLine?: string): NotificationDeliveryContent;
464
+ /** v1's input bounds, which v2 keeps. */
465
+ export declare const notificationInputBounds: Readonly<{
466
+ readonly keyLength: 2048;
467
+ readonly entries: 128;
468
+ readonly stringLength: 16384;
469
+ }>;
470
+ /** Key prefixes the scheduler reserves; a caller's send key may not start with one. */
471
+ export declare const notificationReservedKeyPrefixes: readonly ["reminder:", "notice:"];
472
+ /** Why `key` is not a valid send or reminder key, or `undefined`. */
473
+ export declare function notificationKeyIssue(key: string): string | undefined;
474
+ /** The reserved prefix `key` starts with, if any. */
475
+ export declare function notificationReservedKeyPrefix(key: string): string | undefined;
476
+ /**
477
+ * Why `data` is not valid template data or routing data, or `undefined`: at
478
+ * most 128 entries of strings, finite numbers, or booleans, each string at most
479
+ * 16,384 characters. With `stringsOnly`, every value must be a string (routing
480
+ * `data`).
481
+ */
482
+ export declare function notificationTemplateDataIssue(data: unknown, options?: {
483
+ readonly stringsOnly?: boolean;
484
+ }): string | undefined;
485
+ /** The request a send key is bound to (RFC "Policy gate", binding). */
486
+ export interface NotificationSendRequest {
487
+ readonly player: string;
488
+ readonly provider?: string;
489
+ readonly topic: string;
490
+ readonly templateData?: NotificationTemplateData;
491
+ readonly data?: Readonly<Record<string, string>>;
492
+ readonly locale?: string;
493
+ readonly link?: string;
494
+ }
495
+ /**
496
+ * The fingerprint a stored key is bound to: the player, provider, topic,
497
+ * template data, routing data, locale, and link, in a canonical JSON form. The
498
+ * resolved address is left out, so a replay after a token rotation still
499
+ * matches.
500
+ */
501
+ export declare function notificationSendFingerprint(request: NotificationSendRequest): string;
502
+ export type NotificationReminderRoute = 'local' | 'remote' | 'none';
503
+ /**
504
+ * A reminder's answer (RFC `ReminderResult`). The local route reports the
505
+ * gate's `permission-denied` as `permission-required`.
506
+ */
507
+ export type NotificationReminderStatus = 'scheduled' | 'replaced' | 'cancelled' | 'permission-required' | 'request-missing' | 'preference-off' | 'advertising-consent-missing' | 'frequency-capped' | 'quiet-hours' | 'capacity-exceeded' | 'action-required' | 'unavailable';
508
+ export interface ReminderResult {
509
+ readonly route: NotificationReminderRoute;
510
+ readonly status: NotificationReminderStatus;
511
+ }
512
+ /**
513
+ * Reminder routing: local when the topic has the `local` channel, is not
514
+ * `advertising`, and the provider supports local notifications; else remote
515
+ * when the topic has the `remote` channel and the provider supports remote
516
+ * delivery; else none. A reminder never takes both routes, and an advertising
517
+ * topic never takes the local route even when a catalog that skipped
518
+ * validation declares it.
519
+ */
520
+ export declare function routeNotificationReminder(topic: Pick<NotificationTopicDefinition, 'channels' | 'purpose'>, capabilities: Pick<NotificationCapabilities, 'local' | 'remote'>): NotificationReminderRoute;
521
+ export interface EvaluateLocalNotificationReminderInput {
522
+ readonly topic: NotificationTopicDefinition;
523
+ /** The topic's effective permission: the host read, or the latest record when the host answers `unknown`. */
524
+ readonly permission: NotificationPermissionState;
525
+ /** The player's consent records, oldest first. */
526
+ readonly consent: readonly NotificationConsentRecord[];
527
+ readonly fireAt: string;
528
+ /** Quiet-hours zone: the registered zone, else the deployment's zone. */
529
+ readonly timeZone: string;
530
+ /** The topic's other pending local reminders, for `frequency.maxPerDay`. */
531
+ readonly pending?: readonly {
532
+ readonly fireAt: string;
533
+ }[];
534
+ }
535
+ export type LocalNotificationReminderDecision = {
536
+ readonly status: 'schedule';
537
+ readonly fireAt: string;
538
+ } | {
539
+ readonly status: 'permission-required' | 'request-missing' | 'preference-off' | 'quiet-hours' | 'frequency-capped';
540
+ };
541
+ /**
542
+ * The gate steps a client can evaluate before scheduling a local reminder, in
543
+ * order: delivery permission (`permission-required`), the topic preference,
544
+ * the topic's quiet hours, and `frequency.maxPerDay` over the topic's pending
545
+ * local reminders within any 24 hours (`frequency-capped`). As at gate step
546
+ * 6, a `fireAt` inside the quiet hours moves to the first time outside them,
547
+ * hopping over a window that a clock change makes end inside another, and a
548
+ * topic with `deferrable: false` is not scheduled there (`quiet-hours`).
549
+ * Without quiet hours `fireAt` is kept. The night window does not apply,
550
+ * because advertising never takes the local route.
551
+ */
552
+ export declare function evaluateLocalNotificationReminder(input: EvaluateLocalNotificationReminderInput): LocalNotificationReminderDecision;
553
+ export interface EvaluateNotificationReminderConsentInput {
554
+ readonly entry: NotificationDeliveryProviderEntry;
555
+ readonly topic: NotificationTopicDeclaration;
556
+ readonly consent: readonly NotificationConsentRecord[];
557
+ /** The host's answer for a server-readable provider. */
558
+ readonly serverPermission?: NotificationPermissionState;
559
+ /** For device addressing: `granted` when any of the player's endpoints reported it. */
560
+ readonly endpointPermission?: NotificationPermissionState;
561
+ readonly history: readonly NotificationDeliveryHistoryEntry[];
562
+ }
563
+ /**
564
+ * Gate steps 3 to 5 at the time a server reminder is set, against the
565
+ * backend's records or the host read: `undefined` when they pass, else the
566
+ * reminder status to answer (`permission-required` for a permission, agreement,
567
+ * or allowance refusal). The scheduler runs the whole gate again at send time.
568
+ */
569
+ export declare function evaluateNotificationReminderConsent(input: EvaluateNotificationReminderConsentInput): 'permission-required' | 'request-missing' | 'preference-off' | 'advertising-consent-missing' | undefined;
570
+ /**
571
+ * Idempotency key of a server reminder's send:
572
+ * `reminder:<player>:<key>:<fireAt>`, with the player and key percent-encoded
573
+ * (`encodeURIComponent`) so ids containing `:` cannot collide, and `fireAt` in
574
+ * the canonical timestamp form.
575
+ */
576
+ export declare function createNotificationReminderIdempotencyKey(input: {
577
+ readonly player: string;
578
+ readonly key: string;
579
+ readonly fireAt: string;
580
+ }): string;
581
+ /** The Korean advertising night window: 21:00 inclusive to 08:00 exclusive, Asia/Seoul. */
582
+ export declare const notificationNightWindow: Readonly<{
583
+ readonly timeZone: 'Asia/Seoul';
584
+ readonly start: '21:00';
585
+ readonly end: '08:00';
586
+ }>;
587
+ export type NotificationWindowCheck = {
588
+ readonly inside: false;
589
+ } | {
590
+ readonly inside: true;
591
+ readonly until: string;
592
+ };
593
+ /** Whether `at` falls in the night window, and the next 08:00 Asia/Seoul when it does. */
594
+ export declare function checkNotificationNightWindow(at: string): NotificationWindowCheck;
595
+ /**
596
+ * Whether `at` falls in the quiet hours in `timeZone`, and the window's next
597
+ * end when it does. An unknown zone throws a `TypeError`.
598
+ */
599
+ export declare function checkNotificationQuietHours(at: string, quietHours: NotificationQuietHours, timeZone: string): NotificationWindowCheck;
600
+ /** Whether `Intl` knows `timeZone`, so quiet hours can be read in it. */
601
+ export declare function isValidNotificationTimeZone(timeZone: string): boolean;
602
+ /** The Asia/Seoul calendar date of an instant, `YYYY-MM-DD`. */
603
+ export declare function toSeoulCalendarDate(at: string): string;
604
+ /**
605
+ * ISO 8601 UTC without a `.000` millisecond part, as the vectors write
606
+ * timestamps. The input needs a date, a time with seconds, and an offset (`Z`
607
+ * or `±HH:MM`): a zone-less time would depend on the process's time zone.
608
+ */
609
+ export declare function normalizeNotificationTimestamp(at: string): string;
610
+ /**
611
+ * Whether `at` is an RFC 3339 instant as every notifications timestamp must
612
+ * be: a date that exists, `T` (not a space), a time, and an offset. Fractions
613
+ * past milliseconds are dropped when it is read.
614
+ */
615
+ export declare function isNotificationTimestamp(at: string): boolean;
616
+ /** How a validated string field is read: an instant (`date-time`) or a calendar date (`date`). */
617
+ export type NotificationTimeFormat = 'date-time' | 'date';
618
+ /**
619
+ * The `date-time` and `date` fields of a validated type, by key. A nested
620
+ * spec applies to an object, and to each item of an array.
621
+ */
622
+ export interface NotificationTimeFields {
623
+ readonly [key: string]: NotificationTimeFormat | NotificationTimeFields;
624
+ }
625
+ /**
626
+ * The check every notifications validator runs after typia's, whose formats
627
+ * accept a day the month does not have (31 April) and a space for `T`: each
628
+ * `date-time` field `fields` names must pass `isNotificationTimestamp`, and
629
+ * each `date` field `isNotificationCalendarDate`. Answers the first field
630
+ * that does not, or `undefined`.
631
+ */
632
+ export declare function notificationTimeFieldIssue(value: unknown, fields: NotificationTimeFields, path?: string): string | undefined;
633
+ /**
634
+ * The time fields of each validated type with any, for
635
+ * `notificationTimeFieldIssue`. A test keeps them equal to the formats in the
636
+ * published JSON Schemas.
637
+ */
638
+ export declare const notificationTimeFields: {
639
+ readonly LocalNotificationRequest: {
640
+ readonly fireAt: 'date-time';
641
+ };
642
+ readonly LocalNotificationEntry: {
643
+ readonly fireAt: 'date-time';
644
+ };
645
+ readonly NotificationBridgeListLocalResult: {
646
+ readonly pending: {
647
+ readonly fireAt: 'date-time';
648
+ };
649
+ };
650
+ readonly NotificationConsentRecord: {
651
+ readonly at: 'date-time';
652
+ };
653
+ readonly NotificationNotice: {
654
+ readonly record: {
655
+ readonly at: 'date-time';
656
+ };
657
+ readonly dueBy: 'date';
658
+ };
659
+ readonly RecordNotificationNoticeInput: {
660
+ readonly record: {
661
+ readonly at: 'date-time';
662
+ };
663
+ readonly dueBy: 'date';
664
+ };
665
+ readonly NotificationConformanceVector: {
666
+ readonly context: {
667
+ readonly now: 'date-time';
668
+ };
669
+ readonly steps: {
670
+ readonly at: 'date-time';
671
+ readonly input: {
672
+ readonly record: {
673
+ readonly at: 'date-time';
674
+ };
675
+ readonly dueBy: 'date';
676
+ readonly fireAt: 'date-time';
677
+ };
678
+ readonly returns: {
679
+ readonly record: {
680
+ readonly at: 'date-time';
681
+ };
682
+ readonly dueBy: 'date';
683
+ readonly until: 'date-time';
684
+ };
685
+ };
686
+ readonly expect: {
687
+ readonly deliveries: {
688
+ readonly until: 'date-time';
689
+ };
690
+ readonly providerCalls: {
691
+ readonly expiresAt: 'date-time';
692
+ };
693
+ readonly local: {
694
+ readonly operations: {
695
+ readonly fireAt: 'date-time';
696
+ };
697
+ readonly pending: {
698
+ readonly fireAt: 'date-time';
699
+ };
700
+ };
701
+ readonly reminders: {
702
+ readonly fireAt: 'date-time';
703
+ };
704
+ readonly notices: {
705
+ readonly record: {
706
+ readonly at: 'date-time';
707
+ };
708
+ readonly dueBy: 'date';
709
+ };
710
+ };
711
+ };
712
+ };
713
+ export type NotificationNoticeKind = 'consent-result' | 'reconfirmation';
714
+ export interface NotificationNotice {
715
+ readonly kind: NotificationNoticeKind;
716
+ /** The advertising or night record the notice reports on, with its date. */
717
+ readonly record: {
718
+ readonly kind: 'advertising' | 'night';
719
+ readonly state: 'granted' | 'withdrawn';
720
+ readonly at: IsoDateTime;
721
+ readonly textVersion?: NonEmptyString;
722
+ };
723
+ /** The last Asia/Seoul calendar date the notice may be given, `YYYY-MM-DD`. */
724
+ readonly dueBy: string & tags.Format<'date'>;
725
+ /**
726
+ * The variant of `notices.copy` the notice is given in, set when it is
727
+ * listed, so the game shows the text the scheduler would send.
728
+ */
729
+ readonly variant: NotificationNoticeVariant;
730
+ }
731
+ /**
732
+ * What names one notice: its kind, the record it reports on (that record's
733
+ * kind and time), and its `dueBy`. A listed notice carries these fields, and
734
+ * `notices.recordShown` takes the listed notice as is or exactly them.
735
+ */
736
+ export interface NotificationNoticeIdentity {
737
+ readonly kind: NotificationNoticeKind;
738
+ readonly record: {
739
+ readonly kind: 'advertising' | 'night';
740
+ /** RFC 3339 with an explicit offset; compared as an instant. */
741
+ readonly at: IsoDateTime;
742
+ };
743
+ /** The notice's last Asia/Seoul calendar date, `YYYY-MM-DD`. */
744
+ readonly dueBy: string & tags.Format<'date'>;
745
+ }
746
+ /** Whether two notices are one: the same kind, record kind, record instant, and `dueBy`. */
747
+ export declare function isSameNotificationNotice(left: NotificationNoticeIdentity, right: NotificationNoticeIdentity): boolean;
748
+ /**
749
+ * The copy a notice is given in: its kind, its record's kind, and for a
750
+ * result notice what the record was, so the text can say what the player did
751
+ * (decree art. 62-2): a consent (`granted`), the withdrawal of a consent
752
+ * (`withdrawn`), or a refusal (`refused`), a `withdrawn` record whose latest
753
+ * earlier record of its kind is not a consent. A re-confirmation always
754
+ * reports on a consent (decree art. 62-3). A named union keeps the JSON
755
+ * Schema names readable.
756
+ */
757
+ export type NotificationNoticeVariant = 'consent-result.advertising.granted' | 'consent-result.advertising.refused' | 'consent-result.advertising.withdrawn' | 'consent-result.night.granted' | 'consent-result.night.refused' | 'consent-result.night.withdrawn' | 'reconfirmation.advertising' | 'reconfirmation.night';
758
+ /** Every `NotificationNoticeVariant`, in this order; a test keeps the two equal. */
759
+ export declare const notificationNoticeVariants: readonly ["consent-result.advertising.granted", "consent-result.advertising.refused", "consent-result.advertising.withdrawn", "consent-result.night.granted", "consent-result.night.refused", "consent-result.night.withdrawn", "reconfirmation.advertising", "reconfirmation.night"];
760
+ /**
761
+ * The variants `notices.copy` may leave out, each with the variant whose text
762
+ * stands in for it: a refusal is given in the withdrawal's text.
763
+ */
764
+ export declare const notificationNoticeVariantFallbacks: Readonly<Partial<Record<NotificationNoticeVariant, NotificationNoticeVariant>>>;
765
+ /**
766
+ * One locale of `notices.copy`: a title and a body for each notice variant.
767
+ * A refusal's text is optional; without it a refusal is given in the
768
+ * withdrawal's text (`notificationNoticeVariantFallbacks`).
769
+ */
770
+ export interface NotificationNoticeLocaleCopy {
771
+ readonly 'consent-result.advertising.granted': NotificationTopicCopy;
772
+ readonly 'consent-result.advertising.refused'?: NotificationTopicCopy;
773
+ readonly 'consent-result.advertising.withdrawn': NotificationTopicCopy;
774
+ readonly 'consent-result.night.granted': NotificationTopicCopy;
775
+ readonly 'consent-result.night.refused'?: NotificationTopicCopy;
776
+ readonly 'consent-result.night.withdrawn': NotificationTopicCopy;
777
+ readonly 'reconfirmation.advertising': NotificationTopicCopy;
778
+ readonly 'reconfirmation.night': NotificationTopicCopy;
779
+ }
780
+ /**
781
+ * The catalog's `notices.copy`: per locale, a title and a body for every
782
+ * notice variant, a refusal's optional. `{{consentDate}}`, the record's
783
+ * Asia/Seoul date, is the only placeholder the server fills, and each
784
+ * variant uses it. The text carries the rest of what the decree requires:
785
+ * the sender's name and, for a result notice (art. 62-2), the processing
786
+ * result, or, for a re-confirmation (art. 62-3(2)), how to keep or withdraw
787
+ * the consent.
788
+ */
789
+ export type NotificationNoticeCopy = Readonly<Record<string, NotificationNoticeLocaleCopy>>;
790
+ /**
791
+ * The catalog's `notices` block, which a backend and a vector take as one:
792
+ * the topic consent notices go out through, and their copy per variant.
793
+ */
794
+ export interface NotificationNoticeSettings {
795
+ /** A remote `service` topic that keeps only the delivery settings. */
796
+ readonly topic: NonEmptyString;
797
+ readonly copy: NotificationNoticeCopy;
798
+ }
799
+ /**
800
+ * The rules `notices.copy` follows wherever it is read (the catalog
801
+ * validator, a backend, and the vector loader), each issue under `path`,
802
+ * with locale and variant keys in brackets (`["ko-KR"]["reconfirmation.night"]`):
803
+ *
804
+ * - an object of locales that includes `defaultLocale`, matched without
805
+ * regard to case;
806
+ * - per locale, a title and a body for each notice variant, a refusal's
807
+ * optional (`notificationNoticeVariantFallbacks`), and no other key;
808
+ * - each title and body a string that is not blank, with no placeholder but
809
+ * `{{consentDate}}` and no brace outside one; and `{{consentDate}}` in the
810
+ * title or the body, because a notice states the date of the act it
811
+ * reports on.
812
+ */
813
+ export declare function notificationNoticeCopyIssues(copy: unknown, defaultLocale: string, path?: string): NotificationTopicIssue[];
814
+ /**
815
+ * One variant's copy per locale, shaped as a topic's `copy`, so the notice
816
+ * topic sends it with the lookup and default-locale fallback of any topic
817
+ * (`findNotificationCopy`). A locale without a refusal's text gives the
818
+ * withdrawal's (`notificationNoticeVariantFallbacks`); a locale without the
819
+ * variant, which a validated catalog never has, is left out rather than
820
+ * found empty.
821
+ */
822
+ export declare function notificationNoticeVariantCopy(copy: NotificationNoticeCopy, variant: NotificationNoticeVariant): Readonly<Record<string, NotificationTopicCopy>>;
823
+ /**
824
+ * Whether a provider can carry a consent notice: it sends free-form content
825
+ * and needs no `title` or `body` in a topic's mapping, which it would send in
826
+ * place of the notice's copy. The catalog validator keeps the notice topic
827
+ * off any other provider, and a scheduler pushes no notice through one.
828
+ */
829
+ export declare function notificationProviderCarriesNotices(entry: Pick<NotificationDeliveryProviderEntry, 'content' | 'mappingFields'>): boolean;
830
+ /**
831
+ * The notice topic as the gate renders one notice from it: its delivery
832
+ * settings, any `title` or `body` dropped from its mapping so nothing stands
833
+ * in for the copy, and the variant's `notices.copy` in place of a copy of its
834
+ * own (`notificationNoticeVariantCopy`).
835
+ */
836
+ export declare function notificationNoticeTopic(topic: NotificationTopicDeclaration, copy: NotificationNoticeCopy, variant: NotificationNoticeVariant): NotificationTopicDeclaration;
837
+ /** Result notice: 14 days after the record's Asia/Seoul date, counted from the next day. */
838
+ export declare function notificationConsentResultDueBy(at: string): string;
839
+ /**
840
+ * The `ordinal`-th re-confirmation notice of a consent: the day before the
841
+ * Asia/Seoul date `2 × ordinal` years after the consent date. A 29 February
842
+ * consent date counts as 28 February.
843
+ */
844
+ export declare function notificationReconfirmationDueBy(at: string, ordinal?: number): string;
845
+ /** Days before its `dueBy` from which a re-confirmation is listed. */
846
+ export declare const notificationReconfirmationListDays = 60;
847
+ /**
848
+ * The notices a player's advertising and night records owe at `at`, ordered
849
+ * by `dueBy` and then `kind`, each with the variant of `notices.copy` it is
850
+ * given in. `records` are in the order they were written. A notice that
851
+ * `given` names is not listed:
852
+ *
853
+ * - a `consent-result` notice for every record, which reports a refusal for
854
+ * a `withdrawn` record whose latest earlier record of its kind is not a
855
+ * consent;
856
+ * - per kind whose latest record is a consent, the first re-confirmation of
857
+ * that consent not given yet, from `notificationReconfirmationListDays`
858
+ * days before its `dueBy`. Each one follows the one before it, so silence
859
+ * keeps the series going, a new consent restarts it, and a withdrawal ends
860
+ * it.
861
+ *
862
+ * A listed notice stays listed after its `dueBy` until it is given.
863
+ */
864
+ export declare function listNotificationNotices(records: readonly NotificationConsentRecord[], given: readonly NotificationNoticeIdentity[], at: string): readonly NotificationNotice[];
865
+ /** Days before `dueBy` from which the scheduler sends a notice nobody recorded. */
866
+ export declare const notificationNoticeLeadDays = 7;
867
+ /**
868
+ * The first Asia/Seoul calendar date the scheduler sends an unrecorded notice:
869
+ * `notificationNoticeLeadDays` days before `dueBy`.
870
+ */
871
+ export declare function notificationNoticeSendFrom(dueBy: string): string;
872
+ /** Whether `date` is a calendar date `YYYY-MM-DD` that exists, such as a notice's `dueBy`. */
873
+ export declare function isNotificationCalendarDate(date: string): boolean;
874
+ /**
875
+ * Idempotency key of a scheduled notice:
876
+ * `notice:<player>:<kind>:<record kind>:<record at>:<dueBy>`, with the player
877
+ * percent-encoded, the record's time in the canonical form, and `dueBy` as
878
+ * `YYYY-MM-DD`. Each notice has its own key: every re-confirmation of one
879
+ * consent, and the notices of an advertising and a night record written at
880
+ * one instant.
881
+ */
882
+ export declare function createNotificationNoticeIdempotencyKey(input: NotificationNoticeIdentity & {
883
+ readonly player: string;
884
+ }): string;
885
+ export {};