@mpgd/game-services 0.20.0 → 0.21.1

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.
@@ -1,4 +1,23 @@
1
- import type { AppStoreEnvironment, AppStoreInAppPurchaseType, AppStoreTransactionPayload } from './app-store-verifier.js';
1
+ import type { PlatformEvidenceEnvelope } from '@mpgd/platform';
2
+ import { type AppStoreEnvironment, type AppStoreInAppPurchaseType, type AppStoreTransactionPayload } from './app-store-verifier.js';
3
+ /** The placeholder JWS the signed-transaction evidence fixture carries; never a real signature. */
4
+ export declare const appStoreConformanceSignedTransaction = "conformance.signed-transaction.jws";
5
+ export interface CreateAppStoreSignedTransactionEvidenceFixtureInput {
6
+ readonly signedTransaction?: string;
7
+ readonly transactionId?: string;
8
+ readonly originalTransactionId?: string;
9
+ readonly bundleId?: string;
10
+ }
11
+ /**
12
+ * Builds the `app-store.signed-transaction.v1` evidence envelope a commerce v2
13
+ * StoreKit client submits, with a placeholder JWS by default. Pair it with a
14
+ * fake `AppStoreSignedTransactionVerifier` that answers
15
+ * `createAppStoreTransactionConformanceFixture` for that string and, under the
16
+ * default `server-api` policy, a fake Server API whose `signedTransactionInfo`
17
+ * decodes to the transaction's current status; the fixture is not signed
18
+ * evidence and contains no Apple credentials.
19
+ */
20
+ export declare function createAppStoreSignedTransactionEvidenceFixture(input?: CreateAppStoreSignedTransactionEvidenceFixtureInput): PlatformEvidenceEnvelope;
2
21
  export interface CreateAppStoreTransactionConformanceFixtureInput {
3
22
  readonly transactionId?: string;
4
23
  readonly originalTransactionId?: string;
@@ -1,3 +1,25 @@
1
+ import { createAppStoreSignedTransactionEvidence, } from './app-store-verifier.js';
2
+ /** The placeholder JWS the signed-transaction evidence fixture carries; never a real signature. */
3
+ export const appStoreConformanceSignedTransaction = 'conformance.signed-transaction.jws';
4
+ /**
5
+ * Builds the `app-store.signed-transaction.v1` evidence envelope a commerce v2
6
+ * StoreKit client submits, with a placeholder JWS by default. Pair it with a
7
+ * fake `AppStoreSignedTransactionVerifier` that answers
8
+ * `createAppStoreTransactionConformanceFixture` for that string and, under the
9
+ * default `server-api` policy, a fake Server API whose `signedTransactionInfo`
10
+ * decodes to the transaction's current status; the fixture is not signed
11
+ * evidence and contains no Apple credentials.
12
+ */
13
+ export function createAppStoreSignedTransactionEvidenceFixture(input = {}) {
14
+ return createAppStoreSignedTransactionEvidence({
15
+ signedTransaction: input.signedTransaction ?? appStoreConformanceSignedTransaction,
16
+ transactionId: input.transactionId ?? '2000000000000001',
17
+ ...(input.originalTransactionId === undefined
18
+ ? {}
19
+ : { originalTransactionId: input.originalTransactionId }),
20
+ ...(input.bundleId === undefined ? {} : { bundleId: input.bundleId }),
21
+ });
22
+ }
1
23
  /**
2
24
  * Builds deterministic decoded JWS data for verifier and provider-adapter
3
25
  * conformance tests. The fixture is not signed evidence and contains no Apple
@@ -1,3 +1,4 @@
1
+ import type { PlatformEvidenceEnvelope } from '@mpgd/platform';
1
2
  import type { GameServicesEvidenceVerifier } from './evidence-verification.js';
2
3
  export type AppStoreEnvironment = 'Production' | 'Sandbox';
3
4
  export declare const appStoreServerApiBaseUrls: Readonly<{
@@ -72,7 +73,44 @@ export interface CreateAppStoreEvidenceVerifierOptions {
72
73
  }) => string | Promise<string>;
73
74
  readonly now?: () => string;
74
75
  readonly clockSkewMs?: number;
76
+ /**
77
+ * How `app-store.signed-transaction.v1` request evidence is used. Defaults
78
+ * to `{ mode: 'server-api' }`: the client JWS is verified offline to
79
+ * establish authenticity and bind the transaction id, and the grant decision
80
+ * is then taken on the transaction's current status fetched from the App
81
+ * Store Server API (`Get Transaction Info`), so a refund that happened after
82
+ * the client cached its JWS is seen. `{ mode: 'offline-only', maxSignedAgeMs }`
83
+ * decides on the client JWS alone and is only for deployments without Server
84
+ * API access: a JWS is a snapshot, so a transaction refunded after it was
85
+ * signed is accepted until `signedDate` falls out of the window. `{ mode:
86
+ * 'ignore' }` fetches by id for every request.
87
+ */
88
+ readonly signedTransactionEvidence?: AppStoreSignedTransactionEvidencePolicy;
75
89
  }
