@mpgd/cli 0.47.0 → 0.48.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.
Files changed (49) hide show
  1. package/dist/command-launcher.d.ts +13 -0
  2. package/dist/command-launcher.js +24 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +126 -7
  5. package/dist/microsoft-store-submission.js +11 -6
  6. package/dist/native-build-info.json +2 -2
  7. package/dist/native-build-target.js +2121 -350
  8. package/dist/windows-host/Core/BridgeCommand.cs +17 -0
  9. package/dist/windows-host/Core/Mpgd.MicrosoftStore.Host.csproj +9 -0
  10. package/dist/windows-host/Core/NativeBridgeContracts.cs +33 -0
  11. package/dist/windows-host/Core/NativeBridgeHost.cs +162 -0
  12. package/dist/windows-host/Core/NativeBridgeOrigin.cs +19 -0
  13. package/dist/windows-host/Core/NativeBridgeProtocol.cs +335 -0
  14. package/dist/windows-host/Core/NativeBridgeRequestGate.cs +76 -0
  15. package/dist/windows-host/Core/NativeBridgeResult.cs +65 -0
  16. package/dist/windows-host/README.md +118 -0
  17. package/dist/windows-host/WinUI/DispatcherQueueBridgeDispatcher.cs +39 -0
  18. package/dist/windows-host/WinUI/WebView2DocumentTransport.cs +46 -0
  19. package/dist/windows-host/WinUI/WebView2NativeBridge.cs +185 -0
  20. package/dist/windows-host/WinUI/WindowStoreContext.cs +20 -0
  21. package/dist/windows-native-tools.d.ts +4 -0
  22. package/dist/windows-native-tools.js +21 -0
  23. package/native-tools/windows-msix/WindowsMsixAcceptance.psm1 +333 -0
  24. package/package.json +24 -16
  25. package/templates/lite-overlay/.agents/skills/use-mpgd-kit/SKILL.md +22 -0
  26. package/templates/lite-overlay/AGENTS.md +27 -0
  27. package/templates/lite-overlay/README.md +37 -0
  28. package/templates/lite-overlay/agent/acceptance.md +27 -0
  29. package/templates/lite-overlay/agent/brief.md +17 -0
  30. package/templates/lite-overlay/apps/target-devvit/README.md +134 -0
  31. package/templates/lite-overlay/docs/MPGD_KIT_WORKFLOWS.md +37 -0
  32. package/templates/lite-overlay/src/lite/tapGame.ts +77 -0
  33. package/templates/lite-overlay/src/platform/devvitEntrypoint.ts +116 -0
  34. package/templates/lite-overlay/src/platform/devvitInlineMode.css +146 -0
  35. package/templates/lite-overlay/src/runtime/createGame.ts +57 -0
  36. package/templates/phaser-game/README.md +5 -3
  37. package/templates/phaser-game/apps/target-ait/package.json +2 -2
  38. package/templates/phaser-game/apps/target-ait/src/main.ts +5 -5
  39. package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +137 -18
  40. package/templates/phaser-game/package.json +1 -1
  41. package/templates/phaser-game/pnpm-workspace.yaml +2 -0
  42. package/templates/phaser-game/src/env.d.ts +2 -0
  43. package/templates/phaser-game/src/platform/buildGateways/ait.ts +1 -1
  44. package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +9 -4
  45. package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +2 -5
  46. package/templates/phaser-game/src/platform/buildGateways/reddit.ts +1 -1
  47. package/templates/phaser-game/src/platform/buildGateways/verse8.ts +1 -2
  48. package/templates/phaser-game/src/platform/verse8Agent8Client.ts +5 -6
  49. package/templates/phaser-game/vite.shared.ts +40 -1
