@mpgd/game-services 0.8.2 → 0.10.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-ssv-conformance.d.ts +19 -0
- package/dist/admob-ssv-conformance.js +258 -0
- package/dist/admob-ssv.d.ts +54 -0
- package/dist/admob-ssv.js +566 -0
- package/dist/app-store-verifier-conformance.d.ts +25 -0
- package/dist/app-store-verifier-conformance.js +28 -0
- package/dist/app-store-verifier.d.ts +87 -0
- package/dist/app-store-verifier.js +439 -0
- package/dist/apps-in-toss-evidence-verification-conformance.d.ts +26 -0
- package/dist/apps-in-toss-evidence-verification-conformance.js +613 -0
- package/dist/apps-in-toss-evidence-verification.d.ts +162 -0
- package/dist/apps-in-toss-evidence-verification.js +458 -0
- package/dist/client.js +51 -5
- package/dist/evidence-verification.d.ts +21 -1
- package/dist/google-play-purchase-conformance.d.ts +26 -0
- package/dist/google-play-purchase-conformance.js +39 -0
- package/dist/google-play-purchase.d.ts +40 -0
- package/dist/google-play-purchase.js +559 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/notification-delivery.js +1 -0
- package/dist/runtime.js +2 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +199 -14
- package/dist/types.d.ts +15 -3
- package/dist/types.js +47 -2
- package/dist/verified-leaderboard-durability-conformance.d.ts +39 -0
- package/dist/verified-leaderboard-durability-conformance.js +193 -0
- package/package.json +42 -5
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
|
|
317
|
-
|
|
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:
|
|
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
|
-
...
|
|
395
|
+
...verificationPayload,
|
|
375
396
|
target: request.target,
|
|
376
397
|
productId: request.productId,
|
|
398
|
+
productType: product.type,
|
|
377
399
|
platformProductId,
|
|
378
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
@@ -2,7 +2,9 @@ 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
4
|
export type GameServicesStoreTarget = Extract<PlatformTarget, 'android' | 'ios' | 'ait'>;
|
|
5
|
-
export type
|
|
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
8
|
export type PurchaseIdempotencyKey = string;
|
|
7
9
|
export type AdRewardIdempotencyKey = string;
|
|
8
10
|
export type EntitlementIdempotencyKey = PurchaseIdempotencyKey | AdRewardIdempotencyKey;
|
|
@@ -22,11 +24,20 @@ export interface VerifyPurchaseResponse {
|
|
|
22
24
|
readonly ledgerEntryId?: string;
|
|
23
25
|
readonly alreadyProcessed: boolean;
|
|
24
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;
|
|
25
35
|
}
|
|
26
36
|
export interface ClaimAdRewardRequest {
|
|
27
|
-
readonly target:
|
|
37
|
+
readonly target: GameServicesAdRewardTarget;
|
|
28
38
|
readonly playerId: string;
|
|
29
39
|
readonly placementId: LogicalAdPlacementId;
|
|
40
|
+
/** Platform evidence identity; AIT uses the game-issued reward correlation identifier. */
|
|
30
41
|
readonly platformImpressionId?: string;
|
|
31
42
|
readonly idempotencyKey: AdRewardIdempotencyKey;
|
|
32
43
|
readonly completedAt: string;
|
|
@@ -39,7 +50,7 @@ export interface ClaimAdRewardResponse {
|
|
|
39
50
|
readonly reason?: string;
|
|
40
51
|
}
|
|
41
52
|
export interface RecordLeaderboardScoreRequest extends LeaderboardScoreInput {
|
|
42
|
-
readonly target:
|
|
53
|
+
readonly target: GameServicesLeaderboardTarget;
|
|
43
54
|
readonly playerId: string;
|
|
44
55
|
readonly platformSubmissionId?: string;
|
|
45
56
|
}
|
|
@@ -80,6 +91,7 @@ export interface LeaderboardScoreTransaction extends RecordLeaderboardScoreReque
|
|
|
80
91
|
}
|
|
81
92
|
export declare function assertVerifyPurchaseRequest(input: VerifyPurchaseRequest): VerifyPurchaseRequest;
|
|
82
93
|
export declare function assertVerifyPurchaseResponse(input: VerifyPurchaseResponse): VerifyPurchaseResponse;
|
|
94
|
+
export declare function assertPurchaseGrantFinalization(input: PurchaseGrantFinalization): PurchaseGrantFinalization;
|
|
83
95
|
export declare function assertClaimAdRewardRequest(input: ClaimAdRewardRequest): ClaimAdRewardRequest;
|
|
84
96
|
export declare function assertClaimAdRewardResponse(input: ClaimAdRewardResponse): ClaimAdRewardResponse;
|
|
85
97
|
export declare function assertRecordLeaderboardScoreRequest(input: RecordLeaderboardScoreRequest): RecordLeaderboardScoreRequest;
|
package/dist/types.js
CHANGED
|
@@ -15,11 +15,35 @@ 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) {
|
|
21
45
|
assertRecord(input, 'ClaimAdRewardRequest');
|
|
22
|
-
|
|
46
|
+
assertAdRewardTarget(input.target);
|
|
23
47
|
assertNonEmptyString(input.playerId, 'playerId');
|
|
24
48
|
assertNonEmptyString(input.placementId, 'placementId');
|
|
25
49
|
assertOptionalNonEmptyString(input.platformImpressionId, 'platformImpressionId');
|
|
@@ -106,6 +130,11 @@ function assertStoreTarget(input) {
|
|
|
106
130
|
throw new Error('target must be android, ios, or ait.');
|
|
107
131
|
}
|
|
108
132
|
}
|
|
133
|
+
function assertAdRewardTarget(input) {
|
|
134
|
+
if (input !== 'android' && input !== 'ios' && input !== 'ait' && input !== 'verse8') {
|
|
135
|
+
throw new Error('target must be android, ios, ait, or verse8.');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
109
138
|
function assertLeaderboardTarget(input) {
|
|
110
139
|
if (input !== 'browser'
|
|
111
140
|
&& input !== 'android'
|
|
@@ -135,6 +164,14 @@ function assertBoolean(input, label) {
|
|
|
135
164
|
throw new Error(`${label} must be a boolean.`);
|
|
136
165
|
}
|
|
137
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
|
+
}
|
|
138
175
|
function assertLedgerSource(input) {
|
|
139
176
|
if (input !== 'purchase' && input !== 'ad_reward' && input !== 'admin') {
|
|
140
177
|
throw new Error('source must be purchase, ad_reward, or admin.');
|
|
@@ -153,7 +190,15 @@ function assertProductGrant(input) {
|
|
|
153
190
|
assertNonEmptyString(input.entitlement, 'grant.entitlement');
|
|
154
191
|
return;
|
|
155
192
|
}
|
|
156
|
-
|
|
193
|
+
if (input.type === 'resource') {
|
|
194
|
+
assertNonEmptyString(input.resource, 'grant.resource');
|
|
195
|
+
assertFiniteNumber(input.amount, 'grant.amount');
|
|
196
|
+
if (input.amount <= 0) {
|
|
197
|
+
throw new Error('grant.amount must be greater than zero for a resource grant.');
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
throw new Error('grant.type must be currency, entitlement, or resource.');
|
|
157
202
|
}
|
|
158
203
|
function assertPayload(input) {
|
|
159
204
|
assertRecord(input, 'payload');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type VerifiedLeaderboardService } from './verified-leaderboard';
|
|
2
|
+
export declare const verifiedLeaderboardDurabilityConformanceScenarios: readonly ['replacement-after-interrupted-retained-write', 'concurrent-retry-after-interrupted-retained-write', 'snapshot-after-interrupted-retained-write'];
|
|
3
|
+
export type VerifiedLeaderboardDurabilityConformanceScenario = (typeof verifiedLeaderboardDurabilityConformanceScenarios)[number];
|
|
4
|
+
export interface CreateVerifiedLeaderboardDurabilityConformanceFixtureInput {
|
|
5
|
+
readonly scenario: VerifiedLeaderboardDurabilityConformanceScenario;
|
|
6
|
+
readonly now: string;
|
|
7
|
+
}
|
|
8
|
+
export interface VerifiedLeaderboardDurabilityConformanceFixture {
|
|
9
|
+
readonly service: VerifiedLeaderboardService;
|
|
10
|
+
/**
|
|
11
|
+
* Arms one retained record so its caller observes a failure after the
|
|
12
|
+
* provider has crossed the recovery boundary. Transactional providers may
|
|
13
|
+
* simulate a committed write with a lost response. Multi-write providers
|
|
14
|
+
* should interrupt at their recoverable handoff boundary.
|
|
15
|
+
*/
|
|
16
|
+
readonly interruptNextRetainedRecord: () => void;
|
|
17
|
+
readonly dispose?: () => Promise<void> | void;
|
|
18
|
+
}
|
|
19
|
+
export type CreateVerifiedLeaderboardDurabilityConformanceFixture = (input: CreateVerifiedLeaderboardDurabilityConformanceFixtureInput) => Promise<VerifiedLeaderboardDurabilityConformanceFixture> | VerifiedLeaderboardDurabilityConformanceFixture;
|
|
20
|
+
export interface RunVerifiedLeaderboardDurabilityConformanceInput {
|
|
21
|
+
/** Return an isolated fixture, or a uniquely namespaced fixture, per scenario. */
|
|
22
|
+
readonly createFixture: CreateVerifiedLeaderboardDurabilityConformanceFixture;
|
|
23
|
+
readonly now?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface VerifiedLeaderboardDurabilityConformanceReport {
|
|
26
|
+
readonly passedScenarios: readonly VerifiedLeaderboardDurabilityConformanceScenario[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Runs provider-neutral recovery checks that require a provider-specific fault
|
|
30
|
+
* boundary. It complements the semantic conformance suite and deliberately
|
|
31
|
+
* does not prescribe a shared persistence implementation.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runVerifiedLeaderboardDurabilityConformance(input: RunVerifiedLeaderboardDurabilityConformanceInput): Promise<VerifiedLeaderboardDurabilityConformanceReport>;
|
|
34
|
+
/**
|
|
35
|
+
* Wraps an atomic provider so the next armed record commits successfully but
|
|
36
|
+
* its caller observes an interruption. Use a provider-specific fixture when
|
|
37
|
+
* the implementation has a recoverable boundary inside a multi-write flow.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createAmbiguousCommitVerifiedLeaderboardDurabilityFixture(provider: VerifiedLeaderboardService, dispose?: () => Promise<void> | void): VerifiedLeaderboardDurabilityConformanceFixture;
|