@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.
- package/dist/capability-conformance.d.ts +6 -5
- package/dist/capability-conformance.js +178 -42
- package/dist/commerce-capabilities.d.ts +0 -6
- package/dist/commerce-capabilities.js +2 -8
- package/dist/commerce-contract.d.ts +12 -2
- package/dist/commerce.d.ts +8 -17
- package/dist/commerce.js +10 -94
- package/dist/index.d.ts +80 -43
- package/dist/index.js +3 -36
- package/dist/notifications-conformance.d.ts +529 -0
- package/dist/notifications-conformance.js +3537 -0
- package/dist/notifications-contract.d.ts +204 -0
- package/dist/notifications-contract.js +1 -0
- package/dist/notifications-policy.d.ts +885 -0
- package/dist/notifications-policy.js +1658 -0
- package/dist/notifications-vectors.d.ts +427 -0
- package/dist/notifications-vectors.js +4949 -0
- package/dist/notifications.d.ts +265 -0
- package/dist/notifications.js +4958 -0
- package/dist/route-vectors.d.ts +23 -0
- package/dist/route-vectors.js +96 -0
- package/dist/routes.d.ts +60 -0
- package/dist/routes.js +209 -0
- package/dist/share-link-conformance.d.ts +47 -0
- package/dist/share-link-conformance.js +100 -0
- package/package.json +28 -4
|
@@ -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 {};
|