@mpgd/game-services 0.9.0 → 0.11.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/server.js CHANGED
@@ -4,7 +4,7 @@ import { createAnalyticsReporter } from '@mpgd/analytics';
4
4
  import { gameServicesBackendEndpoints, } from './client';
5
5
  import { gameServicesContract } from './contract';
6
6
  import { createRejectingGameServicesEvidenceVerifier, } from './evidence-verification';
7
- import { assertClaimAdRewardRequest, assertClaimAdRewardResponse, assertEntitlementLedgerGrant, assertEntitlementLedgerResult, assertLeaderboardScoreTransaction, assertProductGrantTransaction, assertRecordLeaderboardScoreRequest, assertRecordLeaderboardScoreResponse, assertVerifyPurchaseRequest, assertVerifyPurchaseResponse, } from './types';
7
+ import { assertClaimAdRewardRequest, assertClaimAdRewardResponse, assertEntitlementLedgerGrant, assertEntitlementLedgerResult, assertLeaderboardScoreTransaction, assertProductGrantTransaction, assertPurchaseGrantFinalization, assertRecordLeaderboardScoreRequest, assertRecordLeaderboardScoreResponse, assertVerifyPurchaseRequest, assertVerifyPurchaseResponse, } from './types';
8
8
  export class InMemoryGameServicesStore {
9
9
  entitlementTransactionsByKey = new Map();
10
10
  entitlementTransactionsByEvidence = new Map();
@@ -129,6 +129,7 @@ export function createGameServicesBackend(input) {
129
129
  const version = input.version ?? '0.0.0';
130
130
  const evidenceVerifier = input.evidenceVerifier ?? createRejectingGameServicesEvidenceVerifier();
131
131
  const evidenceVerificationTimeoutMs = resolveEvidenceVerificationTimeout(input.evidenceVerificationTimeoutMs);
132
+ const purchaseGrantFinalizationTimeoutMs = resolvePurchaseGrantFinalizationTimeout(input.purchaseGrantFinalizationTimeoutMs);
132
133
  const analytics = createAnalyticsReporter({
133
134
  target: 'server',
134
135
  sessionId: input.analyticsSessionId ?? 'game-services',
@@ -145,6 +146,10 @@ export function createGameServicesBackend(input) {
145
146
  now,
146
147
  evidenceVerifier,
147
148
  evidenceVerificationTimeoutMs,
149
+ purchaseGrantFinalizationTimeoutMs,
150
+ ...(input.purchaseGrantFinalizer === undefined
151
+ ? {}
152
+ : { purchaseGrantFinalizer: input.purchaseGrantFinalizer }),
148
153
  });
149
154
  await analytics.track({
150
155
  name: verification.verified ? 'purchase_granted' : 'purchase_rejected',
@@ -155,6 +160,10 @@ export function createGameServicesBackend(input) {
155
160
  ledgerEntryId: verification.ledgerEntryId,
156
161
  alreadyProcessed: verification.alreadyProcessed,
157
162
  reason: verification.reason,
163
+ finalizationStatus: verification.finalization?.status,
164
+ finalizationAction: verification.finalization?.action,
165
+ finalizationAlreadyCompleted: verification.finalization?.alreadyCompleted,
166
+ finalizationReason: verification.finalization?.reason,
158
167
  },
159
168
  });
160
169
  return verification;
@@ -313,20 +322,25 @@ async function verifyPurchaseWithStore(input, context) {
313
322
  grantId: request.productId,
314
323
  target: request.target,
315
324
  };
316
- const existing = await findEntitlementTransactionByIdempotency(context.store, retryIdentity);
317
- if (existing !== undefined) {
318
- if (!matchesEntitlementRetry(existing, retryIdentity)) {
325
+ const completeExistingRetry = async (transaction) => {
326
+ if (!matchesEntitlementRetry(transaction, retryIdentity)) {
319
327
  return assertVerifyPurchaseResponse({
320
328
  verified: false,
321
329
  alreadyProcessed: false,
322
330
  reason: 'IDEMPOTENCY_KEY_CONFLICT',
323
331
  });
324
332
  }
333
+ const finalization = await finalizeExistingPurchaseGrant(request, transaction, context);
325
334
  return assertVerifyPurchaseResponse({
326
335
  verified: true,
327
- ledgerEntryId: existing.ledgerEntryId,
336
+ ledgerEntryId: transaction.ledgerEntryId,
328
337
  alreadyProcessed: true,
338
+ ...(finalization === undefined ? {} : { finalization }),
329
339
  });
340
+ };
341
+ const existing = await findEntitlementTransactionByIdempotency(context.store, retryIdentity);
342
+ if (existing !== undefined) {
343
+ return completeExistingRetry(existing);
330
344
  }
331
345
  const product = context.catalog.products.find((entry) => entry.id === request.productId);
332
346
  if (product === undefined) {
@@ -354,6 +368,12 @@ async function verifyPurchaseWithStore(input, context) {
354
368
  });
355
369
  }, context.evidenceVerificationTimeoutMs);
356
370
  if (verification.status !== 'verified') {
371
+ // A matching grant can land while provider verification is in flight. Recheck
372
+ // before returning a stale provider state such as an already-consumed token.
373
+ const racedExisting = await findEntitlementTransactionByIdempotency(context.store, retryIdentity);
374
+ if (racedExisting !== undefined) {
375
+ return completeExistingRetry(racedExisting);
376
+ }
357
377
  return assertVerifyPurchaseResponse({
358
378
  verified: false,
359
379
  alreadyProcessed: false,
@@ -362,6 +382,7 @@ async function verifyPurchaseWithStore(input, context) {
362
382
  : verification.reason,
363
383
  });
364
384
  }
385
+ const verificationPayload = createPurchaseVerificationPayload(verification);
365
386
  const grant = await recordEntitlementGrantWithEvidence(context.store, {
366
387
  playerId: request.playerId,
367
388
  grantId: product.id,
@@ -371,11 +392,16 @@ async function verifyPurchaseWithStore(input, context) {
371
392
  grant: product.grant,
372
393
  evidenceVerificationId: verification.verificationId,
373
394
  payload: {
374
- ...verification.payload,
395
+ ...verificationPayload,
375
396
  target: request.target,
376
397
  productId: request.productId,
398
+ productType: product.type,
377
399
  platformProductId,
378
- platformTransactionId: request.platformTransactionId,
400
+ ...(verification.platformEvidenceId === null
401
+ ? {}
402
+ : {
403
+ platformTransactionId: verification.platformEvidenceId ?? request.platformTransactionId,
404
+ }),
379
405
  purchasedAt: request.purchasedAt,
380
406
  evidenceVerificationId: verification.verificationId,
381
407
  evidenceVerifiedAt: verification.verifiedAt,
@@ -388,20 +414,160 @@ async function verifyPurchaseWithStore(input, context) {
388
414
  reason: 'EVIDENCE_ALREADY_PROCESSED',
389
415
  });
390
416
  }
391
- if (grant.result.alreadyProcessed
392
- && !await recordedEntitlementMatchesRetry(context.store, grant.result, retryIdentity)) {
393
- return assertVerifyPurchaseResponse({
394
- verified: false,
395
- alreadyProcessed: false,
396
- reason: 'IDEMPOTENCY_KEY_CONFLICT',
397
- });
417
+ if (grant.result.alreadyProcessed) {
418
+ const recordedTransaction = await context.store.getEntitlementTransaction(grant.result.ledgerEntryId);
419
+ const storedEvidenceVerificationId = recordedTransaction?.evidenceVerificationId
420
+ ?? recordedTransaction?.payload.evidenceVerificationId;
421
+ if (recordedTransaction === undefined
422
+ || !matchesEntitlementRetry(recordedTransaction, retryIdentity)
423
+ || storedEvidenceVerificationId !== verification.verificationId) {
424
+ return assertVerifyPurchaseResponse({
425
+ verified: false,
426
+ alreadyProcessed: false,
427
+ reason: 'IDEMPOTENCY_KEY_CONFLICT',
428
+ });
429
+ }
398
430
  }
431
+ const finalization = await finalizePurchaseGrant(context.purchaseGrantFinalizer, {
432
+ request,
433
+ product,
434
+ platformProductId,
435
+ evidenceVerificationId: verification.verificationId,
436
+ ...(verification.payload === undefined
437
+ ? {}
438
+ : { evidencePayload: verification.payload }),
439
+ ledgerEntryId: grant.result.ledgerEntryId,
440
+ alreadyProcessed: grant.result.alreadyProcessed,
441
+ timeoutMs: context.purchaseGrantFinalizationTimeoutMs,
442
+ });
399
443
  return assertVerifyPurchaseResponse({
400
444
  verified: true,
401
445
  ledgerEntryId: grant.result.ledgerEntryId,
402
446
  alreadyProcessed: grant.result.alreadyProcessed,
447
+ ...(finalization === undefined ? {} : { finalization }),
448
+ });
449
+ }
450
+ async function finalizeExistingPurchaseGrant(request, transaction, context) {
451
+ if (context.purchaseGrantFinalizer === undefined) {
452
+ return undefined;
453
+ }
454
+ const currentProduct = context.catalog.products.find((entry) => entry.id === request.productId);
455
+ const storedPlatformProductId = readStoredPlatformProductId(transaction);
456
+ const product = createStoredPurchaseProduct(request, transaction) ?? currentProduct;
457
+ const platformProductId = storedPlatformProductId
458
+ ?? currentProduct?.platformProductIds[request.target];
459
+ const evidenceVerificationId = transaction.evidenceVerificationId
460
+ ?? transaction.payload.evidenceVerificationId;
461
+ const finalizationRequest = createStoredPurchaseRequest(request, transaction);
462
+ if (product === undefined
463
+ || platformProductId === undefined
464
+ || typeof evidenceVerificationId !== 'string'
465
+ || evidenceVerificationId.length === 0) {
466
+ return assertPurchaseGrantFinalization({
467
+ status: 'pending',
468
+ alreadyCompleted: false,
469
+ reason: 'PURCHASE_FINALIZER_CONTEXT_UNAVAILABLE',
470
+ });
471
+ }
472
+ return finalizePurchaseGrant(context.purchaseGrantFinalizer, {
473
+ request: finalizationRequest,
474
+ product,
475
+ platformProductId,
476
+ evidenceVerificationId,
477
+ evidencePayload: transaction.payload,
478
+ ledgerEntryId: transaction.ledgerEntryId,
479
+ alreadyProcessed: true,
480
+ timeoutMs: context.purchaseGrantFinalizationTimeoutMs,
403
481
  });
404
482
  }
483
+ async function finalizePurchaseGrant(finalizer, input) {
484
+ if (finalizer === undefined) {
485
+ return undefined;
486
+ }
487
+ const controller = new AbortController();
488
+ const timeoutDecision = assertPurchaseGrantFinalization({
489
+ status: 'pending',
490
+ alreadyCompleted: false,
491
+ reason: 'PURCHASE_FINALIZER_TIMEOUT',
492
+ });
493
+ let timedOut = false;
494
+ let timeout;
495
+ try {
496
+ if (finalizer.supportsPurchaseGrant?.(input) === false) {
497
+ return undefined;
498
+ }
499
+ return await Promise.race([
500
+ finalizer.finalizePurchaseGrant({ ...input, signal: controller.signal })
501
+ .then(assertPurchaseGrantFinalization)
502
+ .catch((error) => {
503
+ if (timedOut) {
504
+ return timeoutDecision;
505
+ }
506
+ throw error;
507
+ }),
508
+ new Promise((resolve) => {
509
+ timeout = setTimeout(() => {
510
+ timedOut = true;
511
+ resolve(timeoutDecision);
512
+ controller.abort();
513
+ }, input.timeoutMs);
514
+ }),
515
+ ]);
516
+ }
517
+ catch {
518
+ return assertPurchaseGrantFinalization({
519
+ status: 'pending',
520
+ alreadyCompleted: false,
521
+ reason: 'PURCHASE_FINALIZER_ERROR',
522
+ });
523
+ }
524
+ finally {
525
+ if (timeout !== undefined) {
526
+ clearTimeout(timeout);
527
+ }
528
+ }
529
+ }
530
+ function readStoredPlatformProductId(transaction) {
531
+ const platformProductId = transaction.payload.platformProductId;
532
+ return typeof platformProductId === 'string' && platformProductId.length > 0
533
+ ? platformProductId
534
+ : undefined;
535
+ }
536
+ function createStoredPurchaseProduct(request, transaction) {
537
+ const productType = transaction.payload.productType;
538
+ const platformProductId = readStoredPlatformProductId(transaction);
539
+ if (transaction.grant === undefined
540
+ || platformProductId === undefined
541
+ || (productType !== 'consumable'
542
+ && productType !== 'non_consumable'
543
+ && productType !== 'subscription')) {
544
+ return undefined;
545
+ }
546
+ return {
547
+ id: request.productId,
548
+ type: productType,
549
+ grant: transaction.grant,
550
+ platformProductIds: { [request.target]: platformProductId },
551
+ };
552
+ }
553
+ /**
554
+ * Reuses the current request only for retry dimensions already matched against
555
+ * the stored grant, while restoring provider identity and time metadata from
556
+ * the durable transaction for a coherent finalization retry.
557
+ */
558
+ function createStoredPurchaseRequest(request, transaction) {
559
+ const platformTransactionId = transaction.payload.platformTransactionId;
560
+ const purchasedAt = transaction.payload.purchasedAt;
561
+ return {
562
+ ...request,
563
+ ...(typeof platformTransactionId === 'string' && platformTransactionId.length > 0
564
+ ? { platformTransactionId }
565
+ : {}),
566
+ ...(typeof purchasedAt === 'string' && purchasedAt.length > 0
567
+ ? { purchasedAt }
568
+ : {}),
569
+ };
570
+ }
405
571
  async function claimAdRewardWithStore(input, context) {
406
572
  const request = assertClaimAdRewardRequest(input);
407
573
  const retryIdentity = {
@@ -538,6 +704,14 @@ async function verifyEvidence(verify, timeoutMs) {
538
704
  }
539
705
  }
540
706
  }
707
+ function createPurchaseVerificationPayload(verification) {
708
+ if (verification.platformEvidenceId !== null || verification.payload === undefined) {
709
+ return verification.payload;
710
+ }
711
+ const { platformTransactionId: suppressedPlatformTransactionId, ...payload } = verification.payload;
712
+ void suppressedPlatformTransactionId;
713
+ return payload;
714
+ }
541
715
  function assertEvidenceVerificationDecision(decision) {
542
716
  if (decision.status === 'verified') {
543
717
  assertNonEmptyDecisionString(decision.verificationId, 'verificationId');
@@ -559,6 +733,10 @@ function assertEvidenceVerificationDecision(decision) {
559
733
  }
560
734
  }
561
735
  }
736
+ if (decision.platformEvidenceId !== undefined
737
+ && decision.platformEvidenceId !== null) {
738
+ assertNonEmptyDecisionString(decision.platformEvidenceId, 'platformEvidenceId');
739
+ }
562
740
  return decision;
563
741
  }
564
742
  if (decision.status === 'pending') {
@@ -739,6 +917,13 @@ function resolveEvidenceVerificationTimeout(timeoutMs) {
739
917
  }
740
918
  return resolved;
741
919
  }
920
+ function resolvePurchaseGrantFinalizationTimeout(timeoutMs) {
921
+ const resolved = timeoutMs ?? 15000;
922
+ if (!Number.isFinite(resolved) || resolved <= 0) {
923
+ throw new Error('purchaseGrantFinalizationTimeoutMs must be a positive finite number.');
924
+ }
925
+ return resolved;
926
+ }
742
927
  function assertNonEmptyDecisionString(value, label) {
743
928
  if (typeof value !== 'string' || value.length === 0) {
744
929
  throw new Error(`Evidence verification ${label} must be a non-empty string.`);
package/dist/types.d.ts CHANGED
@@ -24,11 +24,20 @@ export interface VerifyPurchaseResponse {
24
24
  readonly ledgerEntryId?: string;
25
25
  readonly alreadyProcessed: boolean;
26
26
  readonly reason?: string;
27
+ readonly finalization?: PurchaseGrantFinalization;
28
+ }
29
+ export type PurchaseGrantFinalizationAction = 'acknowledge' | 'consume' | 'finish' | 'complete';
30
+ export interface PurchaseGrantFinalization {
31
+ readonly status: 'completed' | 'pending';
32
+ readonly action?: PurchaseGrantFinalizationAction;
33
+ readonly alreadyCompleted: boolean;
34
+ readonly reason?: string;
27
35
  }
28
36
  export interface ClaimAdRewardRequest {
29
37
  readonly target: GameServicesAdRewardTarget;
30
38
  readonly playerId: string;
31
39
  readonly placementId: LogicalAdPlacementId;
40
+ /** Platform evidence identity; AIT uses the game-issued reward correlation identifier. */
32
41
  readonly platformImpressionId?: string;
33
42
  readonly idempotencyKey: AdRewardIdempotencyKey;
34
43
  readonly completedAt: string;
@@ -82,6 +91,7 @@ export interface LeaderboardScoreTransaction extends RecordLeaderboardScoreReque
82
91
  }
83
92
  export declare function assertVerifyPurchaseRequest(input: VerifyPurchaseRequest): VerifyPurchaseRequest;
84
93
  export declare function assertVerifyPurchaseResponse(input: VerifyPurchaseResponse): VerifyPurchaseResponse;
94
+ export declare function assertPurchaseGrantFinalization(input: PurchaseGrantFinalization): PurchaseGrantFinalization;
85
95
  export declare function assertClaimAdRewardRequest(input: ClaimAdRewardRequest): ClaimAdRewardRequest;
86
96
  export declare function assertClaimAdRewardResponse(input: ClaimAdRewardResponse): ClaimAdRewardResponse;
87
97
  export declare function assertRecordLeaderboardScoreRequest(input: RecordLeaderboardScoreRequest): RecordLeaderboardScoreRequest;
package/dist/types.js CHANGED
@@ -15,6 +15,30 @@ export function assertVerifyPurchaseResponse(input) {
15
15
  assertOptionalNonEmptyString(input.ledgerEntryId, 'ledgerEntryId');
16
16
  assertBoolean(input.alreadyProcessed, 'alreadyProcessed');
17
17
  assertOptionalNonEmptyString(input.reason, 'reason');
18
+ if (input.finalization !== undefined) {
19
+ assertPurchaseGrantFinalization(input.finalization);
20
+ }
21
+ return input;
22
+ }
23
+ export function assertPurchaseGrantFinalization(input) {
24
+ assertRecord(input, 'PurchaseGrantFinalization');
25
+ if (input.status !== 'completed' && input.status !== 'pending') {
26
+ throw new Error('finalization.status must be completed or pending.');
27
+ }
28
+ if (input.action !== undefined) {
29
+ assertPurchaseGrantFinalizationAction(input.action);
30
+ }
31
+ if (input.status === 'completed' && input.action === undefined) {
32
+ throw new Error('completed finalization requires an action.');
33
+ }
34
+ assertBoolean(input.alreadyCompleted, 'finalization.alreadyCompleted');
35
+ assertOptionalNonEmptyString(input.reason, 'finalization.reason');
36
+ if (input.status === 'pending' && input.alreadyCompleted) {
37
+ throw new Error('pending finalization cannot already be completed.');
38
+ }
39
+ if (input.status === 'pending' && input.reason === undefined) {
40
+ throw new Error('pending finalization requires a reason.');
41
+ }
18
42
  return input;
19
43
  }
20
44
  export function assertClaimAdRewardRequest(input) {
@@ -140,6 +164,14 @@ function assertBoolean(input, label) {
140
164
  throw new Error(`${label} must be a boolean.`);
141
165
  }
142
166
  }
167
+ function assertPurchaseGrantFinalizationAction(input) {
168
+ if (input !== 'acknowledge'
169
+ && input !== 'consume'
170
+ && input !== 'finish'
171
+ && input !== 'complete') {
172
+ throw new Error('finalization.action must be acknowledge, consume, finish, or complete.');
173
+ }
174
+ }
143
175
  function assertLedgerSource(input) {
144
176
  if (input !== 'purchase' && input !== 'ad_reward' && input !== 'admin') {
145
177
  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.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,6 +29,34 @@
29
29
  "types": "./dist/index.d.ts",
30
30
  "default": "./dist/index.js"
31
31
  },
32
+ "./admob-ssv": {
33
+ "types": "./dist/admob-ssv.d.ts",
34
+ "default": "./dist/admob-ssv.js"
35
+ },
36
+ "./admob-ssv-conformance": {
37
+ "types": "./dist/admob-ssv-conformance.d.ts",
38
+ "default": "./dist/admob-ssv-conformance.js"
39
+ },
40
+ "./app-store-verifier": {
41
+ "types": "./dist/app-store-verifier.d.ts",
42
+ "default": "./dist/app-store-verifier.js"
43
+ },
44
+ "./app-store-verifier-conformance": {
45
+ "types": "./dist/app-store-verifier-conformance.d.ts",
46
+ "default": "./dist/app-store-verifier-conformance.js"
47
+ },
48
+ "./apps-in-toss-evidence-verification": {
49
+ "types": "./dist/apps-in-toss-evidence-verification.d.ts",
50
+ "default": "./dist/apps-in-toss-evidence-verification.js"
51
+ },
52
+ "./apps-in-toss-evidence-verification-conformance": {
53
+ "types": "./dist/apps-in-toss-evidence-verification-conformance.d.ts",
54
+ "default": "./dist/apps-in-toss-evidence-verification-conformance.js"
55
+ },
56
+ "./apps-in-toss-partner-api": {
57
+ "types": "./dist/apps-in-toss-partner-api.d.ts",
58
+ "default": "./dist/apps-in-toss-partner-api.js"
59
+ },
32
60
  "./client": {
33
61
  "types": "./dist/client.d.ts",
34
62
  "default": "./dist/client.js"
@@ -41,6 +69,14 @@
41
69
  "types": "./dist/evidence-verification.d.ts",
42
70
  "default": "./dist/evidence-verification.js"
43
71
  },
72
+ "./google-play-purchase": {
73
+ "types": "./dist/google-play-purchase.d.ts",
74
+ "default": "./dist/google-play-purchase.js"
75
+ },
76
+ "./google-play-purchase-conformance": {
77
+ "types": "./dist/google-play-purchase-conformance.d.ts",
78
+ "default": "./dist/google-play-purchase-conformance.js"
79
+ },
44
80
  "./notification-delivery": {
45
81
  "types": "./dist/notification-delivery.d.ts",
46
82
  "default": "./dist/notification-delivery.js"
@@ -86,12 +122,13 @@
86
122
  "dist"
87
123
  ],
88
124
  "dependencies": {
125
+ "@noble/secp256k1": "3.1.0",
89
126
  "@orpc/client": "2.0.0-beta.14",
90
127
  "@orpc/contract": "2.0.0-beta.14",
91
128
  "@orpc/server": "2.0.0-beta.14",
92
- "@mpgd/analytics": "0.3.7",
93
- "@mpgd/catalog": "0.5.0",
94
- "@mpgd/platform": "0.7.0"
129
+ "@mpgd/catalog": "0.5.1",
130
+ "@mpgd/analytics": "0.3.8",
131
+ "@mpgd/platform": "0.8.0"
95
132
  },
96
133
  "devDependencies": {
97
134
  "ttsc": "0.18.4",
@@ -107,6 +144,6 @@
107
144
  "lint": "ttsc --noEmit",
108
145
  "format": "ttsc format",
109
146
  "fix": "ttsc fix",
110
- "test": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.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/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/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"
147
+ "test": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.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/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"
111
148
  }
112
149
  }