@mpgd/game-services 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -26,7 +26,15 @@ export declare class NotificationDeliveryNotSentError extends Error {
|
|
|
26
26
|
readonly name = "NotificationDeliveryNotSentError";
|
|
27
27
|
}
|
|
28
28
|
export interface NotificationDeepLinkPolicy {
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Exact HTTP(S) origins permitted for absolute links. Root-relative links are
|
|
31
|
+
* allowed when they stay a path on the game origin, so one that names a host
|
|
32
|
+
* after a backslash (`/\host`) is rejected. Either kind is rejected when its
|
|
33
|
+
* path would begin with an empty segment after dot segments collapse
|
|
34
|
+
* (`/.//host`), or after it is percent-decoded once and parsed again
|
|
35
|
+
* (`/%2F/host`, `/%5Chost`, `/%09/host`, `/.%2F/host`), or when it is longer
|
|
36
|
+
* than 2,048 characters after normalization.
|
|
37
|
+
*/
|
|
30
38
|
readonly allowedOrigins?: readonly string[];
|
|
31
39
|
}
|
|
32
40
|
export interface NotificationDeliveryClaimOptions extends NotificationDeepLinkPolicy {
|
|
@@ -20,6 +20,8 @@ const notificationTopics = new Set([
|
|
|
20
20
|
const defaultClaimLeaseMs = 5 * 60 * 1_000;
|
|
21
21
|
const maxCompletionAttempts = 3;
|
|
22
22
|
const maxNotificationTemplateDataEntries = 128;
|
|
23
|
+
/** Caps identifiers, including a deep link after normalization so it normalizes to itself. */
|
|
24
|
+
const maxIdentifierLength = 2_048;
|
|
23
25
|
/** Throw only when the provider can prove that no external send was attempted. */
|
|
24
26
|
export class NotificationDeliveryNotSentError extends Error {
|
|
25
27
|
name = 'NotificationDeliveryNotSentError';
|
|
@@ -337,11 +339,23 @@ function normalizeDeepLink(input, allowedOrigins) {
|
|
|
337
339
|
}
|
|
338
340
|
if (deepLink.startsWith('/') && !deepLink.startsWith('//')) {
|
|
339
341
|
const baseUrl = new URL('https://mpgd.invalid');
|
|
340
|
-
|
|
341
|
-
|
|
342
|
+
let parsed;
|
|
343
|
+
// A backslash after the leading slash parses as an authority (`/\host`),
|
|
344
|
+
// so the link names a host, not a path on the game origin.
|
|
345
|
+
if (!deepLink.startsWith('/\\')) {
|
|
346
|
+
try {
|
|
347
|
+
parsed = new URL(deepLink, baseUrl);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// Rejected below.
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (parsed === undefined
|
|
354
|
+
|| parsed.origin !== baseUrl.origin
|
|
355
|
+
|| hasNetworkPathPrefix(parsed.pathname)) {
|
|
342
356
|
throw new Error('deepLink must stay on the configured game origin.');
|
|
343
357
|
}
|
|
344
|
-
return `${parsed.pathname}${parsed.search}${parsed.hash}
|
|
358
|
+
return requireNormalizedDeepLinkLength(`${parsed.pathname}${parsed.search}${parsed.hash}`);
|
|
345
359
|
}
|
|
346
360
|
let parsed;
|
|
347
361
|
try {
|
|
@@ -359,7 +373,33 @@ function normalizeDeepLink(input, allowedOrigins) {
|
|
|
359
373
|
if (!allowedOrigins.has(parsed.origin)) {
|
|
360
374
|
throw new Error('deepLink origin is not allowed.');
|
|
361
375
|
}
|
|
362
|
-
|
|
376
|
+
if (hasNetworkPathPrefix(parsed.pathname)) {
|
|
377
|
+
throw new Error('deepLink path must not begin with an empty segment, even after percent-decoding.');
|
|
378
|
+
}
|
|
379
|
+
return requireNormalizedDeepLinkLength(parsed.toString());
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Whether a normalized path could be read as a network-path reference to
|
|
383
|
+
* another host. Dot segments (also percent-encoded) can collapse `/.//host`
|
|
384
|
+
* into `//host`. A consumer that percent-decodes the path once and parses it
|
|
385
|
+
* again can reach the same shape from `/%2F/host`, `/%5Chost`, `/%09/host`,
|
|
386
|
+
* or `/.%2F/host`: URL parsing drops tabs and newlines, reads a backslash as
|
|
387
|
+
* a slash, and removes dot segments, so the decoded path is checked after
|
|
388
|
+
* the same steps.
|
|
389
|
+
*/
|
|
390
|
+
function hasNetworkPathPrefix(pathname) {
|
|
391
|
+
const decoded = pathname
|
|
392
|
+
.replace(/%([0-7][0-9a-f])/giu, (_escape, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
|
|
393
|
+
.replace(/[\t\n\r]/gu, '');
|
|
394
|
+
return /^[/\\]{2}/u.test(decoded)
|
|
395
|
+
|| new URL(decoded, 'https://mpgd.invalid').pathname.startsWith('//');
|
|
396
|
+
}
|
|
397
|
+
/** Normalization can grow a link; keep it within the input cap so it normalizes to itself. */
|
|
398
|
+
function requireNormalizedDeepLinkLength(deepLink) {
|
|
399
|
+
if (deepLink.length > maxIdentifierLength) {
|
|
400
|
+
throw new Error(`deepLink must contain at most ${String(maxIdentifierLength)} characters after normalization.`);
|
|
401
|
+
}
|
|
402
|
+
return deepLink;
|
|
363
403
|
}
|
|
364
404
|
function normalizeAllowedDeepLinkOrigins(input) {
|
|
365
405
|
const origins = new Set();
|
|
@@ -427,7 +467,7 @@ function assertRecord(input, label) {
|
|
|
427
467
|
function normalizeIdentifier(input, label) {
|
|
428
468
|
if (typeof input !== 'string'
|
|
429
469
|
|| input.length === 0
|
|
430
|
-
|| input.length >
|
|
470
|
+
|| input.length > maxIdentifierLength
|
|
431
471
|
|| input.trim() !== input) {
|
|
432
472
|
throw new Error(`${label} must be a non-empty, trimmed string.`);
|
|
433
473
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { PlatformGateway, PlatformTarget } from '@mpgd/platform';
|
|
|
3
3
|
import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesClient, type GameServicesHeaderResolver } from './client.js';
|
|
4
4
|
import type { GameServicesLedgerTarget } from './types.js';
|
|
5
5
|
import type { ClientRewardEvidenceRegistry } from './client-reward-evidence.js';
|
|
6
|
+
import type { CommerceReconcilerProduct } from './commerce-reconciler.js';
|
|
6
7
|
import { type MonetizationOperationStore, type RecoverableMonetizationClient } from './monetization-recovery.js';
|
|
7
8
|
export type GameServicesAuthorityMode = 'production' | 'non-production';
|
|
8
9
|
export type GameServicesRuntimeMode = 'disabled' | 'local' | 'http' | 'orpc';
|
|
@@ -34,6 +35,12 @@ interface CreateGameServicesRuntimeCommonInput {
|
|
|
34
35
|
/** Durable, compare-and-swap journal for purchase and rewarded-ad recovery. */
|
|
35
36
|
readonly operationStore?: MonetizationOperationStore;
|
|
36
37
|
readonly rewardEvidenceRegistry?: ClientRewardEvidenceRegistry;
|
|
38
|
+
/**
|
|
39
|
+
* Catalog kinds and store SKUs the commerce v2 reconciler resolves without a
|
|
40
|
+
* store read (`CommerceReconcilerProduct`), for example derived from the
|
|
41
|
+
* product catalog's `platformProductIds` for the gateway target.
|
|
42
|
+
*/
|
|
43
|
+
readonly commerceProducts?: readonly CommerceReconcilerProduct[];
|
|
37
44
|
}
|
|
38
45
|
export type CreateGameServicesRuntimeInput = CreateGameServicesRuntimeCommonInput & ({
|
|
39
46
|
readonly transport?: 'http';
|
package/dist/runtime.js
CHANGED
|
@@ -62,6 +62,7 @@ export function createGameServicesRuntime(input) {
|
|
|
62
62
|
: { analyticsSessionId: input.analyticsSessionId }),
|
|
63
63
|
...(input.now === undefined ? {} : { now: input.now }),
|
|
64
64
|
...(input.rewardEvidenceRegistry === undefined ? {} : { rewardEvidenceRegistry: input.rewardEvidenceRegistry }),
|
|
65
|
+
...(input.commerceProducts === undefined ? {} : { commerceProducts: input.commerceProducts }),
|
|
65
66
|
};
|
|
66
67
|
const monetizationRecovery = input.operationStore === undefined
|
|
67
68
|
? undefined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/game-services",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -202,9 +202,9 @@
|
|
|
202
202
|
"@orpc/client": "2.0.0-beta.14",
|
|
203
203
|
"@orpc/contract": "2.0.0-beta.14",
|
|
204
204
|
"@orpc/server": "2.0.0-beta.14",
|
|
205
|
+
"@mpgd/platform": "0.22.0",
|
|
205
206
|
"@mpgd/analytics": "0.4.8",
|
|
206
|
-
"@mpgd/catalog": "0.9.3"
|
|
207
|
-
"@mpgd/platform": "0.22.0"
|
|
207
|
+
"@mpgd/catalog": "0.9.3"
|
|
208
208
|
},
|
|
209
209
|
"peerDependencies": {
|
|
210
210
|
"@apple/app-store-server-library": "^3.1.0"
|