@peers-app/peers-sdk 0.26.6 → 0.27.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 (182) hide show
  1. package/README.md +3 -1
  2. package/bin/peers-package.js +13 -0
  3. package/define/package.json +4 -0
  4. package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
  5. package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/also-implements.test.js +110 -0
  7. package/dist/contracts/__tests__/builder.test.js +45 -0
  8. package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
  9. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
  10. package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
  11. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
  12. package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
  13. package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
  14. package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
  15. package/dist/contracts/__tests__/registry.test.js +68 -0
  16. package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
  17. package/dist/contracts/__tests__/target-dispatch.js +24 -0
  18. package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
  19. package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
  20. package/dist/contracts/__tests__/validate.test.js +177 -7
  21. package/dist/contracts/__tests__/workflow-context.test.js +0 -11
  22. package/dist/contracts/builder.d.ts +16 -2
  23. package/dist/contracts/builder.js +44 -3
  24. package/dist/contracts/contract-definition-schema.d.ts +251 -0
  25. package/dist/contracts/contract-definition-schema.js +48 -0
  26. package/dist/contracts/contract-provider-router.d.ts +2 -0
  27. package/dist/contracts/contract-provider-router.js +5 -2
  28. package/dist/contracts/contract-proxy.d.ts +52 -18
  29. package/dist/contracts/contract-proxy.js +144 -203
  30. package/dist/contracts/contract-resolvers.d.ts +6 -4
  31. package/dist/contracts/contract-resolvers.js +32 -9
  32. package/dist/contracts/frozen-contracts.d.ts +59 -0
  33. package/dist/contracts/frozen-contracts.js +86 -0
  34. package/dist/contracts/host-contract-client.d.ts +134 -0
  35. package/dist/contracts/host-contract-client.js +396 -0
  36. package/dist/contracts/host-contract-transport.d.ts +2 -0
  37. package/dist/contracts/host-contract-transport.js +8 -0
  38. package/dist/contracts/index.d.ts +6 -4
  39. package/dist/contracts/index.js +11 -5
  40. package/dist/contracts/registry.d.ts +31 -1
  41. package/dist/contracts/registry.js +100 -6
  42. package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
  43. package/dist/contracts/system/contract-catalog-contract.js +17 -5
  44. package/dist/contracts/tool-catalog-service.js +26 -6
  45. package/dist/contracts/tool-catalog.d.ts +1 -1
  46. package/dist/contracts/tool-catalog.js +4 -1
  47. package/dist/contracts/types.d.ts +32 -2
  48. package/dist/contracts/validate.d.ts +16 -1
  49. package/dist/contracts/validate.js +139 -25
  50. package/dist/data/change-tracking.d.ts +17 -5
  51. package/dist/data/change-tracking.js +22 -6
  52. package/dist/data/change-tracking.test.d.ts +1 -0
  53. package/dist/data/change-tracking.test.js +62 -0
  54. package/dist/data/orm/sql.data-source.d.ts +9 -0
  55. package/dist/data/orm/sql.data-source.js +47 -2
  56. package/dist/data/orm/sql.data-source.test.js +142 -0
  57. package/dist/data/orm/table-container.js +9 -9
  58. package/dist/data/orm/table-container.test.js +44 -0
  59. package/dist/data/orm/types.d.ts +41 -1
  60. package/dist/data/orm/types.js +159 -28
  61. package/dist/data/orm/types.test.js +129 -0
  62. package/dist/data/peer-types.d.ts +8 -62
  63. package/dist/data/persistent-vars.d.ts +4 -1
  64. package/dist/data/persistent-vars.js +7 -2
  65. package/dist/data/tools.d.ts +29 -221
  66. package/dist/define/also-implements.test.d.ts +1 -0
  67. package/dist/define/also-implements.test.js +123 -0
  68. package/dist/define/context.d.ts +77 -0
  69. package/dist/define/context.js +2 -0
  70. package/dist/define/contract-ref.d.ts +177 -0
  71. package/dist/define/contract-ref.js +67 -0
  72. package/dist/define/declare-contract.d.ts +167 -0
  73. package/dist/define/declare-contract.js +220 -0
  74. package/dist/define/declare-contract.test.d.ts +1 -0
  75. package/dist/define/declare-contract.test.js +573 -0
  76. package/dist/define/define-package.d.ts +203 -0
  77. package/dist/define/define-package.js +289 -0
  78. package/dist/define/define.test.d.ts +1 -0
  79. package/dist/define/define.test.js +1010 -0
  80. package/dist/define/empty-fields.d.ts +15 -0
  81. package/dist/define/empty-fields.js +134 -0
  82. package/dist/define/empty-fields.test.d.ts +1 -0
  83. package/dist/define/empty-fields.test.js +55 -0
  84. package/dist/define/events-observables.test.d.ts +1 -0
  85. package/dist/define/events-observables.test.js +1013 -0
  86. package/dist/define/events.d.ts +81 -0
  87. package/dist/define/events.js +81 -0
  88. package/dist/define/generated-field.d.ts +20 -0
  89. package/dist/define/generated-field.js +25 -0
  90. package/dist/define/index.d.ts +28 -0
  91. package/dist/define/index.js +62 -0
  92. package/dist/define/lazy-members.d.ts +8 -0
  93. package/dist/define/lazy-members.js +38 -0
  94. package/dist/define/manifest.d.ts +63 -0
  95. package/dist/define/manifest.js +653 -0
  96. package/dist/define/observables.d.ts +146 -0
  97. package/dist/define/observables.js +187 -0
  98. package/dist/define/serve-members.d.ts +21 -0
  99. package/dist/define/serve-members.js +152 -0
  100. package/dist/define/serve-members.test.d.ts +1 -0
  101. package/dist/define/serve-members.test.js +630 -0
  102. package/dist/define/serve.d.ts +22 -0
  103. package/dist/define/serve.js +58 -0
  104. package/dist/define/system-contracts.d.ts +41 -0
  105. package/dist/define/system-contracts.js +21 -0
  106. package/dist/define/table-handle.d.ts +13 -0
  107. package/dist/define/table-handle.js +36 -0
  108. package/dist/define/table.d.ts +128 -0
  109. package/dist/define/table.js +122 -0
  110. package/dist/define/tool.d.ts +129 -0
  111. package/dist/define/tool.js +91 -0
  112. package/dist/define/vars.d.ts +82 -0
  113. package/dist/define/vars.js +256 -0
  114. package/dist/define/vars.test.d.ts +1 -0
  115. package/dist/define/vars.test.js +39 -0
  116. package/dist/define/wire-value.d.ts +12 -0
  117. package/dist/define/wire-value.js +76 -0
  118. package/dist/define/worker-state.d.ts +15 -0
  119. package/dist/define/worker-state.js +45 -0
  120. package/dist/device/tx-encoding.test.js +10 -0
  121. package/dist/index.d.ts +3 -0
  122. package/dist/index.js +5 -1
  123. package/dist/observable.d.ts +42 -2
  124. package/dist/observable.js +25 -7
  125. package/dist/package-build/build-package.d.ts +60 -0
  126. package/dist/package-build/build-package.js +312 -0
  127. package/dist/package-build/build-package.test.d.ts +1 -0
  128. package/dist/package-build/build-package.test.js +204 -0
  129. package/dist/package-build/cli.d.ts +7 -0
  130. package/dist/package-build/cli.js +218 -0
  131. package/dist/package-build/contract-add.d.ts +35 -0
  132. package/dist/package-build/contract-add.js +97 -0
  133. package/dist/package-build/contract-add.test.d.ts +1 -0
  134. package/dist/package-build/contract-add.test.js +467 -0
  135. package/dist/package-build/contract-source.d.ts +59 -0
  136. package/dist/package-build/contract-source.js +471 -0
  137. package/dist/package-build/freeze-contracts.d.ts +56 -0
  138. package/dist/package-build/freeze-contracts.js +198 -0
  139. package/dist/package-build/freeze-contracts.test.d.ts +1 -0
  140. package/dist/package-build/freeze-contracts.test.js +355 -0
  141. package/dist/package-installer/package-creator.d.ts +4 -0
  142. package/dist/package-installer/package-creator.js +57 -16
  143. package/dist/package-installer/package-creator.test.js +86 -0
  144. package/dist/package-loader/contract-members.d.ts +53 -0
  145. package/dist/package-loader/contract-members.js +31 -0
  146. package/dist/package-loader/contract-package-loader.js +5 -1
  147. package/dist/package-loader/contract-package-loader.test.js +11 -0
  148. package/dist/package-loader/host-authority-loading.test.js +9 -5
  149. package/dist/package-loader/index.d.ts +1 -0
  150. package/dist/package-loader/index.js +1 -0
  151. package/dist/package-loader/isolated-artifact.d.ts +80 -17
  152. package/dist/package-loader/isolated-artifact.js +257 -51
  153. package/dist/package-loader/isolated-artifact.test.js +294 -21
  154. package/dist/package-loader/isolated-runtime.d.ts +22 -7
  155. package/dist/package-loader/isolated-table-gateway.js +30 -11
  156. package/dist/package-loader/isolated-table-gateway.test.js +243 -0
  157. package/dist/package-loader/package-loader.d.ts +8 -1
  158. package/dist/package-loader/package-loader.js +39 -20
  159. package/dist/package-loader/package-loader.test.js +47 -0
  160. package/dist/peer-id.d.ts +25 -0
  161. package/dist/peer-id.js +99 -0
  162. package/dist/serial-json.compat.test.d.ts +1 -0
  163. package/dist/serial-json.compat.test.js +369 -0
  164. package/dist/serial-json.d.ts +41 -5
  165. package/dist/serial-json.js +216 -157
  166. package/dist/serial-json.oracle.d.ts +21 -0
  167. package/dist/serial-json.oracle.js +222 -0
  168. package/dist/serial-json.test.js +315 -0
  169. package/dist/testing/index.d.ts +4 -0
  170. package/dist/testing/index.js +8 -0
  171. package/dist/testing/mock-host-contract.d.ts +123 -0
  172. package/dist/testing/mock-host-contract.js +434 -0
  173. package/dist/testing/mock-host-contract.test.d.ts +1 -0
  174. package/dist/testing/mock-host-contract.test.js +782 -0
  175. package/dist/types/field-type.d.ts +35 -18
  176. package/dist/types/field-type.js +4 -1
  177. package/dist/types/zod-types.d.ts +9 -0
  178. package/dist/types/zod-types.js +8 -2
  179. package/dist/utils.d.ts +1 -25
  180. package/dist/utils.js +10 -90
  181. package/package.json +9 -2
  182. package/testing/package.json +4 -0
