@mpgd/game-services 0.23.0 → 0.24.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.
@@ -1,8 +1,8 @@
1
- import { isAuthoritativeMicrosoftStoreCompletion } from './authoritative-purchase.js';
2
1
  import { createGameServicesClient, } from './client.js';
3
2
  import { resolveClientRewardClaim } from './client-reward-evidence.js';
4
- import { summarizeCommercePurchaseOperation, toGameServicesPurchaseResult, } from './commerce-reconciler.js';
3
+ import { summarizeCommercePurchaseOperation, } from './commerce-reconciler.js';
5
4
  import { createDefaultClientRewardEvidenceRegistry } from './default-client-reward-evidence.js';
5
+ import { isRetiredPurchaseOpen } from './monetization-operation-store.js';
6
6
  import { forwardGameServicesProgress, observeGameServicesOperation, } from './operation-progress.js';
7
7
  /**
8
8
  * Client-side recovery coordinates UI callbacks and authoritative backend calls.
@@ -15,24 +15,22 @@ export function createRecoverableMonetizationClient(input) {
15
15
  const allowLegacyRewardCompletion = input.rewardEvidenceRegistry === undefined && input.gateway.ads.provider === undefined;
16
16
  const observedAt = () => input.now?.() ?? new Date().toISOString();
17
17
  const inFlight = new Map();
18
- const unjournaledPurchases = new Map();
19
18
  const unjournaledRewards = new Map();
20
- const unjournaledPurchaseResponses = new Map();
21
19
  const unjournaledRewardResponses = new Map();
22
- const unjournaledPurchaseResults = new Map();
23
20
  const unjournaledRewardResults = new Map();
24
- // One commerce v2 client over the durable journal: its reconciler is the
25
- // purchase path for a gateway with a v2 provider, and v1 records keep the
26
- // per-operation client below.
27
- const commerceClient = input.gateway.commerceV2 === undefined
28
- ? undefined
29
- : createGameServicesClient({ ...input, operationStore: store });
30
- const commerceReconciler = commerceClient?.commerceReconciler;
31
- // The per-operation clients below resume v1 records only: they see a gateway
32
- // without the v2 provider, so they never build a second reconciler over the
33
- // journal or reopen the store UI for a platform result already recorded.
21
+ // One purchase client over the durable journal: its reconciler is the only
22
+ // purchase path. A gateway without a commerce provider gets a client whose
23
+ // `purchase` answers `rejected`; rewarded ads keep the per-operation client
24
+ // below.
25
+ const commerceClient = createGameServicesClient({
26
+ ...input,
27
+ operationStore: store,
28
+ });
29
+ const commerceReconciler = commerceClient.commerceReconciler;
30
+ // The per-operation rewarded-ad clients see a gateway without the commerce
31
+ // provider, so they never build a second reconciler over the journal.
34
32
  const { commerceReconciler: _sharedReconciler, operationStore: _journal, onCommerceEvent: _onCommerceEvent, commerceProducts: _commerceProducts, ...legacyClientInput } = input;
35
- const { commerceV2: _commerceV2, ...legacyGateway } = input.gateway;
33
+ const { commerce: _commerce, ...legacyGateway } = input.gateway;
36
34
  async function serializeOperation(key, task) {
37
35
  const previous = inFlight.get(key) ?? Promise.resolve();
38
36
  const running = previous.catch(() => undefined).then(task);
@@ -57,35 +55,12 @@ export function createRecoverableMonetizationClient(input) {
57
55
  || record.target !== input.target
58
56
  || record.deploymentTarget !== (input.deploymentTarget ?? input.target)
59
57
  || record.input.idempotencyKey !== idempotencyKey
60
- || (record.kind === 'purchase' ? record.input.productId : record.input.placementId)
61
- !== subjectId) {
58
+ || record.input.placementId !== subjectId) {
62
59
  throw new Error('Monetization operation key is bound to another player or subject.');
63
60
  }
64
61
  }
65
- async function purchase(operation, options) {
66
- if (commerceClient !== undefined) {
67
- return commerceClient.purchase(operation, options);
68
- }
69
- const key = operationKey(input.target, 'purchase', operation.idempotencyKey);
70
- return serializeOperation(key, async () => {
71
- const reserved = await store.reserve({
72
- key,
73
- kind: 'purchase',
74
- playerId: input.playerId,
75
- target: input.target,
76
- deploymentTarget: input.deploymentTarget ?? input.target,
77
- revision: 0,
78
- input: operation,
79
- });
80
- if (reserved.record.kind !== 'purchase') {
81
- throw new Error('Monetization operation kind conflicts with its journal key.');
82
- }
83
- assertOwner(reserved.record, 'purchase', operation.idempotencyKey, operation.productId);
84
- if (reserved.record.input.source !== operation.source) {
85
- throw new Error('Purchase idempotency key was reused for another source.');
86
- }
87
- return resumePurchase(reserved.record, reserved.created, options);
88
- });
62
+ function purchase(operation, options) {
63
+ return commerceClient.purchase(operation, options);
89
64
  }
90
65
  async function claimRewardedAd(operation, options) {
91
66
  const key = operationKey(input.target, 'rewarded-ad', operation.idempotencyKey);
@@ -106,163 +81,6 @@ export function createRecoverableMonetizationClient(input) {
106
81
  return resumeReward(reserved.record, reserved.created, options);
107
82
  });
108
83
  }
109
- async function resumePurchase(initial, mayOpenPlatform = false, options) {
110
- // Observe recovery once, so a journal failure resolved into a result still completes.
111
- return observeGameServicesOperation('purchase', options, (progress) => {
112
- return settlePurchase(initial, mayOpenPlatform, progress);
113
- });
114
- }
115
- async function settlePurchase(initial, mayOpenPlatform, progress) {
116
- let record = initial;
117
- const unsavedResponse = unjournaledPurchaseResponses.get(record.key);
118
- if (unsavedResponse !== undefined && record.response?.verified !== true) {
119
- // A result beside the non-grant response is stale, such as another runtime's rejection.
120
- // Drop it in the same write so the client path derives this grant again.
121
- const { result: staleResult, ...current } = record;
122
- try {
123
- record = await save(current, { response: unsavedResponse });
124
- unjournaledPurchaseResponses.delete(record.key);
125
- }
126
- catch {
127
- return recordedPurchaseGrant({ ...current, response: unsavedResponse })
128
- ?? pendingPurchase(record.platform);
129
- }
130
- }
131
- else if (record.response?.verified === true) {
132
- unjournaledPurchaseResponses.delete(record.key);
133
- }
134
- const unsavedResult = unjournaledPurchaseResults.get(record.key);
135
- if (unsavedResult !== undefined && record.result?.status !== 'granted') {
136
- try {
137
- record = await save(record, { result: unsavedResult });
138
- unjournaledPurchaseResults.delete(record.key);
139
- }
140
- catch {
141
- return unsavedResult;
142
- }
143
- }
144
- else if (record.result?.status === 'granted') {
145
- unjournaledPurchaseResults.delete(record.key);
146
- }
147
- const unjournaled = unjournaledPurchases.get(record.key);
148
- if (unjournaled !== undefined && (record.platform === undefined
149
- || record.platform.status === 'pending' && unjournaled.platform.status !== 'pending')) {
150
- try {
151
- record = await save(record, {
152
- platform: unjournaled.platform,
153
- platformCompletedAt: unjournaled.completedAt,
154
- });
155
- unjournaledPurchases.delete(record.key);
156
- }
157
- catch {
158
- return recordedPurchaseGrant({ ...record, platform: unjournaled.platform })
159
- ?? pendingPurchase(unjournaled.platform);
160
- }
161
- }
162
- else if (record.platform !== undefined) {
163
- if (unjournaled !== undefined && !samePlatformResult(record.platform, unjournaled.platform)) {
164
- throw new Error('A platform purchase callback conflicts with the recorded operation.');
165
- }
166
- unjournaledPurchases.delete(record.key);
167
- }
168
- const completed = record.result;
169
- if (completed !== undefined && !purchaseNeedsRetry(record)) {
170
- return completed;
171
- }
172
- if (record.request !== undefined) {
173
- assertPurchaseRequest(record, input);
174
- }
175
- if (record.platform === undefined && !mayOpenPlatform) {
176
- return pendingPurchase();
177
- }
178
- const client = createGameServicesClient({
179
- ...legacyClientInput,
180
- requestNow: () => record.platformCompletedAt ?? observedAt(),
181
- gateway: {
182
- ...legacyGateway,
183
- commerce: {
184
- ...input.gateway.commerce,
185
- async purchase(request) {
186
- if (record.platform !== undefined) {
187
- return record.platform;
188
- }
189
- const platform = await input.gateway.commerce.purchase(request);
190
- const completedAt = observedAt();
191
- const prior = record;
192
- record = { ...record, platform, platformCompletedAt: completedAt };
193
- try {
194
- record = await save(prior, { platform, platformCompletedAt: completedAt });
195
- }
196
- catch (error) {
197
- unjournaledPurchases.set(record.key, { platform, completedAt });
198
- throw error;
199
- }
200
- return platform;
201
- },
202
- },
203
- },
204
- backend: {
205
- ...input.backend,
206
- purchases: {
207
- async verifyPurchase(request) {
208
- if (record.request === undefined) {
209
- record = await save(record, { request });
210
- }
211
- const recordedRequest = record.request;
212
- if (recordedRequest === undefined) {
213
- throw new Error('Purchase request was not journaled before backend dispatch.');
214
- }
215
- let response;
216
- try {
217
- response = await input.backend.purchases.verifyPurchase(recordedRequest);
218
- }
219
- catch {
220
- if (record.response?.verified === true) {
221
- return record.response;
222
- }
223
- return {
224
- verified: false,
225
- alreadyProcessed: false,
226
- disposition: 'pending',
227
- reason: 'BACKEND_UNAVAILABLE',
228
- };
229
- }
230
- if (record.response?.verified === true && !response.verified) {
231
- return record.response;
232
- }
233
- const prior = record;
234
- record = { ...record, response };
235
- try {
236
- record = await save(prior, { response });
237
- unjournaledPurchaseResponses.delete(record.key);
238
- }
239
- catch (error) {
240
- if (response.verified) {
241
- unjournaledPurchaseResponses.set(record.key, response);
242
- }
243
- throw error;
244
- }
245
- return response;
246
- },
247
- },
248
- },
249
- });
250
- try {
251
- const result = await client.purchase(record.input, forwardGameServicesProgress(progress));
252
- if (result.status === 'granted') {
253
- unjournaledPurchaseResults.set(record.key, result);
254
- }
255
- record = await save(record, { result });
256
- unjournaledPurchaseResults.delete(record.key);
257
- return result;
258
- }
259
- catch {
260
- // No UI retry is safe after a possibly completed platform callback.
261
- return recordedPurchaseGrant(record)
262
- ?? unjournaledPurchaseResults.get(record.key)
263
- ?? pendingPurchase(record.platform);
264
- }
265
- }
266
84
  async function resumeReward(initial, mayOpenPlatform = false, options) {
267
85
  return observeGameServicesOperation('rewarded-ad', options, (progress) => {
268
86
  return settleReward(initial, mayOpenPlatform, progress);
@@ -417,50 +235,6 @@ export function createRecoverableMonetizationClient(input) {
417
235
  ?? pendingReward(record.platform);
418
236
  }
419
237
  }
420
- async function recoverPurchaseResult(idempotencyKey, platform) {
421
- const key = operationKey(input.target, 'purchase', idempotencyKey);
422
- return serializeOperation(key, async () => {
423
- const found = await store.read(key);
424
- if (found?.kind !== 'purchase') {
425
- if (commerceReconciler !== undefined) {
426
- // Deprecated thin wrapper: the v2 reconciler learns the result from
427
- // the provider, so the platform callback is not replayed.
428
- await commerceReconciler.reconcile();
429
- const journaled = await commerceReconciler.read(idempotencyKey);
430
- if (journaled === undefined) {
431
- throw new Error('No reserved purchase operation matches this platform callback.');
432
- }
433
- return toGameServicesPurchaseResult(journaled);
434
- }
435
- throw new Error('No reserved purchase operation matches this platform callback.');
436
- }
437
- assertOwner(found, 'purchase', idempotencyKey, found.input.productId);
438
- const transient = unjournaledPurchases.get(key);
439
- if (transient !== undefined && !samePlatformResult(transient.platform, platform)
440
- && !(transient.platform.status === 'pending' && platform.status !== 'pending')) {
441
- throw new Error('A platform purchase callback conflicts with the unjournaled result.');
442
- }
443
- const canAdvance = found.platform?.status === 'pending'
444
- && platform.status !== 'pending' && found.request === undefined;
445
- if (found.platform !== undefined && !samePlatformResult(found.platform, platform)
446
- && !canAdvance) {
447
- throw new Error('A platform purchase callback conflicts with the recorded operation.');
448
- }
449
- let record = found;
450
- if (found.platform === undefined || canAdvance) {
451
- const completedAt = observedAt();
452
- try {
453
- record = await save(found, { platform, platformCompletedAt: completedAt });
454
- }
455
- catch {
456
- unjournaledPurchases.set(key, { platform, completedAt });
457
- return recordedPurchaseGrant({ ...found, platform }) ?? pendingPurchase(platform);
458
- }
459
- }
460
- unjournaledPurchases.delete(key);
461
- return resumePurchase(record);
462
- });
463
- }
464
238
  async function recoverRewardResult(idempotencyKey, platform) {
465
239
  const key = operationKey(input.target, 'rewarded-ad', idempotencyKey);
466
240
  return serializeOperation(key, async () => {
@@ -585,6 +359,104 @@ export function createRecoverableMonetizationClient(input) {
585
359
  })),
586
360
  ];
587
361
  }
362
+ /**
363
+ * Finishes the backend verification a retired v1 purchase record was in the
364
+ * middle of. The grant is the backend's answer, never a local decision, and
365
+ * the store UI is never reopened. Backend or journal trouble leaves the
366
+ * record open for the next pass.
367
+ */
368
+ async function drainRetiredPurchase(initial) {
369
+ const summarize = (latest, status, finalizationPending = false) => ({
370
+ kind: 'purchase',
371
+ idempotencyKey: latest.input.idempotencyKey,
372
+ status,
373
+ finalizationPending,
374
+ });
375
+ try {
376
+ return await serializeOperation(initial.key, async () => {
377
+ const latest = await store.read(initial.key) ?? initial;
378
+ if (latest.kind !== 'purchase' || !isRetiredPurchaseOpen(latest)) {
379
+ return undefined;
380
+ }
381
+ if (latest.key !== operationKey(input.target, 'purchase', latest.input.idempotencyKey)
382
+ || !ownsRecord(latest)) {
383
+ return summarize(latest, 'action-required');
384
+ }
385
+ const platform = latest.platform;
386
+ if (platform?.status !== 'completed' || platform.transactionId === undefined) {
387
+ return undefined;
388
+ }
389
+ const request = latest.request ?? {
390
+ target: latest.target,
391
+ ...(latest.deploymentTarget === latest.target
392
+ ? {}
393
+ : { deploymentTarget: latest.deploymentTarget }),
394
+ playerId: latest.playerId,
395
+ productId: latest.input.productId,
396
+ platformTransactionId: platform.transactionId,
397
+ idempotencyKey: latest.input.idempotencyKey,
398
+ purchasedAt: latest.platformCompletedAt ?? observedAt(),
399
+ ...(platform.evidence === undefined ? {} : { evidence: platform.evidence }),
400
+ };
401
+ if (request.playerId !== input.playerId
402
+ || request.target !== latest.target
403
+ || (request.deploymentTarget ?? request.target) !== latest.deploymentTarget
404
+ || request.productId !== latest.input.productId
405
+ || request.idempotencyKey !== latest.input.idempotencyKey
406
+ || request.platformTransactionId !== platform.transactionId) {
407
+ return summarize(latest, 'action-required');
408
+ }
409
+ // A grant whose consume or acknowledge is still pending is sent once
410
+ // more as well: the backend retries the finalization of a ledger
411
+ // entry it already holds under the same key, as the v1 client did.
412
+ const saved = latest.response;
413
+ let response = saved;
414
+ if (saved?.verified !== true || saved.finalization?.status === 'pending') {
415
+ let answer;
416
+ try {
417
+ answer = await input.backend.purchases.verifyPurchase(request);
418
+ }
419
+ catch {
420
+ // Transport trouble stays retryable; nothing is decided here.
421
+ return saved?.verified === true
422
+ ? summarize(latest, 'granted', true)
423
+ : summarize(latest, 'pending');
424
+ }
425
+ // A recorded grant is never replaced by a later non-grant answer.
426
+ response = saved?.verified === true && !answer.verified ? saved : answer;
427
+ }
428
+ if (response === undefined) {
429
+ return summarize(latest, 'pending');
430
+ }
431
+ const finalizationPending = response.verified
432
+ && response.finalization?.status === 'pending';
433
+ const result = response.verified
434
+ ? {
435
+ status: 'granted',
436
+ purchase: platform,
437
+ verification: response,
438
+ ...(response.ledgerEntryId === undefined
439
+ ? {}
440
+ : { ledgerEntryId: response.ledgerEntryId }),
441
+ }
442
+ : {
443
+ status: response.disposition === 'pending' ? 'pending' : 'rejected',
444
+ purchase: platform,
445
+ verification: response,
446
+ };
447
+ try {
448
+ await save(latest, { request, response, result });
449
+ }
450
+ catch {
451
+ return summarize(latest, result.status === 'granted' ? 'granted' : 'pending', finalizationPending);
452
+ }
453
+ return summarize(latest, result.status === 'granted' ? 'granted' : result.status, finalizationPending);
454
+ });
455
+ }
456
+ catch {
457
+ return summarize(initial, 'pending');
458
+ }
459
+ }
588
460
  async function reconcileLegacy() {
589
461
  const pending = await store.listRecoverable(input.playerId);
590
462
  const summaries = [];
@@ -592,7 +464,14 @@ export function createRecoverableMonetizationClient(input) {
592
464
  if (!ownsRecord(record)) {
593
465
  continue;
594
466
  }
595
- if (record.kind !== 'purchase' && record.kind !== 'rewarded-ad') {
467
+ if (record.kind === 'purchase') {
468
+ const summary = await drainRetiredPurchase(record);
469
+ if (summary !== undefined) {
470
+ summaries.push(summary);
471
+ }
472
+ continue;
473
+ }
474
+ if (record.kind !== 'rewarded-ad') {
596
475
  continue;
597
476
  }
598
477
  const idempotencyKey = typeof record.input?.idempotencyKey === 'string'
@@ -610,19 +489,10 @@ export function createRecoverableMonetizationClient(input) {
610
489
  catch {
611
490
  throw new OperationReadError();
612
491
  }
613
- if (latest.kind === 'commerce-purchase') {
492
+ if (latest.kind !== 'rewarded-ad') {
614
493
  throw new Error('Monetization operation kind conflicts with its journal key.');
615
494
  }
616
- assertOwner(latest, latest.kind, latest.input.idempotencyKey, latest.kind === 'purchase' ? latest.input.productId : latest.input.placementId);
617
- if (latest.kind === 'purchase') {
618
- const result = await resumePurchase(latest);
619
- return {
620
- kind: latest.kind,
621
- idempotencyKey: latest.input.idempotencyKey,
622
- status: result.status,
623
- finalizationPending: result.verification?.finalization?.status === 'pending',
624
- };
625
- }
495
+ assertOwner(latest, latest.kind, latest.input.idempotencyKey, latest.input.placementId);
626
496
  const result = await resumeReward(latest);
627
497
  return {
628
498
  kind: latest.kind,
@@ -647,7 +517,6 @@ export function createRecoverableMonetizationClient(input) {
647
517
  return {
648
518
  purchase,
649
519
  claimRewardedAd,
650
- recoverPurchaseResult,
651
520
  recoverRewardResult,
652
521
  reconcile,
653
522
  restorePurchases,
@@ -678,42 +547,12 @@ function canonicalJson(value) {
678
547
  }
679
548
  return value;
680
549
  }
681
- function pendingPurchase(platform) {
682
- return {
683
- status: 'pending',
684
- purchase: platform ?? { status: 'pending', entitlementIds: [] },
685
- };
686
- }
687
550
  function pendingReward(platform) {
688
551
  return {
689
552
  status: 'pending',
690
553
  reward: platform ?? { status: 'pending', rewardGranted: false },
691
554
  };
692
555
  }
693
- function recordedPurchaseGrant(record) {
694
- if (record.result?.status === 'granted') {
695
- return record.result;
696
- }
697
- if (record.platform !== undefined
698
- && isAuthoritativeMicrosoftStoreCompletion(record.target, record.platform)) {
699
- // The adapter completed this ledger grant before returning, as the client path reports it.
700
- return {
701
- status: 'granted',
702
- purchase: record.platform,
703
- ledgerEntryId: record.platform.transactionId,
704
- };
705
- }
706
- if (record.response?.verified !== true || record.platform === undefined) {
707
- return undefined;
708
- }
709
- return {
710
- status: 'granted',
711
- purchase: record.platform,
712
- verification: record.response,
713
- ...(record.response.ledgerEntryId === undefined
714
- ? {} : { ledgerEntryId: record.response.ledgerEntryId }),
715
- };
716
- }
717
556
  function recordedRewardGrant(record) {
718
557
  if (record.result?.status === 'granted') {
719
558
  return record.result;
@@ -729,22 +568,6 @@ function recordedRewardGrant(record) {
729
568
  ? {} : { ledgerEntryId: record.response.ledgerEntryId }),
730
569
  };
731
570
  }
732
- function purchaseNeedsRetry(record) {
733
- return record.result?.status === 'pending'
734
- || record.response?.finalization?.status === 'pending';
735
- }
736
- function assertPurchaseRequest(record, input) {
737
- const request = record.request;
738
- if (request === undefined || record.platform?.status !== 'completed'
739
- || request.playerId !== record.playerId || request.playerId !== input.playerId
740
- || request.target !== record.target || request.productId !== record.input.productId
741
- || request.idempotencyKey !== record.input.idempotencyKey
742
- || request.platformTransactionId !== record.platform.transactionId
743
- || request.deploymentTarget !== (input.deploymentTarget === input.target
744
- ? undefined : input.deploymentTarget)) {
745
- throw new Error('Journaled purchase verification is not bound to its platform result.');
746
- }
747
- }
748
571
  function assertRewardRequest(record, input) {
749
572
  const request = record.request;
750
573
  const candidate = record.platform === undefined ? undefined : resolveClientRewardClaim(record.platform, input.rewardEvidenceRegistry ?? createDefaultClientRewardEvidenceRegistry(), { allowLegacyCompletion: input.rewardEvidenceRegistry === undefined && input.gateway.ads.provider === undefined });
@@ -1,3 +1,4 @@
1
+ import { hasNetworkPathPrefix, normalizeRootRelativeRoute } from '@mpgd/platform/routes';
1
2
  import { assertOwnEnumerablePropertyLimit } from './validation.js';
2
3
  const platformTargets = new Set([
3
4
  'browser',
@@ -333,29 +334,14 @@ function normalizeTemplateData(input) {
333
334
  return Object.fromEntries(entries);
334
335
  }
335
336
  function normalizeDeepLink(input, allowedOrigins) {
337
+ // normalizeIdentifier already refused control characters.
336
338
  const deepLink = normalizeIdentifier(input, 'deepLink');
337
- if (/[\x00-\x1F\x7F]/u.test(deepLink)) {
338
- throw new Error('deepLink must not contain control characters.');
339
- }
340
339
  if (deepLink.startsWith('/') && !deepLink.startsWith('//')) {
341
- const baseUrl = new URL('https://mpgd.invalid');
342
- let parsed;
343
- // A backslash after the leading slash parses as an authority (`/\host`),
344
- // so the link names a host, not a path on the game origin.
345
- if (!deepLink.startsWith('/\\')) {
346
- try {
347
- parsed = new URL(deepLink, baseUrl);
348
- }
349
- catch {
350
- // Rejected below.
351
- }
352
- }
353
- if (parsed === undefined
354
- || parsed.origin !== baseUrl.origin
355
- || hasNetworkPathPrefix(parsed.pathname)) {
340
+ const route = normalizeRootRelativeRoute(deepLink);
341
+ if (route === undefined) {
356
342
  throw new Error('deepLink must stay on the configured game origin.');
357
343
  }
358
- return requireNormalizedDeepLinkLength(`${parsed.pathname}${parsed.search}${parsed.hash}`);
344
+ return requireNormalizedDeepLinkLength(route);
359
345
  }
360
346
  let parsed;
361
347
  try {
@@ -378,22 +364,6 @@ function normalizeDeepLink(input, allowedOrigins) {
378
364
  }
379
365
  return requireNormalizedDeepLinkLength(parsed.toString());
380
366
  }
381
- /**
382
- * Whether a normalized path could be read as a network-path reference to
383
- * another host. Dot segments (also percent-encoded) can collapse `/.//host`
384
- * into `//host`. A consumer that percent-decodes the path once and parses it
385
- * again can reach the same shape from `/%2F/host`, `/%5Chost`, `/%09/host`,
386
- * or `/.%2F/host`: URL parsing drops tabs and newlines, reads a backslash as
387
- * a slash, and removes dot segments, so the decoded path is checked after
388
- * the same steps.
389
- */
390
- function hasNetworkPathPrefix(pathname) {
391
- const decoded = pathname
392
- .replace(/%([0-7][0-9a-f])/giu, (_escape, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
393
- .replace(/[\t\n\r]/gu, '');
394
- return /^[/\\]{2}/u.test(decoded)
395
- || new URL(decoded, 'https://mpgd.invalid').pathname.startsWith('//');
396
- }
397
367
  /** Normalization can grow a link; keep it within the input cap so it normalizes to itself. */
398
368
  function requireNormalizedDeepLinkLength(deepLink) {
399
369
  if (deepLink.length > maxIdentifierLength) {
package/dist/types.d.ts CHANGED
@@ -5,15 +5,6 @@ import type { ClaimAdRewardResponse, PurchaseGrantFinalization, PurchaseLedgerSt
5
5
  export type { ClaimAdRewardResponse, PurchaseGrantFinalization, PurchaseGrantFinalizationAction, PurchaseLedgerStatus, PurchaseLedgerStatusRequest, PurchaseLedgerStatusResponse, VerifyPurchaseResponse, } from './operation-results.js';
6
6
  export type { PlatformEvidenceEnvelope } from '@mpgd/platform';
7
7
  /**
8
- * The closed v1 purchase target list. The deployment target of a v1 verify
9
- * request names its store implicitly (`android` is Google Play, `ios` the App
10
- * Store, `ait` Apps in Toss); `resolveLegacyCommerceStoreId` is that mapping.
11
- *
12
- * @deprecated `VerifyPurchaseRequest.target` is open since commerce v2 C2
13
- * (`GameServicesStoreTarget` is `StoreId`); C4 removes this alias.
14
- */
15
- export type GameServicesLegacyStoreTarget = Extract<PlatformTarget, 'microsoft-store' | 'android' | 'ios' | 'ait' | 'oneplay'>;
16
- /**
17
8
  * An open store or deployment target identifier (RFC "Open store ids"). The
18
9
  * request validator bounds its shape; the backend's commerce evidence
19
10
  * registry decides which stores it serves.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/game-services",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Client, contract, server, store, and test helpers for authoritative mpgd game services.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -198,13 +198,14 @@
198
198
  "dist"
199
199
  ],
200
200
  "dependencies": {
201
+ "@ait-kit/api-core": "0.6.0",
201
202
  "@noble/secp256k1": "3.1.0",
202
203
  "@orpc/client": "2.0.0-beta.14",
203
204
  "@orpc/contract": "2.0.0-beta.14",
204
205
  "@orpc/server": "2.0.0-beta.14",
205
- "@mpgd/platform": "0.22.0",
206
- "@mpgd/analytics": "0.4.8",
207
- "@mpgd/catalog": "0.9.3"
206
+ "@mpgd/analytics": "0.4.9",
207
+ "@mpgd/catalog": "0.10.0",
208
+ "@mpgd/platform": "0.23.0"
208
209
  },
209
210
  "peerDependencies": {
210
211
  "@apple/app-store-server-library": "^3.1.0"
@@ -233,7 +234,7 @@
233
234
  "fix": "ttsc fix",
234
235
  "build": "cd ../.. && node tools/run-ttsx.mjs tools/package/build-packages.ts @mpgd/game-services",
235
236
  "test:packed-transport": "pnpm build && node test/runtime-transport-package-import.mjs && node test/reward-evidence-import.mjs",
236
- "test:unit": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-progress.test.ts && node tools/run-ttsx.mjs packages/game-services/src/runtime.test.ts && node tools/run-ttsx.mjs packages/game-services/src/types.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-publisher-client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-identity.test.ts && node tools/run-ttsx.mjs packages/game-services/src/platform-order.test.ts && node tools/run-ttsx.mjs packages/game-services/src/progress-link.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notification-delivery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/admob-ssv.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-verifier.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-evidence-verification-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-partner-api.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-durability-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-transport.test.ts && node tools/run-ttsx.mjs packages/game-services/src/guest-session.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-reward-evidence.test.ts && node tools/run-ttsx.mjs packages/game-services/src/ad-reward-verifier-registry.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-reward.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-routing.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-ledger-revocation.test.ts",
237
+ "test:unit": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-progress.test.ts && node tools/run-ttsx.mjs packages/game-services/src/runtime.test.ts && node tools/run-ttsx.mjs packages/game-services/src/types.test.ts && node tools/run-ttsx.mjs packages/game-services/src/server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/google-play-publisher-client.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/microsoft-store-identity.test.ts && node tools/run-ttsx.mjs packages/game-services/src/platform-order.test.ts && node tools/run-ttsx.mjs packages/game-services/src/progress-link.test.ts && node tools/run-ttsx.mjs packages/game-services/src/notification-delivery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/admob-ssv.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-verifier.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-evidence-verification-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-partner-api.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-durability-conformance.test.ts && node tools/run-ttsx.mjs packages/game-services/src/verified-leaderboard-transport.test.ts && node tools/run-ttsx.mjs packages/game-services/src/guest-session.test.ts && node tools/run-ttsx.mjs packages/game-services/src/client-reward-evidence.test.ts && node tools/run-ttsx.mjs packages/game-services/src/ad-reward-verifier-registry.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-reward.test.ts && node tools/run-ttsx.mjs packages/game-services/src/oneplay-purchase.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-routing.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-registry-server.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-ledger-revocation.test.ts && node tools/run-ttsx.mjs packages/game-services/src/apps-in-toss-anonymous-auth.test.ts",
237
238
  "test:recovery": "cd ../.. && node tools/run-ttsx.mjs packages/game-services/src/monetization-recovery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/app-store-recovery.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-reconciler.test.ts && node tools/run-ttsx.mjs packages/game-services/src/commerce-reconciler-conformance.test.ts",
238
239
  "test": "pnpm test:unit && pnpm test:app-store-server && pnpm test:recovery && pnpm test:packed-transport",
239
240
  "test:app-store-server": "vitest run"