@@ -1,7 +1,10 @@
1
1
  import {
2
+ bridgeEventProtocol,
2
3
  bridgeStorageLoadProtocol,
3
4
  createBridgeError,
4
5
  createCommerceV2UnavailableBridgeError,
6
+ createNotificationsV2UnavailableBridgeError,
7
+ isBridgeNotificationsV2Method,
5
8
  type BridgeRequest,
6
9
  type BridgeResponse,
7
10
  type BridgeStorageLoadData,
@@ -90,10 +93,15 @@ export function createDevvitBridgeHandler(
90
93
  }
91
94
 
92
95
  return async function handleBridgeRequest(input: BridgeRequest): Promise<BridgeResponse> {
96
+ // Notifications v2 arrives with the Devvit provider (P3), which serves the
97
+ // game-rendered opt-in from this bridge; until then every v2 method
98
+ // answers the dedicated code, and the v1 `notifications.*` answers stay.
99
+ if (isBridgeNotificationsV2Method(input.method)) {
100
+ return createNotificationsV2UnavailableBridgeError(input, 'Devvit server bridge');
101
+ }
93
102
  switch (input.method) {
94
103
  case 'runtime.getCapabilities':
95
104
  return ok(input, {
96
- nativeIap: false,
97
105
  nativeAds: false,
98
106
  rewardedAds: false,
99
107
  interstitialAds: false,
@@ -104,6 +112,7 @@ export function createDevvitBridgeHandler(
104
112
  socialShare: false,
105
113
  haptics: false,
106
114
  localizedContent: true,
115
+ commerce: await readCommerceCapabilities(input, dependencies.commerce),
107
116
  });
108
117
 
109
118
  case 'identity.getPlayer': {
@@ -164,21 +173,6 @@ export function createDevvitBridgeHandler(
164
173
  case 'notifications.requestSubscription':
165
174
  return ok(input, 'unavailable');
166
175
 
167
- case 'commerce.getProducts':
168
- case 'commerce.getEntitlements':
169
- return ok(input, []);
170
-
171
- case 'commerce.purchase':
172
- return ok(input, {
173
- status: 'cancelled',
174
- entitlementIds: [],
175
- });
176
-
177
- case 'commerce.restore':
178
- return ok(input, {
179
- restoredEntitlements: [],
180
- });
181
-
182
176
  case 'ads.preload':
183
177
  return ok(input, {});
184
178
 
@@ -211,8 +205,9 @@ export function createDevvitBridgeHandler(
211
205
  case 'storage.save':
212
206
  return saveStorage(input, dependencies, storageKeyNamespace, warn);
213
207
 
214
- // Commerce v2 (B6): served by the configured handler over the payments
215
- // plugin; the checkout itself opens in the webview.
208
+ // Commerce v2: served by the configured handler over the payments
209
+ // plugin; the checkout itself opens in the webview. The removed v1
210
+ // `commerce.*` methods fall to UNSUPPORTED_METHOD below.
216
211
  case 'commerce.v2.getCapabilities':
217
212
  case 'commerce.v2.fetchProducts':
218
213
  case 'commerce.v2.requestPurchase':
@@ -233,6 +228,130 @@ export function createDevvitBridgeHandler(
233
228
  };
234
229
  }
235
230
 
231
+ /**
232
+ * The all-false commerce capability block of a server that sells nothing
233
+ * (`createUnsupportedCommerceCapabilities()` of `@mpgd/platform`, kept equal by
234
+ * the bridge test). A fresh object on every read.
235
+ */
236
+ function createUnsupportedCommerceBlock() {
237
+ return {
238
+ purchase: { consumable: false, durable: false, subscription: false, quantity: false },
239
+ catalog: { products: false, offers: false, localizedPrice: false },
240
+ lifecycle: {
241
+ availablePurchases: false,
242
+ pending: false,
243
+ restore: false,
244
+ events: false,
245
+ finish: 'none',
246
+ },
247
+ authority: { evidence: 'client-token', accountBinding: 'optional' },
248
+ availability: 'unsupported',
249
+ };
250
+ }
251
+
252
+ const commerceBlockShape = {
253
+ purchase: {
254
+ consumable: 'boolean',
255
+ durable: 'boolean',
256
+ subscription: 'boolean',
257
+ quantity: 'boolean',
258
+ },
259
+ catalog: { products: 'boolean', offers: 'boolean', localizedPrice: 'boolean' },
260
+ lifecycle: {
261
+ availablePurchases: 'boolean',
262
+ pending: 'boolean',
263
+ restore: 'boolean',
264
+ events: 'boolean',
265
+ finish: ['client', 'server', 'host', 'none'],
266
+ },
267
+ authority: {
268
+ evidence: ['client-token', 'host-callback', 'adapter-settled', 'server-push'],
269
+ accountBinding: ['required', 'optional', 'host'],
270
+ },
271
+ } as const;
272
+ const commerceAvailabilities: readonly unknown[] = [
273
+ 'available',
274
+ 'unsupported',
275
+ 'configuration-required',
276
+ 'action-required',
277
+ 'temporarily-unavailable',
278
+ ];
279
+
280
+ function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>> {
281
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
282
+ }
283
+
284
+ function hasExactKeys(value: Readonly<Record<string, unknown>>, keys: readonly string[]): boolean {
285
+ const own = Object.keys(value);
286
+ return own.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
287
+ }
288
+
289
+ /**
290
+ * A fresh copy of a well-formed commerce capability block (the exact keys and
291
+ * values `PlatformCapabilities.commerce` allows), or undefined for anything
292
+ * else, so a malformed handler answer never reaches the snapshot.
293
+ */
294
+ function copyCommerceBlock(value: unknown): Record<string, unknown> | undefined {
295
+ if (!isPlainRecord(value)
296
+ || !hasExactKeys(value, [...Object.keys(commerceBlockShape), 'availability'])
297
+ || !commerceAvailabilities.includes(value.availability)) {
298
+ return undefined;
299
+ }
300
+ const copy: Record<string, unknown> = { availability: value.availability };
301
+ for (const [section, fields] of Object.entries(commerceBlockShape)) {
302
+ const source = value[section];
303
+ if (!isPlainRecord(source) || !hasExactKeys(source, Object.keys(fields))) {
304
+ return undefined;
305
+ }
306
+ const sectionCopy: Record<string, unknown> = {};
307
+ for (const [field, rule] of Object.entries(fields)) {
308
+ const fieldValue = source[field];
309
+ const valid = rule === 'boolean'
310
+ ? typeof fieldValue === 'boolean'
311
+ : (rule as readonly unknown[]).includes(fieldValue);
312
+ if (!valid) {
313
+ return undefined;
314
+ }
315
+ sectionCopy[field] = fieldValue;
316
+ }
317
+ copy[section] = sectionCopy;
318
+ }
319
+ return copy;
320
+ }
321
+
322
+ /**
323
+ * The `commerce` block of the server snapshot: what the configured commerce
324
+ * handler reports for this server, otherwise the unsupported block. It fails
325
+ * closed: a missing handler, a handler that throws or answers an error, or an
326
+ * answer without a well-formed capability block sells nothing. The webview
327
+ * gateway owns the block it finally reports (its provider's, or the
328
+ * unsupported one).
329
+ */
330
+ async function readCommerceCapabilities(
331
+ input: BridgeRequest,
332
+ handler: DevvitBridgeCommerceHandler | undefined,
333
+ ): Promise<unknown> {
334
+ if (handler === undefined) {
335
+ return createUnsupportedCommerceBlock();
336
+ }
337
+
338
+ try {
339
+ const response = await handler.request({
340
+ ...input,
341
+ method: 'commerce.v2.getCapabilities',
342
+ payload: { protocol: bridgeEventProtocol },
343
+ });
344
+ const data: unknown = response.ok ? response.data : undefined;
345
+ const capabilities = typeof data === 'object' && data !== null
346
+ ? (data as { readonly capabilities?: unknown }).capabilities
347
+ : undefined;
348
+
349
+ return copyCommerceBlock(capabilities) ?? createUnsupportedCommerceBlock();
350
+ } catch {
351
+ return createUnsupportedCommerceBlock();
352
+ }
353
+ }
354
+
236
355
  async function loadStorage(
237
356
  input: BridgeRequest,
238
357
  dependencies: DevvitBridgeHandlerDependencies,
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "@ait-co/polyfill": "^0.1.19",
59
- "@apps-in-toss/web-framework": "3.5.0",
59
+ "@apps-in-toss/web-framework": "3.6.0",
60
60
  "@capacitor/app": "8.1.1",
61
61
  "@capacitor/core": "8.5.2",
62
62
  "@mpgd/adapter-ait": "__MPGD_DEPENDENCY_VERSION_ADAPTER_AIT__",
@@ -1,3 +1,5 @@
1
+ # Run the generated POSIX target scripts consistently on Windows and Unix.
2
+ shellEmulator: true
1
3
  packages:
2
4
  - '.'
3
5
  - 'apps/*'
@@ -2,6 +2,8 @@
2
2
 
3
3
  declare const __APP_TARGET__: string;
4
4
  declare const __MPGD_CONFIG_TARGET__: string;
5
+ /** The Apps in Toss app name `intoss://` share links carry; see `vite.shared.ts`. */
6
+ declare const __MPGD_AIT_APP_NAME__: string;
5
7
  declare const __MPGD_PLATFORM_TARGET__:
6
8
  | import('@mpgd/target-config').EffectivePlatformTargetMetadata
7
9
  | undefined;
@@ -8,7 +8,7 @@ import type { RuntimeConfig } from '../runtimeDetector';
8
8
  * host side: the wrapper (`apps/target-ait`) installs the IAP products from
9
9
  * the catalog's `platformProductIds.ait` entries and serves the six
10
10
  * `commerce.v2.*` methods plus the `commerceV2` event channel, so the gateway
11
- * exposes `commerceV2` whenever the installed bridge has that channel. The
11
+ * exposes `commerce` whenever the installed bridge has that channel. The
12
12
  * host's grant callback only confirms through the game backend; nothing is
13
13
  * granted from this side.
14
14
  */
@@ -5,15 +5,20 @@ import type { RuntimeConfig } from '../runtimeDetector';
5
5
 
6
6
  /**
7
7
  * Local Apps in Toss playtest. The sandbox bridge keeps its build-id gate: only
8
- * the `ait-sandbox` build (`pnpm dev:ait`) gets the `commerceV2` channel and
8
+ * the `ait-sandbox` build (`pnpm dev:ait`) gets the commerce event channel and
9
9
  * the evidence-only v2 stub; any other build id gets no channel, so the
10
- * gateway offers no `commerceV2` and the v2 methods answer
11
- * `COMMERCE_V2_UNAVAILABLE`.
10
+ * gateway offers no `commerce` provider and the v2 methods answer
11
+ * `COMMERCE_V2_UNAVAILABLE`. Every build answers the `notifications.v2.*`
12
+ * methods with `NOTIFICATIONS_V2_UNAVAILABLE` until the Apps in Toss
13
+ * notifications provider lands.
12
14
  */
13
15
  export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
14
16
  return createAitPlatformGateway({
15
17
  appVersion: runtime.appVersion,
16
18
  buildId: runtime.buildId,
17
- fallbackBridge: createAitSandboxBridge({ buildId: runtime.buildId }),
19
+ fallbackBridge: createAitSandboxBridge({
20
+ appName: __MPGD_AIT_APP_NAME__,
21
+ buildId: runtime.buildId,
22
+ }),
18
23
  });
19
24
  }
@@ -41,8 +41,8 @@ function resolveMicrosoftStoreProducts(target: string): readonly MicrosoftStoreC
41
41
  }
42
42
 
43
43
  /**
44
- * Commerce v2 over the Digital Goods path (`createMicrosoftStoreCommerceProvider`)
45
- * installed as `commerceV2` with the v1 wrapper. The purchase authority stays
44
+ * Commerce over the Digital Goods path (`createMicrosoftStoreCommerceProvider`)
45
+ * installed as `gateway.commerce` with its capability block. The purchase authority stays
46
46
  * `configuration-required` until the game points it at its backend: the
47
47
  * authority's ledger entry is the only settlement, and the reconciler still
48
48
  * verifies it through game-services before anything is granted.
@@ -64,9 +64,6 @@ export async function createBuildGateway(runtime: RuntimeConfig): Promise<Platfo
64
64
  async verifyAndGrant() {
65
65
  return { status: 'failed' };
66
66
  },
67
- async getEntitlements() {
68
- return [];
69
- },
70
67
  },
71
68
  });
72
69
  return withMicrosoftStoreCommerceProvider(base, commerce);
@@ -20,7 +20,7 @@ const productCatalog = productCatalogJson as ProductCatalog;
20
20
  * which serves it only after the Devvit server opted in
21
21
  * (`permissions.payments` plus the same catalog SKUs), and the grant comes
22
22
  * from the fulfillment handler's ledger entry. Without SKUs the gateway
23
- * offers no `commerceV2` at all.
23
+ * offers no `commerce` provider at all.
24
24
  */
25
25
  export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
26
26
  const commerce = createCommerceOptions();
@@ -18,7 +18,7 @@ import { resolveStarterVerse8Agent8Client } from '../verse8Agent8Client';
18
18
  const productCatalog = productCatalogJson as ProductCatalog;
19
19
 
20
20
  /**
21
- * Verse8 gateway. VXShop and the commerce v2 provider are installed only when
21
+ * Verse8 gateway. VXShop and the commerce provider are installed only when
22
22
  * the game provides its authenticated Agent8 client (`verse8Agent8Client.ts`)
23
23
  * and the catalog names VXShop product ids (`platformProductIds.verse8`):
24
24
  * `vxShop.purchases` reads the purchases the Agent8 server recorded, which is
@@ -58,7 +58,6 @@ function createVxShopOptions(runtime: RuntimeConfig): Verse8CommerceOptions | un
58
58
  return {
59
59
  purchaseEventAuthority: 'agent8-server',
60
60
  products,
61
- loadEntitlements: async () => (await agent8.getCommerceSnapshot()).entitlements,
62
61
  purchases: {
63
62
  listPurchases: () => agent8.listPurchases(),
64
63
  },
@@ -1,23 +1,22 @@
1
1
  import type { Verse8RecordedPurchase } from '@mpgd/adapter-verse8';
2
- import type { Entitlement } from '@mpgd/platform';
3
2
 
4
3
  /**
5
4
  * The game-owned Agent8 remote client the Verse8 gateway reads commerce state
6
- * through: two authenticated server methods exposed for the sender's own
5
+ * through: one authenticated server method exposed for the sender's own
7
6
  * account (see docs/VERSE8_COMMERCE.md, "Iframe client boundary"). The
8
7
  * starter installs no Agent8 browser SDK, so `resolveStarterVerse8Agent8Client`
9
8
  * answers `undefined` until the game provides one; VXShop then stays off and
10
- * the gateway offers no `commerceV2`, exactly as before. The client only
9
+ * the gateway offers no `commerce` provider and sells nothing. The client only
11
10
  * reads: `$onItemPurchased` on the Agent8 server is the sole grant path.
11
+ * Entitlements are not read from the client: they come from the backend
12
+ * ledger through game-services.
12
13
  */
13
14
  export interface StarterVerse8Agent8Client {
14
- /** Server-owned balances and entitlements (`getSnapshot` of the Agent8 commerce service). */
15
- getCommerceSnapshot(): Promise<{ readonly entitlements: readonly Entitlement[] }>;
16
15
  /** The purchases `$onItemPurchased` recorded (`listPurchases` of the Agent8 commerce service). */
17
16
  listPurchases(): Promise<readonly Verse8RecordedPurchase[]>;
18
17
  }
19
18
 
20
- /** Return the game's authenticated Agent8 client here to enable VXShop and commerce v2 on Verse8. */
19
+ /** Return the game's authenticated Agent8 client here to enable VXShop and commerce on Verse8. */
21
20
  export function resolveStarterVerse8Agent8Client(): StarterVerse8Agent8Client | undefined {
22
21
  return undefined;
23
22
  }
@@ -17,6 +17,8 @@ interface RuntimePlatformTargetMetadata {
17
17
  readonly crazyGamesLaunch?: 'basic' | 'full';
18
18
  readonly authoritativeGameServices?: boolean;
19
19
  readonly integrations?: Record<string, unknown>;
20
+ /** An Apps in Toss target's `metadata.appName`, for the sandbox bridge only. */
21
+ readonly aitAppName?: string;
20
22
  }
21
23
 
22
24
  // Sandbox gateways self-complete purchases and rewards without any backend
@@ -100,7 +102,10 @@ export function createGameViteSharedConfig<SharedPluginOption = PluginOption>(
100
102
  __APP_TARGET__: JSON.stringify(appTarget),
101
103
  __MPGD_CONFIG_TARGET__: JSON.stringify(configTarget),
102
104
  __MPGD_PLATFORM_TARGET__:
103
- platformTarget === undefined ? 'undefined' : JSON.stringify(platformTarget),
105
+ platformTarget === undefined
106
+ ? 'undefined'
107
+ : JSON.stringify(toEffectivePlatformTargetMetadata(platformTarget)),
108
+ __MPGD_AIT_APP_NAME__: JSON.stringify(resolveAitAppName(platformTarget)),
104
109
  __MPGD_TARGET_CONFIG_MATRIX__:
105
110
  runtimeTargetConfigMatrix === undefined
106
111
  ? 'undefined'
@@ -264,6 +269,8 @@ function readRuntimePlatformTarget(
264
269
  throw new Error(`Platform target ${configTarget} authoritativeGameServices must be a boolean.`);
265
270
  }
266
271
 
272
+ const aitAppName = readAitAppName(target);
273
+
267
274
  return {
268
275
  kind: target.kind,
269
276
  adapter: target.adapter,
@@ -272,9 +279,41 @@ function readRuntimePlatformTarget(
272
279
  : { authoritativeGameServices: target.authoritativeGameServices }),
273
280
  ...(target.crazyGamesLaunch === undefined ? {} : { crazyGamesLaunch: target.crazyGamesLaunch as 'basic' | 'full' }),
274
281
  ...(target.integrations === undefined ? {} : { integrations: target.integrations }),
282
+ ...(aitAppName === undefined ? {} : { aitAppName }),
283
+ };
284
+ }
285
+
286
+ function toEffectivePlatformTargetMetadata(
287
+ input: RuntimePlatformTargetMetadata,
288
+ ): Omit<RuntimePlatformTargetMetadata, 'aitAppName'> {
289
+ return {
290
+ kind: input.kind,
291
+ adapter: input.adapter,
292
+ ...(input.authoritativeGameServices === undefined
293
+ ? {}
294
+ : { authoritativeGameServices: input.authoritativeGameServices }),
295
+ ...(input.crazyGamesLaunch === undefined ? {} : { crazyGamesLaunch: input.crazyGamesLaunch }),
296
+ ...(input.integrations === undefined ? {} : { integrations: input.integrations }),
275
297
  };
276
298
  }
277
299
 
300
+ /**
301
+ * The app name `intoss://` share links must carry in the Apps in Toss sandbox
302
+ * bridge, the one the wrapper's host bridge uses: `MPGD_AIT_APP_NAME`, else
303
+ * the target's `metadata.appName`, else the game name.
304
+ */
305
+ function resolveAitAppName(platformTarget: RuntimePlatformTargetMetadata | undefined): string {
306
+ return process.env.MPGD_AIT_APP_NAME?.trim() || platformTarget?.aitAppName || '__GAME_NAME__';
307
+ }
308
+
309
+ function readAitAppName(target: Record<string, unknown>): string | undefined {
310
+ const appName = target.kind === 'apps-in-toss' && isRecord(target.metadata)
311
+ ? target.metadata.appName
312
+ : undefined;
313
+
314
+ return typeof appName === 'string' && appName.trim().length > 0 ? appName.trim() : undefined;
315
+ }
316
+
278
317
  function isRecord(input: unknown): input is Record<string, unknown> {
279
318
  return typeof input === 'object' && input !== null && !Array.isArray(input);
280
319
  }