@mpgd/game-services 0.28.1 → 0.29.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/README.md +18 -3
- package/dist/account-deletion.d.ts +306 -0
- package/dist/account-deletion.js +491 -0
- package/dist/app-store-recovery.d.ts +6 -0
- package/dist/app-store-recovery.js +26 -16
- package/dist/app-store-server-notifications.d.ts +158 -0
- package/dist/app-store-server-notifications.js +270 -0
- package/dist/app-store-server.d.ts +8 -0
- package/dist/app-store-server.js +135 -37
- package/dist/app-store-verifier.d.ts +34 -0
- package/dist/app-store-verifier.js +161 -31
- package/dist/commerce-ledger.d.ts +55 -0
- package/dist/commerce-ledger.js +198 -0
- package/dist/commerce-reconciler.js +5 -0
- package/dist/evidence-verification.d.ts +19 -0
- package/dist/google-play-publisher-client.d.ts +2 -1
- package/dist/google-play-publisher-client.js +80 -0
- package/dist/google-play-purchase.d.ts +7 -0
- package/dist/google-play-purchase.js +32 -7
- package/dist/google-play-store-server.d.ts +160 -0
- package/dist/google-play-store-server.js +297 -0
- package/dist/guest-session.d.ts +112 -2
- package/dist/guest-session.js +196 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/notifications/delivery-provider.d.ts +7 -0
- package/dist/notifications/fcm.d.ts +30 -3
- package/dist/notifications/fcm.js +72 -13
- package/dist/notifications/service.js +33 -5
- package/dist/notifications/store-conformance.js +7 -1
- package/dist/notifications/store.d.ts +9 -0
- package/dist/notifications/store.js +41 -0
- package/dist/operation-results.d.ts +7 -0
- package/dist/purchase-sharing.d.ts +41 -0
- package/dist/purchase-sharing.js +37 -0
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.js +13 -0
- package/dist/server.d.ts +60 -1
- package/dist/server.js +536 -42
- package/dist/types.d.ts +30 -0
- package/dist/types.js +22 -0
- package/dist/verified-leaderboard.d.ts +13 -0
- package/dist/verified-leaderboard.js +31 -0
- package/package.json +17 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertNotificationConsentRecord, assertNotificationDeployment, assertNotificationEndpointRegistration, assertRecordNotificationNoticeInput, createNotificationNoticeIdempotencyKey, createNotificationReminderIdempotencyKey, evaluateNotificationDelivery, evaluateNotificationReminderConsent, fillNotificationTemplate, isNotificationTimestamp, isSameNotificationNotice, isValidNotificationTimeZone, latestNotificationConsent, listNotificationNotices, normalizeNotificationTimestamp, NotificationError, notificationKeyIssue, notificationNoticeCopyIssues, notificationNoticeSendFrom, notificationNoticeTopic, notificationProviderCarriesNotices, notificationReservedKeyPrefix, notificationSendFingerprint, notificationTemplateDataIssue, toSeoulCalendarDate, } from '@mpgd/platform/notifications';
|
|
1
|
+
import { assertNotificationConsentRecord, assertNotificationDeployment, assertNotificationEndpointRegistration, assertRecordNotificationNoticeInput, createNotificationNoticeIdempotencyKey, createNotificationReminderIdempotencyKey, evaluateNotificationDelivery, evaluateNotificationReminderConsent, fillNotificationTemplate, isNotificationTimestamp, isSameNotificationNotice, isValidNotificationTimeZone, latestNotificationConsent, listNotificationNotices, normalizeNotificationTimestamp, NotificationError, notificationKeyIssue, notificationNoticeCopyIssues, notificationNoticeSendFrom, notificationNoticeTopic, notificationProviderCarriesNotices, notificationReservedKeyPrefix, notificationSendFingerprint, notificationShellRenderedAdvertisingTargets, notificationTemplateDataIssue, toSeoulCalendarDate, } from '@mpgd/platform/notifications';
|
|
2
2
|
import { fcmInstallationIdRefusal, notificationExpiredProviderCode, } from './delivery-provider.js';
|
|
3
3
|
import { notificationHistoryRetentionDays, } from './registry.js';
|
|
4
4
|
import { bestNotificationOutcome, formatNotificationInstant, isNotificationRowAbandoned, isNotificationRowDue, notificationAmbiguousReplayHours, notificationSendOutcome, } from './store.js';
|
|
@@ -179,12 +179,15 @@ export function createNotificationService(input) {
|
|
|
179
179
|
}
|
|
180
180
|
};
|
|
181
181
|
// Topics.
|
|
182
|
+
/**
|
|
183
|
+
* The catalog targets from which a recipient's topic is read: the target
|
|
184
|
+
* its registration named while the provider still serves it, else every
|
|
185
|
+
* target the provider serves, in order.
|
|
186
|
+
*/
|
|
187
|
+
const candidateTargetsOf = (provider, target) => target !== undefined && provider.targets.includes(target) ? [target] : provider.targets;
|
|
182
188
|
/** The topic as declared for the recipient's target, else for the provider's first target that declares it. */
|
|
183
189
|
const topicFor = (provider, target, topicId, origin) => {
|
|
184
|
-
const
|
|
185
|
-
? [target]
|
|
186
|
-
: provider.targets;
|
|
187
|
-
for (const candidate of targets) {
|
|
190
|
+
for (const candidate of candidateTargetsOf(provider, target)) {
|
|
188
191
|
const topic = catalog.topicsFor(candidate).find((declared) => declared.id === topicId);
|
|
189
192
|
if (topic !== undefined) {
|
|
190
193
|
// A notice says only its variant of notices.copy.
|
|
@@ -206,6 +209,24 @@ export function createNotificationService(input) {
|
|
|
206
209
|
: [];
|
|
207
210
|
const noticeListed = async (player, notice, at) => (await noticesOf(player, at)).some((listed) => isSameNotificationNotice(listed, notice));
|
|
208
211
|
// Recipients (gate step 2).
|
|
212
|
+
/**
|
|
213
|
+
* Whether an endpoint's topic may be read from a target whose pushes need
|
|
214
|
+
* the shell to render advertising (Android), as `topicFor` picks it: the
|
|
215
|
+
* named target while the provider serves it, else any target it serves.
|
|
216
|
+
*/
|
|
217
|
+
const mayNeedShellRendering = (provider, target) => candidateTargetsOf(provider, target)
|
|
218
|
+
.some((candidate) => notificationShellRenderedAdvertisingTargets.includes(candidate));
|
|
219
|
+
/**
|
|
220
|
+
* Whether a device endpoint shows the actions a push carries: where its
|
|
221
|
+
* topic may come from a target that needs the shell to render advertising
|
|
222
|
+
* (Android), only when its latest registration said the shell does;
|
|
223
|
+
* elsewhere (iOS's category) always. Gate step 1 refuses advertising
|
|
224
|
+
* under `kr` to an endpoint without `true`.
|
|
225
|
+
*/
|
|
226
|
+
const endpointActionsOf = (provider, endpoint) => ({
|
|
227
|
+
endpointShowsActions: !mayNeedShellRendering(provider, endpoint.target)
|
|
228
|
+
|| endpoint.address.rendersAdvertising === true,
|
|
229
|
+
});
|
|
209
230
|
const usable = (endpoint, at) => endpoint.status === 'valid'
|
|
210
231
|
&& at - Date.parse(endpoint.lastRegisteredAt) < endpointStaleMs
|
|
211
232
|
&& (endpoint.sessionExpiresAt === undefined || Date.parse(endpoint.sessionExpiresAt) > at);
|
|
@@ -409,6 +430,7 @@ export function createNotificationService(input) {
|
|
|
409
430
|
consent,
|
|
410
431
|
...(serverPermission === undefined ? {} : { serverPermission }),
|
|
411
432
|
...(recipient.endpoint === undefined ? {} : { endpointPermission: recipient.endpoint.permission }),
|
|
433
|
+
...(recipient.endpoint === undefined ? {} : endpointActionsOf(recipient.provider, recipient.endpoint)),
|
|
412
434
|
history,
|
|
413
435
|
at: formatNotificationInstant(at),
|
|
414
436
|
...(recipient.timeZone === undefined ? {} : { timeZone: recipient.timeZone }),
|
|
@@ -1980,6 +2002,11 @@ export function createNotificationService(input) {
|
|
|
1980
2002
|
if (request.target !== undefined && !provider.targets.includes(request.target)) {
|
|
1981
2003
|
throw invalidInput(`Provider ${provider.entry.provider} does not serve target ${String(request.target)}.`);
|
|
1982
2004
|
}
|
|
2005
|
+
// Only a target whose pushes need the shell to render advertising keeps
|
|
2006
|
+
// the claim, so the FCM provider never sends another device (iOS) a
|
|
2007
|
+
// data message it would not show.
|
|
2008
|
+
const rendersAdvertising = address.rendersAdvertising === true
|
|
2009
|
+
&& mayNeedShellRendering(provider, request.target);
|
|
1983
2010
|
await store.putEndpoint({
|
|
1984
2011
|
provider: provider.entry.provider,
|
|
1985
2012
|
installationId: current.installationId,
|
|
@@ -1988,6 +2015,7 @@ export function createNotificationService(input) {
|
|
|
1988
2015
|
kind: address.kind,
|
|
1989
2016
|
value: address.value,
|
|
1990
2017
|
...(address.environment === undefined ? {} : { environment: address.environment }),
|
|
2018
|
+
...(rendersAdvertising ? { rendersAdvertising: true } : {}),
|
|
1991
2019
|
},
|
|
1992
2020
|
permission: address.permission,
|
|
1993
2021
|
...(registration.locale === undefined ? {} : { locale: registration.locale }),
|
|
@@ -65,9 +65,15 @@ export async function runNotificationStoreConformance(createStore) {
|
|
|
65
65
|
timeZone: 'Asia/Seoul',
|
|
66
66
|
target: 'android',
|
|
67
67
|
sessionExpiresAt: at(86_400_000),
|
|
68
|
-
address: {
|
|
68
|
+
address: {
|
|
69
|
+
kind: 'fcm-token',
|
|
70
|
+
value: 'tok-1',
|
|
71
|
+
environment: 'production',
|
|
72
|
+
rendersAdvertising: true,
|
|
73
|
+
},
|
|
69
74
|
}));
|
|
70
75
|
equal(stored.status, 'valid', 'endpoints: a new endpoint is valid');
|
|
76
|
+
deepEqual((await store.getEndpoint({ provider: 'fcm', installationId: 'i-1' }))?.address, { kind: 'fcm-token', value: 'tok-1', environment: 'production', rendersAdvertising: true }, 'endpoints: the address round-trips, with the shell-rendered advertising claim');
|
|
71
77
|
deepEqual(await store.getEndpoint({ provider: 'fcm', installationId: 'i-1' }), stored, 'endpoints: read back as written');
|
|
72
78
|
// The same address on another installation moves there.
|
|
73
79
|
await store.putEndpoint(endpoint('i-2', 'tok-1'));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PlayerDataErasureInput, type PlayerDataErasureResult } from '../account-deletion.js';
|
|
1
2
|
import { type NotificationConsentRecord, type NotificationDeferralReason, type NotificationDeliveryOutcome, type NotificationNoticeIdentity, type NotificationNoticeVariant, type NotificationPermissionState, type NotificationTemplateData } from '@mpgd/platform/notifications';
|
|
2
3
|
import type { NotificationDeviceAddress } from './delivery-provider.js';
|
|
3
4
|
/**
|
|
@@ -417,6 +418,14 @@ export interface InMemoryNotificationStoreState {
|
|
|
417
418
|
export interface InMemoryNotificationStore extends NotificationStore {
|
|
418
419
|
/** A deep copy of every row, read synchronously. */
|
|
419
420
|
inspect(): InMemoryNotificationStoreState;
|
|
421
|
+
/**
|
|
422
|
+
* Account deletion (a `PlayerDataEraser`): removes every row of the player
|
|
423
|
+
* (consent records, endpoints, platform registrations, notices given,
|
|
424
|
+
* sends, delivery history, and schedule entries). Nothing is kept: no
|
|
425
|
+
* notification data is under a legal retention period. The Worker's D1
|
|
426
|
+
* deployment erases these tables in its own batch.
|
|
427
|
+
*/
|
|
428
|
+
erasePlayerData(input: PlayerDataErasureInput): Promise<PlayerDataErasureResult>;
|
|
420
429
|
}
|
|
421
430
|
/**
|
|
422
431
|
* A process-local store for tests, conformance runs, and local development.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertPlayerDataErasureInput, } from '../account-deletion.js';
|
|
1
2
|
import { normalizeNotificationTimestamp, } from '@mpgd/platform/notifications';
|
|
2
3
|
export function notificationRowOutcome(row) {
|
|
3
4
|
switch (row.state.status) {
|
|
@@ -163,6 +164,46 @@ export function createInMemoryNotificationStore() {
|
|
|
163
164
|
notice.dueBy,
|
|
164
165
|
]);
|
|
165
166
|
return {
|
|
167
|
+
async erasePlayerData(input) {
|
|
168
|
+
const player = assertPlayerDataErasureInput(input).playerId;
|
|
169
|
+
let removed = 0;
|
|
170
|
+
const keep = (rows, belongs) => {
|
|
171
|
+
let kept = 0;
|
|
172
|
+
for (const row of rows) {
|
|
173
|
+
if (belongs(row)) {
|
|
174
|
+
removed += 1;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
rows[kept] = row;
|
|
178
|
+
kept += 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
rows.length = kept;
|
|
182
|
+
};
|
|
183
|
+
keep(consent, (row) => row.player === player);
|
|
184
|
+
keep(endpoints, (endpoint) => endpoint.player === player);
|
|
185
|
+
keep(noticesGiven, (notice) => notice.player === player);
|
|
186
|
+
keep(history, (entry) => entry.player === player);
|
|
187
|
+
for (const [key, registration] of [...platformRegistrations]) {
|
|
188
|
+
if (registration.player === player) {
|
|
189
|
+
platformRegistrations.delete(key);
|
|
190
|
+
removed += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const [key, held] of [...sends]) {
|
|
194
|
+
if (held.send.request.player === player) {
|
|
195
|
+
sends.delete(key);
|
|
196
|
+
removed += 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const [key, held] of [...schedule]) {
|
|
200
|
+
if (held.entry.player === player) {
|
|
201
|
+
schedule.delete(key);
|
|
202
|
+
removed += 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { deleted: removed, retained: 0 };
|
|
206
|
+
},
|
|
166
207
|
inspect() {
|
|
167
208
|
return copy({
|
|
168
209
|
consent: consent.map((row) => ({ ...row.record, player: row.player })),
|
|
@@ -7,6 +7,13 @@ export interface VerifyPurchaseResponse {
|
|
|
7
7
|
readonly alreadyProcessed: boolean;
|
|
8
8
|
readonly reason?: string;
|
|
9
9
|
readonly finalization?: PurchaseGrantFinalization;
|
|
10
|
+
/**
|
|
11
|
+
* This player's grant comes from a verified restore of a purchase the
|
|
12
|
+
* store binds to another player (a reinstall or another device): the game
|
|
13
|
+
* can say the purchase was restored to this device. Present only on a
|
|
14
|
+
* verified response.
|
|
15
|
+
*/
|
|
16
|
+
readonly restoredAcrossPlayers?: true;
|
|
10
17
|
}
|
|
11
18
|
export type PurchaseGrantFinalizationAction = 'acknowledge' | 'consume' | 'finish' | 'complete';
|
|
12
19
|
export interface PurchaseGrantFinalization {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restore sharing (RFC "Restore sharing"): a guest lives in the device's
|
|
3
|
+
* secure store, so a reinstall, a new device, or a second device on the same
|
|
4
|
+
* store account is a new guest while the store keeps returning the
|
|
5
|
+
* purchase's non-consumable with the buyer's account binding (Google Play
|
|
6
|
+
* `obfuscatedAccountId`, StoreKit `appAccountToken`). Under the `share`
|
|
7
|
+
* policy the backend accepts the store's own verification of a restored
|
|
8
|
+
* non-consumable as the proof of ownership and grants the restoring player
|
|
9
|
+
* its own entitlement, linked to the purchase. Earlier holders keep theirs,
|
|
10
|
+
* up to `maxHolders` players per purchase; a refund or revocation of the
|
|
11
|
+
* purchase revokes every holder. Consumables and subscriptions are never
|
|
12
|
+
* shared.
|
|
13
|
+
*/
|
|
14
|
+
/** How the backend answers a restored non-consumable the store binds to another player. */
|
|
15
|
+
export interface PurchaseRestoreSharingPolicy {
|
|
16
|
+
/**
|
|
17
|
+
* `share` grants the restoring player its own entitlement; `refuse`
|
|
18
|
+
* answers the store's binding mismatch (`GOOGLE_PLAY_ACCOUNT_MISMATCH`,
|
|
19
|
+
* `APP_STORE_ACCOUNT_TOKEN_MISMATCH`) as before.
|
|
20
|
+
*/
|
|
21
|
+
readonly mode: 'share' | 'refuse';
|
|
22
|
+
/**
|
|
23
|
+
* At most this many players hold one purchase at the same time (the first
|
|
24
|
+
* grant included; a deleted player's retained record and reversed grants
|
|
25
|
+
* do not count). A restore past it answers `pending` with
|
|
26
|
+
* `PURCHASE_HOLDER_LIMIT_REACHED`; no holder is evicted. Defaults to 5.
|
|
27
|
+
*/
|
|
28
|
+
readonly maxHolders?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare const defaultPurchaseRestoreMaxHolders = 5;
|
|
31
|
+
/** The policy `createGameServicesBackend` uses when none is passed: restores across players are refused. */
|
|
32
|
+
export declare const refusePurchaseRestoreSharing: PurchaseRestoreSharingPolicy;
|
|
33
|
+
/** The reference stack's policy: share, up to the default number of holders. */
|
|
34
|
+
export declare const sharePurchaseRestores: PurchaseRestoreSharingPolicy;
|
|
35
|
+
/** The largest `maxHolders` a policy accepts; a purchase's holders are read one by one. */
|
|
36
|
+
export declare const maxPurchaseRestoreHolders = 100;
|
|
37
|
+
export interface ResolvedPurchaseRestoreSharingPolicy {
|
|
38
|
+
readonly mode: 'share' | 'refuse';
|
|
39
|
+
readonly maxHolders: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function resolvePurchaseRestoreSharingPolicy(input: PurchaseRestoreSharingPolicy | undefined): ResolvedPurchaseRestoreSharingPolicy;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restore sharing (RFC "Restore sharing"): a guest lives in the device's
|
|
3
|
+
* secure store, so a reinstall, a new device, or a second device on the same
|
|
4
|
+
* store account is a new guest while the store keeps returning the
|
|
5
|
+
* purchase's non-consumable with the buyer's account binding (Google Play
|
|
6
|
+
* `obfuscatedAccountId`, StoreKit `appAccountToken`). Under the `share`
|
|
7
|
+
* policy the backend accepts the store's own verification of a restored
|
|
8
|
+
* non-consumable as the proof of ownership and grants the restoring player
|
|
9
|
+
* its own entitlement, linked to the purchase. Earlier holders keep theirs,
|
|
10
|
+
* up to `maxHolders` players per purchase; a refund or revocation of the
|
|
11
|
+
* purchase revokes every holder. Consumables and subscriptions are never
|
|
12
|
+
* shared.
|
|
13
|
+
*/
|
|
14
|
+
export const defaultPurchaseRestoreMaxHolders = 5;
|
|
15
|
+
/** The policy `createGameServicesBackend` uses when none is passed: restores across players are refused. */
|
|
16
|
+
export const refusePurchaseRestoreSharing = Object.freeze({
|
|
17
|
+
mode: 'refuse',
|
|
18
|
+
});
|
|
19
|
+
/** The reference stack's policy: share, up to the default number of holders. */
|
|
20
|
+
export const sharePurchaseRestores = Object.freeze({
|
|
21
|
+
mode: 'share',
|
|
22
|
+
});
|
|
23
|
+
/** The largest `maxHolders` a policy accepts; a purchase's holders are read one by one. */
|
|
24
|
+
export const maxPurchaseRestoreHolders = 100;
|
|
25
|
+
export function resolvePurchaseRestoreSharingPolicy(input) {
|
|
26
|
+
const policy = input ?? refusePurchaseRestoreSharing;
|
|
27
|
+
if (policy.mode !== 'share' && policy.mode !== 'refuse') {
|
|
28
|
+
throw new Error('restoreSharing.mode must be share or refuse.');
|
|
29
|
+
}
|
|
30
|
+
const maxHolders = policy.maxHolders ?? defaultPurchaseRestoreMaxHolders;
|
|
31
|
+
if (!Number.isSafeInteger(maxHolders)
|
|
32
|
+
|| maxHolders < 1
|
|
33
|
+
|| maxHolders > maxPurchaseRestoreHolders) {
|
|
34
|
+
throw new Error(`restoreSharing.maxHolders must be an integer from 1 to ${String(maxPurchaseRestoreHolders)}.`);
|
|
35
|
+
}
|
|
36
|
+
return { mode: policy.mode, maxHolders };
|
|
37
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AccountDeletionClient } from './account-deletion.js';
|
|
1
2
|
import type { AnalyticsSink } from '@mpgd/analytics';
|
|
2
3
|
import type { PlatformGateway, PlatformTarget } from '@mpgd/platform';
|
|
3
4
|
import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesClient, type GameServicesHeaderResolver } from './client.js';
|
|
@@ -15,6 +16,11 @@ export interface GameServicesRuntime {
|
|
|
15
16
|
readonly target?: GameServicesLedgerTarget;
|
|
16
17
|
readonly client?: GameServicesClient;
|
|
17
18
|
readonly monetizationRecovery?: RecoverableMonetizationClient;
|
|
19
|
+
/**
|
|
20
|
+
* The signed-in player's account deletion (`POST /game-services/account/delete`)
|
|
21
|
+
* with the runtime's headers; present with an authoritative `baseUrl`.
|
|
22
|
+
*/
|
|
23
|
+
readonly accountDeletion?: AccountDeletionClient;
|
|
18
24
|
}
|
|
19
25
|
interface CreateGameServicesRuntimeCommonInput {
|
|
20
26
|
readonly gateway: PlatformGateway;
|
package/dist/runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAccountDeletionClient } from './account-deletion.js';
|
|
1
2
|
import { createGameServicesClient, createGameServicesFetchBackendTransport, createGameServicesHttpBackendApi, createGameServicesOrpcBackendApi, createGameServicesOrpcClient, resolveGameServicesRequestHeaders, } from './client.js';
|
|
2
3
|
import { createRecoverableMonetizationClient, } from './monetization-recovery.js';
|
|
3
4
|
export function createGameServicesRuntime(input) {
|
|
@@ -93,6 +94,18 @@ export function createGameServicesRuntime(input) {
|
|
|
93
94
|
target,
|
|
94
95
|
client: routedClient,
|
|
95
96
|
...(monetizationRecovery === undefined ? {} : { monetizationRecovery }),
|
|
97
|
+
...(baseUrl === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: {
|
|
100
|
+
accountDeletion: createAccountDeletionClient({
|
|
101
|
+
// The deletion route is plain HTTP next to the oRPC handler: an
|
|
102
|
+
// oRPC URL names the handler's `/rpc` prefix, which the route
|
|
103
|
+
// does not sit under.
|
|
104
|
+
baseUrl: input.transport === 'orpc' ? baseUrl.replace(/\/rpc\/?$/u, '') : baseUrl,
|
|
105
|
+
...(input.headers === undefined ? {} : { headers: input.headers }),
|
|
106
|
+
...(input.getHeaders === undefined ? {} : { getHeaders: input.getHeaders }),
|
|
107
|
+
}),
|
|
108
|
+
}),
|
|
96
109
|
};
|
|
97
110
|
}
|
|
98
111
|
function withRuntimeHeaders(transport, input) {
|
package/dist/server.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { type AnalyticsSink } from '@mpgd/analytics';
|
|
|
2
2
|
import { type AdPlacements, type ProductCatalog } from '@mpgd/catalog';
|
|
3
3
|
import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesBackendTransportRequest, type GameServicesBackendTransportResponse } from './client.js';
|
|
4
4
|
import { type CommerceEvidenceRegistry } from './commerce-evidence-registry.js';
|
|
5
|
+
import { type PlayerDataErasureInput, type PlayerDataErasureResult } from './account-deletion.js';
|
|
5
6
|
import { type GameServicesHealthResponse } from './contract.js';
|
|
6
7
|
import { type GameServicesEvidenceVerifier, type GameServicesPurchaseGrantFinalizer } from './evidence-verification.js';
|
|
7
|
-
import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerResult, type EntitlementLedgerRevocation, type EntitlementRevocationTransaction, type GameServicesAdRewardTarget, type GameServicesStoreTarget, type LeaderboardScoreTransaction, type ProductGrantTransaction, type PurchaseLedgerStatusRequest, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types.js';
|
|
8
|
+
import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerNote, type EntitlementLedgerResult, type EntitlementLedgerRevocation, type EntitlementRevocationTransaction, type GameServicesAdRewardTarget, type GameServicesStoreTarget, type LeaderboardScoreTransaction, type ProductGrantTransaction, type PurchaseLedgerStatusRequest, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types.js';
|
|
9
|
+
import { type PurchaseRestoreSharingPolicy } from './purchase-sharing.js';
|
|
8
10
|
export { gameServicesRequestLimits, GameServicesRequestValidationError } from './validation.js';
|
|
9
11
|
export { listActiveEntitlementTransactions, readPurchaseLedgerStatuses, revokeEntitlementGrant, RevocationKeyConflictError, type RevokeEntitlementGrantResult, } from './commerce-ledger.js';
|
|
10
12
|
type CorsHeaders = Record<string, string>;
|
|
@@ -33,6 +35,15 @@ export interface CreateGameServicesBackendInput {
|
|
|
33
35
|
*/
|
|
34
36
|
readonly commerceEvidenceRegistry?: CommerceEvidenceRegistry;
|
|
35
37
|
/**
|
|
38
|
+
* How a restored non-consumable that the store binds to another player is
|
|
39
|
+
* answered (RFC "Restore sharing"). `{ mode: 'share' }` grants the
|
|
40
|
+
* restoring player its own entitlement while earlier holders keep theirs,
|
|
41
|
+
* up to `maxHolders` (5) players per purchase. Defaults to
|
|
42
|
+
* `{ mode: 'refuse' }`, which keeps answering the binding mismatch; the
|
|
43
|
+
* reference Worker shares by default (`GAME_SERVICES_RESTORE_SHARING`).
|
|
44
|
+
*/
|
|
45
|
+
readonly restoreSharing?: PurchaseRestoreSharingPolicy;
|
|
46
|
+
/**
|
|
36
47
|
* Receives unexpected (non-validation) failures such as store errors before the
|
|
37
48
|
* transport answers with the generic `INTERNAL_ERROR` code. Defaults to `console.error`.
|
|
38
49
|
* The hook is diagnostics only: if it throws or returns a rejecting promise, the failure
|
|
@@ -87,11 +98,42 @@ export interface GameServicesStore {
|
|
|
87
98
|
findEntitlementRevocations?(input: {
|
|
88
99
|
readonly ledgerEntryIds: readonly string[];
|
|
89
100
|
}): Promise<readonly EntitlementRevocationTransaction[]>;
|
|
101
|
+
/**
|
|
102
|
+
* The grants of `source` whose `evidenceVerificationId` starts with
|
|
103
|
+
* `prefix` (at most `limit`), any order: restore sharing lists a purchase's
|
|
104
|
+
* shared grants with it in one indexed read (RFC "Restore sharing").
|
|
105
|
+
* Optional: without it the shares are read slot by slot.
|
|
106
|
+
*/
|
|
107
|
+
findEntitlementTransactionsByEvidencePrefix?(input: {
|
|
108
|
+
readonly source: EntitlementLedgerGrant['source'];
|
|
109
|
+
readonly prefix: string;
|
|
110
|
+
readonly limit: number;
|
|
111
|
+
}): Promise<readonly ProductGrantTransaction[]>;
|
|
112
|
+
/**
|
|
113
|
+
* Keeps a ledger note (a store event that grants nothing), once per
|
|
114
|
+
* `noteId`: a replay answers `alreadyRecorded: true` and keeps the first.
|
|
115
|
+
* Optional: a store without it still answers the event, with a log only.
|
|
116
|
+
*/
|
|
117
|
+
recordEntitlementNote?(input: EntitlementLedgerNote): Promise<{
|
|
118
|
+
readonly alreadyRecorded: boolean;
|
|
119
|
+
}>;
|
|
120
|
+
/** The notes about the given grants, oldest first. */
|
|
121
|
+
findEntitlementNotes?(input: {
|
|
122
|
+
readonly ledgerEntryIds: readonly string[];
|
|
123
|
+
}): Promise<readonly EntitlementLedgerNote[]>;
|
|
90
124
|
recordLeaderboardScore(input: RecordLeaderboardScoreRequest, options?: {
|
|
91
125
|
readonly recordedAt?: string;
|
|
92
126
|
}): Promise<RecordLeaderboardScoreResponse>;
|
|
93
127
|
getLeaderboardTransaction(ledgerEntryId: string): Promise<LeaderboardScoreTransaction | undefined>;
|
|
94
128
|
listLeaderboardTransactions(): Promise<readonly LeaderboardScoreTransaction[]>;
|
|
129
|
+
/**
|
|
130
|
+
* Account deletion (`./account-deletion`): deletes the player's grants,
|
|
131
|
+
* revocations, notes, and scores, except purchase grants and what reverses
|
|
132
|
+
* or annotates them, which are kept detached from the player (new ids,
|
|
133
|
+
* `retainedPlayerId`, no `playerId` payload key). Idempotent. Optional: the
|
|
134
|
+
* Worker's D1 deployment erases its tables in one batch instead.
|
|
135
|
+
*/
|
|
136
|
+
erasePlayerData?(input: PlayerDataErasureInput): Promise<PlayerDataErasureResult>;
|
|
95
137
|
}
|
|
96
138
|
export interface EntitlementPlatformEvidenceIdentity {
|
|
97
139
|
readonly source: Extract<EntitlementLedgerGrant['source'], 'purchase' | 'ad_reward'>;
|
|
@@ -122,6 +164,7 @@ export declare class InMemoryGameServicesStore implements GameServicesStore {
|
|
|
122
164
|
private readonly revocationsByKey;
|
|
123
165
|
private readonly revocationsByGrant;
|
|
124
166
|
private readonly revocationsByEvidence;
|
|
167
|
+
private readonly notesById;
|
|
125
168
|
private readonly leaderboardTransactionsByRun;
|
|
126
169
|
private readonly leaderboardTransactionsById;
|
|
127
170
|
recordEntitlementGrant(input: EntitlementLedgerGrant): Promise<EntitlementLedgerResult>;
|
|
@@ -134,6 +177,11 @@ export declare class InMemoryGameServicesStore implements GameServicesStore {
|
|
|
134
177
|
readonly source: EntitlementLedgerGrant['source'];
|
|
135
178
|
readonly evidenceVerificationId: string;
|
|
136
179
|
}): Promise<ProductGrantTransaction | undefined>;
|
|
180
|
+
findEntitlementTransactionsByEvidencePrefix(input: {
|
|
181
|
+
readonly source: EntitlementLedgerGrant['source'];
|
|
182
|
+
readonly prefix: string;
|
|
183
|
+
readonly limit: number;
|
|
184
|
+
}): Promise<readonly ProductGrantTransaction[]>;
|
|
137
185
|
findEntitlementTransactionByPlatformEvidence(input: EntitlementPlatformEvidenceIdentity): Promise<ProductGrantTransaction | undefined>;
|
|
138
186
|
getEntitlementTransaction(ledgerEntryId: string): Promise<ProductGrantTransaction | undefined>;
|
|
139
187
|
listEntitlementTransactions(): Promise<readonly ProductGrantTransaction[]>;
|
|
@@ -142,11 +190,22 @@ export declare class InMemoryGameServicesStore implements GameServicesStore {
|
|
|
142
190
|
findEntitlementRevocations(input: {
|
|
143
191
|
readonly ledgerEntryIds: readonly string[];
|
|
144
192
|
}): Promise<readonly EntitlementRevocationTransaction[]>;
|
|
193
|
+
recordEntitlementNote(input: EntitlementLedgerNote): Promise<{
|
|
194
|
+
readonly alreadyRecorded: boolean;
|
|
195
|
+
}>;
|
|
196
|
+
findEntitlementNotes(input: {
|
|
197
|
+
readonly ledgerEntryIds: readonly string[];
|
|
198
|
+
}): Promise<readonly EntitlementLedgerNote[]>;
|
|
145
199
|
recordLeaderboardScore(input: RecordLeaderboardScoreRequest, options?: {
|
|
146
200
|
readonly recordedAt?: string;
|
|
147
201
|
}): Promise<RecordLeaderboardScoreResponse>;
|
|
148
202
|
getLeaderboardTransaction(ledgerEntryId: string): Promise<LeaderboardScoreTransaction | undefined>;
|
|
149
203
|
listLeaderboardTransactions(): Promise<readonly LeaderboardScoreTransaction[]>;
|
|
204
|
+
erasePlayerData(input: PlayerDataErasureInput): Promise<PlayerDataErasureResult>;
|
|
205
|
+
private rememberEntitlementRevocation;
|
|
206
|
+
private forgetEntitlementRevocation;
|
|
207
|
+
private forgetEntitlementTransaction;
|
|
208
|
+
private rememberEntitlementTransaction;
|
|
150
209
|
private rankFor;
|
|
151
210
|
private sortedLeaderboardTransactions;
|
|
152
211
|
}
|