@mpgd/game-services 0.21.1 → 0.22.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/types.d.ts CHANGED
@@ -1,9 +1,24 @@
1
1
  import type { ProductGrant } from '@mpgd/catalog';
2
2
  import type { LeaderboardScoreInput, LogicalAdPlacementId, LogicalProductId, PlatformEvidenceEnvelope, PlatformTarget } from '@mpgd/platform';
3
- import type { ClaimAdRewardResponse, PurchaseGrantFinalization, VerifyPurchaseResponse } from './operation-results.js';
4
- export type { ClaimAdRewardResponse, PurchaseGrantFinalization, PurchaseGrantFinalizationAction, VerifyPurchaseResponse, } from './operation-results.js';
3
+ import type { StoreId } from '@mpgd/platform/commerce';
4
+ import type { ClaimAdRewardResponse, PurchaseGrantFinalization, PurchaseLedgerStatusRequest, PurchaseLedgerStatusResponse, VerifyPurchaseResponse } from './operation-results.js';
5
+ export type { ClaimAdRewardResponse, PurchaseGrantFinalization, PurchaseGrantFinalizationAction, PurchaseLedgerStatus, PurchaseLedgerStatusRequest, PurchaseLedgerStatusResponse, VerifyPurchaseResponse, } from './operation-results.js';
5
6
  export type { PlatformEvidenceEnvelope } from '@mpgd/platform';
6
- export type GameServicesStoreTarget = Extract<PlatformTarget, 'microsoft-store' | 'android' | 'ios' | 'ait' | 'oneplay'>;
7
+ /**
8
+ * The closed v1 purchase target list. The deployment target of a v1 verify
9
+ * request names its store implicitly (`android` is Google Play, `ios` the App
10
+ * Store, `ait` Apps in Toss); `resolveLegacyCommerceStoreId` is that mapping.
11
+ *
12
+ * @deprecated `VerifyPurchaseRequest.target` is open since commerce v2 C2
13
+ * (`GameServicesStoreTarget` is `StoreId`); C4 removes this alias.
14
+ */
15
+ export type GameServicesLegacyStoreTarget = Extract<PlatformTarget, 'microsoft-store' | 'android' | 'ios' | 'ait' | 'oneplay'>;
16
+ /**
17
+ * An open store or deployment target identifier (RFC "Open store ids"). The
18
+ * request validator bounds its shape; the backend's commerce evidence
19
+ * registry decides which stores it serves.
20
+ */
21
+ export type GameServicesStoreTarget = StoreId;
7
22
  export type GameServicesAdRewardTarget = GameServicesLedgerTarget;
8
23
  export type GameServicesLeaderboardTarget = Extract<PlatformTarget, 'browser' | 'microsoft-store' | 'android' | 'ios' | 'ait' | 'reddit'>;
9
24
  export type GameServicesLedgerTarget = Extract<PlatformTarget, 'browser' | 'microsoft-store' | 'android' | 'ios' | 'ait' | 'reddit' | 'verse8' | 'oneplay'>;
@@ -13,7 +28,17 @@ export type EntitlementIdempotencyKey = PurchaseIdempotencyKey | AdRewardIdempot
13
28
  export type EntitlementLedgerSource = 'purchase' | 'ad_reward' | 'admin';
14
29
  export type EntitlementLedgerPayload = Record<string, string | number | boolean>;
