@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 +1 -0
- package/dist/client.js +6 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +3 -0
- package/dist/server.d.ts +5 -2
- package/dist/server.js +50 -6
- package/dist/types.d.ts +5 -0
- package/dist/types.js +30 -0
- package/package.json +2 -2
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
|
|
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.
|
|
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.
|
|
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.
|
|
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": {
|