@mpgd/game-services 0.6.0 → 0.8.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/client.js +2 -0
- package/dist/evidence-verification.d.ts +34 -0
- package/dist/evidence-verification.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/platform-order.d.ts +22 -0
- package/dist/platform-order.js +65 -0
- package/dist/server.d.ts +33 -0
- package/dist/server.js +426 -6
- package/dist/types.d.ts +7 -2
- package/dist/types.js +13 -8
- package/dist/verified-leaderboard-conformance.js +77 -0
- package/dist/verified-leaderboard-transport.js +27 -2
- package/dist/verified-leaderboard.d.ts +9 -0
- package/dist/verified-leaderboard.js +49 -0
- package/package.json +13 -5
package/dist/client.js
CHANGED
|
@@ -69,6 +69,7 @@ export function createGameServicesClient(input) {
|
|
|
69
69
|
platformTransactionId: purchase.transactionId,
|
|
70
70
|
idempotencyKey: purchaseInput.idempotencyKey,
|
|
71
71
|
purchasedAt: now(),
|
|
72
|
+
...(purchase.evidence === undefined ? {} : { evidence: purchase.evidence }),
|
|
72
73
|
});
|
|
73
74
|
const result = {
|
|
74
75
|
status: verification.verified ? 'granted' : 'rejected',
|
|
@@ -135,6 +136,7 @@ export function createGameServicesClient(input) {
|
|
|
135
136
|
: { platformImpressionId: reward.ledgerEntryId }),
|
|
136
137
|
idempotencyKey: rewardInput.idempotencyKey,
|
|
137
138
|
completedAt: now(),
|
|
139
|
+
...(reward.evidence === undefined ? {} : { evidence: reward.evidence }),
|
|
138
140
|
});
|
|
139
141
|
const result = {
|
|
140
142
|
status: claim.granted ? 'granted' : 'rejected',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AdPlacements, ProductCatalog } from '@mpgd/catalog';
|
|
2
|
+
import type { ClaimAdRewardRequest, EntitlementLedgerPayload, VerifyPurchaseRequest } from './types';
|
|
3
|
+
export type EvidenceVerificationDecision = {
|
|
4
|
+
readonly status: 'verified';
|
|
5
|
+
readonly verificationId: string;
|
|
6
|
+
readonly verifiedAt: string;
|
|
7
|
+
readonly payload?: EntitlementLedgerPayload;
|
|
8
|
+
} | {
|
|
9
|
+
readonly status: 'pending';
|
|
10
|
+
readonly reason?: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly status: 'rejected';
|
|
13
|
+
readonly reason: string;
|
|
14
|
+
};
|
|
15
|
+
export interface VerifyPurchaseEvidenceInput {
|
|
16
|
+
readonly request: VerifyPurchaseRequest;
|
|
17
|
+
readonly product: ProductCatalog['products'][number];
|
|
18
|
+
readonly platformProductId: string;
|
|
19
|
+
readonly signal: AbortSignal;
|
|
20
|
+
readonly timeoutMs: number;
|
|
21
|
+
}
|
|
22
|
+
export interface VerifyAdRewardEvidenceInput {
|
|
23
|
+
readonly request: ClaimAdRewardRequest;
|
|
24
|
+
readonly placement: AdPlacements['placements'][number];
|
|
25
|
+
readonly platformPlacementId?: string;
|
|
26
|
+
readonly signal: AbortSignal;
|
|
27
|
+
readonly timeoutMs: number;
|
|
28
|
+
}
|
|
29
|
+
export interface GameServicesEvidenceVerifier {
|
|
30
|
+
verifyPurchase(input: VerifyPurchaseEvidenceInput): Promise<EvidenceVerificationDecision>;
|
|
31
|
+
verifyAdReward(input: VerifyAdRewardEvidenceInput): Promise<EvidenceVerificationDecision>;
|
|
32
|
+
}
|
|
33
|
+
export declare function createDevelopmentGameServicesEvidenceVerifier(now?: () => string): GameServicesEvidenceVerifier;
|
|
34
|
+
export declare function createRejectingGameServicesEvidenceVerifier(): GameServicesEvidenceVerifier;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function createDevelopmentGameServicesEvidenceVerifier(now = () => new Date().toISOString()) {
|
|
2
|
+
return {
|
|
3
|
+
async verifyPurchase({ request }) {
|
|
4
|
+
return {
|
|
5
|
+
status: 'verified',
|
|
6
|
+
verificationId: `development:purchase:${request.target}:${request.platformTransactionId}`,
|
|
7
|
+
verifiedAt: now(),
|
|
8
|
+
};
|
|
9
|
+
},
|
|
10
|
+
async verifyAdReward({ request }) {
|
|
11
|
+
return {
|
|
12
|
+
status: 'verified',
|
|
13
|
+
verificationId: `development:ad-reward:${request.target}:${request.platformImpressionId ?? request.idempotencyKey}`,
|
|
14
|
+
verifiedAt: now(),
|
|
15
|
+
};
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function createRejectingGameServicesEvidenceVerifier() {
|
|
20
|
+
return {
|
|
21
|
+
async verifyPurchase() {
|
|
22
|
+
return {
|
|
23
|
+
status: 'rejected',
|
|
24
|
+
reason: 'EVIDENCE_VERIFIER_UNAVAILABLE',
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
async verifyAdReward() {
|
|
28
|
+
return {
|
|
29
|
+
status: 'rejected',
|
|
30
|
+
reason: 'EVIDENCE_VERIFIER_UNAVAILABLE',
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './client';
|
|
2
2
|
export * from './contract';
|
|
3
|
+
export * from './evidence-verification';
|
|
3
4
|
export * from './notification-delivery';
|
|
5
|
+
export * from './platform-order';
|
|
4
6
|
export * from './progress-link';
|
|
5
7
|
export * from './runtime.js';
|
|
6
8
|
export * from './server';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './client';
|
|
2
2
|
export * from './contract';
|
|
3
|
+
export * from './evidence-verification';
|
|
3
4
|
export * from './notification-delivery';
|
|
5
|
+
export * from './platform-order';
|
|
4
6
|
export * from './progress-link';
|
|
5
7
|
export * from './runtime.js';
|
|
6
8
|
export * from './server';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { PlatformEvidenceEnvelope } from '@mpgd/platform';
|
|
2
|
+
export type PlatformOrderTarget = 'reddit';
|
|
3
|
+
export interface FulfillPlatformOrderInput {
|
|
4
|
+
readonly target: PlatformOrderTarget;
|
|
5
|
+
readonly orderId: string;
|
|
6
|
+
readonly playerId: string;
|
|
7
|
+
readonly platformSku: string;
|
|
8
|
+
readonly paidAt: string;
|
|
9
|
+
readonly evidence: PlatformEvidenceEnvelope;
|
|
10
|
+
}
|
|
11
|
+
export interface RefundPlatformOrderInput {
|
|
12
|
+
readonly target: PlatformOrderTarget;
|
|
13
|
+
readonly orderId: string;
|
|
14
|
+
readonly playerId: string;
|
|
15
|
+
readonly platformSku: string;
|
|
16
|
+
readonly refundedAt: string;
|
|
17
|
+
readonly evidence: PlatformEvidenceEnvelope;
|
|
18
|
+
}
|
|
19
|
+
export type PlatformOrderIdempotencyKey = `${PlatformOrderTarget}:${string}`;
|
|
20
|
+
export declare function createPlatformOrderIdempotencyKey(input: Pick<FulfillPlatformOrderInput, 'target' | 'orderId'>): PlatformOrderIdempotencyKey;
|
|
21
|
+
export declare function assertFulfillPlatformOrderInput(input: FulfillPlatformOrderInput): FulfillPlatformOrderInput;
|
|
22
|
+
export declare function assertRefundPlatformOrderInput(input: RefundPlatformOrderInput): RefundPlatformOrderInput;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function createPlatformOrderIdempotencyKey(input) {
|
|
2
|
+
assertPlatformOrderTarget(input.target);
|
|
3
|
+
assertIdentifier(input.orderId, 'orderId');
|
|
4
|
+
return `${input.target}:${input.orderId}`;
|
|
5
|
+
}
|
|
6
|
+
export function assertFulfillPlatformOrderInput(input) {
|
|
7
|
+
assertPlatformOrderBase(input, 'FulfillPlatformOrderInput');
|
|
8
|
+
assertIsoTimestamp(input.paidAt, 'paidAt');
|
|
9
|
+
return input;
|
|
10
|
+
}
|
|
11
|
+
export function assertRefundPlatformOrderInput(input) {
|
|
12
|
+
assertPlatformOrderBase(input, 'RefundPlatformOrderInput');
|
|
13
|
+
assertIsoTimestamp(input.refundedAt, 'refundedAt');
|
|
14
|
+
return input;
|
|
15
|
+
}
|
|
16
|
+
function assertPlatformOrderBase(input, label) {
|
|
17
|
+
assertRecord(input, label);
|
|
18
|
+
assertPlatformOrderTarget(input.target);
|
|
19
|
+
assertIdentifier(input.orderId, 'orderId');
|
|
20
|
+
assertIdentifier(input.playerId, 'playerId');
|
|
21
|
+
assertIdentifier(input.platformSku, 'platformSku');
|
|
22
|
+
assertEvidenceEnvelope(input.evidence);
|
|
23
|
+
}
|
|
24
|
+
function assertPlatformOrderTarget(input) {
|
|
25
|
+
if (input !== 'reddit') {
|
|
26
|
+
throw new TypeError('target must be reddit.');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function assertIdentifier(input, label) {
|
|
30
|
+
if (typeof input !== 'string'
|
|
31
|
+
|| input.length === 0
|
|
32
|
+
|| input.length > 256
|
|
33
|
+
|| input.trim() !== input
|
|
34
|
+
|| /[\u0000-\u001f\u007f]/u.test(input)) {
|
|
35
|
+
throw new TypeError(`${label} must be a non-empty identifier.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertIsoTimestamp(input, label) {
|
|
39
|
+
if (typeof input !== 'string'
|
|
40
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/u.test(input)
|
|
41
|
+
|| Number.isNaN(Date.parse(input))) {
|
|
42
|
+
throw new TypeError(`${label} must be an ISO 8601 UTC timestamp.`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function assertEvidenceEnvelope(input) {
|
|
46
|
+
assertRecord(input, 'evidence');
|
|
47
|
+
assertIdentifier(input.schema, 'evidence.schema');
|
|
48
|
+
assertRecord(input.payload, 'evidence.payload');
|
|
49
|
+
for (const [key, value] of Object.entries(input.payload)) {
|
|
50
|
+
assertIdentifier(key, 'evidence.payload key');
|
|
51
|
+
if (typeof value !== 'string'
|
|
52
|
+
&& typeof value !== 'number'
|
|
53
|
+
&& typeof value !== 'boolean') {
|
|
54
|
+
throw new TypeError('evidence.payload values must be strings, numbers, or booleans.');
|
|
55
|
+
}
|
|
56
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
57
|
+
throw new TypeError('evidence.payload numbers must be finite.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function assertRecord(input, label) {
|
|
62
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
63
|
+
throw new TypeError(`${label} must be an object.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type AnalyticsSink } from '@mpgd/analytics';
|
|
|
2
2
|
import type { AdPlacements, 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
|
+
import { type GameServicesEvidenceVerifier } from './evidence-verification';
|
|
5
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
7
|
type CorsHeaders = Record<string, string>;
|
|
7
8
|
export interface CreateGameServicesBackendInput {
|
|
@@ -12,6 +13,8 @@ export interface CreateGameServicesBackendInput {
|
|
|
12
13
|
readonly analyticsSessionId?: string;
|
|
13
14
|
readonly now?: () => string;
|
|
14
15
|
readonly version?: string;
|
|
16
|
+
readonly evidenceVerifier?: GameServicesEvidenceVerifier;
|
|
17
|
+
readonly evidenceVerificationTimeoutMs?: number;
|
|
15
18
|
}
|
|
16
19
|
export interface GameServicesBackendApiHandler {
|
|
17
20
|
readonly version?: string;
|
|
@@ -22,6 +25,16 @@ export interface GameServicesBackendErrorResponse {
|
|
|
22
25
|
}
|
|
23
26
|
export interface GameServicesStore {
|
|
24
27
|
recordEntitlementGrant(input: EntitlementLedgerGrant): Promise<EntitlementLedgerResult>;
|
|
28
|
+
findEntitlementTransactionByIdempotency?(input: {
|
|
29
|
+
readonly source: EntitlementLedgerGrant['source'];
|
|
30
|
+
readonly playerId: string;
|
|
31
|
+
readonly idempotencyKey: string;
|
|
32
|
+
}): Promise<ProductGrantTransaction | undefined>;
|
|
33
|
+
findEntitlementTransactionByEvidenceVerificationId?(input: {
|
|
34
|
+
readonly source: EntitlementLedgerGrant['source'];
|
|
35
|
+
readonly evidenceVerificationId: string;
|
|
36
|
+
}): Promise<ProductGrantTransaction | undefined>;
|
|
37
|
+
findEntitlementTransactionByPlatformEvidence?(input: EntitlementPlatformEvidenceIdentity): Promise<ProductGrantTransaction | undefined>;
|
|
25
38
|
getEntitlementTransaction(ledgerEntryId: string): Promise<ProductGrantTransaction | undefined>;
|
|
26
39
|
listEntitlementTransactions(): Promise<readonly ProductGrantTransaction[]>;
|
|
27
40
|
recordLeaderboardScore(input: RecordLeaderboardScoreRequest, options?: {
|
|
@@ -30,6 +43,11 @@ export interface GameServicesStore {
|
|
|
30
43
|
getLeaderboardTransaction(ledgerEntryId: string): Promise<LeaderboardScoreTransaction | undefined>;
|
|
31
44
|
listLeaderboardTransactions(): Promise<readonly LeaderboardScoreTransaction[]>;
|
|
32
45
|
}
|
|
46
|
+
export interface EntitlementPlatformEvidenceIdentity {
|
|
47
|
+
readonly source: Extract<EntitlementLedgerGrant['source'], 'purchase' | 'ad_reward'>;
|
|
48
|
+
readonly target: string;
|
|
49
|
+
readonly platformEvidenceId: string;
|
|
50
|
+
}
|
|
33
51
|
export interface CreateGameServicesFetchHandlerOptions {
|
|
34
52
|
readonly corsHeaders?: CorsHeaders;
|
|
35
53
|
readonly healthPath?: string;
|
|
@@ -43,10 +61,22 @@ export interface GameServicesOrpcContext {
|
|
|
43
61
|
}
|
|
44
62
|
export declare class InMemoryGameServicesStore implements GameServicesStore {
|
|
45
63
|
private readonly entitlementTransactionsByKey;
|
|
64
|
+
private readonly entitlementTransactionsByEvidence;
|
|
65
|
+
private readonly entitlementTransactionsByPlatformEvidence;
|
|
46
66
|
private readonly entitlementTransactionsById;
|
|
47
67
|
private readonly leaderboardTransactionsByRun;
|
|
48
68
|
private readonly leaderboardTransactionsById;
|
|
49
69
|
recordEntitlementGrant(input: EntitlementLedgerGrant): Promise<EntitlementLedgerResult>;
|
|
70
|
+
findEntitlementTransactionByIdempotency(input: {
|
|
71
|
+
readonly source: EntitlementLedgerGrant['source'];
|
|
72
|
+
readonly playerId: string;
|
|
73
|
+
readonly idempotencyKey: string;
|
|
74
|
+
}): Promise<ProductGrantTransaction | undefined>;
|
|
75
|
+
findEntitlementTransactionByEvidenceVerificationId(input: {
|
|
76
|
+
readonly source: EntitlementLedgerGrant['source'];
|
|
77
|
+
readonly evidenceVerificationId: string;
|
|
78
|
+
}): Promise<ProductGrantTransaction | undefined>;
|
|
79
|
+
findEntitlementTransactionByPlatformEvidence(input: EntitlementPlatformEvidenceIdentity): Promise<ProductGrantTransaction | undefined>;
|
|
50
80
|
getEntitlementTransaction(ledgerEntryId: string): Promise<ProductGrantTransaction | undefined>;
|
|
51
81
|
listEntitlementTransactions(): Promise<readonly ProductGrantTransaction[]>;
|
|
52
82
|
recordLeaderboardScore(input: RecordLeaderboardScoreRequest, options?: {
|
|
@@ -57,6 +87,9 @@ export declare class InMemoryGameServicesStore implements GameServicesStore {
|
|
|
57
87
|
private rankFor;
|
|
58
88
|
private sortedLeaderboardTransactions;
|
|
59
89
|
}
|
|
90
|
+
export declare class EvidenceAlreadyProcessedError extends Error {
|
|
91
|
+
constructor();
|
|
92
|
+
}
|
|
60
93
|
export declare function createInMemoryGameServicesStore(): InMemoryGameServicesStore;
|
|
61
94
|
export declare function createGameServicesBackend(input: CreateGameServicesBackendInput): GameServicesBackendApi;
|
|
62
95
|
export declare function createGameServicesBackendApiHandler(input: CreateGameServicesBackendInput): GameServicesBackendApiHandler;
|