90
+ export type AppStoreSignedTransactionEvidencePolicy = {
91
+ readonly mode: 'ignore';
92
+ } | {
93
+ readonly mode: 'server-api';
94
+ } | {
95
+ readonly mode: 'offline-only';
96
+ /** Maximum age of the JWS `signedDate` at verification time; older evidence is rejected. */
97
+ readonly maxSignedAgeMs: number;
98
+ };
99
+ /**
100
+ * Evidence schema of a commerce v2 App Store purchase (RFC B3): the payload's
101
+ * `signedTransaction` is the StoreKit-verified JWS the client received. The
102
+ * server verifies it offline and, by default, confirms the transaction's
103
+ * current status through the Server API before a grant.
104
+ */
105
+ export declare const appStoreSignedTransactionEvidenceSchema = "app-store.signed-transaction.v1";
106
+ export interface AppStoreSignedTransactionEvidencePayload {
107
+ readonly signedTransaction: string;
108
+ readonly transactionId: string;
109
+ readonly originalTransactionId?: string;
110
+ readonly bundleId?: string;
111
+ }
112
+ /** Builds the `app-store.signed-transaction.v1` envelope a client submits with `verifyPurchase`. */
113
+ export declare function createAppStoreSignedTransactionEvidence(payload: AppStoreSignedTransactionEvidencePayload): PlatformEvidenceEnvelope;
76
114
  export interface AppStoreFetchResponse {
77
115
  readonly status: number;
78
116
  readonly body: ReadableStream<Uint8Array> | null;
@@ -16,6 +16,56 @@ class AppStoreResponseTooLargeError extends Error {
16
16
  this.name = 'AppStoreResponseTooLargeError';
17
17
  }
18
18
  }
