@mpgd/game-services 0.16.0 → 0.17.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/admob-client-reward.d.ts +27 -0
- package/dist/admob-client-reward.js +84 -0
- package/dist/admob-ssv.d.ts +2 -0
- package/dist/admob-ssv.js +5 -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 +17 -18
- 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 +4 -0
- package/dist/index.js +4 -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.d.ts +20 -2
- package/dist/server.js +246 -86
- package/dist/types.d.ts +12 -0
- package/dist/types.js +81 -27
- package/dist/validation.d.ts +18 -0
- package/dist/validation.js +26 -0
- package/package.json +19 -6
package/dist/client.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createORPCClient } from '@orpc/client';
|
|
2
2
|
import { RPCLink } from '@orpc/client/fetch';
|
|
3
3
|
import { createAnalyticsReporter } from '@mpgd/analytics';
|
|
4
|
+
import { isAuthoritativeMicrosoftStoreCompletion } from './authoritative-purchase.js';
|
|
5
|
+
import { isClientRewardEvidence, resolveRewardPlatformImpressionId, } from './admob-client-reward.js';
|
|
4
6
|
import { observeGameServicesOperation } from './operation-progress.js';
|
|
5
7
|
export const gameServicesBackendEndpoints = {
|
|
6
8
|
verifyPurchase: '/game-services/purchases/verify',
|
|
@@ -54,6 +56,7 @@ export async function resolveGameServicesRequestHeaders(input) {
|
|
|
54
56
|
}
|
|
55
57
|
export function createGameServicesClient(input) {
|
|
56
58
|
const now = input.now ?? (() => new Date().toISOString());
|
|
59
|
+
const requestNow = input.requestNow ?? now;
|
|
57
60
|
const analytics = createAnalyticsReporter({
|
|
58
61
|
target: input.target,
|
|
59
62
|
sessionId: input.analyticsSessionId ?? input.playerId,
|
|
@@ -88,7 +91,7 @@ export function createGameServicesClient(input) {
|
|
|
88
91
|
if (target === 'verse8') {
|
|
89
92
|
const status = purchase.status === 'completed' ? 'rejected' : purchase.status;
|
|
90
93
|
await analytics.track({
|
|
91
|
-
name: 'purchase_rejected',
|
|
94
|
+
name: purchase.status === 'pending' ? 'purchase_pending' : 'purchase_rejected',
|
|
92
95
|
properties: {
|
|
93
96
|
productId: purchaseInput.productId,
|
|
94
97
|
status,
|
|
@@ -123,7 +126,7 @@ export function createGameServicesClient(input) {
|
|
|
123
126
|
}
|
|
124
127
|
if (purchase.status !== 'completed' || purchase.transactionId === undefined) {
|
|
125
128
|
await analytics.track({
|
|
126
|
-
name: 'purchase_rejected',
|
|
129
|
+
name: purchase.status === 'pending' ? 'purchase_pending' : 'purchase_rejected',
|
|
127
130
|
properties: {
|
|
128
131
|
productId: purchaseInput.productId,
|
|
129
132
|
status: purchase.status,
|
|
@@ -144,14 +147,14 @@ export function createGameServicesClient(input) {
|
|
|
144
147
|
productId: purchaseInput.productId,
|
|
145
148
|
platformTransactionId: purchase.transactionId,
|
|
146
149
|
idempotencyKey: purchaseInput.idempotencyKey,
|
|
147
|
-
purchasedAt:
|
|
150
|
+
purchasedAt: requestNow(),
|
|
148
151
|
...(purchase.evidence === undefined ? {} : { evidence: purchase.evidence }),
|
|
149
152
|
};
|
|
150
153
|
progress.serverRequested();
|
|
151
154
|
const verification = await input.backend.purchases.verifyPurchase(verificationRequest);
|
|
152
155
|
progress.serverResult(verification.verified);
|
|
153
156
|
const result = {
|
|
154
|
-
status: verification.verified ? 'granted' : 'rejected',
|
|
157
|
+
status: verification.verified ? 'granted' : (verification.disposition ?? 'rejected'),
|
|
155
158
|
purchase,
|
|
156
159
|
verification,
|
|
157
160
|
...(verification.ledgerEntryId === undefined
|
|
@@ -159,7 +162,7 @@ export function createGameServicesClient(input) {
|
|
|
159
162
|
: { ledgerEntryId: verification.ledgerEntryId }),
|
|
160
163
|
};
|
|
161
164
|
await analytics.track({
|
|
162
|
-
name: verification.verified ? 'purchase_granted' : 'purchase_rejected',
|
|
165
|
+
name: verification.verified ? 'purchase_granted' : verification.disposition === 'pending' ? 'purchase_pending' : 'purchase_rejected',
|
|
163
166
|
properties: {
|
|
164
167
|
productId: purchaseInput.productId,
|
|
165
168
|
status: result.status,
|
|
@@ -196,9 +199,10 @@ export function createGameServicesClient(input) {
|
|
|
196
199
|
progress.platformRequested();
|
|
197
200
|
const reward = await input.gateway.ads.showRewarded(rewardInput);
|
|
198
201
|
progress.platformResult(reward.status);
|
|
199
|
-
if (reward.status !== 'completed' || !reward.rewardGranted)
|
|
202
|
+
if ((reward.status !== 'completed' || !reward.rewardGranted)
|
|
203
|
+
&& !isClientRewardEvidence(reward)) {
|
|
200
204
|
await analytics.track({
|
|
201
|
-
name: 'rewarded_ad_rejected',
|
|
205
|
+
name: reward.status === 'pending' ? 'rewarded_ad_pending' : 'rewarded_ad_rejected',
|
|
202
206
|
properties: {
|
|
203
207
|
placementId: rewardInput.placementId,
|
|
204
208
|
status: reward.status,
|
|
@@ -210,6 +214,7 @@ export function createGameServicesClient(input) {
|
|
|
210
214
|
reward,
|
|
211
215
|
};
|
|
212
216
|
}
|
|
217
|
+
const platformImpressionId = resolveRewardPlatformImpressionId(reward);
|
|
213
218
|
const claimRequest = {
|
|
214
219
|
target,
|
|
215
220
|
...(input.deploymentTarget === undefined || input.deploymentTarget === target
|
|
@@ -217,24 +222,24 @@ export function createGameServicesClient(input) {
|
|
|
217
222
|
: { deploymentTarget: input.deploymentTarget }),
|
|
218
223
|
playerId: input.playerId,
|
|
219
224
|
placementId: rewardInput.placementId,
|
|
220
|
-
...(
|
|
225
|
+
...(platformImpressionId === undefined
|
|
221
226
|
? {}
|
|
222
|
-
: { platformImpressionId
|
|
227
|
+
: { platformImpressionId }),
|
|
223
228
|
idempotencyKey: rewardInput.idempotencyKey,
|
|
224
|
-
completedAt:
|
|
229
|
+
completedAt: requestNow(),
|
|
225
230
|
...(reward.evidence === undefined ? {} : { evidence: reward.evidence }),
|
|
226
231
|
};
|
|
227
232
|
progress.serverRequested();
|
|
228
233
|
const claim = await input.backend.adRewards.claimAdReward(claimRequest);
|
|
229
234
|
progress.serverResult(claim.granted);
|
|
230
235
|
const result = {
|
|
231
|
-
status: claim.granted ? 'granted' : 'rejected',
|
|
236
|
+
status: claim.granted ? 'granted' : (claim.disposition ?? 'rejected'),
|
|
232
237
|
reward,
|
|
233
238
|
claim,
|
|
234
239
|
...(claim.ledgerEntryId === undefined ? {} : { ledgerEntryId: claim.ledgerEntryId }),
|
|
235
240
|
};
|
|
236
241
|
await analytics.track({
|
|
237
|
-
name: claim.granted ? 'rewarded_ad_granted' : 'rewarded_ad_rejected',
|
|
242
|
+
name: claim.granted ? 'rewarded_ad_granted' : claim.disposition === 'pending' ? 'rewarded_ad_pending' : 'rewarded_ad_rejected',
|
|
238
243
|
properties: {
|
|
239
244
|
placementId: rewardInput.placementId,
|
|
240
245
|
status: result.status,
|
|
@@ -402,12 +407,6 @@ function purchaseRejectionReason(purchase) {
|
|
|
402
407
|
}
|
|
403
408
|
return undefined;
|
|
404
409
|
}
|
|
405
|
-
function isAuthoritativeMicrosoftStoreCompletion(target, purchase) {
|
|
406
|
-
return target === 'microsoft-store'
|
|
407
|
-
&& purchase.status === 'completed'
|
|
408
|
-
&& purchase.transactionId !== undefined
|
|
409
|
-
&& purchase.authoritativeGrant?.ledgerEntryId === purchase.transactionId;
|
|
410
|
-
}
|
|
411
410
|
function isGameServicesCommerceTarget(target) {
|
|
412
411
|
return target === 'microsoft-store'
|
|
413
412
|
|| target === 'android'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GooglePlayProductPurchaseClient } from './google-play-purchase.js';
|
|
2
|
+
export interface GooglePlayPublisherClientOptions {
|
|
3
|
+
/** Supplies an androidpublisher-scoped OAuth access token for each request. */
|
|
4
|
+
readonly getAccessToken: (signal: AbortSignal) => Promise<string> | string;
|
|
5
|
+
readonly fetch?: typeof fetch;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Google Play purchase verification and post-ledger finalization transport.
|
|
9
|
+
* The caller owns OAuth credential rotation and must never expose this client to the game.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createGooglePlayPublisherClient(options: GooglePlayPublisherClientOptions): GooglePlayProductPurchaseClient;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const publisherOrigin = 'https://androidpublisher.googleapis.com';
|
|
2
|
+
/**
|
|
3
|
+
* Google Play purchase verification and post-ledger finalization transport.
|
|
4
|
+
* The caller owns OAuth credential rotation and must never expose this client to the game.
|
|
5
|
+
*/
|
|
6
|
+
export function createGooglePlayPublisherClient(options) {
|
|
7
|
+
const requestFetch = options.fetch ?? fetch;
|
|
8
|
+
async function request(method, path, signal) {
|
|
9
|
+
const accessToken = await options.getAccessToken(signal);
|
|
10
|
+
if (accessToken.length === 0 || /[\s\x00-\x1f\x7f]/u.test(accessToken)) {
|
|
11
|
+
throw new TypeError('Google Play Publisher access token is invalid.');
|
|
12
|
+
}
|
|
13
|
+
let response;
|
|
14
|
+
try {
|
|
15
|
+
response = await requestFetch(`${publisherOrigin}${path}`, {
|
|
16
|
+
method,
|
|
17
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
18
|
+
redirect: 'error',
|
|
19
|
+
signal,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
if (signal.aborted) {
|
|
24
|
+
throw new DOMException('Google Play Publisher request was cancelled.', 'AbortError');
|
|
25
|
+
}
|
|
26
|
+
// Fetch errors can contain the request URL (including the purchase token).
|
|
27
|
+
throw new Error('Google Play Publisher request failed.');
|
|
28
|
+
}
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
await discardBody(response);
|
|
31
|
+
// Do not forward an error body or URL: both can contain purchase credentials.
|
|
32
|
+
throw new Error(`Google Play Publisher request failed (HTTP ${response.status}).`);
|
|
33
|
+
}
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
async getProductPurchaseV2({ packageName, purchaseToken, signal }) {
|
|
38
|
+
const path = `${purchasePath(packageName)}/productsv2/tokens/${pathPart(purchaseToken)}`;
|
|
39
|
+
const response = await request('GET', path, signal);
|
|
40
|
+
try {
|
|
41
|
+
return await readBoundedJson(response);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
if (signal.aborted) {
|
|
45
|
+
throw new DOMException('Google Play Publisher request was cancelled.', 'AbortError');
|
|
46
|
+
}
|
|
47
|
+
throw new Error('Google Play Publisher returned invalid purchase JSON.');
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async acknowledgeProductPurchase({ packageName, productId, purchaseToken, signal }) {
|
|
51
|
+
const path = `${purchasePath(packageName)}/products/${pathPart(productId)}`
|
|
52
|
+
+ `/tokens/${pathPart(purchaseToken)}:acknowledge`;
|
|
53
|
+
await discardBody(await request('POST', path, signal));
|
|
54
|
+
},
|
|
55
|
+
async consumeProductPurchase({ packageName, productId, purchaseToken, signal }) {
|
|
56
|
+
const path = `${purchasePath(packageName)}/products/${pathPart(productId)}`
|
|
57
|
+
+ `/tokens/${pathPart(purchaseToken)}:consume`;
|
|
58
|
+
await discardBody(await request('POST', path, signal));
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function purchasePath(packageName) {
|
|
63
|
+
return `/androidpublisher/v3/applications/${pathPart(packageName)}/purchases`;
|
|
64
|
+
}
|
|
65
|
+
function pathPart(value) {
|
|
66
|
+
if (value.length === 0
|
|
67
|
+
|| value === '.'
|
|
68
|
+
|| value === '..'
|
|
69
|
+
|| value.trim() !== value
|
|
70
|
+
|| /[\x00-\x1f\x7f]/u.test(value)) {
|
|
71
|
+
throw new TypeError('Google Play Publisher path parameter is invalid.');
|
|
72
|
+
}
|
|
73
|
+
return encodeURIComponent(value);
|
|
74
|
+
}
|
|
75
|
+
async function discardBody(response) {
|
|
76
|
+
await response.body?.cancel().catch(() => undefined);
|
|
77
|
+
}
|
|
78
|
+
async function readBoundedJson(response) {
|
|
79
|
+
const body = response.body;
|
|
80
|
+
if (body === null) {
|
|
81
|
+
throw new Error('Missing Google Play Publisher response body.');
|
|
82
|
+
}
|
|
83
|
+
const reader = body.getReader();
|
|
84
|
+
const chunks = [];
|
|
85
|
+
let size = 0;
|
|
86
|
+
try {
|
|
87
|
+
while (true) {
|
|
88
|
+
const next = await reader.read();
|
|
89
|
+
if (next.done) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
size += next.value.byteLength;
|
|
93
|
+
if (size > 1024 * 1024) {
|
|
94
|
+
throw new Error('Google Play Publisher response exceeds maximum size.');
|
|
95
|
+
}
|
|
96
|
+
chunks.push(next.value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
await reader.cancel().catch(() => undefined);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
reader.releaseLock();
|
|
105
|
+
}
|
|
106
|
+
const bytes = new Uint8Array(size);
|
|
107
|
+
let offset = 0;
|
|
108
|
+
for (const chunk of chunks) {
|
|
109
|
+
bytes.set(chunk, offset);
|
|
110
|
+
offset += chunk.byteLength;
|
|
111
|
+
}
|
|
112
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
113
|
+
}
|
|
@@ -38,3 +38,5 @@ export interface GooglePlayProductPurchaseBoundary extends GameServicesPurchaseG
|
|
|
38
38
|
verifyPurchase(input: VerifyPurchaseEvidenceInput): ReturnType<GameServicesEvidenceVerifier['verifyPurchase']>;
|
|
39
39
|
}
|
|
40
40
|
export declare function createGooglePlayProductPurchaseBoundary(input: CreateGooglePlayProductPurchaseBoundaryInput): GooglePlayProductPurchaseBoundary;
|
|
41
|
+
/** A non-secret transaction ID for purchases whose Google order ID is absent or delayed. */
|
|
42
|
+
export declare function createGooglePlayTokenTransactionId(purchaseToken: string): Promise<string>;
|
|
@@ -45,10 +45,7 @@ export function createGooglePlayProductPurchaseBoundary(input) {
|
|
|
45
45
|
purchaseToken,
|
|
46
46
|
expectedProductId: verificationInput.platformProductId,
|
|
47
47
|
expectedProductType: verificationInput.product.type,
|
|
48
|
-
orderMatch:
|
|
49
|
-
mode: 'if-present',
|
|
50
|
-
orderId: verificationInput.request.platformTransactionId,
|
|
51
|
-
},
|
|
48
|
+
orderMatch: createGooglePlayObservedOrderMatch(verificationInput.request.platformTransactionId, await createGooglePlayTokenTransactionId(purchaseToken)),
|
|
52
49
|
accountBinding,
|
|
53
50
|
allowConsumed: false,
|
|
54
51
|
signal: verificationInput.signal,
|
|
@@ -146,7 +143,7 @@ async function finalizeGooglePlayPurchase(input, packageName, purchaseToken, pro
|
|
|
146
143
|
purchaseToken,
|
|
147
144
|
expectedProductId: finalizationInput.platformProductId,
|
|
148
145
|
expectedProductType: productType,
|
|
149
|
-
orderMatch: createGooglePlayFinalizationOrderMatch(verifiedContext, finalizationInput.request.platformTransactionId),
|
|
146
|
+
orderMatch: createGooglePlayFinalizationOrderMatch(verifiedContext, finalizationInput.request.platformTransactionId, await createGooglePlayTokenTransactionId(purchaseToken)),
|
|
150
147
|
accountBinding,
|
|
151
148
|
allowConsumed: true,
|
|
152
149
|
signal: finalizationInput.signal,
|
|
@@ -369,15 +366,32 @@ async function inspectGooglePlayPurchase(input) {
|
|
|
369
366
|
},
|
|
370
367
|
};
|
|
371
368
|
}
|
|
372
|
-
function createGooglePlayFinalizationOrderMatch(verifiedContext, clientOrderId) {
|
|
369
|
+
function createGooglePlayFinalizationOrderMatch(verifiedContext, clientOrderId, tokenTransactionId) {
|
|
373
370
|
if (verifiedContext === undefined) {
|
|
374
|
-
return
|
|
371
|
+
return createGooglePlayObservedOrderMatch(clientOrderId, tokenTransactionId);
|
|
375
372
|
}
|
|
376
373
|
if (verifiedContext.orderId === undefined) {
|
|
377
374
|
return { mode: 'token-only' };
|
|
378
375
|
}
|
|
379
376
|
return { mode: 'exact', orderId: verifiedContext.orderId };
|
|
380
377
|
}
|
|
378
|
+
function createGooglePlayObservedOrderMatch(observedTransactionId, tokenTransactionId) {
|
|
379
|
+
return observedTransactionId === tokenTransactionId
|
|
380
|
+
? { mode: 'token-only' }
|
|
381
|
+
: { mode: 'if-present', orderId: observedTransactionId };
|
|
382
|
+
}
|
|
383
|
+
/** A non-secret transaction ID for purchases whose Google order ID is absent or delayed. */
|
|
384
|
+
export async function createGooglePlayTokenTransactionId(purchaseToken) {
|
|
385
|
+
if (readOptionalIdentifier(purchaseToken, 4096) === undefined) {
|
|
386
|
+
throw new TypeError('Google Play purchase token is invalid.');
|
|
387
|
+
}
|
|
388
|
+
const subtle = globalThis.crypto?.subtle;
|
|
389
|
+
if (subtle === undefined) {
|
|
390
|
+
throw new Error('Web Crypto is required to hash Google Play purchase tokens.');
|
|
391
|
+
}
|
|
392
|
+
const digest = new Uint8Array(await subtle.digest('SHA-256', new TextEncoder().encode(purchaseToken)));
|
|
393
|
+
return `play-token-sha256:${[...digest].map((value) => value.toString(16).padStart(2, '0')).join('')}`;
|
|
394
|
+
}
|
|
381
395
|
function readGooglePlayVerifiedContext(payload, packageName, productId, purchaseTokenDigest) {
|
|
382
396
|
if (payload?.googlePlayPackageName !== packageName
|
|
383
397
|
|| payload.googlePlayProductId !== productId
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
export * from './admob-ssv.js';
|
|
2
2
|
export * from './app-store-verifier.js';
|
|
3
|
+
export * from './app-store-recovery.js';
|
|
3
4
|
export * from './apps-in-toss-evidence-verification.js';
|
|
4
5
|
export * from './apps-in-toss-partner-api.js';
|
|
5
6
|
export * from './client.js';
|
|
7
|
+
export * from './admob-client-reward.js';
|
|
6
8
|
export * from './contract.js';
|
|
7
9
|
export * from './evidence-verification.js';
|
|
8
10
|
export * from './guest-session.js';
|
|
9
11
|
export * from './google-play-purchase.js';
|
|
12
|
+
export * from './google-play-publisher-client.js';
|
|
10
13
|
export * from './microsoft-store-identity.js';
|
|
11
14
|
export * from './microsoft-store-purchase.js';
|
|
15
|
+
export * from './monetization-recovery.js';
|
|
12
16
|
export * from './notification-delivery.js';
|
|
13
17
|
export * from './platform-order.js';
|
|
14
18
|
export * from './progress-link.js';
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
export * from './admob-ssv.js';
|
|
2
2
|
export * from './app-store-verifier.js';
|
|
3
|
+
export * from './app-store-recovery.js';
|
|
3
4
|
export * from './apps-in-toss-evidence-verification.js';
|
|
4
5
|
export * from './apps-in-toss-partner-api.js';
|
|
5
6
|
export * from './client.js';
|
|
7
|
+
export * from './admob-client-reward.js';
|
|
6
8
|
export * from './contract.js';
|
|
7
9
|
export * from './evidence-verification.js';
|
|
8
10
|
export * from './guest-session.js';
|
|
9
11
|
export * from './google-play-purchase.js';
|
|
12
|
+
export * from './google-play-publisher-client.js';
|
|
10
13
|
export * from './microsoft-store-identity.js';
|
|
11
14
|
export * from './microsoft-store-purchase.js';
|
|
15
|
+
export * from './monetization-recovery.js';
|
|
12
16
|
export * from './notification-delivery.js';
|
|
13
17
|
export * from './platform-order.js';
|
|
14
18
|
export * from './progress-link.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { PurchaseResult, RewardedAdResult } from '@mpgd/platform';
|
|
2
|
+
import { type CreateGameServicesClientInput } from './client.js';
|
|
3
|
+
import type { GameServicesOperationClient, GameServicesPurchaseInput, GameServicesPurchaseResult, GameServicesRewardedAdInput, GameServicesRewardedAdResult } from './operations.js';
|
|
4
|
+
import type { ClaimAdRewardRequest, ClaimAdRewardResponse, VerifyPurchaseRequest, VerifyPurchaseResponse } from './types.js';
|
|
5
|
+
interface OperationIdentity {
|
|
6
|
+
/** The store must enforce uniqueness across users, not just within one account. */
|
|
7
|
+
readonly key: string;
|
|
8
|
+
readonly playerId: string;
|
|
9
|
+
readonly target: string;
|
|
10
|
+
readonly deploymentTarget: string;
|
|
11
|
+
readonly revision: number;
|
|
12
|
+
/** Fixed when a platform result is first journaled, before backend dispatch. */
|
|
13
|
+
readonly platformCompletedAt?: string;
|
|
14
|
+
}
|
|
15
|
+
export type MonetizationOperationRecord = (OperationIdentity & {
|
|
16
|
+
readonly kind: 'purchase';
|
|
17
|
+
readonly input: GameServicesPurchaseInput;
|
|
18
|
+
readonly platform?: PurchaseResult;
|
|
19
|
+
readonly request?: VerifyPurchaseRequest;
|
|
20
|
+
readonly response?: VerifyPurchaseResponse;
|
|
21
|
+
readonly result?: GameServicesPurchaseResult;
|
|
22
|
+
}) | (OperationIdentity & {
|
|
23
|
+
readonly kind: 'rewarded-ad';
|
|
24
|
+
readonly input: GameServicesRewardedAdInput;
|
|
25
|
+
readonly platform?: RewardedAdResult;
|
|
26
|
+
readonly request?: ClaimAdRewardRequest;
|
|
27
|
+
readonly response?: ClaimAdRewardResponse;
|
|
28
|
+
readonly result?: GameServicesRewardedAdResult;
|
|
29
|
+
});
|
|
30
|
+
/** Games must supply a durable, atomic implementation; an in-memory map is not recovery. */
|
|
31
|
+
export interface MonetizationOperationStore {
|
|
32
|
+
/** Atomically insert or return the existing operation for this global key. */
|
|
33
|
+
reserve(record: MonetizationOperationRecord): Promise<{
|
|
34
|
+
readonly created: boolean;
|
|
35
|
+
readonly record: MonetizationOperationRecord;
|
|
36
|
+
}>;
|
|
37
|
+
read(key: string): Promise<MonetizationOperationRecord | undefined>;
|
|
38
|
+
/** Compare-and-swap; reject stale revisions and changes to immutable identity. */
|
|
39
|
+
replace(expectedRevision: number, record: MonetizationOperationRecord): Promise<void>;
|
|
40
|
+
/** Return only unresolved operations; keep terminal records addressable by read for deduplication. */
|
|
41
|
+
listRecoverable(playerId: string): Promise<readonly MonetizationOperationRecord[]>;
|
|
42
|
+
}
|
|
43
|
+
export interface CreateRecoverableMonetizationClientInput extends CreateGameServicesClientInput {
|
|
44
|
+
readonly operationStore: MonetizationOperationStore;
|
|
45
|
+
}
|
|
46
|
+
export interface MonetizationOperationSummary {
|
|
47
|
+
readonly kind: MonetizationOperationRecord['kind'];
|
|
48
|
+
readonly idempotencyKey: string;
|
|
49
|
+
readonly status: 'pending' | 'granted' | 'rejected' | 'cancelled' | 'failed' | 'skipped' | 'unavailable' | 'action-required';
|
|
50
|
+
readonly finalizationPending: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface RecoverableMonetizationClient extends GameServicesOperationClient {
|
|
53
|
+
recoverPurchaseResult(idempotencyKey: string, platform: PurchaseResult): Promise<GameServicesPurchaseResult>;
|
|
54
|
+
recoverRewardResult(idempotencyKey: string, platform: RewardedAdResult): Promise<GameServicesRewardedAdResult>;
|
|
55
|
+
reconcile(): Promise<readonly MonetizationOperationSummary[]>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Client-side recovery coordinates UI callbacks and authoritative backend calls.
|
|
59
|
+
* It never grants a wallet locally. Journal evidence can contain receipts/tokens,
|
|
60
|
+
* so the game's durable store must encrypt and access-control its records.
|
|
61
|
+
*/
|
|
62
|
+
export declare function createRecoverableMonetizationClient(input: CreateRecoverableMonetizationClientInput): RecoverableMonetizationClient;
|
|
63
|
+
export {};
|