@mpgd/game-services 0.11.0 → 0.11.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.
package/dist/client.d.ts CHANGED
@@ -74,6 +74,7 @@ export interface CreateGameServicesClientInput {
74
74
  readonly backend: GameServicesBackendApi;
75
75
  readonly playerId: string;
76
76
  readonly target: GameServicesLedgerTarget;
77
+ readonly deploymentTarget?: string;
77
78
  readonly analytics?: AnalyticsSink;
78
79
  readonly analyticsSessionId?: string;
79
80
  readonly now?: () => string;
package/dist/client.js CHANGED
@@ -83,6 +83,9 @@ export function createGameServicesClient(input) {
83
83
  }
84
84
  const verification = await input.backend.purchases.verifyPurchase({
85
85
  target,
86
+ ...(input.deploymentTarget === undefined || input.deploymentTarget === target
87
+ ? {}
88
+ : { deploymentTarget: input.deploymentTarget }),
86
89
  playerId: input.playerId,
87
90
  productId: purchaseInput.productId,
88
91
  platformTransactionId: purchase.transactionId,
@@ -148,6 +151,9 @@ export function createGameServicesClient(input) {
148
151
  }
149
152
  const claim = await input.backend.adRewards.claimAdReward({
150
153
  target,
154
+ ...(input.deploymentTarget === undefined || input.deploymentTarget === target
155
+ ? {}
156
+ : { deploymentTarget: input.deploymentTarget }),
151
157
  playerId: input.playerId,
152
158
  placementId: rewardInput.placementId,
153
159
  ...(reward.ledgerEntryId === undefined
package/dist/runtime.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface CreateGameServicesRuntimeInput {
17
17
  readonly playerId: string;
18
18
  readonly authorityMode: GameServicesAuthorityMode;
19
19
  readonly target?: PlatformTarget | string;
20
+ readonly deploymentTarget?: string;
20
21
  readonly baseUrl?: string;
21
22
  readonly transport?: 'http' | 'orpc';
22
23
  readonly allowLocalBackend?: boolean;
package/dist/runtime.js CHANGED
@@ -46,6 +46,9 @@ export function createGameServicesRuntime(input) {
46
46
  backend,
47
47
  playerId: input.playerId,
48
48
  target,
49
+ ...(input.deploymentTarget === undefined
50
+ ? {}
51
+ : { deploymentTarget: input.deploymentTarget }),
49
52
  ...(input.analytics === undefined ? {} : { analytics: input.analytics }),
50
53
  ...(input.analyticsSessionId === undefined
51
54
  ? {}
package/dist/server.d.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import { type AnalyticsSink } from '@mpgd/analytics';
2
- import type { AdPlacements, ProductCatalog } from '@mpgd/catalog';
2
+ 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 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 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;
10
10
  readonly placements: AdPlacements;
11
+ /** Trusted deployment-config bindings keyed by the request's platform target. */
12
+ readonly deploymentTargetBindings?: GameServicesDeploymentTargetBindings;
11
13
  readonly store?: GameServicesStore;
12
14
  readonly analytics?: AnalyticsSink;
13
15
  readonly analyticsSessionId?: string;
@@ -18,6 +20,7 @@ export interface CreateGameServicesBackendInput {
18
20
  readonly purchaseGrantFinalizationTimeoutMs?: number;
19
21
  readonly purchaseGrantFinalizer?: GameServicesPurchaseGrantFinalizer;
20
22
  }
23
+ export type GameServicesDeploymentTargetBindings = Readonly<Partial<Record<GameServicesAdRewardTarget, string>>>;
21
24
  export interface GameServicesBackendApiHandler {
22
25
  readonly version?: string;
23
26
  handle(request: GameServicesBackendTransportRequest): Promise<GameServicesBackendTransportResponse>;
package/dist/server.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { implement } from '@orpc/server';
2
2
  import { RPCHandler } from '@orpc/server/fetch';
3
3
  import { createAnalyticsReporter } from '@mpgd/analytics';
4
+ import { resolveAdPlacementPlatformId, resolveProductPlatformId, } from '@mpgd/catalog';
4
5
  import { gameServicesBackendEndpoints, } from './client';
5
6
  import { gameServicesContract } from './contract';
6
7
  import { createRejectingGameServicesEvidenceVerifier, } from './evidence-verification';
7
- import { assertClaimAdRewardRequest, assertClaimAdRewardResponse, assertEntitlementLedgerGrant, assertEntitlementLedgerResult, assertLeaderboardScoreTransaction, assertProductGrantTransaction, assertPurchaseGrantFinalization, assertRecordLeaderboardScoreRequest, assertRecordLeaderboardScoreResponse, assertVerifyPurchaseRequest, assertVerifyPurchaseResponse, } from './types';
8
+ import { assertClaimAdRewardRequest, assertClaimAdRewardResponse, assertEntitlementLedgerGrant, assertEntitlementLedgerResult, assertGameServicesDeploymentTarget, assertLeaderboardScoreTransaction, assertProductGrantTransaction, assertPurchaseGrantFinalization, assertRecordLeaderboardScoreRequest, assertRecordLeaderboardScoreResponse, assertVerifyPurchaseRequest, assertVerifyPurchaseResponse, } from './types';
8
9
  export class InMemoryGameServicesStore {
9
10
  entitlementTransactionsByKey = new Map();
10
11
  entitlementTransactionsByEvidence = new Map();
@@ -130,6 +131,7 @@ export function createGameServicesBackend(input) {
130
131
  const evidenceVerifier = input.evidenceVerifier ?? createRejectingGameServicesEvidenceVerifier();
131
132
  const evidenceVerificationTimeoutMs = resolveEvidenceVerificationTimeout(input.evidenceVerificationTimeoutMs);
132
133
  const purchaseGrantFinalizationTimeoutMs = resolvePurchaseGrantFinalizationTimeout(input.purchaseGrantFinalizationTimeoutMs);
134
+ const deploymentTargetBindings = resolveDeploymentTargetBindings(input.deploymentTargetBindings);
133
135
  const analytics = createAnalyticsReporter({
134
136
  target: 'server',
135
137
  sessionId: input.analyticsSessionId ?? 'game-services',
@@ -147,6 +149,7 @@ export function createGameServicesBackend(input) {
147
149
  evidenceVerifier,
148
150
  evidenceVerificationTimeoutMs,
149
151
  purchaseGrantFinalizationTimeoutMs,
152
+ deploymentTargetBindings,
150
153
  ...(input.purchaseGrantFinalizer === undefined
151
154
  ? {}
152
155
  : { purchaseGrantFinalizer: input.purchaseGrantFinalizer }),
@@ -177,6 +180,7 @@ export function createGameServicesBackend(input) {
177
180
  now,
178
181
  evidenceVerifier,
179
182
  evidenceVerificationTimeoutMs,
183
+ deploymentTargetBindings,
180
184
  });
181
185
  await analytics.track({
182
186
  name: claim.granted ? 'rewarded_ad_granted' : 'rewarded_ad_rejected',
@@ -313,14 +317,44 @@ export function createGameServicesHttpFetchHandler(handler, options = {}) {
313
317
  }
314
318
  };
315
319
  }
320
+ const gameServicesDeploymentTargetPlatforms = new Set([
321
+ 'android',
322
+ 'ios',
323
+ 'ait',
324
+ 'verse8',
325
+ ]);
326
+ function resolveDeploymentTargetBindings(input) {
327
+ const bindings = {};
328
+ for (const [platformTarget, deploymentTarget] of Object.entries(input ?? {})) {
329
+ if (!gameServicesDeploymentTargetPlatforms.has(platformTarget)) {
330
+ throw new Error(`Unsupported deployment target binding platform: ${platformTarget}.`);
331
+ }
332
+ assertGameServicesDeploymentTarget(deploymentTarget);
333
+ bindings[platformTarget] = deploymentTarget;
334
+ }
335
+ return bindings;
336
+ }
337
+ function bindRequestDeploymentTarget(request, bindings) {
338
+ const deploymentTarget = bindings[request.target] ?? request.target;
339
+ if (request.deploymentTarget !== undefined
340
+ && request.deploymentTarget !== deploymentTarget) {
341
+ throw new Error(`deploymentTarget must match the backend binding for ${request.target}.`);
342
+ }
343
+ return deploymentTarget === request.target || request.deploymentTarget === deploymentTarget
344
+ ? request
345
+ : { ...request, deploymentTarget };
346
+ }
316
347
  async function verifyPurchaseWithStore(input, context) {
317
- const request = assertVerifyPurchaseRequest(input);
348
+ const request = bindRequestDeploymentTarget(assertVerifyPurchaseRequest(input), context.deploymentTargetBindings);
318
349
  const retryIdentity = {
319
350
  source: 'purchase',
320
351
  playerId: request.playerId,
321
352
  idempotencyKey: request.idempotencyKey,
322
353
  grantId: request.productId,
323
354
  target: request.target,
355
+ ...(request.deploymentTarget === undefined
356
+ ? {}
357
+ : { deploymentTarget: request.deploymentTarget }),
324
358
  };
325
359
  const completeExistingRetry = async (transaction) => {
326
360
  if (!matchesEntitlementRetry(transaction, retryIdentity)) {
@@ -350,7 +384,7 @@ async function verifyPurchaseWithStore(input, context) {
350
384
  reason: 'UNKNOWN_PRODUCT',
351
385
  });
352
386
  }
353
- const platformProductId = product.platformProductIds[request.target];
387
+ const platformProductId = resolveProductPlatformId(product, request.deploymentTarget ?? request.target);
354
388
  if (platformProductId === undefined) {
355
389
  return assertVerifyPurchaseResponse({
356
390
  verified: false,
@@ -394,6 +428,9 @@ async function verifyPurchaseWithStore(input, context) {
394
428
  payload: {
395
429
  ...verificationPayload,
396
430
  target: request.target,
431
+ ...(request.deploymentTarget === undefined
432
+ ? {}
433
+ : { deploymentTarget: request.deploymentTarget }),
397
434
  productId: request.productId,
398
435
  productType: product.type,
399
436
  platformProductId,
@@ -569,13 +606,16 @@ function createStoredPurchaseRequest(request, transaction) {
569
606
  };
570
607
  }
571
608
  async function claimAdRewardWithStore(input, context) {
572
- const request = assertClaimAdRewardRequest(input);
609
+ const request = bindRequestDeploymentTarget(assertClaimAdRewardRequest(input), context.deploymentTargetBindings);
573
610
  const retryIdentity = {
574
611
  source: 'ad_reward',
575
612
  playerId: request.playerId,
576
613
  idempotencyKey: request.idempotencyKey,
577
614
  grantId: request.placementId,
578
615
  target: request.target,
616
+ ...(request.deploymentTarget === undefined
617
+ ? {}
618
+ : { deploymentTarget: request.deploymentTarget }),
579
619
  };
580
620
  const existing = await findEntitlementTransactionByIdempotency(context.store, retryIdentity);
581
621
  if (existing !== undefined) {
@@ -600,7 +640,7 @@ async function claimAdRewardWithStore(input, context) {
600
640
  reason: placement === undefined ? 'UNKNOWN_PLACEMENT' : 'NOT_REWARDED_PLACEMENT',
601
641
  });
602
642
  }
603
- const platformPlacementId = placement.platformPlacementIds[request.target];
643
+ const platformPlacementId = resolveAdPlacementPlatformId(placement, request.deploymentTarget ?? request.target);
604
644
  const verification = await verifyEvidence((signal) => {
605
645
  return context.evidenceVerifier.verifyAdReward({
606
646
  request,
@@ -622,6 +662,9 @@ async function claimAdRewardWithStore(input, context) {
622
662
  const payload = {
623
663
  ...verification.payload,
624
664
  target: request.target,
665
+ ...(request.deploymentTarget === undefined
666
+ ? {}
667
+ : { deploymentTarget: request.deploymentTarget }),
625
668
  placementId: request.placementId,
626
669
  rewardType: placement.reward.type,
627
670
  amount: placement.reward.amount,
@@ -908,7 +951,8 @@ function matchesEntitlementRetry(transaction, identity) {
908
951
  && transaction.playerId === identity.playerId
909
952
  && transaction.idempotencyKey === identity.idempotencyKey
910
953
  && transaction.grantId === identity.grantId
911
- && transaction.payload.target === identity.target;
954
+ && transaction.payload.target === identity.target
955
+ && transaction.payload.deploymentTarget === identity.deploymentTarget;
912
956
  }
913
957
  function resolveEvidenceVerificationTimeout(timeoutMs) {
914
958
  const resolved = timeoutMs ?? 10000;
package/dist/types.d.ts CHANGED
@@ -12,6 +12,8 @@ export type EntitlementLedgerSource = 'purchase' | 'ad_reward' | 'admin';
12
12
  export type EntitlementLedgerPayload = Record<string, string | number | boolean>;
13
13
  export interface VerifyPurchaseRequest {
14
14
  readonly target: GameServicesStoreTarget;
15
+ /** Deployment config key used to resolve game-owned platform product identifiers. */
16
+ readonly deploymentTarget?: string;
15
17
  readonly playerId: string;
16
18
  readonly productId: LogicalProductId;
17
19
  readonly platformTransactionId: string;
@@ -35,6 +37,8 @@ export interface PurchaseGrantFinalization {
35
37
  }
36
38
  export interface ClaimAdRewardRequest {
37
39
  readonly target: GameServicesAdRewardTarget;
40
+ /** Deployment config key used to resolve game-owned platform placement identifiers. */
41
+ readonly deploymentTarget?: string;
38
42
  readonly playerId: string;
39
43
  readonly placementId: LogicalAdPlacementId;
40
44
  /** Platform evidence identity; AIT uses the game-issued reward correlation identifier. */
@@ -100,3 +104,4 @@ export declare function assertEntitlementLedgerGrant(input: EntitlementLedgerGra
100
104
  export declare function assertEntitlementLedgerResult(input: EntitlementLedgerResult): EntitlementLedgerResult;
101
105
  export declare function assertProductGrantTransaction(input: ProductGrantTransaction): ProductGrantTransaction;
102
106
  export declare function assertLeaderboardScoreTransaction(input: LeaderboardScoreTransaction): LeaderboardScoreTransaction;
107
+ export declare function assertGameServicesDeploymentTarget(input: unknown): asserts input is string;
package/dist/types.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export function assertVerifyPurchaseRequest(input) {
2
2
  assertRecord(input, 'VerifyPurchaseRequest');
3
3
  assertStoreTarget(input.target);
4
+ assertOptionalGameServicesDeploymentTarget(input.deploymentTarget);
4
5
  assertNonEmptyString(input.playerId, 'playerId');
5
6
  assertNonEmptyString(input.productId, 'productId');
6
7
  assertNonEmptyString(input.platformTransactionId, 'platformTransactionId');
@@ -44,6 +45,7 @@ export function assertPurchaseGrantFinalization(input) {
44
45
  export function assertClaimAdRewardRequest(input) {
45
46
  assertRecord(input, 'ClaimAdRewardRequest');
46
47
  assertAdRewardTarget(input.target);
48
+ assertOptionalGameServicesDeploymentTarget(input.deploymentTarget);
47
49
  assertNonEmptyString(input.playerId, 'playerId');
48
50
  assertNonEmptyString(input.placementId, 'placementId');
49
51
  assertOptionalNonEmptyString(input.platformImpressionId, 'platformImpressionId');
@@ -135,6 +137,34 @@ function assertAdRewardTarget(input) {
135
137
  throw new Error('target must be android, ios, ait, or verse8.');
136
138
  }
137
139
  }
140
+ export function assertGameServicesDeploymentTarget(input) {
141
+ if (typeof input !== 'string'
142
+ || input.length > 64
143
+ || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(input)
144
+ || reservedDeploymentTargetNames.has(input)) {
145
+ throw new Error('deploymentTarget must use lowercase kebab-case and avoid reserved names.');
146
+ }
147
+ }
148
+ function assertOptionalGameServicesDeploymentTarget(input) {
149
+ if (input === undefined) {
150
+ return;
151
+ }
152
+ assertGameServicesDeploymentTarget(input);
153
+ }
154
+ const reservedDeploymentTargetNames = new Set([
155
+ 'aux',
156
+ 'browser',
157
+ 'con',
158
+ 'devvit',
159
+ 'index',
160
+ 'msstore',
161
+ 'nul',
162
+ 'prn',
163
+ 'constructor',
164
+ 'prototype',
165
+ ...Array.from({ length: 9 }, (_, index) => `com${String(index + 1)}`),
166
+ ...Array.from({ length: 9 }, (_, index) => `lpt${String(index + 1)}`),
167
+ ]);
138
168
  function assertLeaderboardTarget(input) {
139
169
  if (input !== 'browser'
140
170
  && input !== 'android'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/game-services",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -126,8 +126,8 @@
126
126
  "@orpc/client": "2.0.0-beta.14",
127
127
  "@orpc/contract": "2.0.0-beta.14",
128
128
  "@orpc/server": "2.0.0-beta.14",
129
- "@mpgd/catalog": "0.5.1",
130
129
  "@mpgd/analytics": "0.3.8",
130
+ "@mpgd/catalog": "0.5.2",
131
131
  "@mpgd/platform": "0.8.0"
132
132
  },
133
133
  "devDependencies": {