@mpgd/platform 0.21.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 {
@@ -355,6 +355,14 @@ export interface CommerceConformanceContext extends CommerceConformanceFixtureCo
355
355
  export interface CommerceConformanceRunOptions {
356
356
  /** Supplies a provider over the caller's fake SDK; defaults to the fake store or the reference wallet. */
357
357
  readonly createProvider?: (context: CommerceConformanceContext) => CommerceConformanceHarness | Promise<CommerceConformanceHarness>;
358
+ /**
359
+ * Supplies the runtime under test over the harness provider, backend, and
360
+ * journal; defaults to the reference reconciler
361
+ * (`createCommerceConformanceRuntime`). The reference run an adaptive
362
+ * vector is derived from always uses the reference reconciler, so the
363
+ * derived expectations stay the RFC's regardless of the runtime under test.
364
+ */
365
+ readonly createRuntime?: (input: CreateCommerceConformanceRuntimeInput, context: CommerceConformanceContext) => CommerceConformanceRuntime | Promise<CommerceConformanceRuntime>;
358
366
  readonly playerId?: string;
359
367
  readonly titleId?: string;
360
368
  }
@@ -1107,7 +1107,7 @@ async function runVectorIn(vector, options, family) {
1107
1107
  // A harness that cannot be built is a failed run, never a rejected promise.
1108
1108
  let execution;
1109
1109
  try {
1110
- execution = await prepareExecution(vector, options.createProvider ?? createCommerceConformanceHarness, ids);
1110
+ execution = await prepareExecution(vector, options.createProvider ?? createCommerceConformanceHarness, ids, options.createRuntime);
1111
1111
  }
1112
1112
  catch (error) {
1113
1113
  return failed([
@@ -1200,7 +1200,7 @@ async function runVectorIn(vector, options, family) {
1200
1200
  ...(adaptive === undefined ? {} : { adaptive }),
1201
1201
  };
1202
1202
  }
1203
- async function prepareExecution(vector, createHarness, ids) {
1203
+ async function prepareExecution(vector, createHarness, ids, createRuntime) {
1204
1204
  const clock = createCommerceConformanceClock();
1205
1205
  const journal = createCommerceConformanceJournal();
1206
1206
  const harnessRef = {};
@@ -1224,14 +1224,17 @@ async function prepareExecution(vector, createHarness, ids) {
1224
1224
  };
1225
1225
  const harness = await createHarness(context);
1226
1226
  harnessRef.current = harness;
1227
- const runtime = createCommerceConformanceRuntime({
1227
+ const runtimeInput = {
1228
1228
  provider: harness.provider,
1229
1229
  backend,
1230
1230
  journal,
1231
1231
  clock,
1232
1232
  catalog: context.catalog,
1233
1233
  playerId: ids.playerId,
1234
- });
1234
+ };
1235
+ const runtime = createRuntime === undefined
1236
+ ? createCommerceConformanceRuntime(runtimeInput)
1237
+ : await createRuntime(runtimeInput, context);
1235
1238
  return { vector, harness, runtime, backend, journal, clock, adaptation };
1236
1239
  }
1237
1240
  /** Runs every step; a thrown step ends the run with a failure at that step. */
@@ -1,8 +1,8 @@
1
- import type { CommerceCapabilities, 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,13 +193,29 @@ 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. */
199
209
  requestPurchase(input: RequestPurchaseInput): Promise<RequestPurchaseResult>;
200
210
  /** Purchases the store still reports as unfinished. */
201
211
  getAvailablePurchases(input?: GetAvailablePurchasesInput): Promise<readonly StorePurchase[]>;
202
- /** Required when `lifecycle.finish` is 'client' or 'host'. */
212
+ /**
213
+ * Required when `lifecycle.finish` is 'client' or 'host'. Idempotent per
214
+ * transaction: the reconciler calls it again after a crash between a
215
+ * successful finish and its journal write, and on every pass while a
216
+ * finish keeps failing, so finishing an already finished transaction
217
+ * answers `{ finished: true }` rather than failing.
218
+ */
203
219
  finishTransaction?(input: FinishTransactionInput): Promise<FinishTransactionResult>;
204
220
  /** Asks the store to resynchronize, for example `AppStore.sync`. */
205
221
  restorePurchases?(): Promise<void>;
@@ -219,4 +235,27 @@ export interface ActiveSubscription {
219
235
  readonly expiresAt: IsoTimestamp;
220
236
  readonly autoRenew: boolean;
221
237
  }
238
+ /** What the backend accepts for one store (RFC "Provider capabilities endpoint"). */
239
+ export interface CommerceServiceStoreCapabilities {
240
+ readonly store: StoreId;
241
+ /** Evidence schemas the backend routes for this store. */
242
+ readonly schemas: readonly NonEmptyString[];
243
+ /** Who finishes a granted purchase of this store, as the backend expects it. */
244
+ readonly finish: CommerceFinishOwner;
245
+ readonly purchase: {
246
+ readonly consumable: boolean;
247
+ readonly durable: boolean;
248
+ readonly subscription: boolean;
249
+ };
250
+ }
251
+ /**
252
+ * The backend's published commerce capabilities (RFC "Provider capabilities
253
+ * endpoint"): the protocol version it speaks and one row per store its
254
+ * evidence registry accepts. The client runtime compares it with its
255
+ * provider's capability block at start.
256
+ */
257
+ export interface CommerceServiceCapabilities {
258
+ readonly commerceProtocolVersion: NonEmptyString;
259
+ readonly stores: readonly CommerceServiceStoreCapabilities[];
260
+ }
222
261
  export {};
@@ -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, 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
@@ -11,7 +11,7 @@ export type { CommerceAccountBindingPolicy, CommerceAuthorityCapabilities, Comme
11
11
  */
12
12
  export declare const commerceProtocolVersion: '2.0.0-draft.1';
13
13
  export declare const commerceProtocol: 'mpgd.commerce.v2';
14
- export type { ActiveSubscription, ActiveSubscriptionState, CommerceErrorCode, CommerceErrorInfo, CommerceEvent, CommerceLogicalProductId, CommerceProvider, FetchProductsInput, FetchProductsType, FinishTransactionInput, FinishTransactionResult, GetAvailablePurchasesInput, PurchaseKind, PurchaseSource, RequestPurchaseInput, RequestPurchaseResult, StoreId, StoreOffer, StorePrice, StoreProduct, StorePurchase, StorePurchaseAccountBinding, StorePurchaseState, } from './commerce-contract.js';
14
+ export type { ActiveSubscription, ActiveSubscriptionState, CommerceErrorCode, CommerceErrorInfo, CommerceEvent, CommerceLogicalProductId, CommerceProvider, CommerceServiceCapabilities, CommerceServiceStoreCapabilities, FetchProductsInput, FetchProductsType, FinishTransactionInput, FinishTransactionResult, GetAvailablePurchasesInput, PurchaseKind, PurchaseSource, RequestPurchaseInput, RequestPurchaseResult, StoreId, StoreOffer, StorePrice, StoreProduct, StorePurchase, StorePurchaseAccountBinding, StorePurchaseState, } from './commerce-contract.js';
15
15
  export declare const commerceErrorCodes: readonly ["USER_CANCELLED", "OUTCOME_UNKNOWN", "ALREADY_OWNED", "IN_PROGRESS", "ITEM_UNAVAILABLE", "NOT_PREPARED", "NETWORK", "STORE_UNAVAILABLE", "CONFIGURATION_REQUIRED", "ACTION_REQUIRED", "ACCOUNT_BINDING_REQUIRED", "INSUFFICIENT_BALANCE", "INVALID_RESPONSE", "UNKNOWN"];
16
16
  /** Bridge event envelope; `sequence` is per session and starts at 0. */
17
17
  export interface CommerceEventEnvelope {
@@ -250,11 +250,16 @@ export interface DerivedPurchaseIdempotencyKeyInput {
250
250
  * the same key: `store:<storeId>:<transactionId>` by default, with each
251
251
  * component percent-encoded (`encodeURIComponent`) so ids containing `:`
252
252
  * cannot collide, and the forms v1 ledgers already use for Google Play
253
- * (`play-token-sha256:<hex>`) and Apps in Toss (`apps-in-toss:purchase:<orderId>`),
254
- * which stay byte-identical.
253
+ * (`play-token-sha256:<hex>`) and Apps in Toss
254
+ * (`apps-in-toss:purchase:<encodeURIComponent(orderId)>`), which stay
255
+ * byte-identical.
255
256
  */
256
257
  export declare function createDerivedPurchaseIdempotencyKey(input: DerivedPurchaseIdempotencyKeyInput): Promise<string>;
257
- /** 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
+ */
258
263
  export declare function createGooglePlayTokenPurchaseKey(purchaseToken: string): Promise<string>;
259
264
  export declare const commerceEventBufferLimit = 64;
260
265
  export interface CommerceEventBuffer {
@@ -292,6 +297,8 @@ export declare const isActiveSubscription: (input: unknown) => input is ActiveSu
292
297
  export declare const assertFetchProductsInput: (input: unknown) => FetchProductsInput;
293
298
  export declare const assertCommerceCapabilities: (input: unknown) => CommerceCapabilities;
294
299
  export declare const isCommerceCapabilities: (input: unknown) => input is CommerceCapabilities;
300
+ export declare const assertCommerceServiceCapabilities: (input: unknown) => CommerceServiceCapabilities;
301
+ export declare const isCommerceServiceCapabilities: (input: unknown) => input is CommerceServiceCapabilities;
295
302
  export declare const assertCommerceErrorInfo: (input: unknown) => CommerceErrorInfo;
296
303
  export declare const assertCreditOffer: (input: unknown) => CreditOffer;
297
304
  export declare const assertCreditSpendInput: (input: unknown) => CreditSpendInput;
@@ -314,16 +321,3 @@ export declare const assertCommerceBridgeFinishTransactionPayload: (input: unkno
314
321
  export declare const assertCommerceBridgeFinishTransactionResult: (input: unknown) => FinishTransactionResult;
315
322
  export declare const assertCommerceBridgeRestorePurchasesPayload: (input: unknown) => CommerceBridgeRestorePurchasesPayload;
316
323
  export declare const assertCommerceBridgeRestorePurchasesResult: (input: unknown) => CommerceBridgeRestorePurchasesResult;
317
- export interface ToCommerceAdapterOptions {
318
- /** Forwarded to `getAvailablePurchases` during `restore`. Defaults to false. */
319
- readonly includePending?: boolean;
320
- }
321
- /**
322
- * Serves v1 `CommerceAdapter` callers from a v2 provider. The wrapper never
323
- * grants: a `purchased` outcome becomes a `completed` result with no
324
- * entitlements, and `authoritativeGrant` appears only when the provider reports
325
- * an adapter settlement that already points at a ledger entry. `restore`
326
- * returns settled purchases from `getAvailablePurchases` and no entitlements;
327
- * entitlements come from the ledger, not from the provider.
328
- */
329
- export declare function toCommerceAdapter(provider: CommerceProvider, options?: ToCommerceAdapterOptions): CommerceAdapter;