15
30
  export interface VerifyPurchaseRequest {
31
+ /** The ledger (deployment platform) target; see `store` for the store the purchase was made in. */
16
32
  readonly target: GameServicesStoreTarget;
33
+ /**
34
+ * The open store id the purchase was made in (RFC "Open store ids"), for
35
+ * example `google-play` or `credit-wallet`. A v1 request without it names
36
+ * its store through `target`: `android` is `google-play`, `ios` is
37
+ * `app-store`, `ait` is `apps-in-toss`, `reddit` is `devvit`, and every
38
+ * other target (`microsoft-store`, `verse8`, `oneplay`) is its own store id
39
+ * (`resolveLegacyCommerceStoreId`).
40
+ */
41
+ readonly store?: StoreId;
17
42
  /** Deployment config key used to resolve game-owned platform product identifiers. */
18
43
  readonly deploymentTarget?: string;
19
44
  readonly playerId: string;
@@ -62,6 +87,44 @@ export interface EntitlementLedgerResult {
62
87
  readonly ledgerEntryId: string;
63
88
  readonly alreadyProcessed: boolean;
64
89
  }
90
+ /** Why a grant was reversed (RFC "Revocation and expiry entries"). */
91
+ export type EntitlementRevocationReason = 'refund' | 'chargeback' | 'voided' | 'admin';
92
+ /**
93
+ * A `revoke` ledger entry: it references the grant it reverses and never
94
+ * removes it, so the ledger stays append-only. Store notification handlers
95
+ * (a Play voided purchase, an App Store refund, an Apps in Toss or Devvit
96
+ * refunded order) write one through `revokeEntitlementGrant`; the entry's
97
+ * `idempotencyKey` makes the notification replay-safe.
98
+ */
99
+ export interface EntitlementLedgerRevocation {
100
+ readonly playerId: string;
101
+ /** The `ledgerEntryId` of the grant this entry reverses. */
102
+ readonly revokesLedgerEntryId: string;
103
+ readonly idempotencyKey: string;
104
+ readonly reason: EntitlementRevocationReason;
105
+ readonly revokedAt: string;
106
+ readonly payload: EntitlementLedgerPayload;
107
+ /** The notification's own authority identity, when the handler has one. */
108
+ readonly evidenceVerificationId?: string;
109
+ }
110
+ export interface EntitlementRevocationTransaction extends EntitlementLedgerRevocation {
111
+ readonly ledgerEntryId: string;
112
+ }
113
+ /**
114
+ * A subscription `period` ledger entry: it extends an earlier grant with a
115
+ * renewal period and its expiry. This is the shape only; no reference store
116
+ * verifier writes one until a subscription verifier exists.
117
+ */
118
+ export interface EntitlementLedgerPeriod {
119
+ readonly playerId: string;
120
+ /** The `ledgerEntryId` of the subscription grant this period extends. */
121
+ readonly extendsLedgerEntryId: string;
122
+ readonly idempotencyKey: string;
123
+ readonly periodIndex: number;
124
+ readonly startedAt: string;
125
+ readonly expiresAt: string;
126
+ readonly payload: EntitlementLedgerPayload;
127
+ }
65
128
  export interface ProductGrantTransaction {
66
129
  readonly ledgerEntryId: string;
67
130
  readonly playerId: string;
@@ -83,6 +146,14 @@ export interface LeaderboardScoreTransaction extends RecordLeaderboardScoreReque
83
146
  */
84
147
  export declare function assertVerifyPurchaseRequest(input: VerifyPurchaseRequest): VerifyPurchaseRequest;
85
148
  export declare function assertVerifyPurchaseResponse(input: VerifyPurchaseResponse): VerifyPurchaseResponse;
149
+ /** Bounds one ledger status read; a client asks for the grants of one journal window. */
150
+ export declare const maxPurchaseLedgerStatusEntries = 256;
151
+ /**
152
+ * Validates an untrusted ledger status read. Failures throw
153
+ * `GameServicesRequestValidationError` so transports can answer with a stable 400.
154
+ */
155
+ export declare function assertPurchaseLedgerStatusRequest(input: PurchaseLedgerStatusRequest): PurchaseLedgerStatusRequest;
156
+ export declare function assertPurchaseLedgerStatusResponse(input: PurchaseLedgerStatusResponse): PurchaseLedgerStatusResponse;
86
157
  export declare function assertPurchaseGrantFinalization(input: PurchaseGrantFinalization): PurchaseGrantFinalization;
87
158
  /**
88
159
  * Validates an untrusted ad reward claim. Failures throw
@@ -98,6 +169,11 @@ export declare function assertRecordLeaderboardScoreRequest(input: RecordLeaderb
98
169
  export declare function assertRecordLeaderboardScoreResponse(input: RecordLeaderboardScoreResponse): RecordLeaderboardScoreResponse;
99
170
  export declare function assertEntitlementLedgerGrant(input: EntitlementLedgerGrant): EntitlementLedgerGrant;
100
171
  export declare function assertEntitlementLedgerResult(input: EntitlementLedgerResult): EntitlementLedgerResult;
172
+ export declare function assertEntitlementLedgerRevocation(input: EntitlementLedgerRevocation): EntitlementLedgerRevocation;
173
+ export declare function assertEntitlementRevocationTransaction(input: EntitlementRevocationTransaction): EntitlementRevocationTransaction;
174
+ export declare function assertEntitlementLedgerPeriod(input: EntitlementLedgerPeriod): EntitlementLedgerPeriod;
101
175
  export declare function assertProductGrantTransaction(input: ProductGrantTransaction): ProductGrantTransaction;
102
176
  export declare function assertLeaderboardScoreTransaction(input: LeaderboardScoreTransaction): LeaderboardScoreTransaction;
177
+ /** A store id is a bounded kebab-case identifier such as `google-play` or `credit-wallet`. */
178
+ export declare function assertCommerceStoreId(input: unknown, label?: string): asserts input is StoreId;
103
179
  export declare function assertGameServicesDeploymentTarget(input: unknown): asserts input is string;
package/dist/types.js CHANGED
@@ -7,6 +7,9 @@ export function assertVerifyPurchaseRequest(input) {
7
7
  return validateRequest(() => {
8
8
  assertRecord(input, 'VerifyPurchaseRequest');
9
9
  assertStoreTarget(input.target);
10
+ if (input.store !== undefined) {
11
+ assertCommerceStoreId(input.store, 'store');
12
+ }
10
13
  assertOptionalGameServicesDeploymentTarget(input.deploymentTarget);
11
14
  assertRequestString(input.playerId, 'playerId');
12
15
  assertRequestString(input.productId, 'productId');
@@ -29,6 +32,49 @@ export function assertVerifyPurchaseResponse(input) {
29
32
  }
30
33
  return input;
31
34
  }
35
+ /** Bounds one ledger status read; a client asks for the grants of one journal window. */
36
+ export const maxPurchaseLedgerStatusEntries = 256;
37
+ /**
38
+ * Validates an untrusted ledger status read. Failures throw
39
+ * `GameServicesRequestValidationError` so transports can answer with a stable 400.
40
+ */
41
+ export function assertPurchaseLedgerStatusRequest(input) {
42
+ return validateRequest(() => {
43
+ assertRecord(input, 'PurchaseLedgerStatusRequest');
44
+ if (!Array.isArray(input.ledgerEntryIds)) {
45
+ throw new Error('ledgerEntryIds must be an array.');
46
+ }
47
+ if (input.ledgerEntryIds.length > maxPurchaseLedgerStatusEntries) {
48
+ throw new Error(`ledgerEntryIds must not exceed ${String(maxPurchaseLedgerStatusEntries)} entries.`);
49
+ }
50
+ for (const ledgerEntryId of input.ledgerEntryIds) {
51
+ assertRequestString(ledgerEntryId, 'ledgerEntryIds[]');
52
+ }
53
+ assertRequestString(input.playerId, 'playerId');
54
+ return input;
55
+ });
56
+ }
57
+ export function assertPurchaseLedgerStatusResponse(input) {
58
+ assertRecord(input, 'PurchaseLedgerStatusResponse');
59
+ if (!Array.isArray(input.statuses)) {
60
+ throw new Error('statuses must be an array.');
61
+ }
62
+ for (const status of input.statuses) {
63
+ assertRecord(status, 'statuses[]');
64
+ assertNonEmptyString(status.ledgerEntryId, 'statuses[].ledgerEntryId');
65
+ if (status.status !== 'active' && status.status !== 'reversed' && status.status !== 'unknown') {
66
+ throw new Error('statuses[].status must be active, reversed, or unknown.');
67
+ }
68
+ assertOptionalNonEmptyString(status.reversedBy, 'statuses[].reversedBy');
69
+ if (status.status === 'reversed' && status.reversedBy === undefined) {
70
+ throw new Error('a reversed status names the entry that reversed it.');
71
+ }
72
+ if (status.status !== 'reversed' && status.reversedBy !== undefined) {
73
+ throw new Error('only a reversed status names a reversing entry.');
74
+ }
75
+ }
76
+ return input;
77
+ }
32
78
  export function assertPurchaseGrantFinalization(input) {
33
79
  assertRecord(input, 'PurchaseGrantFinalization');
34
80
  if (input.status !== 'completed' && input.status !== 'pending') {
@@ -124,6 +170,38 @@ export function assertEntitlementLedgerResult(input) {
124
170
  assertBoolean(input.alreadyProcessed, 'alreadyProcessed');
125
171
  return input;
126
172
  }
173
+ export function assertEntitlementLedgerRevocation(input) {
174
+ assertRecord(input, 'EntitlementLedgerRevocation');
175
+ assertNonEmptyString(input.playerId, 'playerId');
176
+ assertNonEmptyString(input.revokesLedgerEntryId, 'revokesLedgerEntryId');
177
+ assertNonEmptyString(input.idempotencyKey, 'idempotencyKey');
178
+ assertRevocationReason(input.reason);
179
+ assertNonEmptyString(input.revokedAt, 'revokedAt');
180
+ assertPayload(input.payload);
181
+ assertOptionalNonEmptyString(input.evidenceVerificationId, 'evidenceVerificationId');
182
+ return input;
183
+ }
184
+ export function assertEntitlementRevocationTransaction(input) {
185
+ assertEntitlementLedgerRevocation(input);
186
+ assertNonEmptyString(input.ledgerEntryId, 'ledgerEntryId');
187
+ return input;
188
+ }
189
+ export function assertEntitlementLedgerPeriod(input) {
190
+ assertRecord(input, 'EntitlementLedgerPeriod');
191
+ assertNonEmptyString(input.playerId, 'playerId');
192
+ assertNonEmptyString(input.extendsLedgerEntryId, 'extendsLedgerEntryId');
193
+ assertNonEmptyString(input.idempotencyKey, 'idempotencyKey');
194
+ if (!Number.isSafeInteger(input.periodIndex) || input.periodIndex < 0) {
195
+ throw new Error('periodIndex must be a non-negative integer.');
196
+ }
197
+ assertNonEmptyString(input.startedAt, 'startedAt');
198
+ assertNonEmptyString(input.expiresAt, 'expiresAt');
199
+ if (!(Date.parse(input.expiresAt) > Date.parse(input.startedAt))) {
200
+ throw new Error('expiresAt must be a timestamp after startedAt.');
201
+ }
202
+ assertPayload(input.payload);
203
+ return input;
204
+ }
127
205
  export function assertProductGrantTransaction(input) {
128
206
  assertRecord(input, 'ProductGrantTransaction');
129
207
  assertNonEmptyString(input.ledgerEntryId, 'ledgerEntryId');
@@ -150,13 +228,21 @@ function assertRecord(input, label) {
150
228
  throw new Error(`${label} must be an object.`);
151
229
  }
152
230
  }
231
+ /**
232
+ * The purchase target is open (RFC "Open store ids"): any bounded kebab-case
233
+ * identifier passes here, and the backend's commerce evidence registry decides
234
+ * whether the store it names is served.
235
+ */
153
236
  function assertStoreTarget(input) {
154
- if (input !== 'microsoft-store'
155
- && input !== 'android'
156
- && input !== 'ios'
157
- && input !== 'ait'
158
- && input !== 'oneplay') {
159
- throw new Error('target must be microsoft-store, android, ios, ait, or oneplay.');
237
+ assertCommerceStoreId(input, 'target');
238
+ }
239
+ /** A store id is a bounded kebab-case identifier such as `google-play` or `credit-wallet`. */
240
+ export function assertCommerceStoreId(input, label = 'store') {
241
+ if (typeof input !== 'string'
242
+ || input.length === 0
243
+ || input.length > 64
244
+ || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(input)) {
245
+ throw new Error(`${label} must be a lowercase kebab-case store identifier.`);
160
246
  }
161
247
  }
162
248
  function assertAdRewardTarget(input) {
@@ -262,6 +348,11 @@ function assertPurchaseGrantFinalizationAction(input) {
262
348
  throw new Error('finalization.action must be acknowledge, consume, finish, or complete.');
263
349
  }
264
350
  }
351
+ function assertRevocationReason(input) {
352
+ if (input !== 'refund' && input !== 'chargeback' && input !== 'voided' && input !== 'admin') {
353
+ throw new Error('reason must be refund, chargeback, voided, or admin.');
354
+ }
355
+ }
265
356
  function assertLedgerSource(input) {
266
357
  if (input !== 'purchase' && input !== 'ad_reward' && input !== 'admin') {
267
358
  throw new Error('source must be purchase, ad_reward, or admin.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/game-services",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -77,6 +77,18 @@
77
77
  "types": "./dist/apps-in-toss-partner-api.d.ts",
78
78
  "default": "./dist/apps-in-toss-partner-api.js"
79
79
  },
80
+ "./commerce-evidence-registry": {
81
+ "types": "./dist/commerce-evidence-registry.d.ts",
82
+ "default": "./dist/commerce-evidence-registry.js"
83
+ },
84
+ "./commerce-reconciler": {
85
+ "types": "./dist/commerce-reconciler.d.ts",
86
+ "default": "./dist/commerce-reconciler.js"
87
+ },
88
+ "./monetization-operation-store": {
89
+ "types": "./dist/monetization-operation-store.d.ts",
90
+ "default": "./dist/monetization-operation-store.js"
91
+ },
80
92
  "./client": {
81
93
  "types": "./dist/client.d.ts",
82
94
  "default": "./dist/client.js"
@@ -190,9 +202,9 @@
190
202
  "@orpc/client": "2.0.0-beta.14",
191
203
  "@orpc/contract": "2.0.0-beta.14",
192
204
  "@orpc/server": "2.0.0-beta.14",
193
- "@mpgd/catalog": "0.9.2",
194
- "@mpgd/platform": "0.21.0",
195
- "@mpgd/analytics": "0.4.7"
205
+ "@mpgd/analytics": "0.4.8",
206
+ "@mpgd/catalog": "0.9.3",
207
+ "@mpgd/platform": "0.22.0"
196
208
  },
197
209
  "peerDependencies": {
198
210
  "@apple/app-store-server-library": "^3.1.0"
@@ -221,8 +233,8 @@
221
233
  "fix": "ttsc fix",
222
234
  "build": "cd ../.. && node tools/run-ttsx.mjs tools/package/build-packages.ts @mpgd/game-services",
223
235
  "test:packed-transport": "pnpm build && node test/runtime-transport-package-import.mjs && node test/reward-evidence-import.mjs",
224
- "test:unit": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-progress.test.ts && node tools/run-ttsx.mjs packages/game-services/src/runtime.test.ts && node tools/run-ttsx.mjs packages/game-services/src/types.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-publisher-client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-identity.test.ts && node tools/run-ttsx.mjs packages/game-services/src/platform-order.test.ts && node tools/run-ttsx.mjs packages/game-services/src/progress-link.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notification-delivery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/admob-ssv.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-verifier.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-evidence-verification-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-partner-api.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-durability-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-transport.test.ts && node tools/run-ttsx.mjs packages/game-services/src/guest-session.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-reward-evidence.test.ts && node tools/run-ttsx.mjs packages/game-services/src/ad-reward-verifier-registry.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-reward.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-purchase.test.ts",
225
- "test:recovery": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/monetization-recovery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-recovery.test.ts",
236
+ "test:unit": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-progress.test.ts && node tools/run-ttsx.mjs packages/game-services/src/runtime.test.ts && node tools/run-ttsx.mjs packages/game-services/src/types.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-publisher-client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-identity.test.ts && node tools/run-ttsx.mjs packages/game-services/src/platform-order.test.ts && node tools/run-ttsx.mjs packages/game-services/src/progress-link.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notification-delivery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/admob-ssv.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-verifier.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-evidence-verification-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-partner-api.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-durability-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-transport.test.ts && node tools/run-ttsx.mjs packages/game-services/src/guest-session.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-reward-evidence.test.ts && node tools/run-ttsx.mjs packages/game-services/src/ad-reward-verifier-registry.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-reward.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-routing.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-ledger-revocation.test.ts",
237
+ "test:recovery": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/monetization-recovery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-recovery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-reconciler.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-reconciler-conformance.test.ts",
226
238
  "test": "pnpm test:unit && pnpm test:app-store-server && pnpm test:recovery && pnpm test:packed-transport",
227
239
  "test:app-store-server": "vitest run"
228
240
  }