@mpgd/game-services 0.28.0 → 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.
Files changed (44) hide show
  1. package/README.md +18 -3
  2. package/dist/account-deletion.d.ts +306 -0
  3. package/dist/account-deletion.js +491 -0
  4. package/dist/app-store-recovery.d.ts +6 -0
  5. package/dist/app-store-recovery.js +26 -16
  6. package/dist/app-store-server-notifications.d.ts +158 -0
  7. package/dist/app-store-server-notifications.js +270 -0
  8. package/dist/app-store-server.d.ts +8 -0
  9. package/dist/app-store-server.js +135 -37
  10. package/dist/app-store-verifier.d.ts +34 -0
  11. package/dist/app-store-verifier.js +161 -31
  12. package/dist/commerce-ledger.d.ts +55 -0
  13. package/dist/commerce-ledger.js +198 -0
  14. package/dist/commerce-reconciler.js +5 -0
  15. package/dist/evidence-verification.d.ts +19 -0
  16. package/dist/google-play-publisher-client.d.ts +2 -1
  17. package/dist/google-play-publisher-client.js +80 -0
  18. package/dist/google-play-purchase.d.ts +7 -0
  19. package/dist/google-play-purchase.js +32 -7
  20. package/dist/google-play-store-server.d.ts +160 -0
  21. package/dist/google-play-store-server.js +297 -0
  22. package/dist/guest-session.d.ts +112 -2
  23. package/dist/guest-session.js +196 -1
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.js +4 -0
  26. package/dist/notifications/delivery-provider.d.ts +7 -0
  27. package/dist/notifications/fcm.d.ts +30 -3
  28. package/dist/notifications/fcm.js +72 -13
  29. package/dist/notifications/service.js +33 -5
  30. package/dist/notifications/store-conformance.js +7 -1
  31. package/dist/notifications/store.d.ts +9 -0
  32. package/dist/notifications/store.js +41 -0
  33. package/dist/operation-results.d.ts +7 -0
  34. package/dist/purchase-sharing.d.ts +41 -0
  35. package/dist/purchase-sharing.js +37 -0
  36. package/dist/runtime.d.ts +6 -0
  37. package/dist/runtime.js +13 -0
  38. package/dist/server.d.ts +60 -1
  39. package/dist/server.js +536 -42
  40. package/dist/types.d.ts +30 -0
  41. package/dist/types.js +22 -0
  42. package/dist/verified-leaderboard.d.ts +13 -0
  43. package/dist/verified-leaderboard.js +31 -0
  44. 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 targets = target !== undefined && provider.targets.includes(target)
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: { kind: 'fcm-token', value: 'tok-1', environment: 'production' },
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
  }