@mpgd/game-services 0.11.1 → 0.12.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.
@@ -8,6 +8,7 @@ const platformTargets = new Set([
8
8
  'verse8',
9
9
  'telegram',
10
10
  'tauri',
11
+ 'microsoft-store',
11
12
  ]);
12
13
  const notificationTopics = new Set([
13
14
  'daily-ready',
package/dist/runtime.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface CreateGameServicesRuntimeInput {
20
20
  readonly deploymentTarget?: string;
21
21
  readonly baseUrl?: string;
22
22
  readonly transport?: 'http' | 'orpc';
23
+ /** Static headers sent with every authoritative game-services request. */
24
+ readonly headers?: Record<string, string>;
23
25
  readonly allowLocalBackend?: boolean;
24
26
  readonly localBackend?: GameServicesBackendApi;
25
27
  readonly analytics?: AnalyticsSink;
package/dist/runtime.js CHANGED
@@ -19,9 +19,15 @@ export function createGameServicesRuntime(input) {
19
19
  }
20
20
  mode = input.transport === 'orpc' ? 'orpc' : 'http';
21
21
  backend = mode === 'orpc'
22
- ? createGameServicesOrpcBackendApi(createGameServicesOrpcClient({ url: baseUrl }))
22
+ ? createGameServicesOrpcBackendApi(createGameServicesOrpcClient({
23
+ url: baseUrl,
24
+ ...(input.headers === undefined ? {} : { headers: input.headers }),
25
+ }))
23
26
  : createGameServicesHttpBackendApi({
24
- transport: createGameServicesFetchBackendTransport({ baseUrl }),
27
+ transport: createGameServicesFetchBackendTransport({
28
+ baseUrl,
29
+ ...(input.headers === undefined ? {} : { headers: input.headers }),
30
+ }),
25
31
  });
26
32
  }
27
33
  else {
@@ -59,6 +65,7 @@ export function createGameServicesRuntime(input) {
59
65
  }
60
66
  export function resolveGameServicesLedgerTarget(target) {
61
67
  if (target === 'browser'
68
+ || target === 'microsoft-store'
62
69
  || target === 'android'
63
70
  || target === 'ios'
64
71
  || target === 'ait'
package/dist/server.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type AdPlacements, type ProductCatalog } from '@mpgd/catalog';
3
3
  import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesBackendTransportRequest, type GameServicesBackendTransportResponse } from './client';
4
4
  import { type GameServicesHealthResponse } from './contract';
5
5
  import { type GameServicesEvidenceVerifier, type GameServicesPurchaseGrantFinalizer } from './evidence-verification';
6
- import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerResult, type GameServicesAdRewardTarget, type LeaderboardScoreTransaction, type ProductGrantTransaction, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types';
6
+ import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerResult, type GameServicesAdRewardTarget, type GameServicesStoreTarget, type LeaderboardScoreTransaction, type ProductGrantTransaction, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types';
7
7
  type CorsHeaders = Record<string, string>;
8
8
  export interface CreateGameServicesBackendInput {
9
9
  readonly catalog: ProductCatalog;
@@ -20,7 +20,7 @@ export interface CreateGameServicesBackendInput {
20
20
  readonly purchaseGrantFinalizationTimeoutMs?: number;
21
21
  readonly purchaseGrantFinalizer?: GameServicesPurchaseGrantFinalizer;
22
22
  }
23
- export type GameServicesDeploymentTargetBindings = Readonly<Partial<Record<GameServicesAdRewardTarget, string>>>;
23
+ export type GameServicesDeploymentTargetBindings = Readonly<Partial<Record<GameServicesAdRewardTarget | GameServicesStoreTarget, string>>>;
24
24
  export interface GameServicesBackendApiHandler {
25
25
  readonly version?: string;
26
26
  handle(request: GameServicesBackendTransportRequest): Promise<GameServicesBackendTransportResponse>;
package/dist/server.js CHANGED
@@ -318,6 +318,7 @@ export function createGameServicesHttpFetchHandler(handler, options = {}) {
318
318
  };
319
319
  }
320
320
  const gameServicesDeploymentTargetPlatforms = new Set([
321
+ 'microsoft-store',
321
322
  'android',
322
323
  'ios',
323
324
  'ait',
@@ -445,6 +446,30 @@ async function verifyPurchaseWithStore(input, context) {
445
446
  },
446
447
  });
447
448
  if (grant.status === 'evidence_already_processed') {
449
+ const existingEvidence = await findEntitlementTransactionByEvidenceVerificationId(context.store, { source: 'purchase', evidenceVerificationId: verification.verificationId });
450
+ if (existingEvidence !== undefined
451
+ && matchesPurchaseEvidenceRecovery(existingEvidence, {
452
+ playerId: request.playerId,
453
+ grantId: request.productId,
454
+ target: request.target,
455
+ ...(request.deploymentTarget === undefined
456
+ ? {}
457
+ : { deploymentTarget: request.deploymentTarget }),
458
+ evidenceVerificationId: verification.verificationId,
459
+ })) {
460
+ const finalization = await finalizeExistingPurchaseGrant(request, existingEvidence, context);
461
+ // A different idempotency key is evidence replay unless a configured finalizer can resume
462
+ // the provider operation. Keep generic stores fail-closed while allowing Store consume
463
+ // recovery to report the already-recorded grant.
464
+ if (finalization !== undefined) {
465
+ return assertVerifyPurchaseResponse({
466
+ verified: true,
467
+ ledgerEntryId: existingEvidence.ledgerEntryId,
468
+ alreadyProcessed: true,
469
+ finalization,
470
+ });
471
+ }
472
+ }
448
473
  return assertVerifyPurchaseResponse({
449
474
  verified: false,
450
475
  alreadyProcessed: false,
@@ -947,9 +972,20 @@ async function recordedEntitlementMatchesRetry(store, result, identity) {
947
972
  return transaction !== undefined && matchesEntitlementRetry(transaction, identity);
948
973
  }
949
974
  function matchesEntitlementRetry(transaction, identity) {
975
+ return matchesEntitlementTransactionIdentity(transaction, identity)
976
+ && transaction.idempotencyKey === identity.idempotencyKey;
977
+ }
978
+ function matchesPurchaseEvidenceRecovery(transaction, identity) {
979
+ return matchesEntitlementTransactionIdentity(transaction, {
980
+ ...identity,
981
+ source: 'purchase',
982
+ })
983
+ && (transaction.evidenceVerificationId
984
+ ?? transaction.payload.evidenceVerificationId) === identity.evidenceVerificationId;
985
+ }
986
+ function matchesEntitlementTransactionIdentity(transaction, identity) {
950
987
  return transaction.source === identity.source
951
988
  && transaction.playerId === identity.playerId
952
- && transaction.idempotencyKey === identity.idempotencyKey
953
989
  && transaction.grantId === identity.grantId
954
990
  && transaction.payload.target === identity.target
955
991
  && transaction.payload.deploymentTarget === identity.deploymentTarget;
package/dist/types.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { ProductGrant } from '@mpgd/catalog';
2
2
  import type { LeaderboardScoreInput, LogicalAdPlacementId, LogicalProductId, PlatformEvidenceEnvelope, PlatformTarget } from '@mpgd/platform';
3
3
  export type { PlatformEvidenceEnvelope } from '@mpgd/platform';
4
- export type GameServicesStoreTarget = Extract<PlatformTarget, 'android' | 'ios' | 'ait'>;
4
+ export type GameServicesStoreTarget = Extract<PlatformTarget, 'microsoft-store' | 'android' | 'ios' | 'ait'>;
5
5
  export type GameServicesAdRewardTarget = Extract<PlatformTarget, 'android' | 'ios' | 'ait' | 'verse8'>;
6
- export type GameServicesLeaderboardTarget = Extract<PlatformTarget, 'browser' | 'android' | 'ios' | 'ait' | 'reddit'>;
7
- export type GameServicesLedgerTarget = Extract<PlatformTarget, 'browser' | 'android' | 'ios' | 'ait' | 'reddit' | 'verse8'>;
6
+ export type GameServicesLeaderboardTarget = Extract<PlatformTarget, 'browser' | 'microsoft-store' | 'android' | 'ios' | 'ait' | 'reddit'>;
7
+ export type GameServicesLedgerTarget = Extract<PlatformTarget, 'browser' | 'microsoft-store' | 'android' | 'ios' | 'ait' | 'reddit' | 'verse8'>;
8
8
  export type PurchaseIdempotencyKey = string;
9
9
  export type AdRewardIdempotencyKey = string;
10
10
  export type EntitlementIdempotencyKey = PurchaseIdempotencyKey | AdRewardIdempotencyKey;
package/dist/types.js CHANGED
@@ -128,8 +128,11 @@ function assertRecord(input, label) {
128
128
  }
129
129
  }
130
130
  function assertStoreTarget(input) {
131
- if (input !== 'android' && input !== 'ios' && input !== 'ait') {
132
- throw new Error('target must be android, ios, or ait.');
131
+ if (input !== 'microsoft-store'
132
+ && input !== 'android'
133
+ && input !== 'ios'
134
+ && input !== 'ait') {
135
+ throw new Error('target must be microsoft-store, android, ios, or ait.');
133
136
  }
134
137
  }
135
138
  function assertAdRewardTarget(input) {
@@ -167,11 +170,12 @@ const reservedDeploymentTargetNames = new Set([
167
170
  ]);
168
171
  function assertLeaderboardTarget(input) {
169
172
  if (input !== 'browser'
173
+ && input !== 'microsoft-store'
170
174
  && input !== 'android'
171
175
  && input !== 'ios'
172
176
  && input !== 'ait'
173
177
  && input !== 'reddit') {
174
- throw new Error('target must be browser, android, ios, ait, or reddit.');
178
+ throw new Error('target must be browser, microsoft-store, android, ios, ait, or reddit.');
175
179
  }
176
180
  }
177
181
  function assertNonEmptyString(input, label) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/game-services",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -73,6 +73,10 @@
73
73
  "types": "./dist/google-play-purchase.d.ts",
74
74
  "default": "./dist/google-play-purchase.js"
75
75
  },
76
+ "./microsoft-store-purchase": {
77
+ "types": "./dist/microsoft-store-purchase.d.ts",
78
+ "default": "./dist/microsoft-store-purchase.js"
79
+ },
76
80
  "./google-play-purchase-conformance": {
77
81
  "types": "./dist/google-play-purchase-conformance.d.ts",
78
82
  "default": "./dist/google-play-purchase-conformance.js"
@@ -126,9 +130,9 @@
126
130
  "@orpc/client": "2.0.0-beta.14",
127
131
  "@orpc/contract": "2.0.0-beta.14",
128
132
  "@orpc/server": "2.0.0-beta.14",
129
- "@mpgd/analytics": "0.3.8",
130
- "@mpgd/catalog": "0.5.2",
131
- "@mpgd/platform": "0.8.0"
133
+ "@mpgd/catalog": "0.6.0",
134
+ "@mpgd/platform": "0.9.0",
135
+ "@mpgd/analytics": "0.3.9"
132
136
  },
133
137
  "devDependencies": {
134
138
  "ttsc": "0.18.4",
@@ -144,6 +148,6 @@
144
148
  "lint": "ttsc --noEmit",
145
149
  "format": "ttsc format",
146
150
  "fix": "ttsc fix",
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"
151
+ "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/microsoft-store-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"
148
152
  }
149
153
  }