@mpgd/game-services 0.16.0 → 0.17.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/admob-client-reward.d.ts +3 -0
- package/dist/admob-client-reward.js +21 -0
- package/dist/admob-ssv.d.ts +2 -0
- package/dist/admob-ssv.js +2 -0
- package/dist/app-store-recovery.d.ts +38 -0
- package/dist/app-store-recovery.js +271 -0
- package/dist/app-store-verifier.d.ts +7 -0
- package/dist/app-store-verifier.js +41 -3
- package/dist/authoritative-purchase.d.ts +9 -0
- package/dist/authoritative-purchase.js +10 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +14 -16
- package/dist/google-play-publisher-client.d.ts +11 -0
- package/dist/google-play-publisher-client.js +113 -0
- package/dist/google-play-purchase.d.ts +2 -0
- package/dist/google-play-purchase.js +21 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/monetization-recovery.d.ts +63 -0
- package/dist/monetization-recovery.js +632 -0
- package/dist/operation-progress.d.ts +7 -1
- package/dist/operation-progress.js +23 -0
- package/dist/operation-results.d.ts +4 -0
- package/dist/operations.d.ts +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +29 -14
- package/dist/server.js +94 -60
- package/dist/types.js +13 -0
- package/package.json +19 -6
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Shared response shapes with no schema, DOM or transport dependencies. */
|
|
2
2
|
export interface VerifyPurchaseResponse {
|
|
3
3
|
readonly verified: boolean;
|
|
4
|
+
/** Explicit non-grant decision; older servers may omit this field. */
|
|
5
|
+
readonly disposition?: 'pending' | 'rejected';
|
|
4
6
|
readonly ledgerEntryId?: string;
|
|
5
7
|
readonly alreadyProcessed: boolean;
|
|
6
8
|
readonly reason?: string;
|
|
@@ -15,6 +17,8 @@ export interface PurchaseGrantFinalization {
|
|
|
15
17
|
}
|
|
16
18
|
export interface ClaimAdRewardResponse {
|
|
17
19
|
readonly granted: boolean;
|
|
20
|
+
/** Explicit non-grant decision; older servers may omit this field. */
|
|
21
|
+
readonly disposition?: 'pending' | 'rejected';
|
|
18
22
|
readonly ledgerEntryId?: string;
|
|
19
23
|
readonly alreadyProcessed: boolean;
|
|
20
24
|
readonly reason?: string;
|
package/dist/operations.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface GameServicesRewardedAdInput {
|
|
|
23
23
|
readonly idempotencyKey: string;
|
|
24
24
|
}
|
|
25
25
|
export interface GameServicesRewardedAdResult {
|
|
26
|
-
readonly status: 'granted' | 'skipped' | 'unavailable' | 'failed' | 'rejected';
|
|
26
|
+
readonly status: 'granted' | 'pending' | 'skipped' | 'unavailable' | 'failed' | 'rejected';
|
|
27
27
|
readonly reward: RewardedAdResult;
|
|
28
28
|
readonly claim?: ClaimAdRewardResponse;
|
|
29
29
|
readonly ledgerEntryId?: string;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AnalyticsSink } from '@mpgd/analytics';
|
|
|
2
2
|
import type { PlatformGateway, PlatformTarget } from '@mpgd/platform';
|
|
3
3
|
import { type GameServicesBackendApi, type GameServicesBackendTransport, type GameServicesClient, type GameServicesHeaderResolver } from './client.js';
|
|
4
4
|
import type { GameServicesLedgerTarget } from './types.js';
|
|
5
|
+
import { type MonetizationOperationStore, type RecoverableMonetizationClient } from './monetization-recovery.js';
|
|
5
6
|
export type GameServicesAuthorityMode = 'production' | 'non-production';
|
|
6
7
|
export type GameServicesRuntimeMode = 'disabled' | 'local' | 'http' | 'orpc';
|
|
7
8
|
export type GameServicesDisabledReason = 'unsupported_target' | 'missing_authoritative_backend' | 'invalid_authoritative_backend' | 'local_backend_not_allowed' | 'local_backend_unavailable';
|
|
@@ -11,6 +12,7 @@ export interface GameServicesRuntime {
|
|
|
11
12
|
readonly baseUrl?: string;
|
|
12
13
|
readonly target?: GameServicesLedgerTarget;
|
|
13
14
|
readonly client?: GameServicesClient;
|
|
15
|
+
readonly monetizationRecovery?: RecoverableMonetizationClient;
|
|
14
16
|
}
|
|
15
17
|
interface CreateGameServicesRuntimeCommonInput {
|
|
16
18
|
readonly gateway: PlatformGateway;
|
|
@@ -28,6 +30,8 @@ interface CreateGameServicesRuntimeCommonInput {
|
|
|
28
30
|
readonly analytics?: AnalyticsSink;
|
|
29
31
|
readonly analyticsSessionId?: string;
|
|
30
32
|
readonly now?: () => string;
|
|
33
|
+
/** Durable, compare-and-swap journal for purchase and rewarded-ad recovery. */
|
|
34
|
+
readonly operationStore?: MonetizationOperationStore;
|
|
31
35
|
}
|
|
32
36
|
export type CreateGameServicesRuntimeInput = CreateGameServicesRuntimeCommonInput & ({
|
|
33
37
|
readonly transport?: 'http';
|
package/dist/runtime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createGameServicesClient, createGameServicesFetchBackendTransport, createGameServicesHttpBackendApi, createGameServicesOrpcBackendApi, createGameServicesOrpcClient, resolveGameServicesRequestHeaders, } from './client.js';
|
|
2
|
+
import { createRecoverableMonetizationClient, } from './monetization-recovery.js';
|
|
2
3
|
export function createGameServicesRuntime(input) {
|
|
3
4
|
assertAuthorityMode(input.authorityMode);
|
|
4
5
|
assertTransport(input.transport);
|
|
@@ -47,24 +48,38 @@ export function createGameServicesRuntime(input) {
|
|
|
47
48
|
mode = 'local';
|
|
48
49
|
backend = input.localBackend;
|
|
49
50
|
}
|
|
51
|
+
const clientInput = {
|
|
52
|
+
gateway: input.gateway,
|
|
53
|
+
backend,
|
|
54
|
+
playerId: input.playerId,
|
|
55
|
+
target,
|
|
56
|
+
...(input.deploymentTarget === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { deploymentTarget: input.deploymentTarget }),
|
|
59
|
+
...(input.analytics === undefined ? {} : { analytics: input.analytics }),
|
|
60
|
+
...(input.analyticsSessionId === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { analyticsSessionId: input.analyticsSessionId }),
|
|
63
|
+
...(input.now === undefined ? {} : { now: input.now }),
|
|
64
|
+
};
|
|
65
|
+
const client = createGameServicesClient(clientInput);
|
|
66
|
+
const monetizationRecovery = input.operationStore === undefined
|
|
67
|
+
? undefined
|
|
68
|
+
: createRecoverableMonetizationClient({ ...clientInput, operationStore: input.operationStore });
|
|
69
|
+
let routedClient = client;
|
|
70
|
+
if (monetizationRecovery !== undefined) {
|
|
71
|
+
routedClient = {
|
|
72
|
+
...client,
|
|
73
|
+
purchase: monetizationRecovery.purchase,
|
|
74
|
+
claimRewardedAd: monetizationRecovery.claimRewardedAd,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
50
77
|
return {
|
|
51
78
|
mode,
|
|
52
79
|
...(baseUrl === undefined ? {} : { baseUrl }),
|
|
53
80
|
target,
|
|
54
|
-
client:
|
|
55
|
-
|
|
56
|
-
backend,
|
|
57
|
-
playerId: input.playerId,
|
|
58
|
-
target,
|
|
59
|
-
...(input.deploymentTarget === undefined
|
|
60
|
-
? {}
|
|
61
|
-
: { deploymentTarget: input.deploymentTarget }),
|
|
62
|
-
...(input.analytics === undefined ? {} : { analytics: input.analytics }),
|
|
63
|
-
...(input.analyticsSessionId === undefined
|
|
64
|
-
? {}
|
|
65
|
-
: { analyticsSessionId: input.analyticsSessionId }),
|
|
66
|
-
...(input.now === undefined ? {} : { now: input.now }),
|
|
67
|
-
}),
|
|
81
|
+
client: routedClient,
|
|
82
|
+
...(monetizationRecovery === undefined ? {} : { monetizationRecovery }),
|
|
68
83
|
};
|
|
69
84
|
}
|
|
70
85
|
function withRuntimeHeaders(transport, input) {
|
package/dist/server.js
CHANGED
|
@@ -142,7 +142,7 @@ export function createGameServicesBackend(input) {
|
|
|
142
142
|
version,
|
|
143
143
|
purchases: {
|
|
144
144
|
async verifyPurchase(request) {
|
|
145
|
-
|
|
145
|
+
return verifyPurchaseWithStore(request, {
|
|
146
146
|
catalog: input.catalog,
|
|
147
147
|
store,
|
|
148
148
|
now,
|
|
@@ -153,47 +153,49 @@ export function createGameServicesBackend(input) {
|
|
|
153
153
|
...(input.purchaseGrantFinalizer === undefined
|
|
154
154
|
? {}
|
|
155
155
|
: { purchaseGrantFinalizer: input.purchaseGrantFinalizer }),
|
|
156
|
+
}, async (verification) => {
|
|
157
|
+
await analytics.track({
|
|
158
|
+
name: verification.verified ? 'purchase_granted'
|
|
159
|
+
: verification.disposition === 'pending' ? 'purchase_pending' : 'purchase_rejected',
|
|
160
|
+
properties: {
|
|
161
|
+
target: request.target,
|
|
162
|
+
playerId: request.playerId,
|
|
163
|
+
productId: request.productId,
|
|
164
|
+
ledgerEntryId: verification.ledgerEntryId,
|
|
165
|
+
alreadyProcessed: verification.alreadyProcessed,
|
|
166
|
+
reason: verification.reason,
|
|
167
|
+
finalizationStatus: verification.finalization?.status,
|
|
168
|
+
finalizationAction: verification.finalization?.action,
|
|
169
|
+
finalizationAlreadyCompleted: verification.finalization?.alreadyCompleted,
|
|
170
|
+
finalizationReason: verification.finalization?.reason,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
156
173
|
});
|
|
157
|
-
await analytics.track({
|
|
158
|
-
name: verification.verified ? 'purchase_granted' : 'purchase_rejected',
|
|
159
|
-
properties: {
|
|
160
|
-
target: request.target,
|
|
161
|
-
playerId: request.playerId,
|
|
162
|
-
productId: request.productId,
|
|
163
|
-
ledgerEntryId: verification.ledgerEntryId,
|
|
164
|
-
alreadyProcessed: verification.alreadyProcessed,
|
|
165
|
-
reason: verification.reason,
|
|
166
|
-
finalizationStatus: verification.finalization?.status,
|
|
167
|
-
finalizationAction: verification.finalization?.action,
|
|
168
|
-
finalizationAlreadyCompleted: verification.finalization?.alreadyCompleted,
|
|
169
|
-
finalizationReason: verification.finalization?.reason,
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
return verification;
|
|
173
174
|
},
|
|
174
175
|
},
|
|
175
176
|
adRewards: {
|
|
176
177
|
async claimAdReward(request) {
|
|
177
|
-
|
|
178
|
+
return claimAdRewardWithStore(request, {
|
|
178
179
|
placements: input.placements,
|
|
179
180
|
store,
|
|
180
181
|
now,
|
|
181
182
|
evidenceVerifier,
|
|
182
183
|
evidenceVerificationTimeoutMs,
|
|
183
184
|
deploymentTargetBindings,
|
|
185
|
+
}, async (claim) => {
|
|
186
|
+
await analytics.track({
|
|
187
|
+
name: claim.granted ? 'rewarded_ad_granted'
|
|
188
|
+
: claim.disposition === 'pending' ? 'rewarded_ad_pending' : 'rewarded_ad_rejected',
|
|
189
|
+
properties: {
|
|
190
|
+
target: request.target,
|
|
191
|
+
playerId: request.playerId,
|
|
192
|
+
placementId: request.placementId,
|
|
193
|
+
ledgerEntryId: claim.ledgerEntryId,
|
|
194
|
+
alreadyProcessed: claim.alreadyProcessed,
|
|
195
|
+
reason: claim.reason,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
184
198
|
});
|
|
185
|
-
await analytics.track({
|
|
186
|
-
name: claim.granted ? 'rewarded_ad_granted' : 'rewarded_ad_rejected',
|
|
187
|
-
properties: {
|
|
188
|
-
target: request.target,
|
|
189
|
-
playerId: request.playerId,
|
|
190
|
-
placementId: request.placementId,
|
|
191
|
-
ledgerEntryId: claim.ledgerEntryId,
|
|
192
|
-
alreadyProcessed: claim.alreadyProcessed,
|
|
193
|
-
reason: claim.reason,
|
|
194
|
-
},
|
|
195
|
-
});
|
|
196
|
-
return claim;
|
|
197
199
|
},
|
|
198
200
|
},
|
|
199
201
|
leaderboard: {
|
|
@@ -345,7 +347,7 @@ function bindRequestDeploymentTarget(request, bindings) {
|
|
|
345
347
|
? request
|
|
346
348
|
: { ...request, deploymentTarget };
|
|
347
349
|
}
|
|
348
|
-
async function verifyPurchaseWithStore(input, context) {
|
|
350
|
+
async function verifyPurchaseWithStore(input, context, onDecision) {
|
|
349
351
|
const request = bindRequestDeploymentTarget(assertVerifyPurchaseRequest(input), context.deploymentTargetBindings);
|
|
350
352
|
const retryIdentity = {
|
|
351
353
|
source: 'purchase',
|
|
@@ -357,6 +359,13 @@ async function verifyPurchaseWithStore(input, context) {
|
|
|
357
359
|
? {}
|
|
358
360
|
: { deploymentTarget: request.deploymentTarget }),
|
|
359
361
|
};
|
|
362
|
+
return withEntitlementRetryLock(retryIdentity, async () => {
|
|
363
|
+
const verification = await verifyPurchaseForRetry(request, retryIdentity, context);
|
|
364
|
+
await onDecision(verification);
|
|
365
|
+
return verification;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async function verifyPurchaseForRetry(request, retryIdentity, context) {
|
|
360
369
|
const completeExistingRetry = async (transaction) => {
|
|
361
370
|
if (!matchesEntitlementRetry(transaction, retryIdentity)) {
|
|
362
371
|
return assertVerifyPurchaseResponse({
|
|
@@ -412,6 +421,7 @@ async function verifyPurchaseWithStore(input, context) {
|
|
|
412
421
|
return assertVerifyPurchaseResponse({
|
|
413
422
|
verified: false,
|
|
414
423
|
alreadyProcessed: false,
|
|
424
|
+
disposition: verification.status === 'pending' ? 'pending' : 'rejected',
|
|
415
425
|
reason: verification.status === 'pending'
|
|
416
426
|
? (verification.reason ?? 'EVIDENCE_PENDING')
|
|
417
427
|
: verification.reason,
|
|
@@ -633,7 +643,7 @@ function createStoredPurchaseRequest(request, transaction) {
|
|
|
633
643
|
: {}),
|
|
634
644
|
};
|
|
635
645
|
}
|
|
636
|
-
async function claimAdRewardWithStore(input, context) {
|
|
646
|
+
async function claimAdRewardWithStore(input, context, onDecision) {
|
|
637
647
|
const request = bindRequestDeploymentTarget(assertClaimAdRewardRequest(input), context.deploymentTargetBindings);
|
|
638
648
|
const retryIdentity = {
|
|
639
649
|
source: 'ad_reward',
|
|
@@ -645,9 +655,15 @@ async function claimAdRewardWithStore(input, context) {
|
|
|
645
655
|
? {}
|
|
646
656
|
: { deploymentTarget: request.deploymentTarget }),
|
|
647
657
|
};
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
658
|
+
return withEntitlementRetryLock(retryIdentity, async () => {
|
|
659
|
+
const claim = await claimAdRewardForRetry(request, retryIdentity, context);
|
|
660
|
+
await onDecision(claim);
|
|
661
|
+
return claim;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
async function claimAdRewardForRetry(request, retryIdentity, context) {
|
|
665
|
+
const completeExistingRetry = (transaction) => {
|
|
666
|
+
if (!matchesEntitlementRetry(transaction, retryIdentity)) {
|
|
651
667
|
return assertClaimAdRewardResponse({
|
|
652
668
|
granted: false,
|
|
653
669
|
alreadyProcessed: false,
|
|
@@ -656,9 +672,13 @@ async function claimAdRewardWithStore(input, context) {
|
|
|
656
672
|
}
|
|
657
673
|
return assertClaimAdRewardResponse({
|
|
658
674
|
granted: true,
|
|
659
|
-
ledgerEntryId:
|
|
675
|
+
ledgerEntryId: transaction.ledgerEntryId,
|
|
660
676
|
alreadyProcessed: true,
|
|
661
677
|
});
|
|
678
|
+
};
|
|
679
|
+
const existing = await findEntitlementTransactionByIdempotency(context.store, retryIdentity);
|
|
680
|
+
if (existing !== undefined) {
|
|
681
|
+
return completeExistingRetry(existing);
|
|
662
682
|
}
|
|
663
683
|
const placement = context.placements.placements.find((entry) => entry.id === request.placementId);
|
|
664
684
|
if (placement === undefined || placement.type !== 'rewarded' || placement.reward === undefined) {
|
|
@@ -679,9 +699,16 @@ async function claimAdRewardWithStore(input, context) {
|
|
|
679
699
|
});
|
|
680
700
|
}, context.evidenceVerificationTimeoutMs);
|
|
681
701
|
if (verification.status !== 'verified') {
|
|
702
|
+
// Another backend process can record a matching grant while verification is in flight.
|
|
703
|
+
// Recheck before returning a stale pending or rejected state.
|
|
704
|
+
const racedExisting = await findEntitlementTransactionByIdempotency(context.store, retryIdentity);
|
|
705
|
+
if (racedExisting !== undefined) {
|
|
706
|
+
return completeExistingRetry(racedExisting);
|
|
707
|
+
}
|
|
682
708
|
return assertClaimAdRewardResponse({
|
|
683
709
|
granted: false,
|
|
684
710
|
alreadyProcessed: false,
|
|
711
|
+
disposition: verification.status === 'pending' ? 'pending' : 'rejected',
|
|
685
712
|
reason: verification.status === 'pending'
|
|
686
713
|
? (verification.reason ?? 'EVIDENCE_PENDING')
|
|
687
714
|
: verification.reason,
|
|
@@ -739,24 +766,16 @@ async function claimAdRewardWithStore(input, context) {
|
|
|
739
766
|
async function verifyEvidence(verify, timeoutMs) {
|
|
740
767
|
const controller = new AbortController();
|
|
741
768
|
const timeoutDecision = {
|
|
742
|
-
status: '
|
|
769
|
+
status: 'pending',
|
|
743
770
|
reason: 'EVIDENCE_VERIFIER_TIMEOUT',
|
|
744
771
|
};
|
|
745
|
-
let timedOut = false;
|
|
746
772
|
let timeout;
|
|
773
|
+
let decision;
|
|
747
774
|
try {
|
|
748
|
-
|
|
749
|
-
verify(controller.signal)
|
|
750
|
-
.then(assertEvidenceVerificationDecision)
|
|
751
|
-
.catch((error) => {
|
|
752
|
-
if (timedOut) {
|
|
753
|
-
return timeoutDecision;
|
|
754
|
-
}
|
|
755
|
-
throw error;
|
|
756
|
-
}),
|
|
775
|
+
decision = await Promise.race([
|
|
776
|
+
verify(controller.signal),
|
|
757
777
|
new Promise((resolve) => {
|
|
758
778
|
timeout = setTimeout(() => {
|
|
759
|
-
timedOut = true;
|
|
760
779
|
resolve(timeoutDecision);
|
|
761
780
|
controller.abort();
|
|
762
781
|
}, timeoutMs);
|
|
@@ -765,7 +784,7 @@ async function verifyEvidence(verify, timeoutMs) {
|
|
|
765
784
|
}
|
|
766
785
|
catch {
|
|
767
786
|
return {
|
|
768
|
-
status: '
|
|
787
|
+
status: 'pending',
|
|
769
788
|
reason: 'EVIDENCE_VERIFIER_ERROR',
|
|
770
789
|
};
|
|
771
790
|
}
|
|
@@ -774,6 +793,12 @@ async function verifyEvidence(verify, timeoutMs) {
|
|
|
774
793
|
clearTimeout(timeout);
|
|
775
794
|
}
|
|
776
795
|
}
|
|
796
|
+
try {
|
|
797
|
+
return assertEvidenceVerificationDecision(decision);
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return { status: 'rejected', reason: 'EVIDENCE_VERIFIER_ERROR' };
|
|
801
|
+
}
|
|
777
802
|
}
|
|
778
803
|
function createPurchaseVerificationPayload(verification) {
|
|
779
804
|
if (verification.platformEvidenceId !== null || verification.payload === undefined) {
|
|
@@ -875,34 +900,43 @@ async function recordEntitlementGrantUnchecked(store, grant) {
|
|
|
875
900
|
throw error;
|
|
876
901
|
}
|
|
877
902
|
}
|
|
878
|
-
const
|
|
903
|
+
const entitlementLocks = new WeakMap();
|
|
904
|
+
/** Hosts such as the Worker create a store per request, so retries lock per process instead. */
|
|
905
|
+
const entitlementRetryLockScope = {};
|
|
879
906
|
async function withEntitlementEvidenceLocks(store, lockKeys, task) {
|
|
880
907
|
const orderedLockKeys = [...new Set(lockKeys)].sort((left, right) => left.localeCompare(right));
|
|
881
908
|
async function runWithLock(index) {
|
|
882
909
|
const lockKey = orderedLockKeys[index];
|
|
883
910
|
return lockKey === undefined
|
|
884
911
|
? task()
|
|
885
|
-
:
|
|
912
|
+
: withEntitlementLockKey(store, lockKey, () => runWithLock(index + 1));
|
|
886
913
|
}
|
|
887
914
|
return runWithLock(0);
|
|
888
915
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
916
|
+
/**
|
|
917
|
+
* Serialize one retry identity in this backend process, from its ledger lookup to its grant
|
|
918
|
+
* or non-grant decision, so a verifier decision cannot race a same-key grant here.
|
|
919
|
+
*/
|
|
920
|
+
async function withEntitlementRetryLock(identity, task) {
|
|
921
|
+
return withEntitlementLockKey(entitlementRetryLockScope, createEntitlementIdempotencyKey(identity), task);
|
|
922
|
+
}
|
|
923
|
+
async function withEntitlementLockKey(scope, lockKey, task) {
|
|
924
|
+
const scopeLocks = entitlementLocks.get(scope) ?? new Map();
|
|
925
|
+
entitlementLocks.set(scope, scopeLocks);
|
|
926
|
+
const previous = scopeLocks.get(lockKey) ?? Promise.resolve();
|
|
893
927
|
const gate = createPromiseGate();
|
|
894
928
|
const current = previous.then(() => gate.promise);
|
|
895
|
-
|
|
929
|
+
scopeLocks.set(lockKey, current);
|
|
896
930
|
await previous;
|
|
897
931
|
try {
|
|
898
932
|
return await task();
|
|
899
933
|
}
|
|
900
934
|
finally {
|
|
901
935
|
gate.release();
|
|
902
|
-
if (
|
|
903
|
-
|
|
904
|
-
if (
|
|
905
|
-
|
|
936
|
+
if (scopeLocks.get(lockKey) === current) {
|
|
937
|
+
scopeLocks.delete(lockKey);
|
|
938
|
+
if (scopeLocks.size === 0) {
|
|
939
|
+
entitlementLocks.delete(scope);
|
|
906
940
|
}
|
|
907
941
|
}
|
|
908
942
|
}
|
package/dist/types.js
CHANGED
|
@@ -13,6 +13,7 @@ export function assertVerifyPurchaseRequest(input) {
|
|
|
13
13
|
export function assertVerifyPurchaseResponse(input) {
|
|
14
14
|
assertRecord(input, 'VerifyPurchaseResponse');
|
|
15
15
|
assertBoolean(input.verified, 'verified');
|
|
16
|
+
assertOptionalNonGrantDisposition(input.disposition, input.verified);
|
|
16
17
|
assertOptionalNonEmptyString(input.ledgerEntryId, 'ledgerEntryId');
|
|
17
18
|
assertBoolean(input.alreadyProcessed, 'alreadyProcessed');
|
|
18
19
|
assertOptionalNonEmptyString(input.reason, 'reason');
|
|
@@ -57,6 +58,7 @@ export function assertClaimAdRewardRequest(input) {
|
|
|
57
58
|
export function assertClaimAdRewardResponse(input) {
|
|
58
59
|
assertRecord(input, 'ClaimAdRewardResponse');
|
|
59
60
|
assertBoolean(input.granted, 'granted');
|
|
61
|
+
assertOptionalNonGrantDisposition(input.disposition, input.granted);
|
|
60
62
|
assertOptionalNonEmptyString(input.ledgerEntryId, 'ledgerEntryId');
|
|
61
63
|
assertBoolean(input.alreadyProcessed, 'alreadyProcessed');
|
|
62
64
|
assertOptionalNonEmptyString(input.reason, 'reason');
|
|
@@ -198,6 +200,17 @@ function assertBoolean(input, label) {
|
|
|
198
200
|
throw new Error(`${label} must be a boolean.`);
|
|
199
201
|
}
|
|
200
202
|
}
|
|
203
|
+
function assertOptionalNonGrantDisposition(input, granted) {
|
|
204
|
+
if (input === undefined) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (input !== 'pending' && input !== 'rejected') {
|
|
208
|
+
throw new Error('disposition must be pending or rejected.');
|
|
209
|
+
}
|
|
210
|
+
if (granted) {
|
|
211
|
+
throw new Error('A granted result cannot have a non-grant disposition.');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
201
214
|
function assertPurchaseGrantFinalizationAction(input) {
|
|
202
215
|
if (input !== 'acknowledge'
|
|
203
216
|
&& input !== 'consume'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/game-services",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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,10 @@
|
|
|
29
29
|
"types": "./dist/index.d.ts",
|
|
30
30
|
"default": "./dist/index.js"
|
|
31
31
|
},
|
|
32
|
+
"./admob-client-reward": {
|
|
33
|
+
"types": "./dist/admob-client-reward.d.ts",
|
|
34
|
+
"default": "./dist/admob-client-reward.js"
|
|
35
|
+
},
|
|
32
36
|
"./admob-ssv": {
|
|
33
37
|
"types": "./dist/admob-ssv.d.ts",
|
|
34
38
|
"default": "./dist/admob-ssv.js"
|
|
@@ -41,6 +45,10 @@
|
|
|
41
45
|
"types": "./dist/app-store-verifier.d.ts",
|
|
42
46
|
"default": "./dist/app-store-verifier.js"
|
|
43
47
|
},
|
|
48
|
+
"./app-store-recovery": {
|
|
49
|
+
"types": "./dist/app-store-recovery.d.ts",
|
|
50
|
+
"default": "./dist/app-store-recovery.js"
|
|
51
|
+
},
|
|
44
52
|
"./app-store-verifier-conformance": {
|
|
45
53
|
"types": "./dist/app-store-verifier-conformance.d.ts",
|
|
46
54
|
"default": "./dist/app-store-verifier-conformance.js"
|
|
@@ -73,6 +81,10 @@
|
|
|
73
81
|
"types": "./dist/google-play-purchase.d.ts",
|
|
74
82
|
"default": "./dist/google-play-purchase.js"
|
|
75
83
|
},
|
|
84
|
+
"./google-play-publisher-client": {
|
|
85
|
+
"types": "./dist/google-play-publisher-client.d.ts",
|
|
86
|
+
"default": "./dist/google-play-publisher-client.js"
|
|
87
|
+
},
|
|
76
88
|
"./guest-session": {
|
|
77
89
|
"types": "./dist/guest-session.d.ts",
|
|
78
90
|
"default": "./dist/guest-session.js"
|
|
@@ -142,9 +154,9 @@
|
|
|
142
154
|
"@orpc/client": "2.0.0-beta.14",
|
|
143
155
|
"@orpc/contract": "2.0.0-beta.14",
|
|
144
156
|
"@orpc/server": "2.0.0-beta.14",
|
|
145
|
-
"@mpgd/analytics": "0.
|
|
146
|
-
"@mpgd/
|
|
147
|
-
"@mpgd/
|
|
157
|
+
"@mpgd/analytics": "0.4.0",
|
|
158
|
+
"@mpgd/catalog": "0.7.6",
|
|
159
|
+
"@mpgd/platform": "0.14.0"
|
|
148
160
|
},
|
|
149
161
|
"devDependencies": {
|
|
150
162
|
"@opentelemetry/api": "1.9.0",
|
|
@@ -163,7 +175,8 @@
|
|
|
163
175
|
"fix": "ttsc fix",
|
|
164
176
|
"build": "cd ../.. && node tools/run-ttsx.mjs tools/package/build-packages.ts @mpgd/game-services",
|
|
165
177
|
"test:packed-transport": "pnpm build && node test/runtime-transport-package-import.mjs",
|
|
166
|
-
"test:unit": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-progress.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/microsoft-store-identity.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 && node tools/run-ttsx.mjs packages/game-services/src/guest-session.test.ts",
|
|
167
|
-
"test": "
|
|
178
|
+
"test:unit": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-progress.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/google-play-publisher-client.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/microsoft-store-identity.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 && node tools/run-ttsx.mjs packages/game-services/src/guest-session.test.ts",
|
|
179
|
+
"test:recovery": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/monetization-recovery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-recovery.test.ts",
|
|
180
|
+
"test": "pnpm test:unit && pnpm test:recovery && pnpm test:packed-transport"
|
|
168
181
|
}
|
|
169
182
|
}
|