@peers-app/peers-sdk 0.26.6 → 0.27.1
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 +3 -1
- package/bin/peers-package.js +13 -0
- package/define/package.json +4 -0
- package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
- package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
- package/dist/contracts/__tests__/also-implements.test.js +110 -0
- package/dist/contracts/__tests__/builder.test.js +45 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
- package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
- package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
- package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
- package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
- package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
- package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
- package/dist/contracts/__tests__/registry.test.js +68 -0
- package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
- package/dist/contracts/__tests__/target-dispatch.js +24 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
- package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
- package/dist/contracts/__tests__/validate.test.js +177 -7
- package/dist/contracts/__tests__/workflow-context.test.js +0 -11
- package/dist/contracts/builder.d.ts +16 -2
- package/dist/contracts/builder.js +44 -3
- package/dist/contracts/contract-definition-schema.d.ts +251 -0
- package/dist/contracts/contract-definition-schema.js +48 -0
- package/dist/contracts/contract-provider-router.d.ts +2 -0
- package/dist/contracts/contract-provider-router.js +5 -2
- package/dist/contracts/contract-proxy.d.ts +52 -18
- package/dist/contracts/contract-proxy.js +144 -203
- package/dist/contracts/contract-resolvers.d.ts +6 -4
- package/dist/contracts/contract-resolvers.js +32 -9
- package/dist/contracts/frozen-contracts.d.ts +59 -0
- package/dist/contracts/frozen-contracts.js +86 -0
- package/dist/contracts/host-contract-client.d.ts +134 -0
- package/dist/contracts/host-contract-client.js +396 -0
- package/dist/contracts/host-contract-transport.d.ts +2 -0
- package/dist/contracts/host-contract-transport.js +8 -0
- package/dist/contracts/index.d.ts +6 -4
- package/dist/contracts/index.js +11 -5
- package/dist/contracts/registry.d.ts +31 -1
- package/dist/contracts/registry.js +100 -6
- package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
- package/dist/contracts/system/contract-catalog-contract.js +17 -5
- package/dist/contracts/tool-catalog-service.js +26 -6
- package/dist/contracts/tool-catalog.d.ts +1 -1
- package/dist/contracts/tool-catalog.js +4 -1
- package/dist/contracts/types.d.ts +32 -2
- package/dist/contracts/validate.d.ts +16 -1
- package/dist/contracts/validate.js +139 -25
- package/dist/data/change-tracking.d.ts +17 -5
- package/dist/data/change-tracking.js +22 -6
- package/dist/data/change-tracking.test.d.ts +1 -0
- package/dist/data/change-tracking.test.js +62 -0
- package/dist/data/create-group.js +1 -1
- package/dist/data/group-members.d.ts +14 -0
- package/dist/data/group-members.js +36 -0
- package/dist/data/group-members.test.js +49 -0
- package/dist/data/orm/sql.data-source.d.ts +9 -0
- package/dist/data/orm/sql.data-source.js +47 -2
- package/dist/data/orm/sql.data-source.test.js +142 -0
- package/dist/data/orm/table-container.js +9 -9
- package/dist/data/orm/table-container.test.js +44 -0
- package/dist/data/orm/types.d.ts +41 -1
- package/dist/data/orm/types.js +159 -28
- package/dist/data/orm/types.test.js +129 -0
- package/dist/data/peer-types.d.ts +8 -62
- package/dist/data/persistent-vars.d.ts +4 -1
- package/dist/data/persistent-vars.js +7 -2
- package/dist/data/tools.d.ts +29 -221
- package/dist/define/also-implements.test.d.ts +1 -0
- package/dist/define/also-implements.test.js +123 -0
- package/dist/define/context.d.ts +77 -0
- package/dist/define/context.js +2 -0
- package/dist/define/contract-ref.d.ts +177 -0
- package/dist/define/contract-ref.js +67 -0
- package/dist/define/declare-contract.d.ts +167 -0
- package/dist/define/declare-contract.js +220 -0
- package/dist/define/declare-contract.test.d.ts +1 -0
- package/dist/define/declare-contract.test.js +573 -0
- package/dist/define/define-package.d.ts +203 -0
- package/dist/define/define-package.js +289 -0
- package/dist/define/define.test.d.ts +1 -0
- package/dist/define/define.test.js +1010 -0
- package/dist/define/empty-fields.d.ts +15 -0
- package/dist/define/empty-fields.js +134 -0
- package/dist/define/empty-fields.test.d.ts +1 -0
- package/dist/define/empty-fields.test.js +55 -0
- package/dist/define/events-observables.test.d.ts +1 -0
- package/dist/define/events-observables.test.js +1013 -0
- package/dist/define/events.d.ts +81 -0
- package/dist/define/events.js +81 -0
- package/dist/define/generated-field.d.ts +20 -0
- package/dist/define/generated-field.js +25 -0
- package/dist/define/index.d.ts +28 -0
- package/dist/define/index.js +62 -0
- package/dist/define/lazy-members.d.ts +8 -0
- package/dist/define/lazy-members.js +38 -0
- package/dist/define/manifest.d.ts +63 -0
- package/dist/define/manifest.js +653 -0
- package/dist/define/observables.d.ts +146 -0
- package/dist/define/observables.js +187 -0
- package/dist/define/serve-members.d.ts +21 -0
- package/dist/define/serve-members.js +152 -0
- package/dist/define/serve-members.test.d.ts +1 -0
- package/dist/define/serve-members.test.js +630 -0
- package/dist/define/serve.d.ts +22 -0
- package/dist/define/serve.js +58 -0
- package/dist/define/system-contracts.d.ts +41 -0
- package/dist/define/system-contracts.js +21 -0
- package/dist/define/table-handle.d.ts +13 -0
- package/dist/define/table-handle.js +36 -0
- package/dist/define/table.d.ts +128 -0
- package/dist/define/table.js +122 -0
- package/dist/define/tool.d.ts +129 -0
- package/dist/define/tool.js +91 -0
- package/dist/define/vars.d.ts +82 -0
- package/dist/define/vars.js +256 -0
- package/dist/define/vars.test.d.ts +1 -0
- package/dist/define/vars.test.js +39 -0
- package/dist/define/wire-value.d.ts +12 -0
- package/dist/define/wire-value.js +76 -0
- package/dist/define/worker-state.d.ts +15 -0
- package/dist/define/worker-state.js +45 -0
- package/dist/device/tx-encoding.test.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/invites/group-approval.js +4 -2
- package/dist/invites/group-approval.test.js +2 -0
- package/dist/observable.d.ts +42 -2
- package/dist/observable.js +25 -7
- package/dist/package-build/build-package.d.ts +60 -0
- package/dist/package-build/build-package.js +312 -0
- package/dist/package-build/build-package.test.d.ts +1 -0
- package/dist/package-build/build-package.test.js +204 -0
- package/dist/package-build/cli.d.ts +7 -0
- package/dist/package-build/cli.js +218 -0
- package/dist/package-build/contract-add.d.ts +35 -0
- package/dist/package-build/contract-add.js +97 -0
- package/dist/package-build/contract-add.test.d.ts +1 -0
- package/dist/package-build/contract-add.test.js +467 -0
- package/dist/package-build/contract-source.d.ts +59 -0
- package/dist/package-build/contract-source.js +471 -0
- package/dist/package-build/freeze-contracts.d.ts +56 -0
- package/dist/package-build/freeze-contracts.js +198 -0
- package/dist/package-build/freeze-contracts.test.d.ts +1 -0
- package/dist/package-build/freeze-contracts.test.js +355 -0
- package/dist/package-installer/package-creator.d.ts +4 -0
- package/dist/package-installer/package-creator.js +57 -16
- package/dist/package-installer/package-creator.test.js +86 -0
- package/dist/package-loader/contract-members.d.ts +53 -0
- package/dist/package-loader/contract-members.js +31 -0
- package/dist/package-loader/contract-package-loader.js +5 -1
- package/dist/package-loader/contract-package-loader.test.js +11 -0
- package/dist/package-loader/host-authority-loading.test.js +9 -5
- package/dist/package-loader/index.d.ts +1 -0
- package/dist/package-loader/index.js +1 -0
- package/dist/package-loader/isolated-artifact.d.ts +80 -17
- package/dist/package-loader/isolated-artifact.js +257 -51
- package/dist/package-loader/isolated-artifact.test.js +294 -21
- package/dist/package-loader/isolated-runtime.d.ts +22 -7
- package/dist/package-loader/isolated-table-gateway.js +30 -11
- package/dist/package-loader/isolated-table-gateway.test.js +243 -0
- package/dist/package-loader/package-loader.d.ts +8 -1
- package/dist/package-loader/package-loader.js +39 -20
- package/dist/package-loader/package-loader.test.js +47 -0
- package/dist/peer-id.d.ts +25 -0
- package/dist/peer-id.js +99 -0
- package/dist/serial-json.compat.test.d.ts +1 -0
- package/dist/serial-json.compat.test.js +369 -0
- package/dist/serial-json.d.ts +41 -5
- package/dist/serial-json.js +216 -157
- package/dist/serial-json.oracle.d.ts +21 -0
- package/dist/serial-json.oracle.js +222 -0
- package/dist/serial-json.test.js +315 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/mock-host-contract.d.ts +123 -0
- package/dist/testing/mock-host-contract.js +434 -0
- package/dist/testing/mock-host-contract.test.d.ts +1 -0
- package/dist/testing/mock-host-contract.test.js +782 -0
- package/dist/types/field-type.d.ts +35 -18
- package/dist/types/field-type.js +4 -1
- package/dist/types/zod-types.d.ts +9 -0
- package/dist/types/zod-types.js +8 -2
- package/dist/utils.d.ts +22 -25
- package/dist/utils.js +45 -92
- package/dist/utils.test.js +36 -0
- package/package.json +9 -2
- 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("
|
|
110
|
-
const { artifact
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
artifact
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
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
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
126
|
-
|
|
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
|
-
|
|
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
|
|
184
|
-
*
|
|
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
|
-
...
|
|
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
|
-
|
|
162
|
-
|
|
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((
|
|
276
|
+
const matching = [...tableRows.values()].filter(matchingFilter(filter));
|
|
258
277
|
const pageSize = opts.pageSize ?? matching.length;
|
|
259
|
-
const start = (opts.page
|
|
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((
|
|
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((
|
|
287
|
+
return (0, isolated_value_1.copyIsolatedValue)([...tableRows.values()].find(matchingFilter(filter)));
|
|
269
288
|
}
|
|
270
289
|
case "save":
|
|
271
290
|
case "insert":
|