19
+ /**
20
+ * Evidence schema of a commerce v2 App Store purchase (RFC B3): the payload's
21
+ * `signedTransaction` is the StoreKit-verified JWS the client received. The
22
+ * server verifies it offline and, by default, confirms the transaction's
23
+ * current status through the Server API before a grant.
24
+ */
25
+ export const appStoreSignedTransactionEvidenceSchema = 'app-store.signed-transaction.v1';
26
+ /** Builds the `app-store.signed-transaction.v1` envelope a client submits with `verifyPurchase`. */
27
+ export function createAppStoreSignedTransactionEvidence(payload) {
28
+ assertNonEmptyString(payload.signedTransaction, 'signedTransaction');
29
+ assertNonEmptyString(payload.transactionId, 'transactionId');
30
+ return {
31
+ schema: appStoreSignedTransactionEvidenceSchema,
32
+ payload: {
33
+ signedTransaction: payload.signedTransaction,
34
+ transactionId: payload.transactionId,
35
+ ...(payload.originalTransactionId === undefined
36
+ ? {}
37
+ : { originalTransactionId: payload.originalTransactionId }),
38
+ ...(payload.bundleId === undefined ? {} : { bundleId: payload.bundleId }),
39
+ },
40
+ };
41
+ }
42
+ /**
43
+ * Reads the client JWS from `app-store.signed-transaction.v1` evidence. Any
44
+ * other schema, or no evidence, is `absent` (the Server API path); a present
45
+ * envelope whose payload is malformed, or whose `transactionId` differs from
46
+ * the request, is rejected before any verifier runs.
47
+ */
48
+ function readSignedTransactionEvidence(request) {
49
+ const evidence = request.evidence;
50
+ if (evidence?.schema !== appStoreSignedTransactionEvidenceSchema) {
51
+ return { status: 'absent' };
52
+ }
53
+ const payload = evidence.payload;
54
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
55
+ return { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_EVIDENCE_INVALID' };
56
+ }
57
+ const { signedTransaction, transactionId } = payload;
58
+ if (typeof signedTransaction !== 'string'
59
+ || signedTransaction.length === 0
60
+ || signedTransaction.length > maxSignedTransactionEvidenceLength) {
61
+ return { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_EVIDENCE_INVALID' };
62
+ }
63
+ if (transactionId !== undefined && transactionId !== request.platformTransactionId) {
64
+ return { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_ID_MISMATCH' };
65
+ }
66
+ return { status: 'present', signedTransaction };
67
+ }
68
+ const maxSignedTransactionEvidenceLength = 512 * 1024;
19
69
  export function createAppStoreServerApiClient(options) {
20
70
  const fetch = options.fetch ?? readGlobalFetch();
21
71
  const maxResponseBytes = readPositiveSafeInteger(options.maxResponseBytes ?? 128 * 1024, 'maxResponseBytes');
@@ -82,6 +132,7 @@ export function createAppStoreServerApiClient(options) {
82
132
  export function createAppStoreGameServicesEvidenceVerifier(options) {
83
133
  assertNonEmptyString(options.bundleId, 'bundleId');
84
134
  assertAppStoreEnvironment(options.environment);
135
+ assertSignedTransactionEvidencePolicy(options.signedTransactionEvidence);
85
136
  const clockSkewMs = readNonNegativeSafeInteger(options.clockSkewMs ?? 5 * 60 * 1000, 'clockSkewMs');
86
137
  const now = options.now ?? (() => new Date().toISOString());
87
138
  return {
@@ -103,54 +154,72 @@ async function verifyAppStorePurchase(input, options, now, clockSkewMs) {
103
154
  if (input.product.type === 'subscription') {
104
155
  return { status: 'rejected', reason: 'APP_STORE_SUBSCRIPTION_UNSUPPORTED' };
105
156
  }
106
- let transactionResult;
107
- try {
108
- transactionResult = await options.serverApi.getTransactionInfo({
109
- transactionId: input.request.platformTransactionId,
110
- environment: options.environment,
111
- signal: input.signal,
112
- });
113
- }
114
- catch (error) {
115
- if (!(error instanceof AppStoreDependencyUnavailableError)) {
116
- throw error;
157
+ // Commerce v2 evidence carries the client's StoreKit-verified JWS. Its
158
+ // signature establishes authenticity and binds the transaction id; the
159
+ // grant decision is then taken on the transaction's current status from
160
+ // the Server API (default), or on the JWS alone within a freshness window
161
+ // (offline-only). Without evidence (current clients) the transaction is
162
+ // fetched by id as before.
163
+ const policy = options.signedTransactionEvidence ?? defaultSignedTransactionEvidencePolicy;
164
+ const evidence = policy.mode === 'ignore'
165
+ ? { status: 'absent' }
166
+ : readSignedTransactionEvidence(input.request);
167
+ if (evidence.status === 'rejected') {
168
+ return { status: 'rejected', reason: evidence.reason };
169
+ }
170
+ let offlineTransaction;
171
+ if (evidence.status === 'present') {
172
+ const offline = await verifySignedTransaction(evidence.signedTransaction, input, options);
173
+ if (offline.status !== 'verified') {
174
+ return offline;
117
175
  }
118
- return { status: 'pending', reason: 'APP_STORE_SERVER_API_UNAVAILABLE' };
119
- }
120
- try {
121
- assertAppStoreServerApiTransactionResult(transactionResult);
122
- }
123
- catch {
124
- return { status: 'pending', reason: 'APP_STORE_SERVER_API_INVALID_RESPONSE' };
125
- }
126
- if (transactionResult.status !== 'found') {
127
- return { status: transactionResult.status, reason: transactionResult.reason };
128
- }
129
- let signatureResult;
130
- try {
131
- signatureResult = await options.signedTransactionVerifier.verifyAndDecode({
132
- signedTransaction: transactionResult.signedTransactionInfo,
133
- environment: options.environment,
134
- bundleId: options.bundleId,
135
- signal: input.signal,
136
- });
137
- }
138
- catch (error) {
139
- if (!(error instanceof AppStoreDependencyUnavailableError)) {
140
- throw error;
176
+ if (offline.payload.transactionId !== input.request.platformTransactionId) {
177
+ // A client-supplied JWS for another transaction is a stable rejection,
178
+ // distinct from a Server API answer that disagrees with the request.
179
+ return { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_ID_MISMATCH' };
141
180
  }
142
- return { status: 'pending', reason: 'APP_STORE_SIGNATURE_VERIFIER_UNAVAILABLE' };
181
+ offlineTransaction = offline.payload;
143
182
  }
144
- try {
145
- assertAppStoreSignedTransactionVerificationResult(signatureResult);
146
- }
147
- catch {
148
- return { status: 'rejected', reason: 'APP_STORE_SIGNATURE_RESULT_INVALID' };
183
+ let transaction;
184
+ if (offlineTransaction !== undefined && policy.mode === 'offline-only') {
185
+ transaction = offlineTransaction;
149
186
  }
150
- if (signatureResult.status === 'rejected') {
151
- return { status: 'rejected', reason: signatureResult.reason };
187
+ else {
188
+ // A JWS is a snapshot: the current status (refund, revocation) is read
189
+ // from the Server API before any grant.
190
+ let transactionResult;
191
+ try {
192
+ transactionResult = await options.serverApi.getTransactionInfo({
193
+ transactionId: input.request.platformTransactionId,
194
+ environment: options.environment,
195
+ signal: input.signal,
196
+ });
197
+ }
198
+ catch (error) {
199
+ if (!(error instanceof AppStoreDependencyUnavailableError)) {
200
+ throw error;
201
+ }
202
+ return { status: 'pending', reason: 'APP_STORE_SERVER_API_UNAVAILABLE' };
203
+ }
204
+ try {
205
+ assertAppStoreServerApiTransactionResult(transactionResult);
206
+ }
207
+ catch {
208
+ return { status: 'pending', reason: 'APP_STORE_SERVER_API_INVALID_RESPONSE' };
209
+ }
210
+ if (transactionResult.status !== 'found') {
211
+ return { status: transactionResult.status, reason: transactionResult.reason };
212
+ }
213
+ const fresh = await verifySignedTransaction(transactionResult.signedTransactionInfo, input, options);
214
+ if (fresh.status !== 'verified') {
215
+ return fresh;
216
+ }
217
+ if (offlineTransaction !== undefined
218
+ && fresh.payload.transactionId !== offlineTransaction.transactionId) {
219
+ return { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_ID_MISMATCH' };
220
+ }
221
+ transaction = fresh.payload;
152
222
  }
153
- const transaction = signatureResult.payload;
154
223
  const expectedAccountToken = await resolveExpectedAppAccountToken(input, options);
155
224
  if (expectedAccountToken.status !== 'verified') {
156
225
  return expectedAccountToken;
@@ -160,6 +229,13 @@ async function verifyAppStorePurchase(input, options, now, clockSkewMs) {
160
229
  if (!Number.isFinite(nowMs)) {
161
230
  return { status: 'pending', reason: 'APP_STORE_VERIFIER_CLOCK_INVALID' };
162
231
  }
232
+ if (offlineTransaction !== undefined
233
+ && policy.mode === 'offline-only'
234
+ && nowMs - transaction.signedDate > policy.maxSignedAgeMs) {
235
+ // Offline-only never sees a later refund; a JWS older than the window is
236
+ // not fresh enough to decide a grant on.
237
+ return { status: 'rejected', reason: 'APP_STORE_SIGNED_TRANSACTION_STALE' };
238
+ }
163
239
  const rejection = validateTransaction(input, transaction, options, expectedAccountToken.value, nowMs, clockSkewMs);
164
240
  if (rejection !== undefined) {
165
241
  return { status: 'rejected', reason: rejection };
@@ -179,6 +255,51 @@ async function verifyAppStorePurchase(input, options, now, clockSkewMs) {
179
255
  },
180
256
  };
181
257
  }
258
+ const defaultSignedTransactionEvidencePolicy = {
259
+ mode: 'server-api',
260
+ };
261
+ function assertSignedTransactionEvidencePolicy(input) {
262
+ if (input === undefined) {
263
+ return;
264
+ }
265
+ assertRecord(input, 'signedTransactionEvidence');
266
+ if (input.mode === 'ignore' || input.mode === 'server-api') {
267
+ return;
268
+ }
269
+ if (input.mode === 'offline-only') {
270
+ readPositiveSafeInteger(input.maxSignedAgeMs, 'signedTransactionEvidence.maxSignedAgeMs');
271
+ return;
272
+ }
273
+ throw new Error('signedTransactionEvidence.mode must be ignore, server-api, or offline-only.');
274
+ }
275
+ /** Verifies one JWS through the signature port and validates the decoded payload shape. */
276
+ async function verifySignedTransaction(signedTransaction, input, options) {
277
+ let signatureResult;
278
+ try {
279
+ signatureResult = await options.signedTransactionVerifier.verifyAndDecode({
280
+ signedTransaction,
281
+ environment: options.environment,
282
+ bundleId: options.bundleId,
283
+ signal: input.signal,
284
+ });
285
+ }
286
+ catch (error) {
287
+ if (!(error instanceof AppStoreDependencyUnavailableError)) {
288
+ throw error;
289
+ }
290
+ return { status: 'pending', reason: 'APP_STORE_SIGNATURE_VERIFIER_UNAVAILABLE' };
291
+ }
292
+ try {
293
+ assertAppStoreSignedTransactionVerificationResult(signatureResult);
294
+ }
295
+ catch {
296
+ return { status: 'rejected', reason: 'APP_STORE_SIGNATURE_RESULT_INVALID' };
297
+ }
298
+ if (signatureResult.status === 'rejected') {
299
+ return { status: 'rejected', reason: signatureResult.reason };
300
+ }
301
+ return { status: 'verified', payload: signatureResult.payload };
302
+ }
182
303
  async function resolveExpectedAppAccountToken(input, options) {
183
304
  try {
184
305
  const value = await options.resolveAppAccountToken({
@@ -1,7 +1,30 @@
1
- import type { PurchaseResult } from '@mpgd/platform';
1
+ import type { PlatformGateway, PurchaseResult } from '@mpgd/platform';
2
+ export interface AdapterSettledGrant {
3
+ readonly ledgerEntryId: string;
4
+ readonly alreadyProcessed?: boolean;
5
+ }
6
+ /**
7
+ * Internal: the ledger grant a checkout result already carries, or undefined
8
+ * when the backend must verify. A gateway with a commerce v2 provider decides
9
+ * from the provider's capability block (RFC "Capabilities":
10
+ * `authority.evidence: 'adapter-settled'`) plus the result's
11
+ * `authoritativeGrant`; a provider whose capabilities cannot be read, or that
12
+ * declares any other evidence, answers undefined so the client verifies. A
13
+ * gateway without a v2 provider keeps the deprecated target-name rule until
14
+ * C4 removes it.
15
+ */
16
+ export declare function findAdapterSettledGrant(input: {
17
+ readonly gateway: Pick<PlatformGateway, 'commerceV2'>;
18
+ readonly target: string;
19
+ readonly purchase: PurchaseResult;
20
+ }): Promise<AdapterSettledGrant | undefined>;
2
21
  /**
3
22
  * Internal: the Microsoft Store adapter completes the ledger grant before it returns this
4
23
  * checkout result, so no further backend verification is requested for it.
24
+ *
25
+ * @deprecated Settlement is declared by the commerce v2 capability block; use
26
+ * `findAdapterSettledGrant`, which reads it on gateways that expose `commerceV2`. Kept for
27
+ * gateways that still install only the v1 `createMicrosoftStoreCommerceAdapter`; C4 removes it.
5
28
  */
6
29
  export declare function isAuthoritativeMicrosoftStoreCompletion(target: string, purchase: PurchaseResult): purchase is PurchaseResult & {
7
30
  readonly status: 'completed';
@@ -1,6 +1,58 @@
1
+ /**
2
+ * Internal: the ledger grant a checkout result already carries, or undefined
3
+ * when the backend must verify. A gateway with a commerce v2 provider decides
4
+ * from the provider's capability block (RFC "Capabilities":
5
+ * `authority.evidence: 'adapter-settled'`) plus the result's
6
+ * `authoritativeGrant`; a provider whose capabilities cannot be read, or that
7
+ * declares any other evidence, answers undefined so the client verifies. A
8
+ * gateway without a v2 provider keeps the deprecated target-name rule until
9
+ * C4 removes it.
10
+ */
11
+ export async function findAdapterSettledGrant(input) {
12
+ const { purchase } = input;
13
+ const grant = purchase.authoritativeGrant;
14
+ if (purchase.status !== 'completed' || purchase.transactionId === undefined || grant === undefined) {
15
+ return undefined;
16
+ }
17
+ const provider = input.gateway.commerceV2;
18
+ const settled = provider === undefined
19
+ ? isAuthoritativeMicrosoftStoreCompletion(input.target, purchase)
20
+ : (await readEvidenceAuthority(provider)) === 'adapter-settled';
21
+ if (!settled) {
22
+ return undefined;
23
+ }
24
+ return {
25
+ ledgerEntryId: grant.ledgerEntryId,
26
+ ...(grant.alreadyProcessed === undefined ? {} : { alreadyProcessed: grant.alreadyProcessed }),
27
+ };
28
+ }
29
+ /**
30
+ * A provider's `authority.evidence` declaration is fixed for its lifetime, so
31
+ * one successful read serves every later checkout; a failed read is not
32
+ * cached, and the next checkout probes again.
33
+ */
34
+ const evidenceAuthorityByProvider = new WeakMap();
35
+ async function readEvidenceAuthority(provider) {
36
+ const cached = evidenceAuthorityByProvider.get(provider);
37
+ if (cached !== undefined) {
38
+ return cached;
39
+ }
40
+ try {
41
+ const { evidence } = (await provider.getCapabilities()).authority;
42
+ evidenceAuthorityByProvider.set(provider, evidence);
43
+ return evidence;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
1
49
  /**
2
50
  * Internal: the Microsoft Store adapter completes the ledger grant before it returns this
3
51
  * checkout result, so no further backend verification is requested for it.
52
+ *
53
+ * @deprecated Settlement is declared by the commerce v2 capability block; use
54
+ * `findAdapterSettledGrant`, which reads it on gateways that expose `commerceV2`. Kept for
55
+ * gateways that still install only the v1 `createMicrosoftStoreCommerceAdapter`; C4 removes it.
4
56
  */
5
57
  export function isAuthoritativeMicrosoftStoreCompletion(target, purchase) {
6
58
  return target === 'microsoft-store'
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createORPCClient } from '@orpc/client';
2
2
  import { RPCLink } from '@orpc/client/fetch';
3
3
  import { createAnalyticsReporter } from '@mpgd/analytics';
4
- import { isAuthoritativeMicrosoftStoreCompletion } from './authoritative-purchase.js';
4
+ import { findAdapterSettledGrant } from './authoritative-purchase.js';
5
5
  import { resolveClientRewardClaim, } from './client-reward-evidence.js';
6
6
  import { createDefaultClientRewardEvidenceRegistry } from './default-client-reward-evidence.js';
7
7
  import { observeGameServicesOperation } from './operation-progress.js';
@@ -110,19 +110,27 @@ export function createGameServicesClient(input) {
110
110
  purchase,
111
111
  };
112
112
  }
113
- if (isAuthoritativeMicrosoftStoreCompletion(target, purchase)) {
113
+ // A provider that declares adapter settlement already wrote the ledger
114
+ // grant before returning; the target-name rule remains only for
115
+ // gateways without a commerce v2 provider (removed in C4).
116
+ const settledGrant = await findAdapterSettledGrant({
117
+ gateway: input.gateway,
118
+ target,
119
+ purchase,
120
+ });
121
+ if (settledGrant !== undefined) {
114
122
  const result = {
115
123
  status: 'granted',
116
124
  purchase,
117
- ledgerEntryId: purchase.transactionId,
125
+ ledgerEntryId: settledGrant.ledgerEntryId,
118
126
  };
119
127
  await analytics.track({
120
128
  name: 'purchase_granted',
121
129
  properties: {
122
130
  productId: purchaseInput.productId,
123
131
  status: result.status,
124
- ledgerEntryId: purchase.transactionId,
125
- alreadyProcessed: purchase.authoritativeGrant?.alreadyProcessed,
132
+ ledgerEntryId: settledGrant.ledgerEntryId,
133
+ alreadyProcessed: settledGrant.alreadyProcessed,
126
134
  },
127
135
  });
128
136
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/game-services",
3
- "version": "0.20.0",
3
+ "version": "0.21.1",
4
4
  "description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -190,9 +190,9 @@
190
190
  "@orpc/client": "2.0.0-beta.14",
191
191
  "@orpc/contract": "2.0.0-beta.14",
192
192
  "@orpc/server": "2.0.0-beta.14",
193
- "@mpgd/analytics": "0.4.5",
194
- "@mpgd/platform": "0.19.0",
195
- "@mpgd/catalog": "0.9.0"
193
+ "@mpgd/catalog": "0.9.2",
194
+ "@mpgd/platform": "0.21.0",
195
+ "@mpgd/analytics": "0.4.7"
196
196
  },
197
197
  "peerDependencies": {
198
198
  "@apple/app-store-server-library": "^3.1.0"