@mpgd/platform 0.22.0 → 0.23.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,6 +1,6 @@
1
1
  import type { PlatformCapabilities, PlatformGateway, PlatformTarget } from './index.js';
2
- export declare const platformCapabilityKeys: readonly ["nativeIap", "nativeAds", "rewardedAds", "interstitialAds", "nativeLeaderboard", "remoteLeaderboard", "achievements", "cloudSave", "socialShare", "haptics", "localizedContent"];
3
- export declare const optionalPlatformCapabilityKeys: readonly ["bannerAds", "subscriptionIap"];
2
+ export declare const platformCapabilityKeys: readonly ["nativeAds", "rewardedAds", "interstitialAds", "nativeLeaderboard", "remoteLeaderboard", "achievements", "cloudSave", "socialShare", "haptics", "localizedContent"];
3
+ export declare const optionalPlatformCapabilityKeys: readonly ["bannerAds"];
4
4
  export interface PlatformGatewayCapabilityConformanceTransition {
5
5
  readonly update: () => Promise<void> | void;
6
6
  readonly expectedCapabilities: PlatformCapabilities;
@@ -24,10 +24,11 @@ export interface PlatformGatewayCapabilityConformanceReport {
24
24
  }
25
25
  /**
26
26
  * Runs provider-neutral capability checks against real adapter gateways or
27
- * target-configured wrappers. Every read must return a complete boolean
28
- * snapshot, a fresh object, and the provider's latest state.
27
+ * target-configured wrappers. Every read must return every required boolean
28
+ * key, known readiness states, the commerce and notifications blocks under
29
+ * their own rules, a fresh object, and the provider's latest state.
29
30
  * @evidence docs/specs/platform-capability-snapshots.md#snapshot-shape Checks each gateway read against the required boolean keys and expected provider state.
30
- * @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #a1834db Reviewed required booleans, optional banner/subscription defaults, ordered readiness comparison, and fresh nested rereads.
31
+ * @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #12e3a3a Reviewed required booleans, the required commerce block, the optional banner default, ordered readiness comparison, and fresh nested rereads, including notification identity and dense purpose checks.
31
32
  * @evidence docs/specs/platform-capability-snapshots.md#provider-transitions Checks a fresh provider read after the optional transition update.
32
33
  * @evidenceReview docs/specs/platform-capability-snapshots.md#provider-transitions #839c0c8 Reviewed the changed-state guard and post-update snapshot assertion.
33
34
  * @evidence docs/specs/platform-capability-snapshots.md#fixture-validation Rejects invalid fixture sets and returns the names of passing fixtures.
@@ -1,6 +1,4 @@
1
- import { deriveLegacyCommerceCapabilities } from './commerce-capabilities.js';
2
1
  export const platformCapabilityKeys = Object.freeze([
3
- 'nativeIap',
4
2
  'nativeAds',
5
3
  'rewardedAds',
6
4
  'interstitialAds',
@@ -14,7 +12,6 @@ export const platformCapabilityKeys = Object.freeze([
14
12
  ]);
15
13
  export const optionalPlatformCapabilityKeys = Object.freeze([
16
14
  'bannerAds',
17
- 'subscriptionIap',
18
15
  ]);
19
16
  const allowedPlatformCapabilityKeys = Object.freeze([
20
17
  ...platformCapabilityKeys,
@@ -24,11 +21,10 @@ const allowedPlatformCapabilityKeySet = new Set([
24
21
  ...allowedPlatformCapabilityKeys,
25
22
  'providerAvailability',
26
23
  'commerce',
24
+ 'notifications',
27
25
  ]);
28
26
  const optionalPlatformCapabilityKeySet = new Set(optionalPlatformCapabilityKeys);
29
27
  const providerFeatureSet = new Set([
30
- 'nativeIap',
31
- 'subscriptionIap',
32
28
  'rewardedAds',
33
29
  'interstitialAds',
34
30
  'bannerAds',
@@ -43,8 +39,8 @@ const providerAvailabilitySet = new Set([
43
39
  'temporarily-unavailable',
44
40
  'available',
45
41
  ]);
46
- // The optional commerce v2 block follows the same rules as the boolean
47
- // snapshot: exact keys, typed values, and a fresh object on every read.
42
+ // The commerce block follows the same rules as the boolean snapshot: exact
43
+ // keys, typed values, and a fresh object on every read.
48
44
  const commerceCapabilityBlockKeys = Object.freeze({
49
45
  purchase: ['consumable', 'durable', 'subscription', 'quantity'],
50
46
  catalog: ['products', 'offers', 'localizedPrice'],
@@ -56,12 +52,38 @@ const commerceCapabilityEnums = Object.freeze({
56
52
  'authority.evidence': ['client-token', 'host-callback', 'adapter-settled', 'server-push'],
57
53
  'authority.accountBinding': ['required', 'optional', 'host'],
58
54
  });
55
+ const notificationCapabilityFields = Object.freeze({
56
+ permission: {
57
+ scope: { kind: 'enum', values: ['app', 'topic'] },
58
+ prompt: { kind: 'enum', values: ['os', 'host', 'game'] },
59
+ readable: { kind: 'boolean' },
60
+ serverReadable: { kind: 'boolean' },
61
+ allowance: { kind: 'enum', values: ['persistent', 'per-send'] },
62
+ userGesture: { kind: 'boolean' },
63
+ promptLimit: { kind: 'count', optional: true },
64
+ maxTopicsPerPrompt: { kind: 'count', optional: true },
65
+ settings: { kind: 'boolean' },
66
+ },
67
+ remote: {
68
+ supported: { kind: 'boolean' },
69
+ purposes: { kind: 'purposes' },
70
+ addressing: { kind: 'enum', values: ['device', 'platform-user', 'none'] },
71
+ content: { kind: 'enum', values: ['free-form', 'host-template'] },
72
+ },
73
+ local: {
74
+ supported: { kind: 'boolean' },
75
+ maxPending: { kind: 'count', optional: true },
76
+ },
77
+ });
78
+ const notificationPurposes = ['service', 'requested', 'advertising'];
79
+ const notificationOpenPaths = ['payload', 'landing-url', 'host-link', 'none'];
59
80
  /**
60
81
  * Runs provider-neutral capability checks against real adapter gateways or
61
- * target-configured wrappers. Every read must return a complete boolean
62
- * snapshot, a fresh object, and the provider's latest state.
82
+ * target-configured wrappers. Every read must return every required boolean
83
+ * key, known readiness states, the commerce and notifications blocks under
84
+ * their own rules, a fresh object, and the provider's latest state.
63
85
  * @evidence docs/specs/platform-capability-snapshots.md#snapshot-shape Checks each gateway read against the required boolean keys and expected provider state.
64
- * @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #a1834db Reviewed required booleans, optional banner/subscription defaults, ordered readiness comparison, and fresh nested rereads.
86
+ * @evidenceReview docs/specs/platform-capability-snapshots.md#snapshot-shape #12e3a3a Reviewed required booleans, the required commerce block, the optional banner default, ordered readiness comparison, and fresh nested rereads, including notification identity and dense purpose checks.
65
87
  * @evidence docs/specs/platform-capability-snapshots.md#provider-transitions Checks a fresh provider read after the optional transition update.
66
88
  * @evidenceReview docs/specs/platform-capability-snapshots.md#provider-transitions #839c0c8 Reviewed the changed-state guard and post-update snapshot assertion.
67
89
  * @evidence docs/specs/platform-capability-snapshots.md#fixture-validation Rejects invalid fixture sets and returns the names of passing fixtures.
@@ -102,11 +124,12 @@ async function runFixture(fixture) {
102
124
  : cloneExpectedCapabilities(fixture.transition.expectedCapabilities);
103
125
  assertEqual(fixture.gateway.target, fixture.expectedTarget, 'gateway target must match the fixture target');
104
126
  const first = await fixture.gateway.getCapabilities();
105
- assertCapabilitySnapshot(first, initialExpected);
127
+ assertCapabilitySnapshot(first, initialExpected, fixture.gateway);
106
128
  tryMutateSnapshot(first);
107
129
  const second = await fixture.gateway.getCapabilities();
108
130
  assert(second !== first, 'getCapabilities must return a fresh snapshot object for every read');
109
- assertCapabilitySnapshot(second, initialExpected);
131
+ assertCapabilitySnapshot(second, initialExpected, fixture.gateway);
132
+ assertNotificationSnapshotFreshness(first, second);
110
133
  if (fixture.transition === undefined || transitionExpected === undefined) {
111
134
  return;
112
135
  }
@@ -115,9 +138,23 @@ async function runFixture(fixture) {
115
138
  const updated = await fixture.gateway.getCapabilities();
116
139
  assert(updated !== second, 'getCapabilities must return a fresh snapshot after a provider transition');
117
140
  assert(updated !== first, 'getCapabilities must not reuse an earlier snapshot after a provider transition');
118
- assertCapabilitySnapshot(updated, transitionExpected);
141
+ assertCapabilitySnapshot(updated, transitionExpected, fixture.gateway);
142
+ assertNotificationSnapshotFreshness(first, updated);
143
+ assertNotificationSnapshotFreshness(second, updated);
144
+ }
145
+ function assertNotificationSnapshotFreshness(previous, next) {
146
+ const first = previous.notifications;
147
+ const second = next.notifications;
148
+ if (first === undefined || second === undefined) {
149
+ return;
150
+ }
151
+ assert(second !== first, 'notification capabilities must return a fresh block for every read');
152
+ for (const key of ['permission', 'remote', 'local']) {
153
+ assert(second[key] !== first[key], `notification capabilities must return a fresh ${key} record`);
154
+ }
155
+ assert(second.remote.purposes !== first.remote.purposes, 'notification capabilities must return a fresh purposes list');
119
156
  }
120
- function assertCapabilitySnapshot(actual, expected) {
157
+ function assertCapabilitySnapshot(actual, expected, gateway) {
121
158
  const actualKeys = Object.keys(actual).sort();
122
159
  const missingKeys = platformCapabilityKeys.filter((key) => !Object.hasOwn(actual, key));
123
160
  const unexpectedKeys = actualKeys.filter((key) => !allowedPlatformCapabilityKeySet.has(key));
@@ -132,14 +169,85 @@ function assertCapabilitySnapshot(actual, expected) {
132
169
  assertProviderAvailabilityShape(actual.providerAvailability);
133
170
  assertEqual(normalizedAvailability(actual.providerAvailability), normalizedAvailability(expected.providerAvailability), 'provider availability must match the expected provider state');
134
171
  assertCommerceCapabilityBlock(actual, expected.commerce);
172
+ assertNotificationCapabilityBlock(actual, expected.notifications, gateway);
135
173
  }
136
- function assertCommerceCapabilityBlock(snapshot, expected) {
137
- const actual = snapshot.commerce;
174
+ function assertNotificationCapabilityBlock(snapshot, expected, gateway) {
175
+ const actual = snapshot.notifications;
176
+ // The block describes the exposed provider (notifications RFC "Capabilities").
177
+ assertEqual(actual !== undefined, gateway.notificationsV2 !== undefined, 'notification capabilities must be present exactly when the gateway exposes notificationsV2');
138
178
  if (actual === undefined) {
139
- assert(expected === undefined, 'commerce capabilities must be present when the fixture expects them');
179
+ assert(expected === undefined, 'notification capabilities must be present when the fixture expects them');
140
180
  return;
141
181
  }
142
- assert(expected !== undefined, 'commerce capabilities must be absent when the fixture omits them');
182
+ assert(expected !== undefined, 'notification capabilities must be absent when the fixture omits them');
183
+ assert(isPlainRecord(actual), 'notification capabilities must be a non-array record');
184
+ assertEqual(JSON.stringify(Object.keys(actual).sort()), JSON.stringify(['availability', 'local', 'open', 'permission', 'remote']), 'notification capabilities must contain exactly availability, permission, remote, local, and open');
185
+ for (const [group, fields] of Object.entries(notificationCapabilityFields)) {
186
+ const nested = actual[group];
187
+ assert(isPlainRecord(nested), `notification ${group} capabilities must be a non-array record`);
188
+ const unknownKeys = Object.keys(nested).filter((key) => !Object.hasOwn(fields, key));
189
+ assertEqual(JSON.stringify(unknownKeys), '[]', `notification ${group} capabilities must contain no unknown keys`);
190
+ for (const [key, field] of Object.entries(fields)) {
191
+ const path = `${group}.${key}`;
192
+ if (!Object.hasOwn(nested, key)) {
193
+ assert(field.optional === true, `notification capability ${path} is required`);
194
+ continue;
195
+ }
196
+ assertNotificationCapabilityField(path, field, nested[key]);
197
+ }
198
+ }
199
+ assert(providerAvailabilitySet.has(actual.availability), 'notification availability must be a known readiness state');
200
+ assert(notificationOpenPaths.includes(actual.open), `notification open must be one of ${notificationOpenPaths.join(', ')}`);
201
+ const block = actual;
202
+ assertEqual(block.remote.supported, block.remote.addressing !== 'none', "notification remote.addressing must be 'none' exactly when remote.supported is false");
203
+ assertEqual(normalizedNotifications(block), normalizedNotifications(expected), 'notification capabilities must match the expected provider state');
204
+ assertEqual(snapshot.providerAvailability?.pushNotifications, block.availability, 'providerAvailability.pushNotifications must equal the notification capabilities availability');
205
+ }
206
+ function assertNotificationCapabilityField(path, field, value) {
207
+ switch (field.kind) {
208
+ case 'boolean':
209
+ assertEqual(typeof value, 'boolean', `notification capability ${path} must be a boolean`);
210
+ return;
211
+ case 'enum':
212
+ assert(field.values.includes(value), `notification capability ${path} must be one of ${field.values.join(', ')}`);
213
+ return;
214
+ case 'count':
215
+ assert(Number.isInteger(value) && value >= 1 && value <= 0xffff_ffff, `notification capability ${path} must be a positive integer`);
216
+ return;
217
+ case 'purposes':
218
+ assert(isDenseNotificationPurposeList(value), `notification capability ${path} must list only ${notificationPurposes.join(', ')}`);
219
+ return;
220
+ }
221
+ }
222
+ function isDenseNotificationPurposeList(value) {
223
+ if (!Array.isArray(value)) {
224
+ return false;
225
+ }
226
+ for (let index = 0; index < value.length; index += 1) {
227
+ if (!Object.hasOwn(value, index) || !notificationPurposes.includes(value[index])) {
228
+ return false;
229
+ }
230
+ }
231
+ return true;
232
+ }
233
+ function normalizedNotifications(value) {
234
+ if (value === undefined) {
235
+ return 'undefined';
236
+ }
237
+ return JSON.stringify([
238
+ ['availability', value.availability],
239
+ ['permission', sortedEntries(value.permission)],
240
+ ['remote', sortedEntries({ ...value.remote, purposes: [...value.remote.purposes].sort() })],
241
+ ['local', sortedEntries(value.local)],
242
+ ['open', value.open],
243
+ ]);
244
+ }
245
+ function isPlainRecord(value) {
246
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
247
+ }
248
+ function assertCommerceCapabilityBlock(snapshot, expected) {
249
+ const actual = snapshot.commerce;
250
+ assert(actual !== undefined, 'commerce capabilities are required: a gateway without a commerce provider reports createUnsupportedCommerceCapabilities()');
143
251
  assert(typeof actual === 'object' && actual !== null && !Array.isArray(actual), 'commerce capabilities must be a non-array record');
144
252
  const block = actual;
145
253
  const blockKeys = Object.keys(block).sort();
@@ -162,9 +270,6 @@ function assertCommerceCapabilityBlock(snapshot, expected) {
162
270
  }
163
271
  assert(providerAvailabilitySet.has(block.availability), 'commerce availability must be a known readiness state');
164
272
  assertEqual(normalizedCommerce(block), normalizedCommerce(expected), 'commerce capabilities must match the expected provider state');
165
- const legacy = deriveLegacyCommerceCapabilities(block);
166
- assertEqual(snapshot.nativeIap, legacy.nativeIap, 'nativeIap must equal the value derived from the commerce capabilities');
167
- assertEqual(snapshot.subscriptionIap ?? false, legacy.subscriptionIap, 'subscriptionIap must equal the value derived from the commerce capabilities');
168
273
  }
169
274
  function normalizedCommerce(value) {
170
275
  if (value === undefined) {
@@ -191,41 +296,37 @@ function assertProviderAvailabilityShape(value) {
191
296
  && providerAvailabilitySet.has(state), 'provider availability must contain only known features and readiness states');
192
297
  }
193
298
  }
299
+ // A deep copy of the plain records and lists, whatever their shape: the
300
+ // oracle stays independent of the provider even when a fixture expects a
301
+ // malformed block to be reported as is.
194
302
  function cloneExpectedCapabilities(value) {
195
- return {
196
- ...value,
197
- ...(value.providerAvailability === undefined
198
- ? {}
199
- : { providerAvailability: { ...value.providerAvailability } }),
200
- ...(value.commerce === undefined
201
- ? {}
202
- : {
203
- commerce: {
204
- purchase: { ...value.commerce.purchase },
205
- catalog: { ...value.commerce.catalog },
206
- lifecycle: { ...value.commerce.lifecycle },
207
- authority: { ...value.commerce.authority },
208
- availability: value.commerce.availability,
209
- },
210
- }),
211
- };
303
+ return clonePlainData(value);
304
+ }
305
+ function clonePlainData(value) {
306
+ if (Array.isArray(value)) {
307
+ return value.map((entry) => clonePlainData(entry));
308
+ }
309
+ if (typeof value === 'object' && value !== null) {
310
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, clonePlainData(entry)]));
311
+ }
312
+ return value;
212
313
  }
213
314
  function normalizedAvailability(value) {
214
315
  return JSON.stringify(Object.entries(value ?? {}).sort(([a], [b]) => a.localeCompare(b)));
215
316
  }
216
317
  function tryMutateSnapshot(snapshot) {
217
318
  try {
218
- Reflect.set(snapshot, 'nativeIap', !snapshot.nativeIap);
319
+ Reflect.set(snapshot, 'nativeAds', !snapshot.nativeAds);
219
320
  }
220
321
  catch {
221
322
  // Frozen snapshots already satisfy the isolation requirement.
222
323
  }
223
324
  if (snapshot.providerAvailability !== undefined) {
224
325
  try {
225
- const next = snapshot.providerAvailability.nativeIap === 'available'
326
+ const next = snapshot.providerAvailability.rewardedAds === 'available'
226
327
  ? 'unsupported'
227
328
  : 'available';
228
- Reflect.set(snapshot.providerAvailability, 'nativeIap', next);
329
+ Reflect.set(snapshot.providerAvailability, 'rewardedAds', next);
229
330
  }
230
331
  catch {
231
332
  // A frozen nested readiness record already satisfies isolation.
@@ -234,6 +335,39 @@ function tryMutateSnapshot(snapshot) {
234
335
  if (snapshot.commerce !== undefined) {
235
336
  tryMutateCommerceSnapshot(snapshot.commerce);
236
337
  }
338
+ if (snapshot.notifications !== undefined) {
339
+ tryMutateNotificationSnapshot(snapshot.notifications);
340
+ }
341
+ }
342
+ // Every nested record and the purposes list are probed, as for commerce.
343
+ function tryMutateNotificationSnapshot(notifications) {
344
+ const { purposes } = notifications.remote;
345
+ const probes = [
346
+ [notifications.permission, 'readable', !notifications.permission.readable],
347
+ [
348
+ notifications.remote,
349
+ 'content',
350
+ notifications.remote.content === 'free-form' ? 'host-template' : 'free-form',
351
+ ],
352
+ // A reused list shows on the next read: emptied when it lists purposes,
353
+ // or holding one when it was empty.
354
+ purposes.length === 0 ? [purposes, '0', 'service'] : [purposes, 'length', 0],
355
+ [notifications.local, 'supported', !notifications.local.supported],
356
+ [
357
+ notifications,
358
+ 'availability',
359
+ notifications.availability === 'available' ? 'unsupported' : 'available',
360
+ ],
361
+ [notifications, 'open', notifications.open === 'none' ? 'payload' : 'none'],
362
+ ];
363
+ for (const [target, key, next] of probes) {
364
+ try {
365
+ Reflect.set(target, key, next);
366
+ }
367
+ catch {
368
+ // Frozen values still need distinct references, checked across snapshots.
369
+ }
370
+ }
237
371
  }
238
372
  // Every nested block is probed: a gateway may rebuild the outer object while
239
373
  // reusing one mutable nested record across reads.
@@ -263,7 +397,9 @@ function hasCapabilityDifference(first, second) {
263
397
  .some((key) => (first[key] ?? false) !== (second[key] ?? false))
264
398
  || normalizedAvailability(first.providerAvailability)
265
399
  !== normalizedAvailability(second.providerAvailability)
266
- || normalizedCommerce(first.commerce) !== normalizedCommerce(second.commerce);
400
+ || normalizedCommerce(first.commerce) !== normalizedCommerce(second.commerce)
401
+ || normalizedNotifications(first.notifications)
402
+ !== normalizedNotifications(second.notifications);
267
403
  }
268
404
  function assert(condition, message) {
269
405
  if (!condition) {
@@ -1,9 +1,3 @@
1
1
  import type { CommerceCapabilities } from './index.js';
2
- export interface LegacyCommerceCapabilities {
3
- readonly nativeIap: boolean;
4
- readonly subscriptionIap: boolean;
5
- }
6
- /** Transition rule for the boolean `PlatformCapabilities` flags. */
7
- export declare function deriveLegacyCommerceCapabilities(capabilities: CommerceCapabilities): LegacyCommerceCapabilities;
8
2
  /** A fresh all-false snapshot; callers must not share or mutate a returned object. */
9
3
  export declare function createUnsupportedCommerceCapabilities(): CommerceCapabilities;
@@ -1,11 +1,5 @@
1
- /** Transition rule for the boolean `PlatformCapabilities` flags. */
2
- export function deriveLegacyCommerceCapabilities(capabilities) {
3
- return {
4
- nativeIap: capabilities.availability === 'available'
5
- && (capabilities.purchase.consumable || capabilities.purchase.durable),
6
- subscriptionIap: capabilities.purchase.subscription,
7
- };
8
- }
1
+ // Pure capability helpers shared by `./commerce` and `./capability-conformance`.
2
+ // This file imports no validator so the conformance runner stays free of typia.
9
3
  /** A fresh all-false snapshot; callers must not share or mutate a returned object. */
10
4
  export function createUnsupportedCommerceCapabilities() {
11
5
  return {
@@ -1,8 +1,8 @@
1
- import type { CommerceCapabilities, CommerceFinishOwner, LogicalProductId, PlatformEvidenceEnvelope, StarterLogicalProductId } from './index.js';
1
+ import type { CommerceCapabilities, CommerceFinishOwner, LogicalProductId, PlatformEvidenceEnvelope, PlatformPurchasePresentationEvent, StarterLogicalProductId } from './index.js';
2
2
  /**
3
3
  * Commerce v2 client contract types (RFC "Client contract"). This module is
4
4
  * typia-free on purpose: `./index` references `CommerceProvider` for
5
- * `PlatformGateway.commerceV2`, and headless consumers type-check
5
+ * `PlatformGateway.commerce`, and headless consumers type-check
6
6
  * `@mpgd/platform` with `skipLibCheck: false` and no DOM lib, where typia's
7
7
  * root declarations do not compile. `./commerce` re-exports everything here
8
8
  * and builds the validators and JSON Schemas from these same types.
@@ -193,6 +193,16 @@ export interface FinishTransactionResult {
193
193
  }
194
194
  export interface CommerceProvider {
195
195
  readonly store: StoreId;
196
+ /**
197
+ * Trusted native presentation facts for the purchase UI (opened, closed,
198
+ * never started), independent of the purchase result. A provider whose host
199
+ * can prove when its payment surface closes (ONE play) offers it; the game
200
+ * runtime's presentation gate subscribes when present and treats a result
201
+ * alone as unknown.
202
+ */
203
+ readonly presentation?: {
204
+ subscribe(listener: (event: PlatformPurchasePresentationEvent) => void): () => void;
205
+ };
196
206
  getCapabilities(): Promise<CommerceCapabilities>;
197
207
  fetchProducts(input: FetchProductsInput): Promise<readonly StoreProduct[]>;
198
208
  /** Returns `launched` when the result arrives later as a `purchaseUpdated` event. */
@@ -1,7 +1,7 @@
1
1
  import { type tags } from 'typia';
2
- import { PlatformOperationError, type CommerceAdapter, type CommerceCapabilities } from './index.js';
3
- import type { ActiveSubscription, CommerceErrorCode, CommerceErrorInfo, CommerceEvent, CommerceLogicalProductId, CommerceProvider, CommerceServiceCapabilities, FetchProductsInput, FinishTransactionInput, FinishTransactionResult, GetAvailablePurchasesInput, IsoTimestamp, NonEmptyString, NonNegativeInteger, PositiveQuantity, PurchaseKind, RequestPurchaseInput, RequestPurchaseResult, StoreId, StoreProduct, StorePurchase } from './commerce-contract.js';
4
- export { createUnsupportedCommerceCapabilities, deriveLegacyCommerceCapabilities, type LegacyCommerceCapabilities, } from './commerce-capabilities.js';
2
+ import { PlatformOperationError, type CommerceCapabilities } from './index.js';
3
+ import type { ActiveSubscription, CommerceErrorCode, CommerceErrorInfo, CommerceEvent, CommerceLogicalProductId, CommerceServiceCapabilities, FetchProductsInput, FinishTransactionInput, FinishTransactionResult, GetAvailablePurchasesInput, IsoTimestamp, NonEmptyString, NonNegativeInteger, PositiveQuantity, PurchaseKind, RequestPurchaseInput, RequestPurchaseResult, StoreId, StoreProduct, StorePurchase } from './commerce-contract.js';
4
+ export { createUnsupportedCommerceCapabilities } from './commerce-capabilities.js';
5
5
  export type { CommerceAccountBindingPolicy, CommerceAuthorityCapabilities, CommerceCapabilities, CommerceCatalogCapabilities, CommerceEvidenceAuthority, CommerceFinishOwner, CommerceLifecycleCapabilities, CommercePurchaseCapabilities, } from './index.js';
6
6
  /**
7
7
  * Commerce v2 client contract. Pure types, validators, and helpers only: no
@@ -255,7 +255,11 @@ export interface DerivedPurchaseIdempotencyKeyInput {
255
255
  * byte-identical.
256
256
  */
257
257
  export declare function createDerivedPurchaseIdempotencyKey(input: DerivedPurchaseIdempotencyKeyInput): Promise<string>;
258
- /** Byte-for-byte the v1 `createGooglePlayTokenTransactionId` form; A4 replaces the two copies with this helper. */
258
+ /**
259
+ * The `play-token-sha256:<hex>` idempotency key of a Google Play purchase
260
+ * token. The one shared helper: the plugin and `@mpgd/game-services` use it,
261
+ * and ledgers written by v1 releases keep deduplicating against it.
262
+ */
259
263
  export declare function createGooglePlayTokenPurchaseKey(purchaseToken: string): Promise<string>;
260
264
  export declare const commerceEventBufferLimit = 64;
261
265
  export interface CommerceEventBuffer {
@@ -317,16 +321,3 @@ export declare const assertCommerceBridgeFinishTransactionPayload: (input: unkno
317
321
  export declare const assertCommerceBridgeFinishTransactionResult: (input: unknown) => FinishTransactionResult;
318
322
  export declare const assertCommerceBridgeRestorePurchasesPayload: (input: unknown) => CommerceBridgeRestorePurchasesPayload;
319
323
  export declare const assertCommerceBridgeRestorePurchasesResult: (input: unknown) => CommerceBridgeRestorePurchasesResult;
320
- export interface ToCommerceAdapterOptions {
321
- /** Forwarded to `getAvailablePurchases` during `restore`. Defaults to false. */
322
- readonly includePending?: boolean;
323
- }
324
- /**
325
- * Serves v1 `CommerceAdapter` callers from a v2 provider. The wrapper never
326
- * grants: a `purchased` outcome becomes a `completed` result with no
327
- * entitlements, and `authoritativeGrant` appears only when the provider reports
328
- * an adapter settlement that already points at a ledger entry. `restore`
329
- * returns settled purchases from `getAvailablePurchases` and no entitlements;
330
- * entitlements come from the ledger, not from the provider.
331
- */
332
- export declare function toCommerceAdapter(provider: CommerceProvider, options?: ToCommerceAdapterOptions): CommerceAdapter;
package/dist/commerce.js CHANGED
@@ -6,7 +6,7 @@ import * as _accessExpressionAsString_1 from "typia/lib/internal/_accessExpressi
6
6
  const __typia_transform__accessExpressionAsString = _accessExpressionAsString_1._accessExpressionAsString;
7
7
  import typia from 'typia';
8
8
  import { PlatformOperationError, readPlatformOperationFailure, } from './index.js';
9
- export { createUnsupportedCommerceCapabilities, deriveLegacyCommerceCapabilities, } from './commerce-capabilities.js';
9
+ export { createUnsupportedCommerceCapabilities } from './commerce-capabilities.js';
10
10
  /**
11
11
  * Commerce v2 client contract. Pure types, validators, and helpers only: no
12
12
  * provider lives here, and nothing in this module grants a purchase. The
@@ -39,6 +39,10 @@ export const commerceBridgeMethods = Object.freeze([
39
39
  'commerce.v2.finishTransaction',
40
40
  'commerce.v2.restorePurchases',
41
41
  ]);
42
+ // `satisfies` only rejects an unknown name; this rejects a missing one, so a
43
+ // method added to `CommerceBridgeMethods` cannot be left out of the list.
44
+ const commerceBridgeMethodListIsExhaustive = true;
45
+ void commerceBridgeMethodListIsExhaustive;
42
46
  // Errors.
43
47
  const retryableCommerceErrorCodes = new Set([
44
48
  'NETWORK',
@@ -226,7 +230,11 @@ export async function createDerivedPurchaseIdempotencyKey(input) {
226
230
  }
227
231
  return `store:${encodeURIComponent(store)}:${encodeURIComponent(transactionId)}`;
228
232
  }
229
- /** Byte-for-byte the v1 `createGooglePlayTokenTransactionId` form; A4 replaces the two copies with this helper. */
233
+ /**
234
+ * The `play-token-sha256:<hex>` idempotency key of a Google Play purchase
235
+ * token. The one shared helper: the plugin and `@mpgd/game-services` use it,
236
+ * and ledgers written by v1 releases keep deduplicating against it.
237
+ */
230
238
  export async function createGooglePlayTokenPurchaseKey(purchaseToken) {
231
239
  if (readIdentifier(purchaseToken, 4096) === undefined) {
232
240
  throw new TypeError('Google Play purchase token is invalid.');
@@ -5757,95 +5765,3 @@ export const assertCommerceBridgeRestorePurchasesResult = (() => {
5757
5765
  return input;
5758
5766
  };
5759
5767
  })();
5760
- const purchaseKindToProductType = Object.freeze({
5761
- consumable: 'consumable',
5762
- durable: 'non_consumable',
5763
- subscription: 'subscription',
5764
- });
5765
- /**
5766
- * Serves v1 `CommerceAdapter` callers from a v2 provider. The wrapper never
5767
- * grants: a `purchased` outcome becomes a `completed` result with no
5768
- * entitlements, and `authoritativeGrant` appears only when the provider reports
5769
- * an adapter settlement that already points at a ledger entry. `restore`
5770
- * returns settled purchases from `getAvailablePurchases` and no entitlements;
5771
- * entitlements come from the ledger, not from the provider.
5772
- */
5773
- export function toCommerceAdapter(provider, options = {}) {
5774
- const includePending = options.includePending ?? false;
5775
- return {
5776
- async getProducts() {
5777
- const products = await provider.fetchProducts({ type: 'all' });
5778
- return products.map((product) => ({
5779
- id: product.logicalId,
5780
- type: purchaseKindToProductType[product.kind],
5781
- title: product.title,
5782
- description: product.description,
5783
- price: { formatted: product.price.formatted, currencyCode: product.price.currencyCode },
5784
- }));
5785
- },
5786
- async purchase(input) {
5787
- const result = await provider.requestPurchase({
5788
- logicalId: input.productId,
5789
- idempotencyKey: input.idempotencyKey,
5790
- source: input.source,
5791
- });
5792
- return toPurchaseResult(result);
5793
- },
5794
- async restore() {
5795
- await provider.restorePurchases?.();
5796
- const purchases = await provider.getAvailablePurchases({ includePending });
5797
- const settledPurchases = [];
5798
- for (const purchase of purchases) {
5799
- // A pending purchase is never a settled outcome, even when the adapter
5800
- // already attached a ledger pointer to it.
5801
- if (purchase.settlement === undefined
5802
- || purchase.logicalId === undefined
5803
- || purchase.state === 'pending') {
5804
- continue;
5805
- }
5806
- settledPurchases.push({
5807
- transactionId: purchase.transactionId,
5808
- productId: purchase.logicalId,
5809
- status: purchase.state === 'revoked' ? 'refunded' : 'granted',
5810
- ledgerEntryId: purchase.settlement.ledgerEntryId,
5811
- });
5812
- }
5813
- return { restoredEntitlements: [], settledPurchases };
5814
- },
5815
- async getEntitlements() {
5816
- return [];
5817
- },
5818
- };
5819
- }
5820
- function toPurchaseResult(result) {
5821
- if (result.outcome === 'launched') {
5822
- return { status: 'pending', entitlementIds: [] };
5823
- }
5824
- if (result.outcome === 'purchased') {
5825
- const { purchase } = result;
5826
- return {
5827
- status: purchase.state === 'purchased' ? 'completed' : 'pending',
5828
- transactionId: purchase.transactionId,
5829
- entitlementIds: [],
5830
- evidence: purchase.evidence,
5831
- ...(purchase.settlement === undefined || purchase.state !== 'purchased'
5832
- ? {}
5833
- : { authoritativeGrant: { ledgerEntryId: purchase.settlement.ledgerEntryId } }),
5834
- };
5835
- }
5836
- return {
5837
- status: result.outcome,
5838
- entitlementIds: [],
5839
- diagnostic: toCommerceDiagnostic(result.error),
5840
- };
5841
- }
5842
- function toCommerceDiagnostic(error) {
5843
- const providerCode = error.storeCode;
5844
- return {
5845
- code: error.code,
5846
- retryable: error.retryable,
5847
- ...(providerCode !== undefined && /^[A-Za-z0-9_.:-]{1,128}$/u.test(providerCode)
5848
- ? { providerCode }
5849
- : {}),
5850
- };
5851
- }