@mpgd/game-services 0.24.1 → 0.25.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.
- package/dist/notifications/ait-functional-delivery.d.ts +165 -0
- package/dist/notifications/ait-functional-delivery.js +445 -0
- package/dist/notifications/delivery-provider.d.ts +121 -0
- package/dist/notifications/delivery-provider.js +23 -0
- package/dist/notifications/fcm.d.ts +138 -0
- package/dist/notifications/fcm.js +602 -0
- package/dist/notifications/index.d.ts +12 -0
- package/dist/notifications/index.js +12 -0
- package/dist/notifications/registry.d.ts +86 -0
- package/dist/notifications/registry.js +131 -0
- package/dist/notifications/service.d.ts +222 -0
- package/dist/notifications/service.js +2609 -0
- package/dist/notifications/store-conformance.d.ts +11 -0
- package/dist/notifications/store-conformance.js +795 -0
- package/dist/notifications/store.d.ts +423 -0
- package/dist/notifications/store.js +443 -0
- package/dist/notifications/text.d.ts +2 -0
- package/dist/notifications/text.js +4 -0
- package/package.json +17 -5
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { NotificationDeliveryContent, NotificationPermissionState, NotificationProviderId, NotificationTemplateData, NotificationTopicDeclaration } from '@mpgd/platform/notifications';
|
|
2
|
+
/**
|
|
3
|
+
* The server-side remote delivery provider contract (notifications v2 RFC,
|
|
4
|
+
* "Delivery providers" and "Provider answers"). It is built only from
|
|
5
|
+
* `@mpgd/platform/notifications` types and literal unions, so a provider
|
|
6
|
+
* module (FCM here, Apps in Toss in its own module) fits it structurally
|
|
7
|
+
* without importing anything from this package.
|
|
8
|
+
*
|
|
9
|
+
* A provider sends exactly what the policy gate rendered: the service runs
|
|
10
|
+
* `evaluateNotificationDelivery` immediately before every call, and a provider
|
|
11
|
+
* never decides policy.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Why a Firebase installation id (FID, `fcm-installation`) is refused, at
|
|
15
|
+
* registration and at send. The kind is reserved: FCM is moving to FIDs, but
|
|
16
|
+
* HTTP v1 has no FID send target yet. It lives here, beside the address
|
|
17
|
+
* kinds, so the service can use it without importing the FCM provider.
|
|
18
|
+
*/
|
|
19
|
+
export declare const fcmInstallationIdRefusal = "Firebase installation ids (fcm-installation) are reserved and refused until FCM can send to them; register the FCM registration token (fcm-token).";
|
|
20
|
+
/** A device address as the endpoint registry stores it; FCM keeps the kinds apart. */
|
|
21
|
+
export interface NotificationDeviceAddress {
|
|
22
|
+
readonly kind: 'fcm-token' | 'fcm-installation';
|
|
23
|
+
readonly value: string;
|
|
24
|
+
readonly environment?: 'production' | 'sandbox';
|
|
25
|
+
}
|
|
26
|
+
/** Who one delivery goes to: a device endpoint, or a resolved platform identity. */
|
|
27
|
+
export type NotificationRemoteRecipient = {
|
|
28
|
+
readonly kind: 'device';
|
|
29
|
+
/** The game-services session's installation the endpoint belongs to. */
|
|
30
|
+
readonly installationId: string;
|
|
31
|
+
readonly address: NotificationDeviceAddress;
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'platform-user';
|
|
34
|
+
/**
|
|
35
|
+
* The provider's own recipient id, resolved by the backend from the
|
|
36
|
+
* player's authenticated identity (an Apps in Toss user key, a Reddit
|
|
37
|
+
* user id), never from a key the client sent.
|
|
38
|
+
*/
|
|
39
|
+
readonly id: string;
|
|
40
|
+
};
|
|
41
|
+
/** One recipient's delivery of one send, after the gate allowed it. */
|
|
42
|
+
export interface NotificationRemoteDeliveryRequest {
|
|
43
|
+
/**
|
|
44
|
+
* The ledger key of this delivery: the send key for a platform-user
|
|
45
|
+
* recipient, `<send key>:<encodeURIComponent(installationId)>` for a device
|
|
46
|
+
* endpoint. A replay of the same delivery carries the same key, so a
|
|
47
|
+
* provider that can deduplicate (a provider-side idempotency key or
|
|
48
|
+
* outbox) uses it.
|
|
49
|
+
*/
|
|
50
|
+
readonly idempotencyKey: string;
|
|
51
|
+
/** The send's own idempotency key. */
|
|
52
|
+
readonly sendKey: string;
|
|
53
|
+
readonly provider: NotificationProviderId;
|
|
54
|
+
readonly player: string;
|
|
55
|
+
/** The topic as declared for the recipient's target, with that target's mapping. */
|
|
56
|
+
readonly topic: NotificationTopicDeclaration;
|
|
57
|
+
readonly recipient: NotificationRemoteRecipient;
|
|
58
|
+
/** The content the gate rendered; under `kr` it already carries the advertising label, sender, opt-out line, and action. */
|
|
59
|
+
readonly content: NotificationDeliveryContent;
|
|
60
|
+
/** RFC 3339: the message must not be shown after this instant (FCM `android.ttl` and `apns-expiration`). */
|
|
61
|
+
readonly expiresAt?: string;
|
|
62
|
+
readonly templateData?: NotificationTemplateData;
|
|
63
|
+
/** Untrusted routing data the open carries; never a grant. */
|
|
64
|
+
readonly data?: Readonly<Record<string, string>>;
|
|
65
|
+
/** The send's post or comment link, else the mapping's (providers whose entry sets `link`). */
|
|
66
|
+
readonly link?: string;
|
|
67
|
+
/** The locale the content was resolved for. */
|
|
68
|
+
readonly locale?: string;
|
|
69
|
+
/** The time of the call, RFC 3339; a provider computes relative expiries from it. */
|
|
70
|
+
readonly at: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Every provider answer maps to one of five kinds (RFC "Provider answers"):
|
|
74
|
+
*
|
|
75
|
+
* - `accepted`: the provider took the message.
|
|
76
|
+
* - `unregistered`: the address is dead; the endpoint is invalidated.
|
|
77
|
+
* - `rate-limited`: the provider proves nothing was sent; the delivery waits
|
|
78
|
+
* `retryAfterSeconds` (60 when absent), then backs off.
|
|
79
|
+
* - `rejected`: the provider proves nothing was sent and the same send would
|
|
80
|
+
* fail again. `agreement` also records a provider denial.
|
|
81
|
+
* - `ambiguous`: the provider cannot say whether it sent (a timeout, a 5xx
|
|
82
|
+
* answer). The claim is kept until its lease ends.
|
|
83
|
+
*
|
|
84
|
+
* A provider that throws is treated as `ambiguous`.
|
|
85
|
+
*/
|
|
86
|
+
export type NotificationRemoteDeliveryAnswer = {
|
|
87
|
+
readonly status: 'accepted';
|
|
88
|
+
readonly providerMessageId?: string;
|
|
89
|
+
} | {
|
|
90
|
+
readonly status: 'unregistered';
|
|
91
|
+
} | {
|
|
92
|
+
readonly status: 'rate-limited';
|
|
93
|
+
readonly retryAfterSeconds?: number;
|
|
94
|
+
} | {
|
|
95
|
+
readonly status: 'rejected';
|
|
96
|
+
readonly reason: 'configuration' | 'recipient' | 'agreement';
|
|
97
|
+
/** The provider's own error code, for logs; never shown to players. */
|
|
98
|
+
readonly providerCode?: string;
|
|
99
|
+
} | {
|
|
100
|
+
readonly status: 'ambiguous';
|
|
101
|
+
readonly providerCode?: string;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The provider code of a `rejected` answer for a message that would arrive
|
|
105
|
+
* after its expiry: nothing was sent, and it is routine, not a setup error.
|
|
106
|
+
*/
|
|
107
|
+
export declare const notificationExpiredProviderCode = "MESSAGE_EXPIRED";
|
|
108
|
+
/** A server-side remote delivery provider: `{ id, send }`. */
|
|
109
|
+
export interface NotificationRemoteDeliveryProvider {
|
|
110
|
+
/** Equals the registry entry's `provider`. */
|
|
111
|
+
readonly id: NotificationProviderId;
|
|
112
|
+
send(request: NotificationRemoteDeliveryRequest): Promise<NotificationRemoteDeliveryAnswer>;
|
|
113
|
+
/**
|
|
114
|
+
* The host's permission at send time, for a provider whose registry entry
|
|
115
|
+
* is `serverReadable` (Devvit `isOptedIn`). The gate requires it there.
|
|
116
|
+
*/
|
|
117
|
+
readPermission?(input: {
|
|
118
|
+
readonly player: string;
|
|
119
|
+
readonly topic: string;
|
|
120
|
+
}): Promise<NotificationPermissionState>;
|
|
121
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server-side remote delivery provider contract (notifications v2 RFC,
|
|
3
|
+
* "Delivery providers" and "Provider answers"). It is built only from
|
|
4
|
+
* `@mpgd/platform/notifications` types and literal unions, so a provider
|
|
5
|
+
* module (FCM here, Apps in Toss in its own module) fits it structurally
|
|
6
|
+
* without importing anything from this package.
|
|
7
|
+
*
|
|
8
|
+
* A provider sends exactly what the policy gate rendered: the service runs
|
|
9
|
+
* `evaluateNotificationDelivery` immediately before every call, and a provider
|
|
10
|
+
* never decides policy.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Why a Firebase installation id (FID, `fcm-installation`) is refused, at
|
|
14
|
+
* registration and at send. The kind is reserved: FCM is moving to FIDs, but
|
|
15
|
+
* HTTP v1 has no FID send target yet. It lives here, beside the address
|
|
16
|
+
* kinds, so the service can use it without importing the FCM provider.
|
|
17
|
+
*/
|
|
18
|
+
export const fcmInstallationIdRefusal = 'Firebase installation ids (fcm-installation) are reserved and refused until FCM can send to them; register the FCM registration token (fcm-token).';
|
|
19
|
+
/**
|
|
20
|
+
* The provider code of a `rejected` answer for a message that would arrive
|
|
21
|
+
* after its expiry: nothing was sent, and it is routine, not a setup error.
|
|
22
|
+
*/
|
|
23
|
+
export const notificationExpiredProviderCode = 'MESSAGE_EXPIRED';
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { notificationExpiredProviderCode, type NotificationRemoteDeliveryAnswer, type NotificationRemoteDeliveryProvider, type NotificationRemoteDeliveryRequest } from './delivery-provider.js';
|
|
2
|
+
import type { NotificationProviderRegistration } from './registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* The FCM remote delivery provider (notifications v2 RFC, "Delivery
|
|
5
|
+
* providers"): HTTP v1 `messages:send` with an OAuth access token minted from
|
|
6
|
+
* a service-account JWT signed through WebCrypto. It needs only `fetch` and
|
|
7
|
+
* `crypto.subtle`, so it runs on Cloudflare Workers (no HTTP/2, no Node
|
|
8
|
+
* crypto). iOS devices register through Firebase Messaging, so FCM relays to
|
|
9
|
+
* APNs.
|
|
10
|
+
*
|
|
11
|
+
* Payload, as the Capacitor provider reads it (RFC "Capacitor"): the gate's
|
|
12
|
+
* title and body as the FCM `notification`; `data` carries the send's routing
|
|
13
|
+
* data plus `topic` (the topic id) and `route` (its root-relative route).
|
|
14
|
+
* Android posts to the mapping's `channelId`, else to `mpgd-service`,
|
|
15
|
+
* `mpgd-requested`, or `mpgd-advertising` by purpose. An advertising send
|
|
16
|
+
* names the iOS category `mpgd-advertising`, whose opt-out action the app
|
|
17
|
+
* registers; an Android FCM notification message cannot carry actions, so
|
|
18
|
+
* the shell renders advertising natively (C1). An expiry becomes
|
|
19
|
+
* `android.ttl` and the `apns-expiration` header.
|
|
20
|
+
*/
|
|
21
|
+
export declare const fcmMessagingScope = "https://www.googleapis.com/auth/firebase.messaging";
|
|
22
|
+
export declare const fcmTokenEndpoint = "https://oauth2.googleapis.com/token";
|
|
23
|
+
/**
|
|
24
|
+
* The FCM HTTP v1 API takes registration tokens. A Firebase installation id
|
|
25
|
+
* (FID, the `fcm-installation` kind) is reserved: the kit types it because
|
|
26
|
+
* FCM is moving to FIDs, but HTTP v1 has no FID send target, so this
|
|
27
|
+
* provider refuses one at registration and at send until FCM supports it.
|
|
28
|
+
*/
|
|
29
|
+
export declare const fcmAddressKinds: readonly ["fcm-token"];
|
|
30
|
+
/** Data keys this provider writes; a send's routing data may not use them. */
|
|
31
|
+
export declare const fcmReservedDataKeys: readonly ["topic", "route"];
|
|
32
|
+
/** The iOS notification category of advertising, which carries the opt-out action. */
|
|
33
|
+
export declare const fcmAdvertisingCategory = "mpgd-advertising";
|
|
34
|
+
/** The fields of a Google service-account key this provider reads. */
|
|
35
|
+
export interface FcmServiceAccount {
|
|
36
|
+
readonly projectId: string;
|
|
37
|
+
readonly clientEmail: string;
|
|
38
|
+
/** A PKCS #8 PEM `PRIVATE KEY` block, as a service-account key file holds it. */
|
|
39
|
+
readonly privateKey: string;
|
|
40
|
+
readonly privateKeyId?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Reads a service-account key file (its JSON text or the parsed object).
|
|
44
|
+
* Errors name the missing field and never include a value.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseFcmServiceAccount(input: unknown): FcmServiceAccount;
|
|
47
|
+
type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
|
|
48
|
+
export interface FcmAccessTokenSource {
|
|
49
|
+
/** A valid access token, minted or from the cache. */
|
|
50
|
+
get(): Promise<string>;
|
|
51
|
+
/** Drops `token` from the cache after FCM refused it. */
|
|
52
|
+
invalidate(token: string): void;
|
|
53
|
+
}
|
|
54
|
+
/** Why no access token could be minted, and whether trying again later can help. */
|
|
55
|
+
export declare class FcmAccessTokenError extends Error {
|
|
56
|
+
readonly name = "FcmAccessTokenError";
|
|
57
|
+
readonly retryable: boolean;
|
|
58
|
+
readonly code: string;
|
|
59
|
+
readonly retryAfterSeconds: number | undefined;
|
|
60
|
+
constructor(code: string, retryable: boolean, retryAfterSeconds?: number, options?: {
|
|
61
|
+
readonly cause?: unknown;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Mints and caches OAuth access tokens for the FCM scope. The token lives
|
|
66
|
+
* only in this object's memory: it is never logged, persisted, or put in an
|
|
67
|
+
* error. Concurrent callers share one request, a failed request is not
|
|
68
|
+
* cached, and a token is refreshed five minutes before it expires.
|
|
69
|
+
*/
|
|
70
|
+
export declare function createFcmAccessTokenSource(input: {
|
|
71
|
+
readonly serviceAccount: FcmServiceAccount;
|
|
72
|
+
readonly fetch?: FetchLike;
|
|
73
|
+
readonly now?: () => number;
|
|
74
|
+
readonly subtle?: SubtleCrypto;
|
|
75
|
+
readonly timeoutMs?: number;
|
|
76
|
+
}): FcmAccessTokenSource;
|
|
77
|
+
export interface CreateFcmDeliveryProviderInput {
|
|
78
|
+
readonly serviceAccount: FcmServiceAccount;
|
|
79
|
+
/** Defaults to the service account's project. */
|
|
80
|
+
readonly projectId?: string;
|
|
81
|
+
/** The provider id; `fcm` by default. */
|
|
82
|
+
readonly id?: string;
|
|
83
|
+
readonly fetch?: FetchLike;
|
|
84
|
+
readonly now?: () => number;
|
|
85
|
+
readonly subtle?: SubtleCrypto;
|
|
86
|
+
/** Per HTTP request; 10 seconds by default. */
|
|
87
|
+
readonly timeoutMs?: number;
|
|
88
|
+
/** A shared token source, for example one per isolate. */
|
|
89
|
+
readonly tokens?: FcmAccessTokenSource;
|
|
90
|
+
}
|
|
91
|
+
/** The FCM HTTP v1 message this provider sends, or why it cannot send one. */
|
|
92
|
+
export type FcmMessageBuild = {
|
|
93
|
+
readonly status: 'ready';
|
|
94
|
+
readonly message: Readonly<Record<string, unknown>>;
|
|
95
|
+
} | {
|
|
96
|
+
readonly status: 'invalid';
|
|
97
|
+
readonly reason: string;
|
|
98
|
+
/**
|
|
99
|
+
* `MESSAGE_EXPIRED` (`notificationExpiredProviderCode`) for a message
|
|
100
|
+
* that would arrive too late, `INSTALLATION_ID_RESERVED` for an FID
|
|
101
|
+
* recipient, else `INVALID_MESSAGE`.
|
|
102
|
+
*/
|
|
103
|
+
readonly code: 'INVALID_MESSAGE' | 'INSTALLATION_ID_RESERVED' | typeof notificationExpiredProviderCode;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Builds the HTTP v1 `message` for one delivery and checks it against FCM's
|
|
107
|
+
* rules before anything is sent: a registration token address, a title and
|
|
108
|
+
* a body, string routing data without FCM's or this provider's reserved
|
|
109
|
+
* keys, an expiry at least a second away, and a payload within FCM's limit
|
|
110
|
+
* with room for what APNs adds. A message that passes is "known valid", so an
|
|
111
|
+
* `INVALID_ARGUMENT` answer that names the token means a dead token.
|
|
112
|
+
*/
|
|
113
|
+
export declare function buildFcmMessage(request: NotificationRemoteDeliveryRequest): FcmMessageBuild;
|
|
114
|
+
/**
|
|
115
|
+
* Maps an FCM error answer to the provider answer kinds (RFC "Delivery
|
|
116
|
+
* providers"): `UNREGISTERED` is `unregistered`, and so is
|
|
117
|
+
* `INVALID_ARGUMENT` for a message `buildFcmMessage` passed when the answer
|
|
118
|
+
* names the token and no other field; `QUOTA_EXCEEDED` is
|
|
119
|
+
* `rate-limited`; `UNAVAILABLE` and `INTERNAL` are `ambiguous`;
|
|
120
|
+
* `SENDER_ID_MISMATCH`, `THIRD_PARTY_AUTH_ERROR`, and every other refusal are
|
|
121
|
+
* `rejected` with reason `configuration`. An answer without an FCM error code
|
|
122
|
+
* falls back on its HTTP status: 5xx is `ambiguous`, 429 `rate-limited`.
|
|
123
|
+
*/
|
|
124
|
+
export declare function classifyFcmError(input: {
|
|
125
|
+
readonly status: number;
|
|
126
|
+
readonly body: unknown;
|
|
127
|
+
readonly retryAfterSeconds?: number;
|
|
128
|
+
}): NotificationRemoteDeliveryAnswer;
|
|
129
|
+
/** The FCM provider: `{ id, send }` over HTTP v1. */
|
|
130
|
+
export declare function createFcmDeliveryProvider(input: CreateFcmDeliveryProviderInput): NotificationRemoteDeliveryProvider;
|
|
131
|
+
/**
|
|
132
|
+
* The registry registration for FCM: the reference `fcm` entry, the
|
|
133
|
+
* `android` and `ios` targets, and registration-token addresses only.
|
|
134
|
+
*/
|
|
135
|
+
export declare function createFcmProviderRegistration(input: CreateFcmDeliveryProviderInput & {
|
|
136
|
+
readonly targets?: readonly string[];
|
|
137
|
+
}): NotificationProviderRegistration;
|
|
138
|
+
export {};
|