@@ -106,33 +106,27 @@ describe("isolated package artifact", () => {
106
106
  const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
107
107
  expect(manifest.tables).toEqual([]);
108
108
  });
109
- test("validates private action aliases without extending the public contract", () => {
110
- const { artifact, contractId } = makeArtifact();
109
+ test("keeps a private tool out of the public contract definition", () => {
110
+ const { artifact } = makeArtifact();
111
111
  artifact.manifest.provides[0].tools.push({
112
112
  name: "private-write",
113
113
  usageDescription: "Internal write",
114
114
  private: true,
115
115
  suggestedAccessLevel: access_level_1.AccessLevel.Writer,
116
116
  });
117
- artifact.manifest.actionAliases = [
118
- {
119
- name: "write",
120
- target: { contractId, version: 1, toolName: "private-write" },
121
- },
122
- ];
123
117
  const validated = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
124
118
  expect((0, isolated_artifact_1.isolatedContractToDefinition)(validated.provides[0], []).tools.map((tool) => tool.name)).toEqual(["greet"]);
125
- expect((0, isolated_artifact_1.parseIsolatedPackageArtifact)((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)).manifest
126
- .actionAliases).toEqual(artifact.manifest.actionAliases);
127
- artifact.manifest.actionAliases.push(artifact.manifest.actionAliases[0]);
128
- expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow("duplicate action alias");
129
- artifact.manifest.actionAliases = [
130
- {
131
- name: "write",
132
- target: { contractId, version: 1, toolName: "missing" },
133
- },
119
+ });
120
+ test("accepts an older manifest's retired action aliases and ignores them", () => {
121
+ const { artifact, contractId } = makeArtifact();
122
+ const old = artifact.manifest;
123
+ old.actionAliases = [
124
+ { name: "write", target: { contractId, version: 1, toolName: "missing" } },
134
125
  ];
135
- expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow("no registered target");
126
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).not.toThrow();
127
+ expect(() => (0, isolated_artifact_1.parseIsolatedPackageArtifact)((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact))).not.toThrow();
128
+ old.actionAliases = "not even an array";
129
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).not.toThrow();
136
130
  });
137
131
  test("accepts a package-private table without adding it to the contract definition", () => {
138
132
  const { artifact } = makeArtifact();
@@ -201,6 +195,55 @@ describe("isolated package artifact", () => {
201
195
  },
202
196
  ]);
203
197
  });
