@mpgd/game-services 0.9.0 → 0.11.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.
@@ -0,0 +1,559 @@
1
+ export const googlePlayProductPurchaseEvidenceSchema = 'google-play.product-purchase.v2';
2
+ export function createGooglePlayProductPurchaseBoundary(input) {
3
+ const packageName = assertIdentifier(input.packageName, 'packageName', 256);
4
+ if (input.resolveObfuscatedAccountId === undefined
5
+ && input.resolveObfuscatedProfileId === undefined
6
+ && input.allowUnboundAuthenticatedPlayer !== true) {
7
+ throw new TypeError('Google Play purchases require account binding or explicit authenticated-player attribution.');
8
+ }
9
+ const now = input.now ?? (() => new Date().toISOString());
10
+ const inFlightFinalizations = new Map();
11
+ return {
12
+ supportsPurchaseGrant(finalizationInput) {
13
+ return finalizationInput.request.target === 'android'
14
+ && finalizationInput.product.type !== 'subscription'
15
+ && finalizationInput.request.evidence?.schema
16
+ === googlePlayProductPurchaseEvidenceSchema;
17
+ },
18
+ async verifyPurchase(verificationInput) {
19
+ if (verificationInput.request.target !== 'android') {
20
+ return {
21
+ status: 'rejected',
22
+ reason: 'GOOGLE_PLAY_TARGET_REQUIRED',
23
+ };
24
+ }
25
+ if (verificationInput.product.type === 'subscription') {
26
+ return {
27
+ status: 'rejected',
28
+ reason: 'GOOGLE_PLAY_SUBSCRIPTION_VERIFIER_REQUIRED',
29
+ };
30
+ }
31
+ const purchaseToken = readPurchaseToken(verificationInput.request.evidence);
32
+ if (purchaseToken === undefined) {
33
+ return {
34
+ status: 'rejected',
35
+ reason: 'GOOGLE_PLAY_PURCHASE_TOKEN_REQUIRED',
36
+ };
37
+ }
38
+ const accountBinding = await resolveGooglePlayAccountBinding(input, verificationInput.request.playerId, verificationInput.signal);
39
+ if (accountBinding.status === 'rejected') {
40
+ return rejected(accountBinding.reason);
41
+ }
42
+ const inspection = await inspectGooglePlayPurchase({
43
+ client: input.client,
44
+ packageName,
45
+ purchaseToken,
46
+ expectedProductId: verificationInput.platformProductId,
47
+ expectedProductType: verificationInput.product.type,
48
+ orderMatch: {
49
+ mode: 'if-present',
50
+ orderId: verificationInput.request.platformTransactionId,
51
+ },
52
+ accountBinding,
53
+ allowConsumed: false,
54
+ signal: verificationInput.signal,
55
+ });
56
+ if (inspection.status !== 'verified') {
57
+ return inspection;
58
+ }
59
+ return {
60
+ status: 'verified',
61
+ verificationId: createGooglePlayVerificationId(inspection.purchase.purchaseTokenDigest),
62
+ verifiedAt: now(),
63
+ platformEvidenceId: inspection.purchase.orderId ?? null,
64
+ payload: createVerificationPayload(packageName, inspection.purchase),
65
+ };
66
+ },
67
+ async finalizePurchaseGrant(finalizationInput) {
68
+ if (finalizationInput.product.type === 'subscription') {
69
+ return {
70
+ status: 'pending',
71
+ alreadyCompleted: false,
72
+ reason: 'GOOGLE_PLAY_SUBSCRIPTION_VERIFIER_REQUIRED',
73
+ };
74
+ }
75
+ const action = finalizationInput.product.type === 'consumable'
76
+ ? 'consume'
77
+ : 'acknowledge';
78
+ const purchaseToken = readPurchaseToken(finalizationInput.request.evidence);
79
+ if (purchaseToken === undefined) {
80
+ return {
81
+ status: 'pending',
82
+ action,
83
+ alreadyCompleted: false,
84
+ reason: 'GOOGLE_PLAY_PURCHASE_TOKEN_REQUIRED',
85
+ };
86
+ }
87
+ const purchaseTokenDigest = await digestPurchaseToken(packageName, purchaseToken);
88
+ if (finalizationInput.evidenceVerificationId
89
+ !== createGooglePlayVerificationId(purchaseTokenDigest)) {
90
+ return {
91
+ status: 'pending',
92
+ action,
93
+ alreadyCompleted: false,
94
+ reason: 'GOOGLE_PLAY_FINALIZATION_EVIDENCE_MISMATCH',
95
+ };
96
+ }
97
+ const verifiedContext = readGooglePlayVerifiedContext(finalizationInput.evidencePayload, packageName, finalizationInput.platformProductId, purchaseTokenDigest);
98
+ const existing = inFlightFinalizations.get(finalizationInput.evidenceVerificationId);
99
+ if (existing !== undefined) {
100
+ return existing;
101
+ }
102
+ const finalization = finalizeGooglePlayPurchase(input, packageName, purchaseToken, finalizationInput.product.type, action, verifiedContext, finalizationInput);
103
+ inFlightFinalizations.set(finalizationInput.evidenceVerificationId, finalization);
104
+ const releaseFinalization = () => {
105
+ finalizationInput.signal.removeEventListener('abort', releaseFinalization);
106
+ if (inFlightFinalizations.get(finalizationInput.evidenceVerificationId) === finalization) {
107
+ inFlightFinalizations.delete(finalizationInput.evidenceVerificationId);
108
+ }
109
+ };
110
+ finalizationInput.signal.addEventListener('abort', releaseFinalization, { once: true });
111
+ if (finalizationInput.signal.aborted) {
112
+ releaseFinalization();
113
+ }
114
+ try {
115
+ return await finalization;
116
+ }
117
+ finally {
118
+ releaseFinalization();
119
+ }
120
+ },
121
+ };
122
+ }
123
+ async function finalizeGooglePlayPurchase(input, packageName, purchaseToken, productType, action, verifiedContext, finalizationInput) {
124
+ if (finalizationInput.request.target !== 'android') {
125
+ return {
126
+ status: 'pending',
127
+ action,
128
+ alreadyCompleted: false,
129
+ reason: 'GOOGLE_PLAY_TARGET_REQUIRED',
130
+ };
131
+ }
132
+ try {
133
+ const accountBinding = verifiedContext?.accountBinding
134
+ ?? await resolveGooglePlayAccountBinding(input, finalizationInput.request.playerId, finalizationInput.signal);
135
+ if (accountBinding.status === 'rejected') {
136
+ return {
137
+ status: 'pending',
138
+ action,
139
+ alreadyCompleted: false,
140
+ reason: accountBinding.reason,
141
+ };
142
+ }
143
+ const inspection = await inspectGooglePlayPurchase({
144
+ client: input.client,
145
+ packageName,
146
+ purchaseToken,
147
+ expectedProductId: finalizationInput.platformProductId,
148
+ expectedProductType: productType,
149
+ orderMatch: createGooglePlayFinalizationOrderMatch(verifiedContext, finalizationInput.request.platformTransactionId),
150
+ accountBinding,
151
+ allowConsumed: true,
152
+ signal: finalizationInput.signal,
153
+ });
154
+ if (inspection.status !== 'verified') {
155
+ return {
156
+ status: 'pending',
157
+ action,
158
+ alreadyCompleted: false,
159
+ reason: inspection.reason,
160
+ };
161
+ }
162
+ if (action === 'consume') {
163
+ if (inspection.purchase.consumptionState === 'CONSUMPTION_STATE_CONSUMED') {
164
+ return {
165
+ status: 'completed',
166
+ action,
167
+ alreadyCompleted: true,
168
+ };
169
+ }
170
+ if (inspection.purchase.consumptionState
171
+ !== 'CONSUMPTION_STATE_YET_TO_BE_CONSUMED') {
172
+ return {
173
+ status: 'pending',
174
+ action,
175
+ alreadyCompleted: false,
176
+ reason: 'GOOGLE_PLAY_CONSUMPTION_STATE_UNSPECIFIED',
177
+ };
178
+ }
179
+ await input.client.consumeProductPurchase({
180
+ packageName,
181
+ productId: finalizationInput.platformProductId,
182
+ purchaseToken,
183
+ signal: finalizationInput.signal,
184
+ });
185
+ }
186
+ else {
187
+ if (inspection.purchase.acknowledgementState
188
+ === 'ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED') {
189
+ return {
190
+ status: 'completed',
191
+ action,
192
+ alreadyCompleted: true,
193
+ };
194
+ }
195
+ if (inspection.purchase.acknowledgementState
196
+ !== 'ACKNOWLEDGEMENT_STATE_PENDING') {
197
+ return {
198
+ status: 'pending',
199
+ action,
200
+ alreadyCompleted: false,
201
+ reason: 'GOOGLE_PLAY_ACKNOWLEDGEMENT_STATE_UNSPECIFIED',
202
+ };
203
+ }
204
+ await input.client.acknowledgeProductPurchase({
205
+ packageName,
206
+ productId: finalizationInput.platformProductId,
207
+ purchaseToken,
208
+ signal: finalizationInput.signal,
209
+ });
210
+ }
211
+ return {
212
+ status: 'completed',
213
+ action,
214
+ alreadyCompleted: false,
215
+ };
216
+ }
217
+ catch (error) {
218
+ if (isAbortError(error)) {
219
+ throw error;
220
+ }
221
+ return {
222
+ status: 'pending',
223
+ action,
224
+ alreadyCompleted: false,
225
+ reason: 'GOOGLE_PLAY_API_ERROR',
226
+ };
227
+ }
228
+ }
229
+ async function inspectGooglePlayPurchase(input) {
230
+ const raw = await input.client.getProductPurchaseV2({
231
+ packageName: input.packageName,
232
+ purchaseToken: input.purchaseToken,
233
+ signal: input.signal,
234
+ });
235
+ if (!isRecord(raw)) {
236
+ return rejected('GOOGLE_PLAY_RESPONSE_INVALID');
237
+ }
238
+ const stateContext = raw.purchaseStateContext;
239
+ const purchaseState = isRecord(stateContext)
240
+ ? readPurchaseState(stateContext.purchaseState)
241
+ : undefined;
242
+ if (purchaseState === 'PENDING') {
243
+ return {
244
+ status: 'pending',
245
+ reason: 'GOOGLE_PLAY_PURCHASE_PENDING',
246
+ };
247
+ }
248
+ if (purchaseState === 'CANCELLED') {
249
+ return rejected('GOOGLE_PLAY_PURCHASE_CANCELLED');
250
+ }
251
+ if (purchaseState !== 'PURCHASED') {
252
+ return rejected('GOOGLE_PLAY_PURCHASE_STATE_UNSPECIFIED');
253
+ }
254
+ const lineItems = raw.productLineItem;
255
+ if (!Array.isArray(lineItems) || lineItems.length !== 1 || !isRecord(lineItems[0])) {
256
+ return rejected('GOOGLE_PLAY_LINE_ITEM_COUNT_UNSUPPORTED');
257
+ }
258
+ const lineItem = lineItems[0];
259
+ if (lineItem.productId !== input.expectedProductId) {
260
+ return rejected('GOOGLE_PLAY_PRODUCT_MISMATCH');
261
+ }
262
+ const offerDetails = lineItem.productOfferDetails;
263
+ if (!isRecord(offerDetails)) {
264
+ return rejected('GOOGLE_PLAY_OFFER_DETAILS_INVALID');
265
+ }
266
+ if (offerDetails.rentOfferDetails !== undefined) {
267
+ return rejected('GOOGLE_PLAY_RENTAL_UNSUPPORTED');
268
+ }
269
+ const quantity = offerDetails.quantity;
270
+ if (!Number.isSafeInteger(quantity) || quantity !== 1) {
271
+ return rejected('GOOGLE_PLAY_QUANTITY_UNSUPPORTED');
272
+ }
273
+ const refundableQuantity = offerDetails.refundableQuantity;
274
+ if (typeof refundableQuantity !== 'number'
275
+ || !Number.isSafeInteger(refundableQuantity)
276
+ || refundableQuantity < 0
277
+ || refundableQuantity > quantity) {
278
+ return rejected('GOOGLE_PLAY_REFUNDABLE_QUANTITY_INVALID');
279
+ }
280
+ if (refundableQuantity !== quantity) {
281
+ return rejected('GOOGLE_PLAY_PURCHASE_REFUNDED');
282
+ }
283
+ const consumptionState = readConsumptionState(offerDetails.consumptionState);
284
+ if (input.expectedProductType === 'consumable'
285
+ && consumptionState === 'CONSUMPTION_STATE_UNSPECIFIED') {
286
+ return rejected('GOOGLE_PLAY_CONSUMPTION_STATE_UNSPECIFIED');
287
+ }
288
+ if (!input.allowConsumed
289
+ && consumptionState === 'CONSUMPTION_STATE_CONSUMED') {
290
+ return rejected('GOOGLE_PLAY_PURCHASE_ALREADY_CONSUMED');
291
+ }
292
+ const acknowledgementState = readAcknowledgementState(raw.acknowledgementState);
293
+ if (input.expectedProductType === 'non_consumable'
294
+ && acknowledgementState === 'ACKNOWLEDGEMENT_STATE_UNSPECIFIED') {
295
+ return rejected('GOOGLE_PLAY_ACKNOWLEDGEMENT_STATE_UNSPECIFIED');
296
+ }
297
+ const orderId = readOptionalIdentifier(raw.orderId, 512);
298
+ if (raw.orderId !== undefined && orderId === undefined) {
299
+ return rejected('GOOGLE_PLAY_ORDER_INVALID');
300
+ }
301
+ if ((input.orderMatch.mode === 'exact' && orderId !== input.orderMatch.orderId)
302
+ || (input.orderMatch.mode === 'if-present'
303
+ && orderId !== undefined
304
+ && orderId !== input.orderMatch.orderId)) {
305
+ return rejected('GOOGLE_PLAY_ORDER_MISMATCH');
306
+ }
307
+ // `token-only` deliberately skips order matching because the stored token
308
+ // digest is the authoritative replay identity when Google omitted the order.
309
+ const purchaseCompletionTime = readTimestamp(raw.purchaseCompletionTime);
310
+ if (purchaseCompletionTime === undefined) {
311
+ return rejected('GOOGLE_PLAY_COMPLETION_TIME_INVALID');
312
+ }
313
+ const obfuscatedExternalAccountId = readOptionalIdentifier(raw.obfuscatedExternalAccountId, 256);
314
+ if (raw.obfuscatedExternalAccountId !== undefined
315
+ && obfuscatedExternalAccountId === undefined) {
316
+ return rejected('GOOGLE_PLAY_ACCOUNT_INVALID');
317
+ }
318
+ const obfuscatedExternalProfileId = readOptionalIdentifier(raw.obfuscatedExternalProfileId, 256);
319
+ if (raw.obfuscatedExternalProfileId !== undefined
320
+ && obfuscatedExternalProfileId === undefined) {
321
+ return rejected('GOOGLE_PLAY_PROFILE_INVALID');
322
+ }
323
+ if (input.accountBinding.status === 'bound') {
324
+ // Defensive invariant: every bound context must carry at least one
325
+ // identifier, even though the constructors below already enforce it.
326
+ if (input.accountBinding.accountId === undefined
327
+ && input.accountBinding.profileId === undefined) {
328
+ return rejected('GOOGLE_PLAY_ACCOUNT_BINDING_REQUIRED');
329
+ }
330
+ if (input.accountBinding.accountId !== undefined
331
+ && obfuscatedExternalAccountId !== input.accountBinding.accountId) {
332
+ return rejected('GOOGLE_PLAY_ACCOUNT_MISMATCH');
333
+ }
334
+ if (input.accountBinding.profileId !== undefined
335
+ && obfuscatedExternalProfileId !== input.accountBinding.profileId) {
336
+ return rejected('GOOGLE_PLAY_PROFILE_MISMATCH');
337
+ }
338
+ if (input.accountBinding.profileId === undefined
339
+ && obfuscatedExternalProfileId !== undefined) {
340
+ return rejected('GOOGLE_PLAY_PROFILE_BINDING_REQUIRED');
341
+ }
342
+ if (input.accountBinding.accountId === undefined
343
+ && obfuscatedExternalAccountId !== undefined) {
344
+ return rejected('GOOGLE_PLAY_ACCOUNT_BINDING_REQUIRED');
345
+ }
346
+ }
347
+ // The unbound opt-in is safe only when the provider response is also unbound.
348
+ if (input.accountBinding.status === 'unbound'
349
+ && (obfuscatedExternalAccountId !== undefined
350
+ || obfuscatedExternalProfileId !== undefined)) {
351
+ return rejected('GOOGLE_PLAY_ACCOUNT_MISMATCH');
352
+ }
353
+ return {
354
+ status: 'verified',
355
+ purchase: {
356
+ purchaseToken: input.purchaseToken,
357
+ purchaseTokenDigest: await digestPurchaseToken(input.packageName, input.purchaseToken),
358
+ productId: input.expectedProductId,
359
+ ...(orderId === undefined ? {} : { orderId }),
360
+ purchaseCompletionTime,
361
+ consumptionState,
362
+ acknowledgementState,
363
+ ...(obfuscatedExternalAccountId === undefined
364
+ ? {}
365
+ : { obfuscatedExternalAccountId }),
366
+ ...(obfuscatedExternalProfileId === undefined
367
+ ? {}
368
+ : { obfuscatedExternalProfileId }),
369
+ },
370
+ };
371
+ }
372
+ function createGooglePlayFinalizationOrderMatch(verifiedContext, clientOrderId) {
373
+ if (verifiedContext === undefined) {
374
+ return { mode: 'if-present', orderId: clientOrderId };
375
+ }
376
+ if (verifiedContext.orderId === undefined) {
377
+ return { mode: 'token-only' };
378
+ }
379
+ return { mode: 'exact', orderId: verifiedContext.orderId };
380
+ }
381
+ function readGooglePlayVerifiedContext(payload, packageName, productId, purchaseTokenDigest) {
382
+ if (payload?.googlePlayPackageName !== packageName
383
+ || payload.googlePlayProductId !== productId
384
+ || payload.googlePlayPurchaseTokenDigest !== purchaseTokenDigest) {
385
+ return undefined;
386
+ }
387
+ const orderId = readOptionalIdentifier(payload.googlePlayOrderId, 512);
388
+ if (payload.googlePlayOrderId !== undefined && orderId === undefined) {
389
+ return undefined;
390
+ }
391
+ const accountId = readOptionalIdentifier(payload.googlePlayObfuscatedExternalAccountId, 256);
392
+ if (payload.googlePlayObfuscatedExternalAccountId !== undefined
393
+ && accountId === undefined) {
394
+ return undefined;
395
+ }
396
+ const profileId = readOptionalIdentifier(payload.googlePlayObfuscatedExternalProfileId, 256);
397
+ if (payload.googlePlayObfuscatedExternalProfileId !== undefined
398
+ && profileId === undefined) {
399
+ return undefined;
400
+ }
401
+ return {
402
+ ...(orderId === undefined ? {} : { orderId }),
403
+ accountBinding: accountId === undefined && profileId === undefined
404
+ ? { status: 'unbound' }
405
+ : {
406
+ status: 'bound',
407
+ ...(accountId === undefined ? {} : { accountId }),
408
+ ...(profileId === undefined ? {} : { profileId }),
409
+ },
410
+ };
411
+ }
412
+ async function resolveGooglePlayAccountBinding(input, playerId, signal) {
413
+ if (input.resolveObfuscatedAccountId === undefined
414
+ && input.resolveObfuscatedProfileId === undefined) {
415
+ return { status: 'unbound' };
416
+ }
417
+ let resolvedAccountId;
418
+ let resolvedProfileId;
419
+ try {
420
+ [resolvedAccountId, resolvedProfileId] = await Promise.all([
421
+ input.resolveObfuscatedAccountId?.(playerId, signal),
422
+ input.resolveObfuscatedProfileId?.(playerId, signal),
423
+ ]);
424
+ }
425
+ catch (error) {
426
+ if (isAbortError(error)) {
427
+ throw error;
428
+ }
429
+ return {
430
+ status: 'rejected',
431
+ reason: 'GOOGLE_PLAY_ACCOUNT_BINDING_ERROR',
432
+ };
433
+ }
434
+ if (resolvedAccountId === undefined
435
+ && resolvedProfileId === undefined
436
+ && input.allowUnboundAuthenticatedPlayer === true) {
437
+ return { status: 'unbound' };
438
+ }
439
+ const accountId = readOptionalIdentifier(resolvedAccountId, 256);
440
+ const profileId = readOptionalIdentifier(resolvedProfileId, 256);
441
+ if (resolvedAccountId !== undefined && accountId === undefined) {
442
+ return {
443
+ status: 'rejected',
444
+ reason: 'GOOGLE_PLAY_ACCOUNT_BINDING_REQUIRED',
445
+ };
446
+ }
447
+ if (resolvedProfileId !== undefined && profileId === undefined) {
448
+ return {
449
+ status: 'rejected',
450
+ reason: 'GOOGLE_PLAY_PROFILE_BINDING_REQUIRED',
451
+ };
452
+ }
453
+ if (accountId === undefined && profileId === undefined) {
454
+ return {
455
+ status: 'rejected',
456
+ reason: input.resolveObfuscatedAccountId === undefined
457
+ ? 'GOOGLE_PLAY_PROFILE_BINDING_REQUIRED'
458
+ : 'GOOGLE_PLAY_ACCOUNT_BINDING_REQUIRED',
459
+ };
460
+ }
461
+ return {
462
+ status: 'bound',
463
+ ...(accountId === undefined ? {} : { accountId }),
464
+ ...(profileId === undefined ? {} : { profileId }),
465
+ };
466
+ }
467
+ function createVerificationPayload(packageName, purchase) {
468
+ return {
469
+ googlePlayPackageName: packageName,
470
+ googlePlayProductId: purchase.productId,
471
+ googlePlayPurchaseTokenDigest: purchase.purchaseTokenDigest,
472
+ googlePlayPurchaseCompletionTime: purchase.purchaseCompletionTime,
473
+ googlePlayConsumptionState: purchase.consumptionState,
474
+ googlePlayAcknowledgementState: purchase.acknowledgementState,
475
+ ...(purchase.orderId === undefined ? {} : { googlePlayOrderId: purchase.orderId }),
476
+ ...(purchase.obfuscatedExternalAccountId === undefined
477
+ ? {}
478
+ : { googlePlayObfuscatedExternalAccountId: purchase.obfuscatedExternalAccountId }),
479
+ ...(purchase.obfuscatedExternalProfileId === undefined
480
+ ? {}
481
+ : { googlePlayObfuscatedExternalProfileId: purchase.obfuscatedExternalProfileId }),
482
+ };
483
+ }
484
+ function readPurchaseToken(evidence) {
485
+ if (evidence?.schema !== googlePlayProductPurchaseEvidenceSchema) {
486
+ return undefined;
487
+ }
488
+ return readOptionalIdentifier(evidence.payload.purchaseToken, 4096);
489
+ }
490
+ function readPurchaseState(input) {
491
+ if (input === 'PURCHASED'
492
+ || input === 'CANCELLED'
493
+ || input === 'PENDING'
494
+ || input === 'PURCHASE_STATE_UNSPECIFIED') {
495
+ return input;
496
+ }
497
+ return undefined;
498
+ }
499
+ function readConsumptionState(input) {
500
+ if (input === 'CONSUMPTION_STATE_YET_TO_BE_CONSUMED'
501
+ || input === 'CONSUMPTION_STATE_CONSUMED') {
502
+ return input;
503
+ }
504
+ return 'CONSUMPTION_STATE_UNSPECIFIED';
505
+ }
506
+ function readAcknowledgementState(input) {
507
+ if (input === 'ACKNOWLEDGEMENT_STATE_PENDING'
508
+ || input === 'ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED') {
509
+ return input;
510
+ }
511
+ return 'ACKNOWLEDGEMENT_STATE_UNSPECIFIED';
512
+ }
513
+ function readTimestamp(input) {
514
+ if (typeof input !== 'string' || input.length === 0 || !Number.isFinite(Date.parse(input))) {
515
+ return undefined;
516
+ }
517
+ return input;
518
+ }
519
+ function readOptionalIdentifier(input, maxLength) {
520
+ if (typeof input !== 'string'
521
+ || input.length === 0
522
+ || input.length > maxLength
523
+ || input.trim() !== input
524
+ || /[\u0000-\u001f\u007f]/u.test(input)) {
525
+ return undefined;
526
+ }
527
+ return input;
528
+ }
529
+ function assertIdentifier(input, label, maxLength) {
530
+ const identifier = readOptionalIdentifier(input, maxLength);
531
+ if (identifier === undefined) {
532
+ throw new TypeError(`${label} must be a non-empty identifier.`);
533
+ }
534
+ return identifier;
535
+ }
536
+ async function digestPurchaseToken(packageName, purchaseToken) {
537
+ const subtle = globalThis.crypto?.subtle;
538
+ if (subtle === undefined) {
539
+ throw new Error('Web Crypto is required to hash Google Play purchase tokens.');
540
+ }
541
+ const bytes = new TextEncoder().encode(JSON.stringify([packageName, purchaseToken]));
542
+ const digest = new Uint8Array(await subtle.digest('SHA-256', bytes));
543
+ return [...digest].map((value) => value.toString(16).padStart(2, '0')).join('');
544
+ }
545
+ function createGooglePlayVerificationId(purchaseTokenDigest) {
546
+ return `google-play:product:${purchaseTokenDigest}`;
547
+ }
548
+ function rejected(reason) {
549
+ return { status: 'rejected', reason };
550
+ }
551
+ function isAbortError(input) {
552
+ return typeof input === 'object'
553
+ && input !== null
554
+ && 'name' in input
555
+ && input.name === 'AbortError';
556
+ }
557
+ function isRecord(input) {
558
+ return typeof input === 'object' && input !== null && !Array.isArray(input);
559
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
+ export * from './admob-ssv.js';
2
+ export * from './app-store-verifier';
3
+ export * from './apps-in-toss-evidence-verification';
4
+ export * from './apps-in-toss-partner-api';
1
5
  export * from './client';
2
6
  export * from './contract';
3
7
  export * from './evidence-verification';
8
+ export * from './google-play-purchase';
4
9
  export * from './notification-delivery';
5
10
  export * from './platform-order';
6
11
  export * from './progress-link';
package/dist/index.js CHANGED
@@ -1,6 +1,11 @@
1
+ export * from './admob-ssv.js';
2
+ export * from './app-store-verifier';
3
+ export * from './apps-in-toss-evidence-verification';
4
+ export * from './apps-in-toss-partner-api';
1
5
  export * from './client';
2
6
  export * from './contract';
3
7
  export * from './evidence-verification';
8
+ export * from './google-play-purchase';
4
9
  export * from './notification-delivery';
5
10
  export * from './platform-order';
6
11
  export * from './progress-link';
package/dist/server.d.ts CHANGED
@@ -2,7 +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
+ import { type GameServicesEvidenceVerifier, type GameServicesPurchaseGrantFinalizer } from './evidence-verification';
6
6
  import { type ClaimAdRewardRequest, type ClaimAdRewardResponse, type EntitlementLedgerGrant, type EntitlementLedgerResult, type LeaderboardScoreTransaction, type ProductGrantTransaction, type RecordLeaderboardScoreRequest, type RecordLeaderboardScoreResponse, type VerifyPurchaseRequest, type VerifyPurchaseResponse } from './types';
7
7
  type CorsHeaders = Record<string, string>;
8
8
  export interface CreateGameServicesBackendInput {
@@ -15,6 +15,8 @@ export interface CreateGameServicesBackendInput {
15
15
  readonly version?: string;
16
16
  readonly evidenceVerifier?: GameServicesEvidenceVerifier;
17
17
  readonly evidenceVerificationTimeoutMs?: number;
18
+ readonly purchaseGrantFinalizationTimeoutMs?: number;
19
+ readonly purchaseGrantFinalizer?: GameServicesPurchaseGrantFinalizer;
18
20
  }
19
21
  export interface GameServicesBackendApiHandler {
20
22
  readonly version?: string;