@mpgd/game-services 0.11.1 → 0.12.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,835 @@
1
+ export const microsoftStoreDigitalGoodsEvidenceSchema = 'mpgd.microsoft-store.digital-goods.v1';
2
+ export const microsoftStoreCollectionsQueryUrl = 'https://collections.mp.microsoft.com/v9.0/collections/publisherQuery';
3
+ export const microsoftStoreCollectionsConsumeUrl = 'https://collections.mp.microsoft.com/v8.0/collections/consume';
4
+ /** Process-local test/development store. Production requires durable atomic shared storage. */
5
+ export class InMemoryMicrosoftStoreRecoveryOwnershipStore {
6
+ owners = new Map();
7
+ async claim(input) {
8
+ const key = createRecoveryOwnershipKey(input);
9
+ const owner = this.owners.get(key);
10
+ if (owner !== undefined && owner.playerId !== input.playerId) {
11
+ return undefined;
12
+ }
13
+ if (owner === undefined || owner.providerPurchaseId !== input.providerPurchaseId) {
14
+ this.owners.set(key, Object.freeze({ ...input }));
15
+ return Object.freeze({ ...input });
16
+ }
17
+ return Object.freeze({ ...owner });
18
+ }
19
+ async get(input) {
20
+ const owner = this.owners.get(createRecoveryOwnershipKey(input));
21
+ return owner === undefined ? undefined : Object.freeze({ ...owner });
22
+ }
23
+ async release(input) {
24
+ const key = createRecoveryOwnershipKey(input);
25
+ const owner = this.owners.get(key);
26
+ if (owner?.playerId === input.playerId
27
+ && owner.generation === input.generation
28
+ && owner.providerPurchaseId === input.providerPurchaseId) {
29
+ this.owners.delete(key);
30
+ }
31
+ }
32
+ }
33
+ export function createInMemoryMicrosoftStoreRecoveryOwnershipStore() {
34
+ return new InMemoryMicrosoftStoreRecoveryOwnershipStore();
35
+ }
36
+ export class MicrosoftStoreCollectionsUnavailableError extends Error {
37
+ cause;
38
+ constructor(message, cause) {
39
+ super(message);
40
+ this.name = 'MicrosoftStoreCollectionsUnavailableError';
41
+ this.cause = cause;
42
+ }
43
+ }
44
+ export function createMicrosoftStoreCollectionsClient(input = {}) {
45
+ const fetch = input.fetch ?? readGlobalFetch();
46
+ const maxResponseBytes = positiveSafeInteger(input.maxResponseBytes ?? 128 * 1024, 'maxResponseBytes');
47
+ const userAgent = optionalHeaderValue(input.userAgent);
48
+ async function post(url, credentials, body, signal) {
49
+ assertCredentials(credentials);
50
+ const accessToken = credentials.accessToken;
51
+ let response;
52
+ try {
53
+ response = await fetch(url, {
54
+ method: 'POST',
55
+ headers: {
56
+ accept: 'application/json',
57
+ authorization: `Bearer ${accessToken}`,
58
+ 'content-type': 'application/json',
59
+ ...(userAgent === undefined ? {} : { 'user-agent': userAgent }),
60
+ },
61
+ body: JSON.stringify(body),
62
+ signal,
63
+ });
64
+ }
65
+ catch (error) {
66
+ if (signal.aborted) {
67
+ throw error;
68
+ }
69
+ throw new MicrosoftStoreCollectionsUnavailableError('Microsoft Store Collections transport is unavailable.', error);
70
+ }
71
+ if (response.status !== 200) {
72
+ await cancelBody(response.body);
73
+ throw new MicrosoftStoreCollectionsUnavailableError(`Microsoft Store Collections returned HTTP ${String(response.status)}.`);
74
+ }
75
+ return readBoundedJson(response.body, maxResponseBytes, signal);
76
+ }
77
+ return {
78
+ queryProduct({ credentials, storeId, signal }) {
79
+ const normalizedStoreId = identifier(storeId, 'storeId');
80
+ return post(microsoftStoreCollectionsQueryUrl, credentials, {
81
+ beneficiaries: [beneficiary(credentials.userStoreId)],
82
+ productSkuIds: [{ productId: normalizedStoreId }],
83
+ excludeDuplicates: true,
84
+ validityType: 'Valid',
85
+ maxPageSize: 10,
86
+ ...(credentials.sandbox === undefined ? {} : { sbx: credentials.sandbox }),
87
+ }, signal);
88
+ },
89
+ consumeProduct({ credentials, storeId, trackingId, signal }) {
90
+ return post(microsoftStoreCollectionsConsumeUrl, credentials, {
91
+ beneficiary: beneficiary(credentials.userStoreId),
92
+ // Developer-managed v8 consume has no skuId or removeQuantity. Those fields are
93
+ // unsupported here or apply only to Store-managed consumables.
94
+ productId: identifier(storeId, 'storeId'),
95
+ trackingId: guid(trackingId, 'trackingId'),
96
+ includeOrderIds: true,
97
+ ...(credentials.sandbox === undefined ? {} : { sbx: credentials.sandbox }),
98
+ }, signal);
99
+ },
100
+ };
101
+ }
102
+ export function createMicrosoftStorePurchaseBoundary(input) {
103
+ const storeIds = normalizeStoreIds(input.storeIds);
104
+ const inAppOfferTokens = normalizeInAppOfferTokens(input.inAppOfferTokens, storeIds);
105
+ const historicalProductMappings = normalizeHistoricalProductMappings(input.historicalProductMappings ?? {}, storeIds, inAppOfferTokens);
106
+ const now = input.now ?? (() => new Date().toISOString());
107
+ const inFlightFinalizations = new Map();
108
+ async function resolveRecoveryOwnership(ownershipInput) {
109
+ const playerId = optionalIdentifier(ownershipInput.playerId);
110
+ const productId = optionalIdentifier(ownershipInput.productId);
111
+ const generation = optionalIdentifier(ownershipInput.idempotencyKey);
112
+ const clientEvidence = readRecoveryOwnershipEvidence(ownershipInput);
113
+ if (playerId === undefined
114
+ || productId === undefined
115
+ || clientEvidence === undefined) {
116
+ return { status: 'denied' };
117
+ }
118
+ const currentInAppOfferToken = inAppOfferTokens.get(productId);
119
+ if (currentInAppOfferToken === undefined
120
+ || ownershipInput.inAppOfferToken !== currentInAppOfferToken) {
121
+ return { status: 'denied' };
122
+ }
123
+ const storeId = resolveMappedStoreId(productId, currentInAppOfferToken, clientEvidence.itemId, storeIds, historicalProductMappings);
124
+ if (storeId === undefined) {
125
+ return { status: 'denied' };
126
+ }
127
+ let credentials;
128
+ try {
129
+ credentials = await input.resolveCredentials(playerId, ownershipInput.signal);
130
+ assertCredentials(credentials);
131
+ }
132
+ catch (error) {
133
+ if (ownershipInput.signal.aborted) {
134
+ throw error;
135
+ }
136
+ return { status: 'unavailable' };
137
+ }
138
+ if (credentials.sandbox !== undefined && credentials.sandbox !== 'RETAIL') {
139
+ return { status: 'denied' };
140
+ }
141
+ let accountBindingHash;
142
+ try {
143
+ accountBindingHash = await createAccountBindingHash(credentials.accountBindingId);
144
+ }
145
+ catch {
146
+ return { status: 'unavailable' };
147
+ }
148
+ let response;
149
+ try {
150
+ response = await input.client.queryProduct({
151
+ credentials,
152
+ storeId,
153
+ signal: ownershipInput.signal,
154
+ });
155
+ }
156
+ catch (error) {
157
+ if (ownershipInput.signal.aborted) {
158
+ throw error;
159
+ }
160
+ return { status: 'unavailable' };
161
+ }
162
+ const item = inspectPublisherQuery(response, storeId);
163
+ if (item.status === 'rejected') {
164
+ return { status: 'denied' };
165
+ }
166
+ return {
167
+ status: 'granted',
168
+ record: {
169
+ accountBindingHash,
170
+ storeId,
171
+ playerId,
172
+ },
173
+ ...(item.status === 'verified'
174
+ ? { providerPurchaseId: createVerificationId(item.item) }
175
+ : {}),
176
+ ...(generation === undefined ? {} : { requestedGeneration: generation }),
177
+ };
178
+ }
179
+ return {
180
+ async claimRecoveryOwnership(ownershipInput) {
181
+ const ownership = await resolveRecoveryOwnership(ownershipInput);
182
+ if (ownership.status !== 'granted') {
183
+ return ownership;
184
+ }
185
+ if (ownership.requestedGeneration === undefined) {
186
+ return { status: 'denied' };
187
+ }
188
+ if (ownership.providerPurchaseId === undefined) {
189
+ return { status: 'unavailable' };
190
+ }
191
+ try {
192
+ const effective = await input.recoveryOwnershipStore.claim({
193
+ ...ownership.record,
194
+ generation: ownership.requestedGeneration,
195
+ providerPurchaseId: ownership.providerPurchaseId,
196
+ });
197
+ return effective?.playerId === ownership.record.playerId
198
+ && effective.generation === ownership.requestedGeneration
199
+ && effective.providerPurchaseId === ownership.providerPurchaseId
200
+ ? { status: 'granted', idempotencyKey: effective.generation }
201
+ : { status: 'denied' };
202
+ }
203
+ catch {
204
+ return { status: 'unavailable' };
205
+ }
206
+ },
207
+ async hasRecoveryOwnership(ownershipInput) {
208
+ const ownership = await resolveRecoveryOwnership(ownershipInput);
209
+ if (ownership.status !== 'granted') {
210
+ return ownership;
211
+ }
212
+ try {
213
+ const existing = await input.recoveryOwnershipStore.get(ownership.record);
214
+ if (existing === undefined) {
215
+ return { status: 'denied' };
216
+ }
217
+ if (existing.playerId !== ownership.record.playerId) {
218
+ return { status: 'denied' };
219
+ }
220
+ if (ownership.providerPurchaseId === undefined) {
221
+ // Collections stops listing an item after consume. Preserve only an exact durable
222
+ // generation so its already-granted finalizer can retry a failed conditional release;
223
+ // a generationless listing still cannot establish player ownership.
224
+ if (ownership.requestedGeneration === undefined) {
225
+ return { status: 'unavailable' };
226
+ }
227
+ return ownership.requestedGeneration === existing.generation
228
+ ? { status: 'granted', idempotencyKey: existing.generation }
229
+ : { status: 'denied' };
230
+ }
231
+ if (existing.providerPurchaseId === ownership.providerPurchaseId) {
232
+ return (ownership.requestedGeneration === undefined
233
+ || existing.generation === ownership.requestedGeneration)
234
+ ? { status: 'granted', idempotencyKey: existing.generation }
235
+ : { status: 'denied' };
236
+ }
237
+ if (ownership.requestedGeneration !== undefined) {
238
+ return { status: 'denied' };
239
+ }
240
+ const effective = await input.recoveryOwnershipStore.claim({
241
+ ...ownership.record,
242
+ generation: ownership.providerPurchaseId,
243
+ providerPurchaseId: ownership.providerPurchaseId,
244
+ });
245
+ return effective?.playerId === ownership.record.playerId
246
+ && effective.providerPurchaseId === ownership.providerPurchaseId
247
+ ? { status: 'granted', idempotencyKey: effective.generation }
248
+ : { status: 'denied' };
249
+ }
250
+ catch {
251
+ return { status: 'unavailable' };
252
+ }
253
+ },
254
+ supportsPurchaseGrant(finalizationInput) {
255
+ return finalizationInput.request.target === 'microsoft-store'
256
+ && finalizationInput.product.type === 'consumable';
257
+ },
258
+ async verifyPurchase(verificationInput) {
259
+ if (verificationInput.request.target !== 'microsoft-store') {
260
+ return rejected('MICROSOFT_STORE_TARGET_REQUIRED');
261
+ }
262
+ if (verificationInput.product.type !== 'consumable') {
263
+ return rejected('MICROSOFT_STORE_CONSUMABLE_REQUIRED');
264
+ }
265
+ const clientEvidence = readDigitalGoodsEvidence(verificationInput);
266
+ if (clientEvidence === undefined) {
267
+ return rejected('MICROSOFT_STORE_DIGITAL_GOODS_EVIDENCE_REQUIRED');
268
+ }
269
+ const currentStoreId = storeIds.get(verificationInput.product.id);
270
+ if (currentStoreId === undefined) {
271
+ return rejected('MICROSOFT_STORE_PRODUCT_MAPPING_REQUIRED');
272
+ }
273
+ const currentInAppOfferToken = inAppOfferTokens.get(verificationInput.product.id);
274
+ if (currentInAppOfferToken === undefined
275
+ || verificationInput.platformProductId !== currentInAppOfferToken) {
276
+ return rejected('MICROSOFT_STORE_PRODUCT_MAPPING_REQUIRED');
277
+ }
278
+ const storeId = resolveMappedStoreId(verificationInput.product.id, currentInAppOfferToken, clientEvidence.itemId, storeIds, historicalProductMappings);
279
+ if (storeId === undefined) {
280
+ return rejected('MICROSOFT_STORE_DIGITAL_GOODS_EVIDENCE_REQUIRED');
281
+ }
282
+ let credentials;
283
+ try {
284
+ credentials = await input.resolveCredentials(verificationInput.request.playerId, verificationInput.signal);
285
+ }
286
+ catch (error) {
287
+ if (verificationInput.signal.aborted) {
288
+ throw error;
289
+ }
290
+ return pending('MICROSOFT_STORE_CREDENTIALS_UNAVAILABLE');
291
+ }
292
+ try {
293
+ assertCredentials(credentials);
294
+ }
295
+ catch {
296
+ return rejected('MICROSOFT_STORE_CREDENTIALS_INVALID');
297
+ }
298
+ if (credentials.sandbox !== undefined && credentials.sandbox !== 'RETAIL') {
299
+ return pending('MICROSOFT_STORE_XSTS_REQUIRED_FOR_SANDBOX');
300
+ }
301
+ let accountBindingHash;
302
+ try {
303
+ accountBindingHash = await createAccountBindingHash(credentials.accountBindingId);
304
+ }
305
+ catch {
306
+ return pending('MICROSOFT_STORE_ACCOUNT_BINDING_UNAVAILABLE');
307
+ }
308
+ let recoveryOwnership;
309
+ try {
310
+ recoveryOwnership = await input.recoveryOwnershipStore.get({
311
+ accountBindingHash,
312
+ storeId,
313
+ playerId: verificationInput.request.playerId,
314
+ generation: verificationInput.request.idempotencyKey,
315
+ });
316
+ }
317
+ catch {
318
+ return pending('MICROSOFT_STORE_RECOVERY_OWNERSHIP_UNAVAILABLE');
319
+ }
320
+ if (recoveryOwnership === undefined
321
+ || recoveryOwnership.playerId !== verificationInput.request.playerId
322
+ || recoveryOwnership.generation !== verificationInput.request.idempotencyKey) {
323
+ return rejected('MICROSOFT_STORE_RECOVERY_OWNERSHIP_REQUIRED');
324
+ }
325
+ let response;
326
+ try {
327
+ response = await input.client.queryProduct({
328
+ credentials,
329
+ storeId,
330
+ signal: verificationInput.signal,
331
+ });
332
+ }
333
+ catch (error) {
334
+ if (verificationInput.signal.aborted) {
335
+ throw error;
336
+ }
337
+ return pending('MICROSOFT_STORE_COLLECTIONS_UNAVAILABLE');
338
+ }
339
+ const item = inspectPublisherQuery(response, storeId);
340
+ if (item.status !== 'verified') {
341
+ return item;
342
+ }
343
+ const verifiedAt = now();
344
+ if (!validIsoDate(verifiedAt)) {
345
+ return pending('MICROSOFT_STORE_VERIFIER_CLOCK_INVALID');
346
+ }
347
+ const verificationId = createVerificationId(item.item);
348
+ if (recoveryOwnership.providerPurchaseId !== verificationId) {
349
+ return rejected('MICROSOFT_STORE_RECOVERY_OWNERSHIP_REQUIRED');
350
+ }
351
+ return {
352
+ status: 'verified',
353
+ verificationId,
354
+ verifiedAt,
355
+ platformEvidenceId: null,
356
+ payload: {
357
+ microsoftStoreCollectionItemId: item.item.id,
358
+ microsoftStoreModifiedDate: item.item.modifiedDate,
359
+ microsoftStoreProductId: item.item.productId,
360
+ microsoftStoreProductKind: item.item.productKind,
361
+ microsoftStoreQuantity: item.item.quantity,
362
+ microsoftStoreStatus: item.item.status,
363
+ microsoftStoreAccountBindingHash: accountBindingHash,
364
+ microsoftStoreRecoveryOwnershipGeneration: recoveryOwnership.generation,
365
+ ...(item.item.transactionId === undefined
366
+ ? {}
367
+ : { microsoftStoreTransactionId: item.item.transactionId }),
368
+ digitalGoodsItemId: clientEvidence.itemId,
369
+ },
370
+ };
371
+ },
372
+ async finalizePurchaseGrant(finalizationInput) {
373
+ const context = readFinalizationContext(finalizationInput);
374
+ if (context.status === 'pending') {
375
+ return context.result;
376
+ }
377
+ const existing = inFlightFinalizations.get(finalizationInput.evidenceVerificationId);
378
+ if (existing !== undefined) {
379
+ return existing;
380
+ }
381
+ const finalization = consumeMicrosoftStorePurchase(input, finalizationInput, context);
382
+ inFlightFinalizations.set(finalizationInput.evidenceVerificationId, finalization);
383
+ try {
384
+ return await finalization;
385
+ }
386
+ finally {
387
+ if (inFlightFinalizations.get(finalizationInput.evidenceVerificationId) === finalization) {
388
+ inFlightFinalizations.delete(finalizationInput.evidenceVerificationId);
389
+ }
390
+ }
391
+ },
392
+ };
393
+ }
394
+ async function consumeMicrosoftStorePurchase(input, finalizationInput, context) {
395
+ let credentials;
396
+ try {
397
+ credentials = await input.resolveCredentials(finalizationInput.request.playerId, finalizationInput.signal);
398
+ }
399
+ catch (error) {
400
+ if (finalizationInput.signal.aborted) {
401
+ throw error;
402
+ }
403
+ return finalizationPending('MICROSOFT_STORE_CREDENTIALS_UNAVAILABLE');
404
+ }
405
+ try {
406
+ assertCredentials(credentials);
407
+ }
408
+ catch {
409
+ return finalizationPending('MICROSOFT_STORE_CREDENTIALS_INVALID');
410
+ }
411
+ if (credentials.sandbox !== undefined && credentials.sandbox !== 'RETAIL') {
412
+ return finalizationPending('MICROSOFT_STORE_XSTS_REQUIRED_FOR_SANDBOX');
413
+ }
414
+ let accountBindingHash;
415
+ try {
416
+ accountBindingHash = await createAccountBindingHash(credentials.accountBindingId);
417
+ }
418
+ catch {
419
+ return finalizationPending('MICROSOFT_STORE_ACCOUNT_BINDING_UNAVAILABLE');
420
+ }
421
+ if (accountBindingHash !== context.accountBindingHash) {
422
+ return finalizationPending('MICROSOFT_STORE_ACCOUNT_BINDING_CHANGED');
423
+ }
424
+ try {
425
+ const trackingId = await deterministicTrackingId(finalizationInput.evidenceVerificationId);
426
+ const raw = await input.client.consumeProduct({
427
+ credentials,
428
+ storeId: context.storeId,
429
+ trackingId,
430
+ signal: finalizationInput.signal,
431
+ });
432
+ if (!isRecord(raw)) {
433
+ return finalizationPending('MICROSOFT_STORE_CONSUME_RESPONSE_INVALID');
434
+ }
435
+ if (raw.itemId !== context.collectionItemId
436
+ || raw.trackingId !== trackingId
437
+ || raw.newQuantity !== 0) {
438
+ return finalizationPending('MICROSOFT_STORE_CONSUME_RESPONSE_MISMATCH');
439
+ }
440
+ try {
441
+ await input.recoveryOwnershipStore.release({
442
+ accountBindingHash,
443
+ storeId: context.storeId,
444
+ playerId: finalizationInput.request.playerId,
445
+ generation: context.recoveryOwnershipGeneration,
446
+ providerPurchaseId: finalizationInput.evidenceVerificationId,
447
+ });
448
+ }
449
+ catch {
450
+ return finalizationPending('MICROSOFT_STORE_RECOVERY_OWNERSHIP_RELEASE_UNAVAILABLE');
451
+ }
452
+ return {
453
+ status: 'completed',
454
+ action: 'consume',
455
+ alreadyCompleted: false,
456
+ };
457
+ }
458
+ catch (error) {
459
+ if (finalizationInput.signal.aborted) {
460
+ throw error;
461
+ }
462
+ return finalizationPending('MICROSOFT_STORE_CONSUME_UNAVAILABLE');
463
+ }
464
+ }
465
+ function inspectPublisherQuery(raw, expectedStoreId) {
466
+ if (!isRecord(raw)) {
467
+ return rejected('MICROSOFT_STORE_COLLECTIONS_RESPONSE_INVALID');
468
+ }
469
+ // Collections v9 omits `items` when there are no matching entitlements. Treat that
470
+ // provider-defined empty result like an empty list so a just-completed purchase can retry.
471
+ if (raw.items === undefined) {
472
+ return pending('MICROSOFT_STORE_PURCHASE_NOT_PROPAGATED');
473
+ }
474
+ if (!Array.isArray(raw.items)) {
475
+ return rejected('MICROSOFT_STORE_COLLECTIONS_RESPONSE_INVALID');
476
+ }
477
+ const candidates = raw.items.filter((candidate) => (isRecord(candidate) && candidate.productId === expectedStoreId));
478
+ if (candidates.length === 0) {
479
+ return pending('MICROSOFT_STORE_PURCHASE_NOT_PROPAGATED');
480
+ }
481
+ const active = candidates.find((candidate) => candidate.status === 'Active');
482
+ if (active === undefined) {
483
+ return rejected('MICROSOFT_STORE_PURCHASE_NOT_ACTIVE');
484
+ }
485
+ if (active.productKind !== 'UnmanagedConsumable') {
486
+ return rejected('MICROSOFT_STORE_PRODUCT_KIND_MISMATCH');
487
+ }
488
+ if (!Number.isSafeInteger(active.quantity) || Number(active.quantity) < 1) {
489
+ return pending('MICROSOFT_STORE_PURCHASE_ALREADY_CONSUMED');
490
+ }
491
+ if (Number(active.quantity) !== 1) {
492
+ return rejected('MICROSOFT_STORE_PURCHASE_QUANTITY_MISMATCH');
493
+ }
494
+ const id = optionalIdentifier(active.id);
495
+ const modifiedDate = optionalIdentifier(active.modifiedDate);
496
+ const transactionId = optionalIdentifier(active.transactionId);
497
+ if (id === undefined || modifiedDate === undefined || !validIsoDate(modifiedDate)) {
498
+ return rejected('MICROSOFT_STORE_COLLECTIONS_RESPONSE_INVALID');
499
+ }
500
+ return {
501
+ status: 'verified',
502
+ item: {
503
+ id,
504
+ modifiedDate,
505
+ productId: expectedStoreId,
506
+ productKind: 'UnmanagedConsumable',
507
+ quantity: Number(active.quantity),
508
+ status: 'Active',
509
+ ...(transactionId === undefined ? {} : { transactionId }),
510
+ },
511
+ };
512
+ }
513
+ function readDigitalGoodsEvidence(input) {
514
+ const evidence = input.request.evidence;
515
+ if (evidence?.schema !== microsoftStoreDigitalGoodsEvidenceSchema) {
516
+ return undefined;
517
+ }
518
+ const itemId = optionalIdentifier(evidence.payload.itemId);
519
+ const purchaseToken = optionalIdentifier(evidence.payload.purchaseToken);
520
+ if (itemId === undefined
521
+ || purchaseToken === undefined
522
+ || itemId !== purchaseToken
523
+ || input.request.platformTransactionId !== purchaseToken) {
524
+ return undefined;
525
+ }
526
+ return { itemId };
527
+ }
528
+ function readRecoveryOwnershipEvidence(input) {
529
+ if (input.evidence.schema !== microsoftStoreDigitalGoodsEvidenceSchema) {
530
+ return undefined;
531
+ }
532
+ const itemId = optionalIdentifier(input.evidence.payload.itemId);
533
+ const purchaseToken = optionalIdentifier(input.evidence.payload.purchaseToken);
534
+ if (itemId === undefined
535
+ || purchaseToken === undefined
536
+ || itemId !== purchaseToken
537
+ || input.purchaseToken !== purchaseToken) {
538
+ return undefined;
539
+ }
540
+ return { itemId };
541
+ }
542
+ function resolveMappedStoreId(productId, currentInAppOfferToken, purchasedInAppOfferToken, storeIds, historicalProductMappings) {
543
+ const currentStoreId = storeIds.get(productId);
544
+ if (currentStoreId === undefined) {
545
+ return undefined;
546
+ }
547
+ return purchasedInAppOfferToken === currentInAppOfferToken
548
+ ? currentStoreId
549
+ : historicalProductMappings.get(productId)?.get(purchasedInAppOfferToken);
550
+ }
551
+ function createRecoveryOwnershipKey(input) {
552
+ return JSON.stringify([input.accountBindingHash, input.storeId]);
553
+ }
554
+ function readFinalizationContext(input) {
555
+ if (input.request.target !== 'microsoft-store' || input.product.type !== 'consumable') {
556
+ return { status: 'pending', result: finalizationPending('MICROSOFT_STORE_TARGET_REQUIRED') };
557
+ }
558
+ const payloadStoreId = optionalIdentifier(input.evidencePayload?.microsoftStoreProductId);
559
+ const collectionItemId = optionalIdentifier(input.evidencePayload?.microsoftStoreCollectionItemId);
560
+ const modifiedDate = optionalIdentifier(input.evidencePayload?.microsoftStoreModifiedDate);
561
+ const accountBindingHash = optionalIdentifier(input.evidencePayload?.microsoftStoreAccountBindingHash);
562
+ const recoveryOwnershipGeneration = optionalIdentifier(input.evidencePayload?.microsoftStoreRecoveryOwnershipGeneration);
563
+ // The binding and this verifier ship together in the first Store release. Missing bindings are
564
+ // therefore malformed evidence, not legacy evidence, and must fail closed to prevent an account
565
+ // switch between verification and consumption.
566
+ if (payloadStoreId === undefined
567
+ || collectionItemId === undefined
568
+ || modifiedDate === undefined
569
+ || accountBindingHash === undefined
570
+ || recoveryOwnershipGeneration === undefined
571
+ || recoveryOwnershipGeneration !== input.request.idempotencyKey
572
+ || input.evidenceVerificationId !== createVerificationId({
573
+ id: collectionItemId,
574
+ modifiedDate,
575
+ productId: payloadStoreId,
576
+ })) {
577
+ return {
578
+ status: 'pending',
579
+ result: finalizationPending('MICROSOFT_STORE_FINALIZATION_EVIDENCE_MISMATCH'),
580
+ };
581
+ }
582
+ // The provider identity is bound into the durable verification ID. Reuse it for retries so a
583
+ // later catalog migration cannot strand an already-granted, still-unconsumed purchase.
584
+ return {
585
+ status: 'verified',
586
+ collectionItemId,
587
+ storeId: payloadStoreId,
588
+ accountBindingHash,
589
+ recoveryOwnershipGeneration,
590
+ };
591
+ }
592
+ function createVerificationId(item) {
593
+ return `microsoft-store:${item.productId}:${item.id}:${item.modifiedDate}`;
594
+ }
595
+ async function deterministicTrackingId(value) {
596
+ const subtle = readGlobalSubtleCrypto();
597
+ const encoded = new TextEncoder().encode(value);
598
+ const digest = new Uint8Array(await subtle.digest('SHA-256', encoded));
599
+ digest[6] = ((digest.at(6) ?? 0) & 0x0f) | 0x50;
600
+ digest[8] = ((digest.at(8) ?? 0) & 0x3f) | 0x80;
601
+ const hex = Array.from(digest.slice(0, 16), (byte) => byte.toString(16).padStart(2, '0'))
602
+ .join('');
603
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
604
+ }
605
+ async function createAccountBindingHash(accountBindingId) {
606
+ const subtle = readGlobalSubtleCrypto();
607
+ const encoded = new TextEncoder().encode(`mpgd-microsoft-store-account-v1\0${identifier(accountBindingId, 'accountBindingId')}`);
608
+ const digest = new Uint8Array(await subtle.digest('SHA-256', encoded));
609
+ return `sha256:${Array.from(digest, (byte) => byte.toString(16).padStart(2, '0')).join('')}`;
610
+ }
611
+ function normalizeStoreIds(input) {
612
+ const output = new Map();
613
+ const mappedStoreIds = new Set();
614
+ for (const [logicalProductId, storeId] of Object.entries(input)) {
615
+ const normalizedLogicalProductId = identifier(logicalProductId, 'logicalProductId');
616
+ const normalizedStoreId = identifier(storeId, 'storeId');
617
+ if (mappedStoreIds.has(normalizedStoreId)) {
618
+ throw new TypeError(`Duplicate Microsoft Store product mapping: ${normalizedStoreId}`);
619
+ }
620
+ mappedStoreIds.add(normalizedStoreId);
621
+ output.set(normalizedLogicalProductId, normalizedStoreId);
622
+ }
623
+ if (output.size === 0) {
624
+ throw new TypeError('storeIds must contain at least one product mapping.');
625
+ }
626
+ return output;
627
+ }
628
+ function normalizeInAppOfferTokens(input, storeIds) {
629
+ const output = new Map();
630
+ const mappedTokens = new Set();
631
+ for (const [logicalProductId, inAppOfferToken] of Object.entries(input)) {
632
+ const normalizedLogicalProductId = identifier(logicalProductId, 'logicalProductId');
633
+ const normalizedInAppOfferToken = identifier(inAppOfferToken, 'inAppOfferToken');
634
+ if (!storeIds.has(normalizedLogicalProductId)) {
635
+ throw new TypeError(`Microsoft Store Digital Goods mapping requires a Store ID: ${normalizedLogicalProductId}`);
636
+ }
637
+ if (mappedTokens.has(normalizedInAppOfferToken)) {
638
+ throw new TypeError(`Duplicate Microsoft Store Digital Goods mapping: ${normalizedInAppOfferToken}`);
639
+ }
640
+ mappedTokens.add(normalizedInAppOfferToken);
641
+ output.set(normalizedLogicalProductId, normalizedInAppOfferToken);
642
+ }
643
+ for (const logicalProductId of storeIds.keys()) {
644
+ if (!output.has(logicalProductId)) {
645
+ throw new TypeError(`Microsoft Store Store ID mapping requires a Digital Goods token: ${logicalProductId}`);
646
+ }
647
+ }
648
+ return output;
649
+ }
650
+ function normalizeHistoricalProductMappings(input, currentStoreIds, currentInAppOfferTokens) {
651
+ const output = new Map();
652
+ const storeIdOwners = new Map();
653
+ const inAppOfferTokens = new Set();
654
+ for (const [logicalProductId, storeId] of currentStoreIds) {
655
+ storeIdOwners.set(storeId, logicalProductId);
656
+ }
657
+ for (const inAppOfferToken of currentInAppOfferTokens.values()) {
658
+ inAppOfferTokens.add(inAppOfferToken);
659
+ }
660
+ for (const [rawLogicalProductId, rawMappings] of Object.entries(input)) {
661
+ const logicalProductId = identifier(rawLogicalProductId, 'logicalProductId');
662
+ if (!currentStoreIds.has(logicalProductId)) {
663
+ throw new TypeError(`Historical Microsoft Store mapping requires a current product: ${logicalProductId}`);
664
+ }
665
+ if (!Array.isArray(rawMappings)) {
666
+ throw new TypeError(`Historical Microsoft Store mappings must be an array: ${logicalProductId}`);
667
+ }
668
+ const mappings = new Map();
669
+ for (const rawMapping of rawMappings) {
670
+ const inAppOfferToken = identifier(rawMapping.inAppOfferToken, 'historical inAppOfferToken');
671
+ const storeId = identifier(rawMapping.storeId, 'historical storeId');
672
+ if (mappings.has(inAppOfferToken)) {
673
+ throw new TypeError(`Duplicate historical Microsoft Store token: ${inAppOfferToken}`);
674
+ }
675
+ if (inAppOfferTokens.has(inAppOfferToken)) {
676
+ throw new TypeError(`Duplicate Microsoft Store Digital Goods mapping: ${inAppOfferToken}`);
677
+ }
678
+ const owner = storeIdOwners.get(storeId);
679
+ if (owner !== undefined && owner !== logicalProductId) {
680
+ throw new TypeError(`Duplicate Microsoft Store product mapping: ${storeId}`);
681
+ }
682
+ inAppOfferTokens.add(inAppOfferToken);
683
+ storeIdOwners.set(storeId, logicalProductId);
684
+ mappings.set(inAppOfferToken, storeId);
685
+ }
686
+ output.set(logicalProductId, mappings);
687
+ }
688
+ return output;
689
+ }
690
+ function assertCredentials(input) {
691
+ authorizationValue(input.accessToken, 'accessToken');
692
+ identifier(input.userStoreId, 'userStoreId');
693
+ identifier(input.accountBindingId, 'accountBindingId');
694
+ if (input.sandbox !== undefined) {
695
+ identifier(input.sandbox, 'sandbox');
696
+ }
697
+ }
698
+ function beneficiary(userStoreId) {
699
+ return {
700
+ identityType: 'b2b',
701
+ identityValue: identifier(userStoreId, 'userStoreId'),
702
+ localTicketReference: '',
703
+ };
704
+ }
705
+ function identifier(input, label) {
706
+ const value = optionalIdentifier(input);
707
+ if (value === undefined) {
708
+ throw new TypeError(`${label} must be a non-empty identifier.`);
709
+ }
710
+ return value;
711
+ }
712
+ function optionalIdentifier(input) {
713
+ return typeof input === 'string'
714
+ && input.length > 0
715
+ && input.trim() === input
716
+ && input.length <= 4096
717
+ && !/[\p{Cc}\p{Cf}]/u.test(input)
718
+ ? input
719
+ : undefined;
720
+ }
721
+ function authorizationValue(input, label) {
722
+ const value = identifier(input, label);
723
+ if (/\s/u.test(value)) {
724
+ throw new TypeError(`${label} must not contain whitespace.`);
725
+ }
726
+ return value;
727
+ }
728
+ function optionalHeaderValue(input) {
729
+ if (input === undefined) {
730
+ return undefined;
731
+ }
732
+ if (input.length === 0 || /[\r\n]/u.test(input)) {
733
+ throw new TypeError('userAgent must be a non-empty single-line string.');
734
+ }
735
+ return input;
736
+ }
737
+ function guid(input, label) {
738
+ const value = identifier(input, label);
739
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value)) {
740
+ throw new TypeError(`${label} must be a GUID.`);
741
+ }
742
+ return value;
743
+ }
744
+ function positiveSafeInteger(input, label) {
745
+ if (!Number.isSafeInteger(input) || Number(input) <= 0) {
746
+ throw new TypeError(`${label} must be a positive safe integer.`);
747
+ }
748
+ return Number(input);
749
+ }
750
+ function validIsoDate(input) {
751
+ // Collections datetime fields commonly use an explicit `+00:00` offset rather than `Z`.
752
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test(input)
753
+ && Number.isFinite(Date.parse(input));
754
+ }
755
+ function readGlobalSubtleCrypto() {
756
+ const subtle = globalThis.crypto?.subtle;
757
+ if (subtle === undefined) {
758
+ throw new Error('globalThis.crypto.subtle is unavailable. Provide a Web Crypto SubtleCrypto implementation.');
759
+ }
760
+ return subtle;
761
+ }
762
+ function isRecord(input) {
763
+ return typeof input === 'object' && input !== null && !Array.isArray(input);
764
+ }
765
+ function rejected(reason) {
766
+ return { status: 'rejected', reason };
767
+ }
768
+ function pending(reason) {
769
+ return { status: 'pending', reason };
770
+ }
771
+ function finalizationPending(reason) {
772
+ return { status: 'pending', action: 'consume', alreadyCompleted: false, reason };
773
+ }
774
+ function readGlobalFetch() {
775
+ return async (url, init) => {
776
+ const response = await fetch(url, init);
777
+ return { status: response.status, body: response.body };
778
+ };
779
+ }
780
+ async function readBoundedJson(body, maximumBytes, signal) {
781
+ if (body === null) {
782
+ throw new MicrosoftStoreCollectionsUnavailableError('Microsoft Store Collections returned an empty response.');
783
+ }
784
+ const reader = body.getReader();
785
+ const chunks = [];
786
+ let size = 0;
787
+ try {
788
+ while (true) {
789
+ if (signal.aborted) {
790
+ throw signal.reason;
791
+ }
792
+ const chunk = await reader.read();
793
+ if (chunk.done) {
794
+ break;
795
+ }
796
+ size += chunk.value.byteLength;
797
+ if (size > maximumBytes) {
798
+ throw new MicrosoftStoreCollectionsUnavailableError('Microsoft Store Collections response exceeded maxResponseBytes.');
799
+ }
800
+ chunks.push(chunk.value);
801
+ }
802
+ }
803
+ catch (error) {
804
+ try {
805
+ await reader.cancel(error);
806
+ }
807
+ catch {
808
+ // Preserve the bounded-read or abort error that made the response unusable.
809
+ }
810
+ throw error;
811
+ }
812
+ finally {
813
+ reader.releaseLock();
814
+ }
815
+ const bytes = new Uint8Array(size);
816
+ let offset = 0;
817
+ for (const chunk of chunks) {
818
+ bytes.set(chunk, offset);
819
+ offset += chunk.byteLength;
820
+ }
821
+ try {
822
+ return JSON.parse(new TextDecoder().decode(bytes));
823
+ }
824
+ catch (error) {
825
+ throw new MicrosoftStoreCollectionsUnavailableError('Microsoft Store Collections returned invalid JSON.', error);
826
+ }
827
+ }
828
+ async function cancelBody(body) {
829
+ try {
830
+ await body?.cancel();
831
+ }
832
+ catch {
833
+ // The HTTP status already determines the fail-closed result.
834
+ }
835
+ }