198
+ test("accepts local-only tables and keeps the flag out of the contract shape", () => {
199
+ const { artifact } = makeArtifact();
200
+ const fields = [
201
+ { name: "stateId", type: field_type_1.FieldType.id },
202
+ { name: "count", type: field_type_1.FieldType.number },
203
+ ];
204
+ artifact.manifest.tables = [
205
+ { name: "DeviceState", primaryKeyName: "stateId", fields, localOnly: true },
206
+ { name: "SharedState", primaryKeyName: "stateId", fields, localOnly: false },
207
+ ];
208
+ artifact.manifest.provides[0].tables = [{ name: "DeviceState" }, { name: "SharedState" }];
209
+ const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
210
+ const definition = (0, isolated_artifact_1.isolatedContractToDefinition)(manifest.provides[0], manifest.tables ?? []);
211
+ expect(manifest.tables?.[0].localOnly).toBe(true);
212
+ // Sync policy belongs to the provider's table, not to the contract consumers depend on.
213
+ expect(definition.tables.map((table) => table.name)).toEqual(["DeviceState", "SharedState"]);
214
+ for (const table of definition.tables)
215
+ expect(table).not.toHaveProperty("localOnly");
216
+ });
217
+ test("rejects a localOnly flag that isn't a boolean", () => {
218
+ const { artifact } = makeArtifact();
219
+ artifact.manifest.tables = [
220
+ {
221
+ name: "State",
222
+ primaryKeyName: "stateId",
223
+ fields: [{ name: "stateId", type: field_type_1.FieldType.id }],
224
+ localOnly: "yes",
225
+ },
226
+ ];
227
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/localOnly must be a boolean/);
228
+ });
229
+ test("accepts guestApi 2 and 3 and rejects other values", () => {
230
+ for (const guestApi of [2, 3]) {
231
+ const { artifact } = makeArtifact();
232
+ artifact.manifest.guestApi = guestApi;
233
+ expect((0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact).guestApi).toBe(guestApi);
234
+ }
235
+ for (const guestApi of [1, "2", 4]) {
236
+ const { artifact: other } = makeArtifact();
237
+ other.manifest.guestApi = guestApi;
238
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(other)).toThrow(/guestApi must be 2 or 3/);
239
+ }
240
+ });
241
+ test.each([true, false, "jobs", null])("rejects the removed serial option (%s)", (serial) => {
242
+ const { artifact } = makeArtifact();
243
+ const tool = artifact.manifest.provides[0].tools[0];
244
+ tool.serial = serial;
245
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/\.serial is not supported/);
246
+ });
204
247
  test("rejects malformed isolated tables", () => {
205
248
  const { artifact } = makeArtifact();
206
249
  artifact.manifest.tables = [
@@ -307,6 +350,121 @@ describe("isolated package artifact", () => {
307
350
  { contractId: targetContractId, version: { from: 2, to: 4 } },
308
351
  ]);
309
352
  });
353
+ test.each([
354
+ "range",
355
+ "id",
356
+ "version",
357
+ "shape",
358
+ ])("rejects a carried target with a mismatched %s", (problem) => {
359
+ const { artifact } = makeArtifact();
360
+ const definition = (0, isolated_artifact_1.isolatedContractToDefinition)(artifact.manifest.provides[0], []);
361
+ definition.contractId = (0, utils_1.newid)();
362
+ const target = { contractId: definition.contractId, version: 1, definition };
363
+ artifact.manifest.provides[0].alsoImplements = [target];
364
+ if (problem === "range")
365
+ Object.assign(target, { version: { from: 1, to: 2 } });
366
+ if (problem === "id")
367
+ definition.contractId = (0, utils_1.newid)();
368
+ if (problem === "version")
369
+ definition.version = 2;
370
+ if (problem === "shape")
371
+ Object.assign(definition, { tools: null });
372
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/definition/);
373
+ });
374
+ test.each([
375
+ false,
376
+ true,
377
+ ])("rejects a single alias to an explicitly provided version (carried: %p)", (carried) => {
378
+ const { artifact, contractId } = makeArtifact();
379
+ const newer = structuredClone(artifact.manifest.provides[0]);
380
+ newer.version = 3;
381
+ newer.alsoImplements = [
382
+ {
383
+ contractId,
384
+ version: 1,
385
+ ...(carried
386
+ ? { definition: (0, isolated_artifact_1.isolatedContractToDefinition)(artifact.manifest.provides[0], []) }
387
+ : {}),
388
+ },
389
+ ];
390
+ artifact.manifest.provides.push(newer);
391
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/provides explicitly/);
392
+ });
393
+ test("keeps legacy ranges that overlap explicitly provided versions", () => {
394
+ const { artifact, contractId } = makeArtifact();
395
+ const newer = structuredClone(artifact.manifest.provides[0]);
396
+ newer.version = 3;
397
+ newer.alsoImplements = [{ contractId, version: { from: 1, to: 2 } }];
398
+ artifact.manifest.provides.push(newer);
399
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).not.toThrow();
400
+ });
401
+ test.each([
402
+ "table",
403
+ "input",
404
+ "output",
405
+ "event",
406
+ "observable",
407
+ ])("bounds the nesting of a carried %s field", (kind) => {
408
+ const { artifact } = makeArtifact();
409
+ const definition = (0, isolated_artifact_1.isolatedContractToDefinition)(artifact.manifest.provides[0], []);
410
+ definition.contractId = (0, utils_1.newid)();
411
+ let field = { name: "leaf", type: field_type_1.FieldType.string };
412
+ for (let depth = 0; depth < isolated_artifact_1.ISOLATED_MAX_FIELD_DEPTH; depth++) {
413
+ field = { name: "nested", type: field_type_1.FieldType.object, fields: [field] };
414
+ }
415
+ if (kind === "input")
416
+ definition.tools[0].inputFields = [field];
417
+ if (kind === "output")
418
+ definition.tools[0].outputFields = [field];
419
+ if (kind === "table")
420
+ definition.tables = [
421
+ { name: "Rows", description: "", primaryKeyName: "id", fields: [field] },
422
+ ];
423
+ if (kind === "event")
424
+ definition.events = [{ name: "changed", description: "", payloadFields: [field] }];
425
+ if (kind === "observable") {
426
+ const { name: _name, ...value } = field;
427
+ definition.observables = [
428
+ { name: "value", description: "", writable: false, valueType: field_type_1.FieldType.object, value },
429
+ ];
430
+ }
431
+ artifact.manifest.provides[0].alsoImplements = [
432
+ { contractId: definition.contractId, version: 1, definition },
433
+ ];
434
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/nests more than 16 levels/);
435
+ });
436
+ test("validates exposes: other provided contracts, kept out of the definition", () => {
437
+ const { artifact, contractId } = makeArtifact();
438
+ const hubContractId = (0, utils_1.newid)();
439
+ artifact.manifest.provides.push({
440
+ contractId: hubContractId,
441
+ version: 2,
442
+ name: "Hub",
443
+ tools: [],
444
+ events: [{ name: "alert", description: "Attention." }],
445
+ exposes: [{ contractId, version: 1 }],
446
+ });
447
+ const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(JSON.parse(JSON.stringify(artifact)));
448
+ expect(manifest.provides[1].exposes).toEqual([{ contractId, version: 1 }]);
449
+ expect(manifest.provides[0]).not.toHaveProperty("exposes");
450
+ const definition = (0, isolated_artifact_1.isolatedContractToDefinition)(manifest.provides[1], manifest.tables ?? []);
451
+ expect(definition).not.toHaveProperty("exposes");
452
+ const withExposes = (exposes) => {
453
+ const copy = JSON.parse(JSON.stringify(artifact));
454
+ copy.manifest.provides[1].exposes = exposes;
455
+ return () => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(copy);
456
+ };
457
+ expect(withExposes({ contractId, version: 1 })).toThrow("provides[1].exposes must be an array");
458
+ expect(withExposes([{ contractId, version: 1, toolName: "greet" }])).toThrow(/provides\[1\]\.exposes\[0\]/);
459
+ expect(withExposes([{ contractId: "short", version: 1 }])).toThrow(/exposes\[0\]\.contractId/);
460
+ expect(withExposes([{ contractId, version: 0 }])).toThrow(/exposes\[0\]\.version/);
461
+ expect(withExposes([{ contractId: hubContractId, version: 2 }])).toThrow("a contract can't expose itself");
462
+ expect(withExposes([
463
+ { contractId, version: 1 },
464
+ { contractId, version: 1 },
465
+ ])).toThrow("is exposed twice");
466
+ expect(withExposes([{ contractId, version: 2 }])).toThrow(`provides[1] exposes ${contractId}@2, which the package doesn't provide`);
467
+ });
310
468
  test("maps declared events to the public contract definition", () => {
311
469
  const { artifact } = makeArtifact();
312
470
  artifact.manifest.provides[0].events = [
@@ -357,6 +515,103 @@ describe("isolated package artifact", () => {
357
515
  ];
358
516
  expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/type must be a FieldType/);
359
517
  });
518
+ test("checks nested fields and enum values, and carries them into the contract", () => {
519
+ const { artifact } = makeArtifact();
520
+ const tool = artifact.manifest.provides[0].tools[0];
521
+ tool.inputFields = [
522
+ { name: "mode", type: field_type_1.FieldType.string, enumValues: ["on", "off"], optional: true },
523
+ {
524
+ name: "items",
525
+ type: field_type_1.FieldType.object,
526
+ isArray: true,
527
+ fields: [
528
+ { name: "label", type: field_type_1.FieldType.string },
529
+ { name: "size", type: field_type_1.FieldType.any, enumValues: [1, "large", true] },
530
+ ],
531
+ },
532
+ ];
533
+ artifact.manifest.provides[0].observables = [
534
+ {
535
+ name: "state",
536
+ description: "The state.",
537
+ valueType: field_type_1.FieldType.any,
538
+ value: {
539
+ type: field_type_1.FieldType.object,
540
+ optional: true,
541
+ fields: [{ name: "on", type: field_type_1.FieldType.boolean }],
542
+ },
543
+ writable: false,
544
+ },
545
+ ];
546
+ artifact.manifest.guestApi = 3;
547
+ const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact, { strict: true });
548
+ const definition = (0, isolated_artifact_1.isolatedContractToDefinition)(manifest.provides[0], []);
549
+ expect(definition.tools[0].inputFields).toEqual(tool.inputFields);
550
+ expect(definition.observables[0].value).toEqual({
551
+ type: field_type_1.FieldType.object,
552
+ optional: true,
553
+ fields: [{ name: "on", type: field_type_1.FieldType.boolean }],
554
+ });
555
+ });
556
+ test.each([
557
+ [{ name: "x", type: field_type_1.FieldType.string, enumValues: [] }, /enumValues must be a non-empty array/],
558
+ [{ name: "x", type: field_type_1.FieldType.string, enumValues: ["a", 1] }, /enumValues must all be string/],
559
+ [
560
+ { name: "x", type: field_type_1.FieldType.number, enumValues: [Number.NaN] },
561
+ /enumValues must all be number/,
562
+ ],
563
+ [{ name: "x", type: field_type_1.FieldType.id, enumValues: ["a"] }, /enumValues must all be id/],
564
+ [{ name: "x", type: field_type_1.FieldType.string, fields: [] }, /fields is only allowed on object fields/],
565
+ [
566
+ { name: "x", type: field_type_1.FieldType.object, fields: [{ name: "", type: field_type_1.FieldType.string }] },
567
+ /fields\[0\]\.name is required/,
568
+ ],
569
+ [{ name: "x", type: field_type_1.FieldType.object, fields: {} }, /fields must be an array/],
570
+ ])("rejects a malformed field %#", (field, message) => {
571
+ const { artifact } = makeArtifact();
572
+ artifact.manifest.provides[0].tools[0].inputFields = [field];
573
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(message);
574
+ });
575
+ test("refuses fields nested deeper than the limit", () => {
576
+ const nest = (levels) => levels === 1
577
+ ? { name: "leaf", type: field_type_1.FieldType.string }
578
+ : { name: `level${levels}`, type: field_type_1.FieldType.object, fields: [nest(levels - 1)] };
579
+ const { artifact } = makeArtifact();
580
+ artifact.manifest.provides[0].tools[0].inputFields = [nest(isolated_artifact_1.ISOLATED_MAX_FIELD_DEPTH)];
581
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(structuredClone(artifact))).not.toThrow();
582
+ artifact.manifest.provides[0].tools[0].inputFields = [
583
+ nest(isolated_artifact_1.ISOLATED_MAX_FIELD_DEPTH + 1),
584
+ ];
585
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/nests more than 16 levels/);
586
+ });
587
+ test("a host drops field keys it doesn't know; the build refuses them", () => {
588
+ const { artifact } = makeArtifact();
589
+ const withUnknown = () => {
590
+ const copy = structuredClone(artifact);
591
+ copy.manifest.provides[0].tools[0].inputFields = [
592
+ {
593
+ name: "mode",
594
+ type: field_type_1.FieldType.object,
595
+ format: "future",
596
+ fields: [
597
+ // As a manifest arrives: JSON.parse makes "__proto__" an own key.
598
+ JSON.parse('{"name":"on","type":"boolean","__proto__":{"polluted":true}}'),
599
+ ],
600
+ },
601
+ ];
602
+ return copy;
603
+ };
604
+ const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(withUnknown());
605
+ expect(manifest.provides[0].tools[0].inputFields).toEqual([
606
+ { name: "mode", type: field_type_1.FieldType.object, fields: [{ name: "on", type: field_type_1.FieldType.boolean }] },
607
+ ]);
608
+ expect({}.polluted).toBeUndefined();
609
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(withUnknown(), { strict: true })).toThrow(/inputFields\[0\]\.format is not supported/);
610
+ // Other manifest objects still refuse unknown keys: they carry policy, not descriptions.
611
+ const tool = artifact.manifest.provides[0].tools[0];
612
+ tool.cachePolicy = "forever";
613
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/cachePolicy is not supported/);
614
+ });
360
615
  test("rejects unsupported contract features instead of dropping them", () => {
361
616
  const { artifact } = makeArtifact();
362
617
  artifact.manifest.provides[0].streams = [];
@@ -381,12 +636,30 @@ describe("isolated package artifact", () => {
381
636
  },
382
637
  ];
383
638
  expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/must match.*number/);
384
- artifact.manifest.provides[0].observables[0].valueType = field_type_1.FieldType.object;
385
- artifact.manifest.provides[0].observables[0].persistentVar.defaultValue = {
386
- callback: () => undefined,
639
+ const [observable] = artifact.manifest.provides[0].observables;
640
+ observable.valueType = field_type_1.FieldType.object;
641
+ observable.persistentVar = {
642
+ scope: "device",
643
+ name: "sharedCount",
644
+ defaultValue: { callback: () => undefined },
387
645
  };
388
646
  expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/JSON-compatible/);
389
647
  });
