@mpgd/game-services 0.8.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,566 @@
1
+ import { Point, verify as verifySecp256k1 } from '@noble/secp256k1';
2
+ export const admobSsvCustomDataSchema = 'mpgd.admob.ssv.binding.v1';
3
+ export const defaultAdMobSsvMaximumCallbackAgeMs = 86400000;
4
+ export const defaultAdMobSsvMaximumFutureSkewMs = 300000;
5
+ const signatureMarker = '&signature=';
6
+ const maximumCallbackUrlLength = 65536;
7
+ const maximumDateTimestampMs = 8640000000000000;
8
+ const ecPublicKeyObjectIdentifier = Uint8Array.of(0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01);
9
+ const p256ObjectIdentifier = Uint8Array.of(0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07);
10
+ const p384ObjectIdentifier = Uint8Array.of(0x2b, 0x81, 0x04, 0x00, 0x22);
11
+ const p521ObjectIdentifier = Uint8Array.of(0x2b, 0x81, 0x04, 0x00, 0x23);
12
+ const secp256k1ObjectIdentifier = Uint8Array.of(0x2b, 0x81, 0x04, 0x00, 0x0a);
13
+ const requiredSignedParameters = [
14
+ 'ad_network',
15
+ 'ad_unit',
16
+ 'custom_data',
17
+ 'reward_amount',
18
+ 'reward_item',
19
+ 'timestamp',
20
+ 'transaction_id',
21
+ 'user_id',
22
+ ];
23
+ export function encodeAdMobSsvCustomData(input) {
24
+ requireNonEmptyString(input.playerId, 'playerId');
25
+ requireNonEmptyString(input.placementId, 'placementId');
26
+ requireNonEmptyString(input.idempotencyKey, 'idempotencyKey');
27
+ return JSON.stringify({
28
+ schema: admobSsvCustomDataSchema,
29
+ playerId: input.playerId,
30
+ placementId: input.placementId,
31
+ idempotencyKey: input.idempotencyKey,
32
+ });
33
+ }
34
+ export function decodeAdMobSsvCustomData(input) {
35
+ try {
36
+ const parsed = JSON.parse(input);
37
+ if (!isRecord(parsed) || parsed.schema !== admobSsvCustomDataSchema) {
38
+ return undefined;
39
+ }
40
+ if (!isNonEmptyString(parsed.playerId)
41
+ || !isNonEmptyString(parsed.placementId)
42
+ || !isNonEmptyString(parsed.idempotencyKey)) {
43
+ return undefined;
44
+ }
45
+ const binding = {
46
+ playerId: parsed.playerId,
47
+ placementId: parsed.placementId,
48
+ idempotencyKey: parsed.idempotencyKey,
49
+ };
50
+ if (encodeAdMobSsvCustomData(binding) !== input) {
51
+ return undefined;
52
+ }
53
+ return binding;
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ export async function importAdMobSsvPublicKey(base64Spki, subtle = readGlobalSubtleCrypto()) {
60
+ requireNonEmptyString(base64Spki, 'base64Spki');
61
+ const spki = decodeBase64(base64Spki);
62
+ const parsed = parseAdMobSsvSubjectPublicKeyInfo(spki);
63
+ if (parsed === undefined) {
64
+ throw new Error('base64Spki must contain a supported EC SubjectPublicKeyInfo key.');
65
+ }
66
+ if (parsed.namedCurve === 'secp256k1') {
67
+ let publicKey;
68
+ try {
69
+ publicKey = new Uint8Array(Point.fromBytes(parsed.publicKey).toBytes(false));
70
+ }
71
+ catch {
72
+ throw new Error('base64Spki contains an invalid secp256k1 public key.');
73
+ }
74
+ return {
75
+ kind: 'secp256k1',
76
+ publicKey,
77
+ };
78
+ }
79
+ return subtle.importKey('spki', spki, {
80
+ name: 'ECDSA',
81
+ namedCurve: parsed.namedCurve,
82
+ }, false, ['verify']);
83
+ }
84
+ export function createAdMobSsvEvidenceVerifier(input) {
85
+ const subtle = input.subtle ?? readGlobalSubtleCrypto();
86
+ const now = input.now ?? Date.now;
87
+ const maximumCallbackAgeMs = input.maximumCallbackAgeMs
88
+ ?? defaultAdMobSsvMaximumCallbackAgeMs;
89
+ const maximumFutureSkewMs = input.maximumFutureSkewMs
90
+ ?? defaultAdMobSsvMaximumFutureSkewMs;
91
+ const resolveAdUnit = input.resolveAdUnit ?? defaultAdUnit;
92
+ const resolveRewardItem = input.resolveRewardItem ?? defaultRewardItem;
93
+ requireNonNegativeFiniteNumber(maximumCallbackAgeMs, 'maximumCallbackAgeMs');
94
+ requireNonNegativeFiniteNumber(maximumFutureSkewMs, 'maximumFutureSkewMs');
95
+ return {
96
+ async verifyPurchase() {
97
+ return rejected('ADMOB_SSV_PURCHASE_EVIDENCE_UNSUPPORTED');
98
+ },
99
+ async verifyAdReward(verificationInput) {
100
+ return verifyAdMobSsvReward({
101
+ callbackSource: input.callbackSource,
102
+ publicKeySource: input.publicKeySource,
103
+ subtle,
104
+ now,
105
+ maximumCallbackAgeMs,
106
+ maximumFutureSkewMs,
107
+ resolveAdUnit,
108
+ resolveRewardItem,
109
+ }, verificationInput);
110
+ },
111
+ };
112
+ }
113
+ async function verifyAdMobSsvReward(context, input) {
114
+ const { request, placement, platformPlacementId, signal } = input;
115
+ if (request.target !== 'android' && request.target !== 'ios') {
116
+ return rejected('ADMOB_SSV_TARGET_UNSUPPORTED');
117
+ }
118
+ if (platformPlacementId === undefined || platformPlacementId.length === 0) {
119
+ return rejected('ADMOB_SSV_AD_UNIT_UNCONFIGURED');
120
+ }
121
+ let callbackUrl;
122
+ try {
123
+ callbackUrl = await context.callbackSource.findCallback({
124
+ request,
125
+ platformPlacementId,
126
+ signal,
127
+ });
128
+ }
129
+ catch (error) {
130
+ if (signal.aborted) {
131
+ throw error;
132
+ }
133
+ return rejected('ADMOB_SSV_CALLBACK_ERROR');
134
+ }
135
+ if (callbackUrl === undefined) {
136
+ return {
137
+ status: 'pending',
138
+ reason: 'ADMOB_SSV_EVIDENCE_PENDING',
139
+ };
140
+ }
141
+ const callback = parseAdMobSsvCallback(callbackUrl);
142
+ if (callback === undefined) {
143
+ return rejected('ADMOB_SSV_CALLBACK_INVALID');
144
+ }
145
+ let publicKey;
146
+ try {
147
+ publicKey = await context.publicKeySource.getPublicKey({
148
+ keyId: callback.keyId,
149
+ signal,
150
+ });
151
+ }
152
+ catch (error) {
153
+ if (signal.aborted) {
154
+ throw error;
155
+ }
156
+ return rejected('ADMOB_SSV_KEY_ERROR');
157
+ }
158
+ if (publicKey === undefined) {
159
+ return rejected('ADMOB_SSV_KEY_UNAVAILABLE');
160
+ }
161
+ const componentLength = getAdMobSsvSignatureComponentLength(publicKey);
162
+ const signature = componentLength === undefined
163
+ ? undefined
164
+ : convertDerEcdsaSignature(callback.signature, componentLength);
165
+ if (signature === undefined) {
166
+ return rejected('ADMOB_SSV_KEY_OR_SIGNATURE_INVALID');
167
+ }
168
+ let signatureVerified;
169
+ try {
170
+ signatureVerified = await verifyAdMobSsvSignature(context.subtle, publicKey, signature, callback.signedContent);
171
+ }
172
+ catch {
173
+ return rejected('ADMOB_SSV_KEY_OR_SIGNATURE_INVALID');
174
+ }
175
+ if (!signatureVerified) {
176
+ return rejected('ADMOB_SSV_SIGNATURE_INVALID');
177
+ }
178
+ const expectedAdUnit = context.resolveAdUnit({
179
+ request,
180
+ placement,
181
+ platformPlacementId,
182
+ });
183
+ if (!isNonEmptyString(expectedAdUnit)) {
184
+ return rejected('ADMOB_SSV_AD_UNIT_UNCONFIGURED');
185
+ }
186
+ if (callback.adUnit !== expectedAdUnit) {
187
+ return rejected('ADMOB_SSV_AD_UNIT_MISMATCH');
188
+ }
189
+ if (callback.userId !== request.playerId) {
190
+ return rejected('ADMOB_SSV_USER_MISMATCH');
191
+ }
192
+ const binding = decodeAdMobSsvCustomData(callback.customData);
193
+ if (binding === undefined
194
+ || binding.playerId !== request.playerId
195
+ || binding.placementId !== request.placementId
196
+ || binding.idempotencyKey !== request.idempotencyKey) {
197
+ return rejected('ADMOB_SSV_CUSTOM_DATA_MISMATCH');
198
+ }
199
+ const expectedRewardItem = context.resolveRewardItem({ request, placement });
200
+ if (!isNonEmptyString(expectedRewardItem)) {
201
+ return rejected('ADMOB_SSV_REWARD_ITEM_UNCONFIGURED');
202
+ }
203
+ if (callback.rewardAmount !== placement.reward?.amount
204
+ || callback.rewardItem !== expectedRewardItem) {
205
+ return rejected('ADMOB_SSV_REWARD_MISMATCH');
206
+ }
207
+ const nowMs = context.now();
208
+ if (!Number.isFinite(nowMs)) {
209
+ return rejected('ADMOB_SSV_CLOCK_INVALID');
210
+ }
211
+ if (callback.timestampMs > nowMs + context.maximumFutureSkewMs) {
212
+ return rejected('ADMOB_SSV_TIMESTAMP_IN_FUTURE');
213
+ }
214
+ if (callback.timestampMs < nowMs - context.maximumCallbackAgeMs) {
215
+ return rejected('ADMOB_SSV_CALLBACK_EXPIRED');
216
+ }
217
+ return {
218
+ status: 'verified',
219
+ verificationId: `admob:ssv:${callback.transactionId}`,
220
+ verifiedAt: new Date(callback.timestampMs).toISOString(),
221
+ payload: {
222
+ admobSsvTransactionId: callback.transactionId,
223
+ admobSsvAdNetwork: callback.adNetwork,
224
+ admobSsvAdUnit: callback.adUnit,
225
+ admobSsvRewardAmount: callback.rewardAmount,
226
+ admobSsvRewardItem: callback.rewardItem,
227
+ admobSsvKeyId: callback.keyId,
228
+ },
229
+ };
230
+ }
231
+ function parseAdMobSsvCallback(input) {
232
+ try {
233
+ if (input.length === 0 || input.length > maximumCallbackUrlLength) {
234
+ return undefined;
235
+ }
236
+ const url = new URL(input);
237
+ if (url.protocol !== 'https:'
238
+ || url.username.length > 0
239
+ || url.password.length > 0
240
+ || url.hash.length > 0) {
241
+ return undefined;
242
+ }
243
+ const queryStart = input.indexOf('?');
244
+ if (queryStart < 0) {
245
+ return undefined;
246
+ }
247
+ const query = input.slice(queryStart + 1);
248
+ const signatureIndex = query.indexOf(signatureMarker);
249
+ if (signatureIndex <= 0
250
+ || signatureIndex !== query.lastIndexOf(signatureMarker)) {
251
+ return undefined;
252
+ }
253
+ const signedContent = query.slice(0, signatureIndex);
254
+ let decodedSignedContent;
255
+ try {
256
+ decodedSignedContent = decodeURIComponent(signedContent);
257
+ }
258
+ catch {
259
+ return undefined;
260
+ }
261
+ const signatureAndKey = query.slice(signatureIndex + 1);
262
+ const suffixMatch = /^signature=([^&]+)&key_id=([^&]+)$/.exec(signatureAndKey);
263
+ if (suffixMatch === null) {
264
+ return undefined;
265
+ }
266
+ const parameters = new URLSearchParams(signedContent);
267
+ for (const parameter of requiredSignedParameters) {
268
+ if (parameters.getAll(parameter).length !== 1) {
269
+ return undefined;
270
+ }
271
+ }
272
+ const rewardAmount = parseUnsignedInteger(readParameter(parameters, 'reward_amount'));
273
+ const timestampMs = parseUnsignedInteger(readParameter(parameters, 'timestamp'));
274
+ if (rewardAmount === undefined || timestampMs === undefined) {
275
+ return undefined;
276
+ }
277
+ const adNetwork = readParameter(parameters, 'ad_network');
278
+ const adUnit = readParameter(parameters, 'ad_unit');
279
+ const customData = readParameter(parameters, 'custom_data');
280
+ const rewardItem = readParameter(parameters, 'reward_item');
281
+ const transactionId = readParameter(parameters, 'transaction_id');
282
+ const userId = readParameter(parameters, 'user_id');
283
+ const signature = decodeBase64Url(suffixMatch[1] ?? '');
284
+ const keyId = suffixMatch[2] ?? '';
285
+ if (!/^[0-9]+$/.test(adNetwork)
286
+ || !isNonEmptyString(adUnit)
287
+ || !isNonEmptyString(customData)
288
+ || !isNonEmptyString(rewardItem)
289
+ || !/^[A-Fa-f0-9]+$/.test(transactionId)
290
+ || !isNonEmptyString(userId)
291
+ || signature === undefined
292
+ || !/^[0-9]+$/.test(keyId)) {
293
+ return undefined;
294
+ }
295
+ return {
296
+ adNetwork,
297
+ adUnit,
298
+ customData,
299
+ keyId,
300
+ rewardAmount,
301
+ rewardItem,
302
+ signature,
303
+ signedContent: new TextEncoder().encode(decodedSignedContent),
304
+ timestampMs,
305
+ transactionId,
306
+ userId,
307
+ };
308
+ }
309
+ catch {
310
+ return undefined;
311
+ }
312
+ }
313
+ function convertDerEcdsaSignature(signature, componentLength) {
314
+ const sequence = readDerElement(signature, 0, 0x30);
315
+ if (sequence === undefined || sequence.end !== signature.length) {
316
+ return undefined;
317
+ }
318
+ const r = readDerElement(signature, sequence.contentStart, 0x02);
319
+ if (r === undefined) {
320
+ return undefined;
321
+ }
322
+ const s = readDerElement(signature, r.end, 0x02);
323
+ if (s === undefined || s.end !== sequence.end) {
324
+ return undefined;
325
+ }
326
+ const rawR = normalizeDerInteger(signature.slice(r.contentStart, r.end), componentLength);
327
+ const rawS = normalizeDerInteger(signature.slice(s.contentStart, s.end), componentLength);
328
+ if (rawR === undefined || rawS === undefined) {
329
+ return undefined;
330
+ }
331
+ const raw = new Uint8Array(componentLength * 2);
332
+ raw.set(rawR, 0);
333
+ raw.set(rawS, componentLength);
334
+ return raw;
335
+ }
336
+ async function verifyAdMobSsvSignature(subtle, publicKey, signature, signedContent) {
337
+ if (isAdMobSsvSecp256k1PublicKey(publicKey)) {
338
+ const digest = new Uint8Array(await subtle.digest('SHA-256', signedContent));
339
+ return verifySecp256k1(signature, digest, publicKey.publicKey, {
340
+ format: 'compact',
341
+ lowS: false,
342
+ prehash: false,
343
+ });
344
+ }
345
+ return subtle.verify({
346
+ name: 'ECDSA',
347
+ hash: 'SHA-256',
348
+ }, publicKey, signature, signedContent);
349
+ }
350
+ function getAdMobSsvSignatureComponentLength(publicKey) {
351
+ if (isAdMobSsvSecp256k1PublicKey(publicKey)) {
352
+ return 32;
353
+ }
354
+ if (publicKey.type !== 'public' || publicKey.algorithm.name !== 'ECDSA') {
355
+ return undefined;
356
+ }
357
+ const namedCurve = publicKey.algorithm.namedCurve;
358
+ switch (namedCurve) {
359
+ case 'P-256':
360
+ return 32;
361
+ case 'P-384':
362
+ return 48;
363
+ case 'P-521':
364
+ return 66;
365
+ default:
366
+ return undefined;
367
+ }
368
+ }
369
+ function isAdMobSsvSecp256k1PublicKey(input) {
370
+ return 'kind' in input && input.kind === 'secp256k1';
371
+ }
372
+ function parseAdMobSsvSubjectPublicKeyInfo(input) {
373
+ const subjectPublicKeyInfo = readDerElement(input, 0, 0x30);
374
+ if (subjectPublicKeyInfo === undefined || subjectPublicKeyInfo.end !== input.length) {
375
+ return undefined;
376
+ }
377
+ const algorithm = readDerElement(input, subjectPublicKeyInfo.contentStart, 0x30);
378
+ if (algorithm === undefined) {
379
+ return undefined;
380
+ }
381
+ const algorithmObjectIdentifier = readDerElement(input, algorithm.contentStart, 0x06);
382
+ if (algorithmObjectIdentifier === undefined
383
+ || !bytesEqual(input.slice(algorithmObjectIdentifier.contentStart, algorithmObjectIdentifier.end), ecPublicKeyObjectIdentifier)) {
384
+ return undefined;
385
+ }
386
+ const curveObjectIdentifier = readDerElement(input, algorithmObjectIdentifier.end, 0x06);
387
+ if (curveObjectIdentifier === undefined || curveObjectIdentifier.end !== algorithm.end) {
388
+ return undefined;
389
+ }
390
+ const namedCurve = readAdMobSsvSpkiNamedCurve(input.slice(curveObjectIdentifier.contentStart, curveObjectIdentifier.end));
391
+ const subjectPublicKey = readDerElement(input, algorithm.end, 0x03);
392
+ if (namedCurve === undefined
393
+ || subjectPublicKey === undefined
394
+ || subjectPublicKey.end !== subjectPublicKeyInfo.end
395
+ || input[subjectPublicKey.contentStart] !== 0) {
396
+ return undefined;
397
+ }
398
+ return {
399
+ namedCurve,
400
+ publicKey: input.slice(subjectPublicKey.contentStart + 1, subjectPublicKey.end),
401
+ };
402
+ }
403
+ function readAdMobSsvSpkiNamedCurve(objectIdentifier) {
404
+ if (bytesEqual(objectIdentifier, p256ObjectIdentifier)) {
405
+ return 'P-256';
406
+ }
407
+ if (bytesEqual(objectIdentifier, p384ObjectIdentifier)) {
408
+ return 'P-384';
409
+ }
410
+ if (bytesEqual(objectIdentifier, p521ObjectIdentifier)) {
411
+ return 'P-521';
412
+ }
413
+ return bytesEqual(objectIdentifier, secp256k1ObjectIdentifier) ? 'secp256k1' : undefined;
414
+ }
415
+ function bytesEqual(first, second) {
416
+ return first.length === second.length
417
+ && first.every((byte, index) => byte === second[index]);
418
+ }
419
+ function readDerElement(input, offset, expectedTag) {
420
+ if (input[offset] !== expectedTag) {
421
+ return undefined;
422
+ }
423
+ const length = readDerLength(input, offset + 1);
424
+ if (length === undefined) {
425
+ return undefined;
426
+ }
427
+ const end = length.contentStart + length.length;
428
+ if (end > input.length) {
429
+ return undefined;
430
+ }
431
+ return {
432
+ contentStart: length.contentStart,
433
+ end,
434
+ };
435
+ }
436
+ function readDerLength(input, offset) {
437
+ const first = input[offset];
438
+ if (first === undefined) {
439
+ return undefined;
440
+ }
441
+ if ((first & 0x80) === 0) {
442
+ return {
443
+ contentStart: offset + 1,
444
+ length: first,
445
+ };
446
+ }
447
+ const byteCount = first & 0x7f;
448
+ if (byteCount === 0 || byteCount > 4 || offset + byteCount >= input.length) {
449
+ return undefined;
450
+ }
451
+ let length = 0;
452
+ for (let index = 0; index < byteCount; index += 1) {
453
+ const byte = input[offset + 1 + index];
454
+ if (byte === undefined) {
455
+ return undefined;
456
+ }
457
+ length = (length * 256) + byte;
458
+ }
459
+ if (length < 128) {
460
+ return undefined;
461
+ }
462
+ return {
463
+ contentStart: offset + 1 + byteCount,
464
+ length,
465
+ };
466
+ }
467
+ function normalizeDerInteger(input, componentLength) {
468
+ if (input.length === 0) {
469
+ return undefined;
470
+ }
471
+ if (input[0] === 0) {
472
+ if (input.length === 1 || (input[1] ?? 0) < 0x80) {
473
+ return undefined;
474
+ }
475
+ input = input.slice(1);
476
+ }
477
+ else if ((input[0] ?? 0) >= 0x80) {
478
+ return undefined;
479
+ }
480
+ if (input.length > componentLength) {
481
+ return undefined;
482
+ }
483
+ const normalized = new Uint8Array(componentLength);
484
+ normalized.set(input, componentLength - input.length);
485
+ return normalized;
486
+ }
487
+ function defaultAdUnit(input) {
488
+ const separatorIndex = input.platformPlacementId.lastIndexOf('/');
489
+ return separatorIndex < 0
490
+ ? input.platformPlacementId
491
+ : input.platformPlacementId.slice(separatorIndex + 1);
492
+ }
493
+ function defaultRewardItem(input) {
494
+ const reward = input.placement.reward;
495
+ if (reward === undefined) {
496
+ return '';
497
+ }
498
+ return reward.type === 'currency' ? reward.currency : reward.type;
499
+ }
500
+ function readGlobalSubtleCrypto() {
501
+ const subtle = globalThis.crypto?.subtle;
502
+ if (subtle === undefined) {
503
+ throw new Error('globalThis.crypto.subtle is unavailable. Provide a Web Crypto SubtleCrypto implementation.');
504
+ }
505
+ return subtle;
506
+ }
507
+ function decodeBase64(input) {
508
+ const decoded = atob(input);
509
+ const output = new Uint8Array(decoded.length);
510
+ for (let index = 0; index < decoded.length; index += 1) {
511
+ output[index] = decoded.charCodeAt(index);
512
+ }
513
+ return output;
514
+ }
515
+ function decodeBase64Url(input) {
516
+ if (!/^[A-Za-z0-9_-]+$/.test(input)) {
517
+ return undefined;
518
+ }
519
+ const remainder = input.length % 4;
520
+ if (remainder === 1) {
521
+ return undefined;
522
+ }
523
+ const base64 = input.replaceAll('-', '+').replaceAll('_', '/')
524
+ + '='.repeat((4 - remainder) % 4);
525
+ try {
526
+ return decodeBase64(base64);
527
+ }
528
+ catch {
529
+ return undefined;
530
+ }
531
+ }
532
+ function readParameter(parameters, name) {
533
+ return parameters.get(name) ?? '';
534
+ }
535
+ function parseUnsignedInteger(input) {
536
+ if (!/^(0|[1-9][0-9]*)$/.test(input)) {
537
+ return undefined;
538
+ }
539
+ const parsed = Number(input);
540
+ return (Number.isSafeInteger(parsed)
541
+ && parsed <= maximumDateTimestampMs)
542
+ ? parsed
543
+ : undefined;
544
+ }
545
+ function rejected(reason) {
546
+ return {
547
+ status: 'rejected',
548
+ reason,
549
+ };
550
+ }
551
+ function isRecord(input) {
552
+ return typeof input === 'object' && input !== null && !Array.isArray(input);
553
+ }
554
+ function isNonEmptyString(input) {
555
+ return typeof input === 'string' && input.trim().length > 0;
556
+ }
557
+ function requireNonEmptyString(input, name) {
558
+ if (!isNonEmptyString(input)) {
559
+ throw new Error(`${name} must be a non-empty string.`);
560
+ }
561
+ }
562
+ function requireNonNegativeFiniteNumber(input, name) {
563
+ if (!Number.isFinite(input) || input < 0) {
564
+ throw new Error(`${name} must be a non-negative finite number.`);
565
+ }
566
+ }
@@ -0,0 +1,25 @@
1
+ import type { AppStoreEnvironment, AppStoreInAppPurchaseType, AppStoreTransactionPayload } from './app-store-verifier';
2
+ export interface CreateAppStoreTransactionConformanceFixtureInput {
3
+ readonly transactionId?: string;
4
+ readonly originalTransactionId?: string;
5
+ readonly bundleId?: string;
6
+ readonly productId?: string;
7
+ readonly purchaseDate?: number;
8
+ readonly signedDate?: number;
9
+ readonly environment?: AppStoreEnvironment;
10
+ readonly type?: AppStoreInAppPurchaseType;
11
+ readonly includeQuantity?: boolean;
12
+ readonly quantity?: number;
13
+ /** Set to false to omit the default signed account token for fail-closed tests. */
14
+ readonly includeAppAccountToken?: boolean;
15
+ readonly appAccountToken?: string;
16
+ readonly expiresDate?: number;
17
+ readonly revocationDate?: number;
18
+ readonly isUpgraded?: boolean;
19
+ }
20
+ /**
21
+ * Builds deterministic decoded JWS data for verifier and provider-adapter
22
+ * conformance tests. The fixture is not signed evidence and contains no Apple
23
+ * credentials, authorization tokens, private keys, or live transaction IDs.
24
+ */
25
+ export declare function createAppStoreTransactionConformanceFixture(input?: CreateAppStoreTransactionConformanceFixtureInput): AppStoreTransactionPayload;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Builds deterministic decoded JWS data for verifier and provider-adapter
3
+ * conformance tests. The fixture is not signed evidence and contains no Apple
4
+ * credentials, authorization tokens, private keys, or live transaction IDs.
5
+ */
6
+ export function createAppStoreTransactionConformanceFixture(input = {}) {
7
+ return {
8
+ transactionId: input.transactionId ?? '2000000000000001',
9
+ originalTransactionId: input.originalTransactionId ?? '2000000000000000',
10
+ bundleId: input.bundleId ?? 'com.example.game',
11
+ productId: input.productId ?? 'com.example.game.coins100',
12
+ purchaseDate: input.purchaseDate ?? Date.parse('2020-01-02T03:04:05.000Z'),
13
+ signedDate: input.signedDate ?? Date.parse('2020-01-02T03:04:06.000Z'),
14
+ environment: input.environment ?? 'Production',
15
+ type: input.type ?? 'Consumable',
16
+ ...(input.includeQuantity === false ? {} : { quantity: input.quantity ?? 1 }),
17
+ ...(input.includeAppAccountToken === false
18
+ ? {}
19
+ : {
20
+ appAccountToken: input.appAccountToken ?? 'f15f2ed7-f92a-4c5a-90e1-15d26cd729f2',
21
+ }),
22
+ ...(input.expiresDate === undefined ? {} : { expiresDate: input.expiresDate }),
23
+ ...(input.revocationDate === undefined
24
+ ? {}
25
+ : { revocationDate: input.revocationDate }),
26
+ ...(input.isUpgraded === undefined ? {} : { isUpgraded: input.isUpgraded }),
27
+ };
28
+ }