@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.
- 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/apps-in-toss-partner-api.d.ts +48 -0
- package/dist/apps-in-toss-partner-api.js +282 -0
- 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 +5 -0
- package/dist/index.js +5 -0
- package/dist/server.d.ts +3 -1
- package/dist/server.js +199 -14
- package/dist/types.d.ts +10 -0
- package/dist/types.js +32 -0
- package/package.json +42 -5
|
@@ -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;
|