648
+ test("an observable without a persistentVar is held by a guest API 2 worker", () => {
649
+ const { artifact } = makeArtifact();
650
+ const status = {
651
+ name: "status",
652
+ description: "Current status",
653
+ valueType: field_type_1.FieldType.string,
654
+ writable: false,
655
+ };
656
+ artifact.manifest.provides[0].observables = [status];
657
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/observables\[0\]\.persistentVar is required unless the package uses guestApi 2/);
658
+ artifact.manifest.guestApi = 2;
659
+ expect((0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact).provides[0].observables).toEqual([status]);
660
+ artifact.manifest.provides[0].observables = [{ ...status, persistentVar: null }];
661
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/persistentVar must be an object/);
662
+ });
390
663
  test("accepts a network allowlist and rejects a host with a scheme", () => {
391
664
  const { artifact } = makeArtifact();
392
665
  artifact.manifest.network = {
@@ -4,7 +4,7 @@ import type { IContractCall, IContractProviderEndpoint } from "../contracts/cont
4
4
  import type { ContractRegistry } from "../contracts/registry";
5
5
  import type { PersistentVarScope } from "../data/persistent-vars";
6
6
  import type { IField } from "../types/field-type";
7
- import type { IIsolatedContractToolRef, IIsolatedInstallResult, IIsolatedPackageArtifact } from "./isolated-artifact";
7
+ import type { IIsolatedContractToolRef, IIsolatedInstalledPackage, IIsolatedInstallResult, IIsolatedPackageArtifact } from "./isolated-artifact";
8
8
  import type { IPackageWorkerHealth } from "./package-runtime-state";
9
9
  /** Standard table methods exposed through isolated contract-table references. */
10
10
  export declare const ISOLATED_CONTRACT_TABLE_OPERATIONS: readonly ["get", "list", "count", "findOne", "save", "insert", "update", "delete"];
@@ -91,6 +91,8 @@ export interface IIsolatedPackageRuntime {
91
91
  provides(contractId: string, version: number): boolean;
92
92
  /** True when this runtime has installed `packageId`. */
93
93
  isInstalled(packageId: string): boolean;
94
+ /** Read an installed package's declaration without starting its worker or exposing source. */
95
+ getInstalledPackage?(packageId: string): IIsolatedInstalledPackage | undefined;
94
96
  /**
95
97
  * Report whether the package's worker is running and its last worker start or crash
96
98
  * failure since install or replacement. Workers start lazily, so a broken provider only
@@ -120,13 +122,21 @@ export interface IIsolatedPackageRuntime {
120
122
  * @param principal - Trusted caller authority. Defaults to Self when omitted.
121
123
  */
122
124
  invokeOwnedTool?(packageId: string, ref: IIsolatedContractToolRef, args: unknown, principal?: IIsolatedCallPrincipal): Promise<unknown>;
123
- /** Narrow a projected action turn by the trusted consumer's selected contract consume ceiling. */
125
+ /**
126
+ * Narrow a delegated turn by the trusted consumer's selected contract consume ceiling, for
127
+ * `context.dispatchTool` and the Contract Catalog's projections.
128
+ */
124
129
  actionDispatchPrincipal?(principal: IIsolatedCallPrincipal, consumerPackageId?: string, ref?: IIsolatedContractToolRef): IIsolatedCallPrincipal;
125
- /** Project aliases whose wrapper and canonical targets allow the trusted caller. */
126
- authorizedActionNames?(packageId: string, principal: IIsolatedCallPrincipal, consumerPackageId?: string, selectedContract?: {
130
+ /**
131
+ * Project authority for a named provider's exposed contract. Checks the caller's current
132
+ * declarations and provider restrictions, including direct-consume precedence. Undefined
133
+ * means the package cannot reach this target. Tool admission is checked separately.
134
+ */
135
+ exposedContractPrincipal?(principal: IIsolatedCallPrincipal, consumerPackageId: string, target: {
127
136
  contractId: string;
128
137
  version: number;
129
- }): string[] | Promise<string[]>;
138
+ providerPackageId: string;
139
+ }): IIsolatedCallPrincipal | undefined;
130
140
  /** Terminate every worker and drop installed state. */
131
141
  dispose(): Promise<void>;
132
142
  }
@@ -153,6 +163,8 @@ export interface IIsolatedPackageTableDeclaration extends IIsolatedPackageTableR
153
163
  fields: IField[];
154
164
  /** Schema version for host table-definition updates. */
155
165
  versionNumber?: number;
166
+ /** Rows stay on the writing device; see `IIsolatedPackageTable.localOnly`. */
167
+ localOnly?: boolean;
156
168
  }
157
169
  /**
158
170
  * Trusted host capability for isolated-package table persistence.
@@ -180,8 +192,11 @@ export interface IIsolatedPackageTableGateway {
180
192
  /** Insert or replace a copied record and return the stored copy. */
181
193
  save(ref: IIsolatedPackageTableRef, record: Record<string, unknown>): Promise<Record<string, unknown>>;
182
194
  /**
183
- * Invoke one allowlisted standard operation for a consumer-facing contract table.
184
- * The host validates declaration and provider identity before calling the gateway.
195
+ * Invoke one allowlisted standard operation on a declared table: for the owning package
196
+ * (`ctx.callOwnedTable`), private tables included, or for a consumer through a contract that
197
+ * exposes it when the provider predates guest API 3 (guest API 3 sends consumer calls to the
198
+ * provider's worker, whose table class calls this as the owner). The host validates
199
+ * declaration, exposure, and provider identity before calling the gateway.
185
200
  */
186
201
  call(ref: IIsolatedPackageTableRef, operation: IsolatedContractTableOperation, args: unknown[]): Promise<unknown>;
187
202
  /**
@@ -40,10 +40,15 @@ function validateDeclaration(declaration) {
40
40
  if (primaryKey.type !== "id") {
41
41
  throw new Error("Isolated table primary key must refer to a field of type id");
42
42
  }
43
+ if (declaration.localOnly !== undefined && typeof declaration.localOnly !== "boolean") {
44
+ throw new Error("Isolated table localOnly must be a boolean");
45
+ }
46
+ const { localOnly, ...rest } = declaration;
43
47
  return {
44
- ...declaration,
48
+ ...rest,
45
49
  fields: declaration.fields.map((field) => ({ ...field })),
46
50
  versionNumber: versionNumber(declaration),
51
+ ...(localOnly ? { localOnly: true } : {}),
47
52
  };
48
53
  }
49
54
  function toMetaData(rawDeclaration) {
@@ -56,13 +61,12 @@ function toMetaData(rawDeclaration) {
56
61
  primaryKeyName: declaration.primaryKeyName,
57
62
  fields: declaration.fields,
58
63
  versionNumber: versionNumber(declaration),
64
+ // Only when set: stored definitions hash their metadata, and older ones have no such key.
65
+ ...(declaration.localOnly ? { localOnly: true } : {}),
59
66
  };
60
67
  }
61
68
  function normalizedMetaData(metaData) {
62
- return {
63
- ...metaData,
64
- fields: metaData.fields.map(({ defaultValue: _defaultValue, ...field }) => field),
65
- };
69
+ return { ...metaData, fields: (0, types_1.comparableTableFields)(metaData.fields) };
66
70
  }
67
71
  function sameDefinition(existing, incoming) {
68
72
  return ((0, utils_1.simpleObjectHash)(normalizedMetaData(existing)) ===
@@ -106,6 +110,7 @@ function declarationFromMetaData(metaData) {
106
110
  primaryKeyName: metaData.primaryKeyName,
107
111
  fields: metaData.fields,
108
112
  versionNumber: metaData.versionNumber,
113
+ localOnly: metaData.localOnly,
109
114
  });
110
115
  }
111
116
  function metaDataFromStoredDefinition(record, incoming) {
@@ -158,8 +163,22 @@ function copyTableResult(value) {
158
163
  const normalized = Array.isArray(value) ? value.map(normalizeRecord) : normalizeRecord(value);
159
164
  return (0, isolated_value_1.copyIsolatedValue)(normalized);
160
165
  }
161
- function matchesFilter(row, filter = {}) {
162
- return Object.entries(filter).every(([name, value]) => row[name] === value);
166
+ /** Equality and `$in` conditions, as the memory gateway supports; other operators fail. */
167
+ function matchingFilter(filter = {}) {
168
+ const conditions = Object.entries(filter).map(([name, value]) => {
169
+ if (name.startsWith("$")) {
170
+ throw new Error(`The memory table gateway supports equality and $in filters only`);
171
+ }
172
+ if (value === null || typeof value !== "object" || value instanceof Date) {
173
+ return (row) => row[name] === value;
174
+ }
175
+ const candidates = value.$in;
176
+ if (Object.keys(value).length !== 1 || !Array.isArray(candidates)) {
177
+ throw new Error(`The memory table gateway supports equality and $in filters only`);
178
+ }
179
+ return (row) => candidates.includes(row[name]);
180
+ });
181
+ return (row) => conditions.every((matches) => matches(row));
163
182
  }
164
183
  function assertTableEvent(eventName) {
165
184
  if (!isolated_runtime_1.ISOLATED_CONTRACT_TABLE_EVENTS.includes(eventName)) {
@@ -254,18 +273,18 @@ function createMemoryIsolatedPackageTableGateway() {
254
273
  case "list": {
255
274
  const filter = (args[0] ?? {});
256
275
  const opts = (args[1] ?? {});
257
- const matching = [...tableRows.values()].filter((row) => matchesFilter(row, filter));
276
+ const matching = [...tableRows.values()].filter(matchingFilter(filter));
258
277
  const pageSize = opts.pageSize ?? matching.length;
259
- const start = (opts.page ?? 0) * pageSize;
278
+ const start = (Math.max(1, Math.round(opts.page || 1)) - 1) * pageSize;
260
279
  return (0, isolated_value_1.copyIsolatedValue)(matching.slice(start, start + pageSize));
261
280
  }
262
281
  case "count": {
263
282
  const filter = (args[0] ?? {});
264
- return [...tableRows.values()].filter((row) => matchesFilter(row, filter)).length;
283
+ return [...tableRows.values()].filter(matchingFilter(filter)).length;
265
284
  }
266
285
  case "findOne": {
267
286
  const filter = (args[0] ?? {});
268
- return (0, isolated_value_1.copyIsolatedValue)([...tableRows.values()].find((row) => matchesFilter(row, filter)));
287
+ return (0, isolated_value_1.copyIsolatedValue)([...tableRows.values()].find(matchingFilter(filter)));
269
288
  }
270
289
  case "save":
271
290
  case "insert":