@helix3/helix-cli 0.1.18-helix3.339 → 0.1.18-helix3.341

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.
package/README.md CHANGED
@@ -49,7 +49,8 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
49
49
  | `helix item publish <mesh.glb> --title <t> [--collectible-supply <n>] [initial distribution flags] [--dry-run\|--quote]` | Publish a Standard item by default, or one fixed-supply Collectible. The item definition is separate from acquisition routes. |
50
50
  | `helix item distribution create\|list\|update\|disable …` | Manage Marketplace/World Claim or Buy routes without republishing the item or paying another publish fee. |
51
51
  | `helix item visual-qa <item-id> [--submit] [--local-critic] [--storage-state <file>] [--host <id…>]` | Render the item's current package version headless in the real runtime (test-grounds world for vehicles and whole avatars, the customiser for add-ons and wearables on every declared host/base, the item viewer for props), measure it, and with `--submit` record the backend's verdict (numeric asserts + vision critic) against that exact version. **Marketplace/Vault listing, distributions and version moves on a public item require a pass.** See [docs/visual-qa.md](docs/visual-qa.md). |
52
- | `helix item visual-qa <item-id> --package-version <v> [--vehicle-package <file>]` | Render a version the item is **not pinned to yet**, so a refused version move can pass the gate before the pointer moves. Vehicles and avatars go through Try Now (vehicles also need the move's vehicle package), add-ons and wearables through the customiser; the bundle names the candidate, and the vehicle checksum is the one the server minted for that trial. Never falls back to the current version. See [docs/visual-qa-candidate-versions.md](docs/visual-qa-candidate-versions.md). |
52
+ | `helix item visual-qa <item-id> --package-version <v> [--vehicle-package <file>]` | Render a version the item is **not pinned to yet**, so a refused version move can pass the gate before the pointer moves. Vehicles and avatars go through Try Now (vehicles also need the move's vehicle package), add-ons and wearables through the customiser; the bundle names the candidate, and the vehicle checksum is the one the server minted for that trial. Never falls back to the current version. See [docs/visual-qa-candidate-versions.md](docs/visual-qa-candidate-versions.md). With `--submit`, a PASS prints the next command (`helix item set-version …`); a 403 for a non-creator says so on the first line; `--status` also lists the item's pending (sealed, not current) versions. |
53
+ | `helix item set-version <item-id> --package-version <v> --reason <text> [--vehicle-package <file>] [--expected-current-version-id <uuid>] [--json]` | Move an item to a sealed Package Version of its own package (pin resolved from the public Package API; `POST /api/v1/universal-items/<id>/versions`). The flow for a **listed** item: `item publish-version` seals vN and the move is refused → `item visual-qa <id> --package-version vN --submit` → `item set-version`. A refusal prints the server's message first (it names the next command), then the remedy and any hosted render it queued. An unknown version lists the sealed ones. |
53
54
  | `helix device create <dir> --id <slug> --title <t> --model <mesh.glb> --template standing-desk\|clock\|television` | Scaffold a GLB-backed `helix.device/1` definition, including the interactive TV/media-player template. There is intentionally no website Device editor. |
54
55
  | `helix device validate [dir] [--json]` | Validate the definition, GLB bytes and named-node bindings, typed state/controls, capabilities, and Home+World compatibility, including [declarative character hand reach](docs/device-hand-reach.md). |
55
56
  | `helix device test [dir] [--mode single\|multiplayer\|both] [--json]` | Exercise every interaction, persistence remount, and deterministic one-/two-client authority convergence. |
package/dist/device.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const DEVICE_INTERACTION_VERB_LIST: string;
23
23
  * fail `helix device validate` while the client loaded it happily. Add
24
24
  * capabilities to both lists, never to one.
25
25
  */
26
- export declare const DEVICE_CAPABILITIES: readonly ["model.glb", "presentation.nodeMotion", "clock.serverTime", "state.typed", "state.durable", "room.authority", "audio.playback", "presentation.light", "presentation.vfx", "character.posture", "character.animate", "character.hand", "signals.local", "screen.render", "screen.input", "app.web.local", "app.web.remote", "app.image.remote", "app.image.owner", "app.canvas", "device.ports", "physics.solid"];
26
+ export declare const DEVICE_CAPABILITIES: readonly ["model.glb", "presentation.nodeMotion", "clock.serverTime", "state.typed", "state.durable", "room.authority", "audio.playback", "presentation.light", "presentation.vfx", "character.posture", "character.animate", "character.hand", "signals.local", "screen.render", "screen.input", "app.web.local", "app.web.remote", "app.web.panel", "app.image.remote", "app.image.owner", "app.canvas", "device.ports", "physics.solid"];
27
27
  /**
28
28
  * The closed component vocabulary.
29
29
  *
package/dist/device.js CHANGED
@@ -75,6 +75,7 @@ exports.DEVICE_CAPABILITIES = [
75
75
  'screen.input',
76
76
  'app.web.local',
77
77
  'app.web.remote',
78
+ 'app.web.panel',
78
79
  'app.image.remote',
79
80
  'app.image.owner',
80
81
  'app.canvas',
@@ -1232,6 +1233,80 @@ function exerciseValue(interaction, state) {
1232
1233
  }
1233
1234
  return undefined;
1234
1235
  }
1236
+ /** Viewer-local declarations never become room actions or state writes. */
1237
+ function deviceWebPanelInteractionError(definition, interaction) {
1238
+ const plainRecord = (value) => isRecord(value) ? value : null;
1239
+ if (interaction.presentation === undefined)
1240
+ return null;
1241
+ const presentation = plainRecord(interaction.presentation);
1242
+ if (!presentation ||
1243
+ presentation.kind !== 'webPanel' ||
1244
+ Object.keys(presentation).some((key) => key !== 'kind' && key !== 'app') ||
1245
+ typeof presentation.app !== 'string' ||
1246
+ !presentation.app.trim() ||
1247
+ presentation.app.length > 64)
1248
+ return 'presentation must be {kind:webPanel,app:<declared app id>}';
1249
+ if (interaction.action !== undefined || interaction.control !== undefined)
1250
+ return 'webPanel presentation cannot include action or control';
1251
+ if (interaction.surface === 'internal')
1252
+ return 'webPanel presentation cannot use an internal interaction';
1253
+ const capabilities = definition.capabilities;
1254
+ if (!Array.isArray(capabilities) || !capabilities.includes('app.web.panel'))
1255
+ return 'webPanel presentation requires capability app.web.panel';
1256
+ const apps = plainRecord(definition.apps);
1257
+ const app = apps && Object.prototype.hasOwnProperty.call(apps, presentation.app)
1258
+ ? plainRecord(apps[presentation.app])
1259
+ : null;
1260
+ if (!app || (app.type !== 'remoteWeb' && app.type !== 'localWeb'))
1261
+ return 'webPanel presentation must target a declared remoteWeb or localWeb app';
1262
+ if (!capabilities.includes(app.type === 'remoteWeb' ? 'app.web.remote' : 'app.web.local'))
1263
+ return `webPanel app requires capability ${app.type === 'remoteWeb' ? 'app.web.remote' : 'app.web.local'}`;
1264
+ if (typeof app.entry !== 'string' ||
1265
+ !app.entry.trim() ||
1266
+ app.entry.length > 2048)
1267
+ return 'webPanel app entry must be a non-empty string of at most 2048 characters';
1268
+ if (app.type === 'localWeb') {
1269
+ if (app.allowedOrigins !== undefined)
1270
+ return 'webPanel localWeb app cannot declare allowedOrigins';
1271
+ try {
1272
+ const url = new URL(app.entry, 'https://helix.invalid');
1273
+ if (!app.entry.startsWith('/device-apps/') ||
1274
+ app.entry.includes('\\') ||
1275
+ app.entry.split(/[/?#]/).includes('..') ||
1276
+ url.origin !== 'https://helix.invalid' ||
1277
+ !/^\/device-apps\/[A-Za-z0-9][A-Za-z0-9/_.-]*$/.test(url.pathname) ||
1278
+ decodeURIComponent(url.pathname) !== url.pathname)
1279
+ return 'webPanel localWeb entry must use a safe /device-apps/ path';
1280
+ }
1281
+ catch {
1282
+ return 'webPanel localWeb entry must use a safe /device-apps/ path';
1283
+ }
1284
+ }
1285
+ else {
1286
+ try {
1287
+ const url = new URL(app.entry);
1288
+ const origins = app.allowedOrigins;
1289
+ if (url.protocol !== 'https:' ||
1290
+ url.username ||
1291
+ url.password ||
1292
+ !Array.isArray(origins) ||
1293
+ origins.length < 1 ||
1294
+ origins.length > 16 ||
1295
+ !origins.every((origin) => {
1296
+ if (typeof origin !== 'string')
1297
+ return false;
1298
+ const allowed = new URL(origin);
1299
+ return allowed.protocol === 'https:' && allowed.origin === origin;
1300
+ }) ||
1301
+ !origins.includes(url.origin))
1302
+ return 'webPanel remoteWeb entry must use HTTPS and a declared allowedOrigins origin';
1303
+ }
1304
+ catch {
1305
+ return 'webPanel remoteWeb entry must use HTTPS and a declared allowedOrigins origin';
1306
+ }
1307
+ }
1308
+ return null;
1309
+ }
1235
1310
  function validateDeviceProject(input) {
1236
1311
  const target = readDefinitionPath(input);
1237
1312
  const errors = [];
@@ -1444,6 +1519,9 @@ function validateDeviceProject(input) {
1444
1519
  errors.push(`interactions: ${interactions.length} exceeds budget ${DEVICE_BUDGETS.maxInteractions}`);
1445
1520
  const interactionIds = new Set();
1446
1521
  for (const interaction of interactions) {
1522
+ const presentationError = deviceWebPanelInteractionError(definition, interaction);
1523
+ if (presentationError)
1524
+ errors.push(`interaction ${String(interaction.id)}: ${presentationError}`);
1447
1525
  const id = typeof interaction.id === 'string' ? interaction.id : '';
1448
1526
  if (!id)
1449
1527
  errors.push('interaction id must be a non-empty string');
@@ -2177,6 +2255,8 @@ function validateDeviceProject(input) {
2177
2255
  return;
2178
2256
  }
2179
2257
  if (on === 'interact') {
2258
+ if (interactionsById.get(String(trigger.interaction))?.presentation !== undefined)
2259
+ errors.push(`${triggerPath}: webPanel presentation cannot be a graph interaction trigger`);
2180
2260
  if (!interactionIds.has(String(trigger.interaction))) {
2181
2261
  errors.push(`graph references undeclared interaction "${String(trigger.interaction)}"`);
2182
2262
  }
@@ -2308,6 +2388,10 @@ function validateDeviceProject(input) {
2308
2388
  'app.web.remote',
2309
2389
  Object.values(apps).some((raw) => ['remoteWeb', 'youtube'].includes(String(object(raw).type))),
2310
2390
  ],
2391
+ [
2392
+ 'app.web.panel',
2393
+ interactions.some((interaction) => interaction.presentation !== undefined),
2394
+ ],
2311
2395
  ['device.ports', ports.length > 0],
2312
2396
  ];
2313
2397
  for (const [capability, used] of requiresByContent) {