@mpgd/game-services 0.25.1 → 0.27.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/client.d.ts +2 -11
- package/dist/client.js +2 -25
- package/dist/notifications/ait-functional-delivery.d.ts +25 -8
- package/dist/notifications/ait-functional-delivery.js +38 -17
- package/dist/notifications/client.d.ts +168 -0
- package/dist/notifications/client.js +432 -0
- package/dist/notifications/fcm.d.ts +17 -3
- package/dist/notifications/fcm.js +109 -7
- package/dist/notifications/service.d.ts +4 -1
- package/dist/notifications/service.js +221 -37
- package/dist/notifications/store-conformance.js +19 -0
- package/dist/notifications/store.d.ts +5 -1
- package/dist/notifications/store.js +1 -1
- package/dist/request-headers.d.ts +16 -0
- package/dist/request-headers.js +30 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +9 -0
- package/package.json +9 -5
package/dist/client.d.ts
CHANGED
|
@@ -7,8 +7,10 @@ import type { MonetizationOperationStore } from './monetization-recovery.js';
|
|
|
7
7
|
import type { GameServicesOperationClient } from './operations.js';
|
|
8
8
|
export type { GameServicesOperationClient, GameServicesPurchaseInput, GameServicesPurchaseResult, GameServicesRewardedAdInput, GameServicesRewardedAdResult, } from './operations.js';
|
|
9
9
|
import type { GameServicesContractClient } from './contract.js';
|
|
10
|
+
import { type GameServicesHeaderResolver } from './request-headers.js';
|
|
10
11
|
import type { ClaimAdRewardRequest, ClaimAdRewardResponse, GameServicesLedgerTarget, PurchaseLedgerStatusRequest, PurchaseLedgerStatusResponse, RecordLeaderboardScoreRequest, RecordLeaderboardScoreResponse, VerifyPurchaseRequest, VerifyPurchaseResponse } from './types.js';
|
|
11
12
|
export type { GameServicesOperationKind, GameServicesOperationLocation, GameServicesOperationOptions, GameServicesOperationProgress, GameServicesPurchaseProgress, GameServicesRewardedAdProgress, } from './operation-progress.js';
|
|
13
|
+
export { GameServicesHeaderResolutionError, resolveGameServicesRequestHeaders, type GameServicesHeaderResolver, } from './request-headers.js';
|
|
12
14
|
export interface PurchaseVerificationApi {
|
|
13
15
|
verifyPurchase(input: VerifyPurchaseRequest): Promise<VerifyPurchaseResponse>;
|
|
14
16
|
/**
|
|
@@ -75,7 +77,6 @@ export interface CreateGameServicesOrpcClientInput {
|
|
|
75
77
|
readonly headers?: Record<string, string>;
|
|
76
78
|
readonly getHeaders?: GameServicesHeaderResolver;
|
|
77
79
|
}
|
|
78
|
-
export type GameServicesHeaderResolver = () => Readonly<Record<string, string>> | Promise<Readonly<Record<string, string>>>;
|
|
79
80
|
export type GameServicesFetch = (url: string, init: {
|
|
80
81
|
readonly method: 'POST';
|
|
81
82
|
readonly headers: Record<string, string>;
|
|
@@ -96,16 +97,6 @@ export declare class GameServicesBackendTransportError extends Error {
|
|
|
96
97
|
readonly endpoint: GameServicesBackendEndpoint;
|
|
97
98
|
constructor(endpoint: GameServicesBackendEndpoint);
|
|
98
99
|
}
|
|
99
|
-
/** Credentials could not be resolved; no backend request was dispatched. */
|
|
100
|
-
export declare class GameServicesHeaderResolutionError extends Error {
|
|
101
|
-
constructor();
|
|
102
|
-
}
|
|
103
|
-
/** Resolve rotating credentials once and preserve static → dynamic → request precedence. */
|
|
104
|
-
export declare function resolveGameServicesRequestHeaders(input: {
|
|
105
|
-
readonly headers?: Readonly<Record<string, string>> | undefined;
|
|
106
|
-
readonly getHeaders?: GameServicesHeaderResolver | undefined;
|
|
107
|
-
readonly requestHeaders?: Readonly<Record<string, string>> | undefined;
|
|
108
|
-
}): Promise<Readonly<Record<string, string>>>;
|
|
109
100
|
export interface GameServicesClient extends GameServicesOperationClient {
|
|
110
101
|
submitLeaderboardScore(input: GameServicesLeaderboardInput): Promise<GameServicesLeaderboardResult>;
|
|
111
102
|
/**
|
package/dist/client.js
CHANGED
|
@@ -6,6 +6,8 @@ import { createCommerceReconciler, toGameServicesPurchaseResult, } from './comme
|
|
|
6
6
|
import { createDefaultClientRewardEvidenceRegistry } from './default-client-reward-evidence.js';
|
|
7
7
|
import { createInMemoryMonetizationOperationStore } from './monetization-operation-store.js';
|
|
8
8
|
import { observeGameServicesOperation } from './operation-progress.js';
|
|
9
|
+
import { GameServicesHeaderResolutionError, resolveGameServicesRequestHeaders, } from './request-headers.js';
|
|
10
|
+
export { GameServicesHeaderResolutionError, resolveGameServicesRequestHeaders, } from './request-headers.js';
|
|
9
11
|
export const gameServicesBackendEndpoints = {
|
|
10
12
|
verifyPurchase: '/game-services/purchases/verify',
|
|
11
13
|
getPurchaseLedgerStatus: '/game-services/purchases/ledger-status',
|
|
@@ -33,31 +35,6 @@ export class GameServicesBackendTransportError extends Error {
|
|
|
33
35
|
this.endpoint = endpoint;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
|
-
/** Credentials could not be resolved; no backend request was dispatched. */
|
|
37
|
-
export class GameServicesHeaderResolutionError extends Error {
|
|
38
|
-
constructor() {
|
|
39
|
-
super('Game Services request headers could not be resolved before dispatch.');
|
|
40
|
-
this.name = 'GameServicesHeaderResolutionError';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/** Resolve rotating credentials once and preserve static → dynamic → request precedence. */
|
|
44
|
-
export async function resolveGameServicesRequestHeaders(input) {
|
|
45
|
-
let dynamicHeaders;
|
|
46
|
-
try {
|
|
47
|
-
dynamicHeaders = await input.getHeaders?.();
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
// Resolver exceptions can contain credentials; expose a stable outcome.
|
|
51
|
-
throw new GameServicesHeaderResolutionError();
|
|
52
|
-
}
|
|
53
|
-
const merged = new Map();
|
|
54
|
-
for (const layer of [input.headers, dynamicHeaders, input.requestHeaders]) {
|
|
55
|
-
for (const [name, value] of Object.entries(layer ?? {})) {
|
|
56
|
-
merged.set(name.toLowerCase(), value);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return Object.fromEntries(merged);
|
|
60
|
-
}
|
|
61
38
|
export function createGameServicesClient(input) {
|
|
62
39
|
const now = input.now ?? (() => new Date().toISOString());
|
|
63
40
|
const requestNow = input.requestNow ?? now;
|
|
@@ -31,7 +31,12 @@ import type { AppsInTossMutualTlsFetcher } from '../apps-in-toss-partner-api.js'
|
|
|
31
31
|
* recipient who has not agreed to the template. `agreementErrorCodes` and
|
|
32
32
|
* `agreementFailReasons` let a deployment name the ones it observes, so
|
|
33
33
|
* such an answer is `rejected` with reason `agreement` and records the
|
|
34
|
-
* denial; without them it is reason `recipient`.
|
|
34
|
+
* denial; without them it is reason `recipient`. A deployment list cannot
|
|
35
|
+
* override 4095, 5004, 4034, or 4010: those documented codes are classified
|
|
36
|
+
* first, so a rate limit is not stored as an agreement denial.
|
|
37
|
+
* - `baseUrl` must be the partner API host (`apps-in-toss-api.toss.im`).
|
|
38
|
+
* The mTLS client certificate and the recipient key are not sent to any
|
|
39
|
+
* other host unless the caller sets `allowUnlistedHost` (tests only).
|
|
35
40
|
* - Limits: 15,000 calls per minute per app and 10 per minute per user, the
|
|
36
41
|
* reference registry entry's `rate` and `limits`, which the scheduler and
|
|
37
42
|
* the gate apply.
|
|
@@ -39,6 +44,12 @@ import type { AppsInTossMutualTlsFetcher } from '../apps-in-toss-partner-api.js'
|
|
|
39
44
|
export declare const aitFunctionalMessageProviderId: "apps-in-toss";
|
|
40
45
|
export declare const aitFunctionalMessagePath = "api-partner/v1/apps-in-toss/messenger/send-message";
|
|
41
46
|
export declare const defaultAitFunctionalMessageBaseUrl = "https://apps-in-toss-api.toss.im";
|
|
47
|
+
/**
|
|
48
|
+
* Hosts the partner certificate and the recipient key may be sent to.
|
|
49
|
+
* `docs/APPS_IN_TOSS_PRODUCTION_EVIDENCE.md` names this production base and
|
|
50
|
+
* no sandbox API host.
|
|
51
|
+
*/
|
|
52
|
+
export declare const aitFunctionalMessageAllowedHosts: readonly string[];
|
|
42
53
|
export declare const defaultAitFunctionalMessageTimeoutMs = 10000;
|
|
43
54
|
/** The longest `Retry-After` the provider passes on; the scheduler backs off from it. */
|
|
44
55
|
export declare const maximumAitFunctionalMessageRetryAfterSeconds = 86400;
|
|
@@ -112,6 +123,12 @@ export interface CreateAitFunctionalMessageDeliveryProviderOptions {
|
|
|
112
123
|
*/
|
|
113
124
|
readonly recipientKey: 'toss-user' | 'anonymous';
|
|
114
125
|
readonly baseUrl?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Send to a `baseUrl` whose host is not
|
|
128
|
+
* {@link aitFunctionalMessageAllowedHosts}. Tests only: a deployment that
|
|
129
|
+
* sets this can leak the partner certificate and the recipient key.
|
|
130
|
+
*/
|
|
131
|
+
readonly allowUnlistedHost?: boolean;
|
|
115
132
|
/** 1 to 60,000 ms, as the partner API client accepts. */
|
|
116
133
|
readonly timeoutMs?: number;
|
|
117
134
|
/** `FAIL` error codes that mean the recipient has not agreed to the template. */
|
|
@@ -135,13 +152,13 @@ export interface AitFunctionalMessageDeliveryProvider {
|
|
|
135
152
|
* With none sent, the per-channel `fail` lists decide: `rejected` with
|
|
136
153
|
* reason `agreement` when a failure's `reachedFailReason` is one of
|
|
137
154
|
* `agreementFailReasons`, otherwise `recipient`.
|
|
138
|
-
* - `FAIL` with a code
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
155
|
+
* - `FAIL` with a documented code is classified before any deployment list:
|
|
156
|
+
* 4095 is `rate-limited` with `retryAfterSeconds` (at most a day), and
|
|
157
|
+
* 5004, 4034, and 4010 are `rejected` with reason `configuration`. A
|
|
158
|
+
* deployment cannot store those as an agreement denial. Then a code in
|
|
159
|
+
* `agreementErrorCodes` or `recipientErrorCodes` is `rejected` with that
|
|
160
|
+
* reason, on any status except HTTP 429, 5xx, 408, and 425, which the
|
|
161
|
+
* status alone decides (see below). Any other code is `rejected` with reason `recipient` on HTTP 200
|
|
145
162
|
* and `configuration` when the HTTP status refused the request (400, 401,
|
|
146
163
|
* 403, 404, 405, 413, 415, 422): a `FAIL` envelope reports a failed
|
|
147
164
|
* request, and answering `ambiguous` would replay a send that fails the
|
|
@@ -30,7 +30,12 @@ import { notificationTemplateDataIssue, referenceNotificationProviders, } from '
|
|
|
30
30
|
* recipient who has not agreed to the template. `agreementErrorCodes` and
|
|
31
31
|
* `agreementFailReasons` let a deployment name the ones it observes, so
|
|
32
32
|
* such an answer is `rejected` with reason `agreement` and records the
|
|
33
|
-
* denial; without them it is reason `recipient`.
|
|
33
|
+
* denial; without them it is reason `recipient`. A deployment list cannot
|
|
34
|
+
* override 4095, 5004, 4034, or 4010: those documented codes are classified
|
|
35
|
+
* first, so a rate limit is not stored as an agreement denial.
|
|
36
|
+
* - `baseUrl` must be the partner API host (`apps-in-toss-api.toss.im`).
|
|
37
|
+
* The mTLS client certificate and the recipient key are not sent to any
|
|
38
|
+
* other host unless the caller sets `allowUnlistedHost` (tests only).
|
|
34
39
|
* - Limits: 15,000 calls per minute per app and 10 per minute per user, the
|
|
35
40
|
* reference registry entry's `rate` and `limits`, which the scheduler and
|
|
36
41
|
* the gate apply.
|
|
@@ -41,6 +46,12 @@ export const aitFunctionalMessagePath = 'api-partner/v1/apps-in-toss/messenger/s
|
|
|
41
46
|
// `defaultAppsInTossPartnerApiTimeoutMs` (a test keeps them equal), kept here
|
|
42
47
|
// so this module loads none of the partner client's code.
|
|
43
48
|
export const defaultAitFunctionalMessageBaseUrl = 'https://apps-in-toss-api.toss.im';
|
|
49
|
+
/**
|
|
50
|
+
* Hosts the partner certificate and the recipient key may be sent to.
|
|
51
|
+
* `docs/APPS_IN_TOSS_PRODUCTION_EVIDENCE.md` names this production base and
|
|
52
|
+
* no sandbox API host.
|
|
53
|
+
*/
|
|
54
|
+
export const aitFunctionalMessageAllowedHosts = Object.freeze(['apps-in-toss-api.toss.im']);
|
|
44
55
|
export const defaultAitFunctionalMessageTimeoutMs = 10_000;
|
|
45
56
|
/** The longest `Retry-After` the provider passes on; the scheduler backs off from it. */
|
|
46
57
|
export const maximumAitFunctionalMessageRetryAfterSeconds = 86_400;
|
|
@@ -77,13 +88,13 @@ const maximumTimeoutMs = 60_000;
|
|
|
77
88
|
* With none sent, the per-channel `fail` lists decide: `rejected` with
|
|
78
89
|
* reason `agreement` when a failure's `reachedFailReason` is one of
|
|
79
90
|
* `agreementFailReasons`, otherwise `recipient`.
|
|
80
|
-
* - `FAIL` with a code
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
91
|
+
* - `FAIL` with a documented code is classified before any deployment list:
|
|
92
|
+
* 4095 is `rate-limited` with `retryAfterSeconds` (at most a day), and
|
|
93
|
+
* 5004, 4034, and 4010 are `rejected` with reason `configuration`. A
|
|
94
|
+
* deployment cannot store those as an agreement denial. Then a code in
|
|
95
|
+
* `agreementErrorCodes` or `recipientErrorCodes` is `rejected` with that
|
|
96
|
+
* reason, on any status except HTTP 429, 5xx, 408, and 425, which the
|
|
97
|
+
* status alone decides (see below). Any other code is `rejected` with reason `recipient` on HTTP 200
|
|
87
98
|
* and `configuration` when the HTTP status refused the request (400, 401,
|
|
88
99
|
* 403, 404, 405, 413, 415, 422): a `FAIL` envelope reports a failed
|
|
89
100
|
* request, and answering `ambiguous` would replay a send that fails the
|
|
@@ -109,24 +120,29 @@ export function createAitFunctionalMessageDeliveryProvider(options) {
|
|
|
109
120
|
throw new TypeError("recipientKey must be 'toss-user' or 'anonymous'.");
|
|
110
121
|
}
|
|
111
122
|
const recipientHeader = options.recipientKey === 'anonymous' ? 'x-anon-key' : 'x-toss-user-key';
|
|
112
|
-
const url = new URL(aitFunctionalMessagePath, normalizeBaseUrl(options.baseUrl ?? defaultAitFunctionalMessageBaseUrl
|
|
123
|
+
const url = new URL(aitFunctionalMessagePath, normalizeBaseUrl(options.baseUrl ?? defaultAitFunctionalMessageBaseUrl, {
|
|
124
|
+
allowUnlistedHost: options.allowUnlistedHost === true,
|
|
125
|
+
})).href;
|
|
113
126
|
const timeoutMs = normalizeTimeout(options.timeoutMs);
|
|
114
127
|
const agreementErrorCodes = new Set(options.agreementErrorCodes ?? []);
|
|
115
128
|
const recipientErrorCodes = new Set(options.recipientErrorCodes ?? []);
|
|
116
129
|
const agreementFailReasons = new Set((options.agreementFailReasons ?? []).map((reason) => reason.trim()));
|
|
117
|
-
//
|
|
118
|
-
//
|
|
130
|
+
// Documented codes come first (RFC "Delivery providers -> Apps in Toss"),
|
|
131
|
+
// then the deployment's. A list must not turn 4095 into an agreement denial.
|
|
119
132
|
const classifyFailCode = (code, status) => {
|
|
133
|
+
if (code === rateLimitedErrorCode) {
|
|
134
|
+
return { status: 'rate-limited', providerCode: code };
|
|
135
|
+
}
|
|
136
|
+
if (configurationErrorCodes.has(code)) {
|
|
137
|
+
return { status: 'rejected', reason: 'configuration', providerCode: code };
|
|
138
|
+
}
|
|
120
139
|
if (agreementErrorCodes.has(code)) {
|
|
121
140
|
return { status: 'rejected', reason: 'agreement', providerCode: code };
|
|
122
141
|
}
|
|
123
142
|
if (recipientErrorCodes.has(code)) {
|
|
124
143
|
return { status: 'rejected', reason: 'recipient', providerCode: code };
|
|
125
144
|
}
|
|
126
|
-
if (
|
|
127
|
-
return { status: 'rate-limited', providerCode: code };
|
|
128
|
-
}
|
|
129
|
-
if (configurationErrorCodes.has(code) || refusedRequestStatuses.has(status)) {
|
|
145
|
+
if (refusedRequestStatuses.has(status)) {
|
|
130
146
|
return { status: 'rejected', reason: 'configuration', providerCode: code };
|
|
131
147
|
}
|
|
132
148
|
// An undocumented code on another status proves nothing either way.
|
|
@@ -421,8 +437,8 @@ function isCount(value) {
|
|
|
421
437
|
function isRecord(value) {
|
|
422
438
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
423
439
|
}
|
|
424
|
-
/** A credential-free HTTPS base URL ending in `/`,
|
|
425
|
-
function normalizeBaseUrl(input) {
|
|
440
|
+
/** A credential-free HTTPS base URL ending in `/`, on an allowlisted partner host. */
|
|
441
|
+
function normalizeBaseUrl(input, options) {
|
|
426
442
|
const url = new URL(input);
|
|
427
443
|
if (url.protocol !== 'https:'
|
|
428
444
|
|| url.username.length > 0
|
|
@@ -431,6 +447,11 @@ function normalizeBaseUrl(input) {
|
|
|
431
447
|
|| url.hash.length > 0) {
|
|
432
448
|
throw new TypeError('The Apps in Toss partner API baseUrl must be a credential-free HTTPS URL.');
|
|
433
449
|
}
|
|
450
|
+
const allowedHost = aitFunctionalMessageAllowedHosts.includes(url.hostname)
|
|
451
|
+
&& (url.port === '' || url.port === '443');
|
|
452
|
+
if (!allowedHost && !options.allowUnlistedHost) {
|
|
453
|
+
throw new TypeError(`The Apps in Toss partner API baseUrl host must be ${aitFunctionalMessageAllowedHosts.join(' or ')}.`);
|
|
454
|
+
}
|
|
434
455
|
url.pathname = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
|
|
435
456
|
return url.href;
|
|
436
457
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { NotificationError, type NotificationDeliveryProviderEntry, type NotificationErrorCode } from '@mpgd/platform/notifications';
|
|
2
|
+
import type { NotificationRuntimeBackend } from '@mpgd/platform/notifications-runtime';
|
|
3
|
+
import { type GameServicesHeaderResolver } from '../request-headers.js';
|
|
4
|
+
/**
|
|
5
|
+
* The game-services notifications client: the notifications runtime's
|
|
6
|
+
* backend (`NotificationRuntimeBackend` from
|
|
7
|
+
* `@mpgd/platform/notifications-runtime`) over the game-services Worker's
|
|
8
|
+
* session routes under `/game-services/notifications` (see
|
|
9
|
+
* docs/CLOUDFLARE_WORKER_DEPLOY.md, "Sessions" and "Client routes").
|
|
10
|
+
*
|
|
11
|
+
* The Worker learns the player, the session, and the installation from the
|
|
12
|
+
* `Authorization` header alone, through its `GAME_SERVICES_NOTIFICATIONS_AUTH`
|
|
13
|
+
* binding (the identity Worker's `authenticateNotificationSession`); no
|
|
14
|
+
* request carries a player or an installation. The client learns which
|
|
15
|
+
* session is current from the game's identity, which also supplies the
|
|
16
|
+
* header through the same resolver the game gives its commerce client.
|
|
17
|
+
*
|
|
18
|
+
* No platform SDK, DOM, or Phaser import: only `fetch`.
|
|
19
|
+
*/
|
|
20
|
+
/** The Worker's route prefix for notifications. */
|
|
21
|
+
export declare const gameServicesNotificationsRoutePrefix = "/game-services/notifications";
|
|
22
|
+
/** The server-backed session the game is signed in with. */
|
|
23
|
+
export interface GameServicesNotificationSession {
|
|
24
|
+
readonly playerId: string;
|
|
25
|
+
/**
|
|
26
|
+
* The stable session-family id: the `sessionId` the identity service
|
|
27
|
+
* answers to the Worker's `authenticateNotificationSession` for this
|
|
28
|
+
* session's header, the same across token refreshes. A new sign-in starts
|
|
29
|
+
* a new one. A per-token or rotating id is not it (a
|
|
30
|
+
* `GuestSessionCoordinator` view's `sessionId` may rotate on refresh); with
|
|
31
|
+
* one, each rotation counts as a new session and sends the registration
|
|
32
|
+
* again.
|
|
33
|
+
*/
|
|
34
|
+
readonly sessionId: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The game's identity as the notifications client needs it. The game wires
|
|
38
|
+
* it to its own session holder and identity service; the kit does not
|
|
39
|
+
* create sessions.
|
|
40
|
+
*/
|
|
41
|
+
export interface GameServicesNotificationIdentity {
|
|
42
|
+
/** The current session, or `undefined` while signed out. */
|
|
43
|
+
current(): GameServicesNotificationSession | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Calls `listener` when a session becomes current after the subscription:
|
|
46
|
+
* a sign-in, a session restored after the runtime started, or an account
|
|
47
|
+
* switch. Returns the unsubscribe. A repeat for the session already
|
|
48
|
+
* announced (a token refresh) is ignored, and so is one for the session
|
|
49
|
+
* current when the runtime subscribed (at its `start()`); the runtime's
|
|
50
|
+
* start registers again on its own when that session is not the one its
|
|
51
|
+
* last registration was acknowledged for.
|
|
52
|
+
*/
|
|
53
|
+
onAuthenticated(listener: (session: GameServicesNotificationSession) => void): () => void;
|
|
54
|
+
}
|
|
55
|
+
/** The part of the Fetch API the client uses; `globalThis.fetch` satisfies it. */
|
|
56
|
+
export type GameServicesNotificationsFetch = (url: string, init: {
|
|
57
|
+
readonly method: 'GET' | 'POST';
|
|
58
|
+
readonly headers: Record<string, string>;
|
|
59
|
+
readonly body?: string;
|
|
60
|
+
/** Aborted when the request times out. */
|
|
61
|
+
readonly signal?: AbortSignal;
|
|
62
|
+
}) => Promise<GameServicesNotificationsFetchResponse>;
|
|
63
|
+
export interface GameServicesNotificationsFetchResponse {
|
|
64
|
+
readonly status: number;
|
|
65
|
+
text(): Promise<string>;
|
|
66
|
+
}
|
|
67
|
+
export interface CreateGameServicesNotificationsClientInput {
|
|
68
|
+
/**
|
|
69
|
+
* The game-services Worker's origin, as the commerce client takes it
|
|
70
|
+
* (`VITE_MPGD_GAME_SERVICES_URL`). The client sends the session's
|
|
71
|
+
* credentials to it as given: in a production build, check it with
|
|
72
|
+
* `isPublicGameServicesUrl` from `@mpgd/game-services` first.
|
|
73
|
+
*/
|
|
74
|
+
readonly baseUrl: string;
|
|
75
|
+
readonly identity: GameServicesNotificationIdentity;
|
|
76
|
+
/** Defaults to `globalThis.fetch`. */
|
|
77
|
+
readonly fetch?: GameServicesNotificationsFetch;
|
|
78
|
+
/** Static headers sent with every request. */
|
|
79
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
80
|
+
/** Resolves the session's `Authorization` header at request time, as for the commerce client. */
|
|
81
|
+
readonly getHeaders?: GameServicesHeaderResolver;
|
|
82
|
+
/**
|
|
83
|
+
* The catalog target a device registration is for (`android`, `ios`): the
|
|
84
|
+
* Worker applies that target's topic mapping to the device and refuses a
|
|
85
|
+
* registration without it when one provider serves two targets.
|
|
86
|
+
*/
|
|
87
|
+
readonly target?: string;
|
|
88
|
+
/**
|
|
89
|
+
* How long one request may take, from resolving its headers to reading
|
|
90
|
+
* its answer, before it is aborted and answers
|
|
91
|
+
* `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE`; 15 seconds by default, at most
|
|
92
|
+
* 2147483647.
|
|
93
|
+
*/
|
|
94
|
+
readonly timeoutMs?: number;
|
|
95
|
+
}
|
|
96
|
+
/** The default `timeoutMs`: a request still unanswered after this is aborted. */
|
|
97
|
+
export declare const defaultGameServicesNotificationsTimeoutMs = 15000;
|
|
98
|
+
/** The registry the Worker publishes at `GET /providers`. */
|
|
99
|
+
export interface GameServicesNotificationProviders {
|
|
100
|
+
readonly protocolVersion: string;
|
|
101
|
+
readonly providers: readonly NotificationDeliveryProviderEntry[];
|
|
102
|
+
/** Each served target and the provider id that delivers to it. */
|
|
103
|
+
readonly targets: Readonly<Record<string, string>>;
|
|
104
|
+
}
|
|
105
|
+
export interface GameServicesNotificationsClient extends NotificationRuntimeBackend {
|
|
106
|
+
/**
|
|
107
|
+
* The registry the Worker publishes (`GET /providers`, no session needed):
|
|
108
|
+
* pass `providers.find((entry) => entry.provider === provider.provider)` as
|
|
109
|
+
* the runtime's `registryEntry`.
|
|
110
|
+
*/
|
|
111
|
+
getProviders(): Promise<GameServicesNotificationProviders>;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A failed notifications request, with the `NotificationError` code the
|
|
115
|
+
* runtime acts on and what the Worker answered.
|
|
116
|
+
*
|
|
117
|
+
* - 401: the session is not authenticated (signed out, token expired, or
|
|
118
|
+
* the session ended): `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE`, retryable
|
|
119
|
+
* once the game has a session again.
|
|
120
|
+
* - 404 on a session route: the Worker serves no notifications for sessions
|
|
121
|
+
* (no catalog, or no `GAME_SERVICES_NOTIFICATIONS_AUTH` binding), and 405:
|
|
122
|
+
* the Worker does not serve this client's routes;
|
|
123
|
+
* `NOTIFICATIONS_CONFIGURATION_REQUIRED`, not retryable.
|
|
124
|
+
* - 400 and 413: `NOTIFICATIONS_INVALID_INPUT` (or the code the Worker names).
|
|
125
|
+
* - 408, 425, 429, and 503: `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE`. The
|
|
126
|
+
* runtime retries at its next trigger (a read, a start, a sign-in), not
|
|
127
|
+
* on the Worker's `Retry-After`, which the client does not read.
|
|
128
|
+
* - Another status names the code the Worker answered when it is a
|
|
129
|
+
* notifications code (a Worker whose notifications configuration is
|
|
130
|
+
* broken answers 500 with `NOTIFICATIONS_CONFIGURATION_REQUIRED`, not
|
|
131
|
+
* retryable); otherwise a 5xx is `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE`
|
|
132
|
+
* and a 4xx `NOTIFICATIONS_CONFIGURATION_REQUIRED`.
|
|
133
|
+
* - A 2xx whose body is not JSON, or is empty (a captive portal or proxy
|
|
134
|
+
* answered): `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE` with that status. A
|
|
135
|
+
* 2xx with JSON outside the route's contract:
|
|
136
|
+
* `NOTIFICATIONS_CONFIGURATION_REQUIRED`.
|
|
137
|
+
* - `status` 0, `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE`, nothing sent: no
|
|
138
|
+
* signed-in player with a session id, the header resolver failed or did
|
|
139
|
+
* not settle within `timeoutMs`, or the player signed out or changed
|
|
140
|
+
* while the headers were resolved.
|
|
141
|
+
* - `status` 0, `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE`, no answer read: the
|
|
142
|
+
* request failed in transit, timed out (`timeoutMs`), or its body could
|
|
143
|
+
* not be read, so the Worker may have handled it. Retrying may repeat it: reminders and endpoints
|
|
144
|
+
* replace by key, but an advertising or night consent record is written
|
|
145
|
+
* again.
|
|
146
|
+
*/
|
|
147
|
+
export declare class GameServicesNotificationsRequestError extends NotificationError {
|
|
148
|
+
/** The HTTP status, or 0 when no answer arrived. */
|
|
149
|
+
readonly status: number;
|
|
150
|
+
/** The route, such as `/consent`. */
|
|
151
|
+
readonly route: string;
|
|
152
|
+
/** The `error` the Worker answered, such as `UNAUTHORIZED`. */
|
|
153
|
+
readonly serverCode: string | undefined;
|
|
154
|
+
constructor(input: {
|
|
155
|
+
readonly code: NotificationErrorCode;
|
|
156
|
+
readonly route: string;
|
|
157
|
+
readonly status: number;
|
|
158
|
+
readonly message: string;
|
|
159
|
+
readonly serverCode?: string;
|
|
160
|
+
readonly cause?: unknown;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Creates the client. Every session call needs a current session and sends
|
|
165
|
+
* the headers the resolver gives for it; without a session it throws
|
|
166
|
+
* `NOTIFICATIONS_TEMPORARILY_UNAVAILABLE` and sends nothing.
|
|
167
|
+
*/
|
|
168
|
+
export declare function createGameServicesNotificationsClient(input: CreateGameServicesNotificationsClientInput): GameServicesNotificationsClient;
|