@peers-app/peers-sdk 0.25.20 → 0.26.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 (94) hide show
  1. package/dist/contracts/__tests__/builder.test.js +0 -2
  2. package/dist/contracts/__tests__/contract-provider-router.test.js +51 -0
  3. package/dist/contracts/__tests__/contract-proxy.test.js +21 -0
  4. package/dist/contracts/__tests__/contract-resolvers.test.js +84 -0
  5. package/dist/contracts/__tests__/legacy-removal.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/legacy-removal.test.js +90 -0
  7. package/dist/contracts/__tests__/system-contracts.test.js +88 -0
  8. package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +1 -0
  9. package/dist/contracts/__tests__/tool-catalog-inventory.test.js +44 -0
  10. package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +1 -0
  11. package/dist/contracts/__tests__/tool-catalog-service.test.js +446 -0
  12. package/dist/contracts/__tests__/tool-catalog.test.d.ts +1 -0
  13. package/dist/contracts/__tests__/tool-catalog.test.js +649 -0
  14. package/dist/contracts/__tests__/workflow-context.test.d.ts +1 -0
  15. package/dist/contracts/__tests__/workflow-context.test.js +61 -0
  16. package/dist/contracts/builder.d.ts +9 -8
  17. package/dist/contracts/builder.js +34 -16
  18. package/dist/contracts/contract-provider-router.d.ts +12 -1
  19. package/dist/contracts/contract-provider-router.js +40 -2
  20. package/dist/contracts/contract-proxy.d.ts +20 -6
  21. package/dist/contracts/contract-proxy.js +23 -12
  22. package/dist/contracts/contract-resolvers.d.ts +22 -3
  23. package/dist/contracts/contract-resolvers.js +86 -7
  24. package/dist/contracts/extract.d.ts +7 -0
  25. package/dist/contracts/extract.js +3 -0
  26. package/dist/contracts/index.d.ts +11 -4
  27. package/dist/contracts/index.js +14 -2
  28. package/dist/contracts/registry.d.ts +3 -0
  29. package/dist/contracts/registry.js +2 -0
  30. package/dist/contracts/system/desktop-tools-contract.d.ts +5 -0
  31. package/dist/contracts/system/desktop-tools-contract.js +88 -0
  32. package/dist/contracts/system/host-tools-contract.d.ts +5 -0
  33. package/dist/contracts/system/host-tools-contract.js +76 -0
  34. package/dist/contracts/system/index.d.ts +3 -0
  35. package/dist/contracts/system/index.js +3 -0
  36. package/dist/contracts/system/tool-catalog-contract.d.ts +73 -0
  37. package/dist/contracts/system/tool-catalog-contract.js +70 -0
  38. package/dist/contracts/system-contracts.d.ts +3 -1
  39. package/dist/contracts/system-contracts.js +20 -6
  40. package/dist/contracts/tool-catalog-identity.d.ts +35 -0
  41. package/dist/contracts/tool-catalog-identity.js +88 -0
  42. package/dist/contracts/tool-catalog-migrations.d.ts +12 -0
  43. package/dist/contracts/tool-catalog-migrations.js +179 -0
  44. package/dist/contracts/tool-catalog-service.d.ts +30 -0
  45. package/dist/contracts/tool-catalog-service.js +287 -0
  46. package/dist/contracts/tool-catalog.d.ts +91 -0
  47. package/dist/contracts/tool-catalog.js +490 -0
  48. package/dist/contracts/types.d.ts +7 -5
  49. package/dist/data/messages.d.ts +3 -3
  50. package/dist/data/orm/sql.data-source.d.ts +5 -0
  51. package/dist/data/orm/sql.data-source.js +28 -10
  52. package/dist/data/orm/sql.data-source.test.d.ts +1 -1
  53. package/dist/data/orm/sql.data-source.test.js +160 -0
  54. package/dist/data/orm/table-container.d.ts +14 -0
  55. package/dist/data/orm/table-container.js +20 -0
  56. package/dist/data/tool-access-approvals.d.ts +6 -6
  57. package/dist/data/tool-catalog-fields.d.ts +31 -0
  58. package/dist/data/tool-catalog-fields.js +67 -0
  59. package/dist/data/tools.d.ts +38 -10
  60. package/dist/data/tools.js +31 -8
  61. package/dist/data/workflow-logs.d.ts +6 -2
  62. package/dist/data/workflow-logs.js +7 -5
  63. package/dist/data/workflow-runs.d.ts +6 -6
  64. package/dist/index.d.ts +0 -2
  65. package/dist/index.js +1 -3
  66. package/dist/package-installer/package-creator.d.ts +14 -5
  67. package/dist/package-installer/package-creator.js +92 -37
  68. package/dist/package-installer/package-creator.test.js +308 -23
  69. package/dist/package-installer/types.d.ts +8 -0
  70. package/dist/package-loader/contract-package-loader.d.ts +12 -3
  71. package/dist/package-loader/contract-package-loader.js +54 -50
  72. package/dist/package-loader/contract-package-loader.test.d.ts +1 -1
  73. package/dist/package-loader/contract-package-loader.test.js +172 -13
  74. package/dist/package-loader/host-authority-loading.test.js +16 -3
  75. package/dist/package-loader/isolated-artifact.d.ts +14 -2
  76. package/dist/package-loader/isolated-artifact.js +6 -0
  77. package/dist/package-loader/isolated-artifact.test.js +34 -0
  78. package/dist/package-loader/isolated-runtime.d.ts +14 -2
  79. package/dist/package-loader/isolated-table-gateway.d.ts +3 -1
  80. package/dist/package-loader/isolated-table-gateway.js +54 -5
  81. package/dist/package-loader/isolated-table-gateway.test.js +123 -0
  82. package/dist/package-loader/package-loader.d.ts +14 -2
  83. package/dist/package-loader/package-loader.js +184 -62
  84. package/dist/package-loader/package-loader.test.js +695 -2
  85. package/dist/package-loader/resolve-consumed-contracts.js +1 -1
  86. package/dist/system-ids.d.ts +6 -0
  87. package/dist/system-ids.js +7 -1
  88. package/dist/types/peers-package.d.ts +3 -2
  89. package/dist/types/workflow-run-context.d.ts +12 -2
  90. package/package.json +1 -1
  91. package/dist/tools/index.d.ts +0 -1
  92. package/dist/tools/index.js +0 -17
  93. package/dist/tools/tools-factory.d.ts +0 -5
  94. package/dist/tools/tools-factory.js +0 -34
@@ -1,5 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const mockBeforeToolSave = jest.fn();
4
+ jest.mock("../data/tools", () => {
5
+ const actual = jest.requireActual("../data/tools");
6
+ const contexts = new WeakMap();
7
+ return {
8
+ ...actual,
9
+ Tools: (context) => {
10
+ let rows = contexts.get(context);
11
+ if (!rows) {
12
+ rows = new Map();
13
+ contexts.set(context, rows);
14
+ }
15
+ const records = rows;
16
+ return {
17
+ list: async () => [...records.values()],
18
+ get: async (id) => records.get(id),
19
+ save: async (row, opts) => {
20
+ await mockBeforeToolSave(row);
21
+ if (!opts?.insertOnly || !records.has(row.toolId))
22
+ records.set(row.toolId, row);
23
+ return records.get(row.toolId);
24
+ },
25
+ update: async (patch) => {
26
+ const row = { ...records.get(patch.toolId), ...patch };
27
+ records.set(patch.toolId, row);
28
+ return row;
29
+ },
30
+ dataChanged: { subscribe: () => ({ unsubscribe: () => true }) },
31
+ };
32
+ },
33
+ };
34
+ });
3
35
  const mockGet = jest.fn();
4
36
  const mockSignAndSave = jest.fn();
5
37
  const mockComputePackageVersionHash = jest.fn((version, _tag, packageBundleFileHash, _routesBundleFileHash, _uiBundleFileHash) => `hash-${version}-${packageBundleFileHash}`);
@@ -10,14 +42,530 @@ jest.mock("../data/package-versions", () => ({
10
42
  }),
11
43
  computePackageVersionHash: (version, versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash) => mockComputePackageVersionHash(version, versionTag, packageBundleFileHash, routesBundleFileHash, uiBundleFileHash),
12
44
  }));
45
+ const builder_1 = require("../contracts/builder");
46
+ const contract_provider_router_1 = require("../contracts/contract-provider-router");
47
+ const contract_proxy_1 = require("../contracts/contract-proxy");
13
48
  const contract_resolvers_1 = require("../contracts/contract-resolvers");
49
+ const contract_transport_1 = require("../contracts/contract-transport");
14
50
  const registry_1 = require("../contracts/registry");
51
+ const tool_catalog_identity_1 = require("../contracts/tool-catalog-identity");
15
52
  const access_level_1 = require("../data/access-level");
53
+ const groupPermissions = require("../data/group-permissions");
54
+ const tools_1 = require("../data/tools");
16
55
  const system_ids_1 = require("../system-ids");
17
56
  const utils_1 = require("../utils");
18
57
  const isolated_artifact_1 = require("./isolated-artifact");
19
58
  const isolated_runtime_1 = require("./isolated-runtime");
20
59
  const package_loader_1 = require("./package-loader");
60
+ const consumedContracts = require("./resolve-consumed-contracts");
61
+ describe("host load invalidation", () => {
62
+ it.each([
63
+ ["contract", "quarantine"],
64
+ ["contract", "unload"],
65
+ ["legacy", "quarantine"],
66
+ ["legacy", "unload"],
67
+ ])("rejects %s tool invocation when %s completes during authorization", async (kind, action) => {
68
+ const packageId = (0, utils_1.newid)();
69
+ const contractId = (0, utils_1.newid)();
70
+ const context = { dataContextId: (0, utils_1.newid)() };
71
+ const loader = new package_loader_1.PackageLoader(context);
72
+ const toolFn = jest.fn(() => "must not execute");
73
+ loader.require = (() => {
74
+ const instance = {
75
+ tool: { name: "read", usageDescription: "Read", accessLevel: access_level_1.AccessLevel.Self },
76
+ toolFn,
77
+ };
78
+ (0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
79
+ contractId,
80
+ version: 1,
81
+ tools: { read: instance },
82
+ }));
83
+ return kind === "contract"
84
+ ? (0, builder_1.definePackage)((pkg) => {
85
+ pkg.packageId = packageId;
86
+ pkg.version = "1.0.0";
87
+ pkg.contract(contractId, 1, "Authorization race").tools = [instance];
88
+ })
89
+ : { packageId };
90
+ });
91
+ let entered;
92
+ const waiting = new Promise((resolve) => {
93
+ entered = resolve;
94
+ });
95
+ let release;
96
+ const permission = jest
97
+ .spyOn(groupPermissions, "getUserAccessLevel")
98
+ .mockImplementationOnce(() => {
99
+ entered();
100
+ return new Promise((resolve) => {
101
+ release = resolve;
102
+ });
103
+ });
104
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
105
+ let endpoint;
106
+ try {
107
+ await loader._loadBundle({ packageId, name: "Authorization race" }, kind === "contract"
108
+ ? 'module.exports.packageDefinition = require("fixture");'
109
+ : 'module.exports = require("fixture");', undefined, false, false);
110
+ // Legacy resolver-only bundles have no manifest to index; model an existing row.
111
+ const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
112
+ if (kind === "legacy")
113
+ await (0, tools_1.Tools)(context).save({
114
+ toolId,
115
+ name: "read",
116
+ code: "",
117
+ usageDescription: "Read",
118
+ inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
119
+ outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
120
+ contractId,
121
+ contractVersion: 1,
122
+ toolName: "read",
123
+ catalogIdentity: "deterministic",
124
+ catalogState: "resolved",
125
+ });
126
+ endpoint = await loader.resolveContractEndpoint({
127
+ contractId,
128
+ version: 1,
129
+ context: { dataContext: context, callerUserId: "self", localUserId: "self" },
130
+ notify: async () => undefined,
131
+ });
132
+ const call = endpoint.handleCall({
133
+ contractId,
134
+ version: 1,
135
+ member: "tool",
136
+ name: "read",
137
+ operation: "run",
138
+ args: [],
139
+ });
140
+ const rejection = expect(call).rejects.toThrow(action === "quarantine" ? /quarantined/i : /retired/);
141
+ await waiting;
142
+ if (action === "quarantine")
143
+ await (0, tools_1.Tools)(context).update({ toolId, catalogState: "quarantined" });
144
+ else
145
+ await loader.unloadPackage(packageId);
146
+ release(access_level_1.AccessLevel.Self);
147
+ await rejection;
148
+ expect(toolFn).not.toHaveBeenCalled();
149
+ }
150
+ finally {
151
+ release?.(access_level_1.AccessLevel.Self);
152
+ endpoint?.dispose();
153
+ permission.mockRestore();
154
+ warn.mockRestore();
155
+ await loader.dispose();
156
+ }
157
+ });
158
+ it("settles a fresh package's remote and consume handles after its own contract activates", async () => {
159
+ const packageId = (0, utils_1.newid)();
160
+ const contractId = (0, utils_1.newid)();
161
+ const context = { dataContextId: (0, utils_1.newid)() };
162
+ const loader = new package_loader_1.PackageLoader(context);
163
+ Object.assign(context, { packageLoader: loader });
164
+ let definition;
165
+ loader.require = (() => {
166
+ const instance = {
167
+ tool: { name: "read", usageDescription: "Read", accessLevel: access_level_1.AccessLevel.None },
168
+ toolFn: () => "own provider",
169
+ };
170
+ (0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
171
+ contractId,
172
+ version: 1,
173
+ tools: { read: instance },
174
+ }));
175
+ definition = (0, builder_1.definePackage)((pkg) => {
176
+ pkg.packageId = packageId;
177
+ pkg.version = "1.0.0";
178
+ pkg.contract(contractId, 1, "Own contract").tools = [instance];
179
+ pkg.remote(contractId, 1);
180
+ pkg.consumes(contractId, 1);
181
+ });
182
+ return definition;
183
+ });
184
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
185
+ try {
186
+ expect(loader.contractRegistry.getDefinition(contractId, 1)).toBeUndefined();
187
+ await expect(loader._loadBundle({ packageId, name: "Own contract" }, 'module.exports.packageDefinition = require("fixture");', undefined, false, false)).resolves.toMatchObject({ packageId });
188
+ const remoteHandle = definition.remotes?.[0].handle;
189
+ await expect(remoteHandle?.consumer).resolves.toMatchObject({ contractId, version: 1 });
190
+ const remote = (await remoteHandle?.consumer);
191
+ expect(typeof remote?.device((0, utils_1.newid)()).tools.read).toBe("function");
192
+ remote?.dispose();
193
+ const consumeHandle = definition.consumes[0].handle;
194
+ await expect(consumeHandle?.consumer).resolves.toMatchObject({ contractId, version: 1 });
195
+ const consumer = (await consumeHandle?.consumer);
196
+ await expect(consumer?.tools.read()).resolves.toBe("own provider");
197
+ consumer?.dispose();
198
+ }
199
+ finally {
200
+ warn.mockRestore();
201
+ await loader.dispose();
202
+ }
203
+ });
204
+ it.each([
205
+ "persistence",
206
+ "evaluation",
207
+ "invalidation",
208
+ ])("stages host resolvers until activation and retains the old implementation on %s failure", async (failure) => {
209
+ const packageId = (0, utils_1.newid)();
210
+ const contractId = (0, utils_1.newid)();
211
+ const definition = {
212
+ packageId,
213
+ version: "1.0.0",
214
+ contracts: [
215
+ {
216
+ contractId,
217
+ version: 1,
218
+ name: "Host",
219
+ description: "",
220
+ devTag: "dev",
221
+ tables: [],
222
+ observables: [],
223
+ tools: [{ name: "read", usageDescription: "", inputFields: [], outputFields: [] }],
224
+ },
225
+ ],
226
+ consumes: [],
227
+ alsoImplements: new Map(),
228
+ assistants: [],
229
+ appNavs: [],
230
+ tableDefinitions: [],
231
+ };
232
+ const context = { dataContextId: (0, utils_1.newid)() };
233
+ const otherContext = { dataContextId: (0, utils_1.newid)() };
234
+ const loader = new package_loader_1.PackageLoader(context);
235
+ const otherLoader = new package_loader_1.PackageLoader(otherContext);
236
+ let value = "OLD";
237
+ let failEvaluation = false;
238
+ let current = true;
239
+ const fixture = () => {
240
+ const captured = value;
241
+ (0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
242
+ contractId,
243
+ version: 1,
244
+ tools: {
245
+ read: {
246
+ tool: { name: "read", accessLevel: access_level_1.AccessLevel.None },
247
+ toolFn: () => captured,
248
+ },
249
+ },
250
+ }), { providerPackageId: packageId });
251
+ if (failEvaluation)
252
+ throw new Error("evaluation failed");
253
+ return definition;
254
+ };
255
+ loader.require = fixture;
256
+ otherLoader.require = fixture;
257
+ const load = (target = loader) => target._loadBundle({ packageId, name: "Host" }, 'module.exports.packageDefinition = require("fixture");', undefined, false, true, () => {
258
+ if (!current)
259
+ throw new Error("load invalidated");
260
+ });
261
+ const call = async (target = loader) => {
262
+ const endpoint = await target.resolveContractEndpoint({
263
+ contractId,
264
+ version: 1,
265
+ context: { callerUserId: "self", localUserId: "self" },
266
+ notify: async () => undefined,
267
+ });
268
+ try {
269
+ return await endpoint.handleCall({
270
+ contractId,
271
+ version: 1,
272
+ member: "tool",
273
+ name: "read",
274
+ operation: "run",
275
+ args: [],
276
+ });
277
+ }
278
+ finally {
279
+ endpoint.dispose();
280
+ }
281
+ };
282
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
283
+ let release = () => { };
284
+ let consumer;
285
+ let endpoint;
286
+ // External host transports own their routers, so loader-side invalidation
287
+ // cannot evict their cached endpoints directly.
288
+ const [clientTransport, serverTransport] = (0, contract_transport_1.inProcessTransportPair)();
289
+ const externalRouter = (0, contract_provider_router_1.createContractProviderRouter)(serverTransport, {
290
+ authorizeCall: () => ({ context: { dataContext: context } }),
291
+ resolveProviderPackageId: () => packageId,
292
+ resolveEndpoint: (request) => loader.resolveContractEndpoint(request),
293
+ });
294
+ const externalConsumer = (0, contract_proxy_1.createContractConsumer)(definition.contracts[0], clientTransport);
295
+ try {
296
+ await load();
297
+ consumer = await loader.resolveConsumedContract({ contractId, version: 1 });
298
+ expect(await consumer?.tools.read()).toBe("OLD");
299
+ expect(await externalConsumer.tools.read()).toBe("OLD");
300
+ endpoint = await loader.resolveContractEndpoint({
301
+ contractId,
302
+ version: 1,
303
+ context: {},
304
+ notify: async () => undefined,
305
+ });
306
+ expect(await call()).toBe("OLD");
307
+ value = "OTHER CONTEXT";
308
+ await load(otherLoader);
309
+ expect(await call()).toBe("OLD");
310
+ expect(await call(otherLoader)).toBe("OTHER CONTEXT");
311
+ value = "NEW";
312
+ let entered;
313
+ const waiting = new Promise((resolve) => {
314
+ entered = resolve;
315
+ });
316
+ const gate = new Promise((resolve) => {
317
+ release = resolve;
318
+ });
319
+ if (failure === "evaluation")
320
+ failEvaluation = true;
321
+ else
322
+ mockBeforeToolSave.mockImplementationOnce(async () => {
323
+ entered();
324
+ await gate;
325
+ if (failure === "persistence")
326
+ throw new Error("catalog disk failure");
327
+ current = false;
328
+ });
329
+ const loading = load();
330
+ if (failure !== "evaluation") {
331
+ await waiting;
332
+ // Candidate resolver registrations are invisible even while persistence waits.
333
+ expect(await call()).toBe("OLD");
334
+ release();
335
+ }
336
+ expect(await loading).toBeUndefined();
337
+ expect(await consumer?.tools.read()).toBe("OLD");
338
+ expect(await call()).toBe("OLD");
339
+ expect(await call(otherLoader)).toBe("OTHER CONTEXT");
340
+ expect((0, contract_resolvers_1.getContractResolver)(contractId, 1, packageId)).toBeUndefined();
341
+ current = true;
342
+ failEvaluation = false;
343
+ mockBeforeToolSave.mockReset();
344
+ await load();
345
+ expect(await call()).toBe("NEW");
346
+ expect(await consumer?.tools.read()).toBe("NEW");
347
+ expect(await externalConsumer.tools.read()).toBe("NEW");
348
+ await expect(endpoint.handleCall({
349
+ contractId,
350
+ version: 1,
351
+ member: "tool",
352
+ name: "read",
353
+ operation: "run",
354
+ args: [],
355
+ })).rejects.toThrow(/retired/);
356
+ await loader.unloadPackage(packageId);
357
+ await expect(consumer?.tools.read()).rejects.toThrow(/not a registered provider/);
358
+ await expect(externalConsumer.tools.read()).rejects.toThrow(/not a registered provider/);
359
+ expect((0, contract_resolvers_1.getContractResolver)(contractId, 1, packageId, context)).toBeUndefined();
360
+ expect(await call(otherLoader)).toBe("OTHER CONTEXT");
361
+ }
362
+ finally {
363
+ release();
364
+ consumer?.dispose();
365
+ externalConsumer.dispose();
366
+ externalRouter.dispose();
367
+ endpoint?.dispose();
368
+ mockBeforeToolSave.mockReset();
369
+ warn.mockRestore();
370
+ await loader.dispose();
371
+ await otherLoader.dispose();
372
+ }
373
+ });
374
+ it("retires cached legacy endpoints even when a bundle reuses its resolver function", async () => {
375
+ const packageId = (0, utils_1.newid)();
376
+ const contractId = (0, utils_1.newid)();
377
+ const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
378
+ let value = "OLD";
379
+ const resolver = () => {
380
+ const captured = value;
381
+ return {
382
+ contractId,
383
+ version: 1,
384
+ tools: {
385
+ read: {
386
+ tool: { name: "read", accessLevel: access_level_1.AccessLevel.None },
387
+ toolFn: () => captured,
388
+ },
389
+ },
390
+ };
391
+ };
392
+ loader.require = (() => {
393
+ (0, contract_resolvers_1.registerContractResolver)(contractId, 1, resolver);
394
+ return { packageId };
395
+ });
396
+ const load = () => loader._loadBundle({ packageId, name: "legacy" }, 'module.exports = require("fixture");', undefined, false, true);
397
+ const resolve = () => loader.resolveContractEndpoint({
398
+ contractId,
399
+ version: 1,
400
+ context: {},
401
+ notify: async () => undefined,
402
+ });
403
+ const call = (endpoint) => endpoint.handleCall({
404
+ contractId,
405
+ version: 1,
406
+ member: "tool",
407
+ name: "read",
408
+ operation: "run",
409
+ args: [],
410
+ });
411
+ const endpoints = [];
412
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
413
+ let release = () => { };
414
+ try {
415
+ await load();
416
+ const old = await resolve();
417
+ endpoints.push(old);
418
+ expect(await call(old)).toBe("OLD");
419
+ value = "NEW";
420
+ await load();
421
+ await expect(call(old)).rejects.toThrow(/retired/);
422
+ const current = await resolve();
423
+ endpoints.push(current);
424
+ expect(await call(current)).toBe("NEW");
425
+ let entered;
426
+ const waiting = new Promise((resolve) => {
427
+ entered = resolve;
428
+ });
429
+ const gate = new Promise((resolve) => {
430
+ release = resolve;
431
+ });
432
+ const assertTarget = loader.toolCatalog.assertTargetUsable.bind(loader.toolCatalog);
433
+ jest
434
+ .spyOn(loader.toolCatalog, "assertTargetUsable")
435
+ .mockImplementationOnce(async (target) => {
436
+ await assertTarget(target);
437
+ entered();
438
+ await gate;
439
+ });
440
+ const pending = call(current);
441
+ const rejected = expect(pending).rejects.toThrow(/retired/);
442
+ await waiting;
443
+ await loader.unloadPackage(packageId);
444
+ release();
445
+ await rejected;
446
+ await expect(call(current)).rejects.toThrow(/retired/);
447
+ }
448
+ finally {
449
+ release();
450
+ for (const endpoint of endpoints)
451
+ endpoint.dispose();
452
+ await loader.dispose();
453
+ warn.mockRestore();
454
+ }
455
+ });
456
+ it("loads a tool-free legacy bundle and rejects executable replacement without side effects", async () => {
457
+ const packageId = (0, utils_1.newid)();
458
+ const contractId = (0, utils_1.newid)();
459
+ const context = {
460
+ dataContextId: (0, utils_1.newid)(),
461
+ tableContainer: { registerTableDefinitions: jest.fn() },
462
+ };
463
+ const loader = new package_loader_1.PackageLoader(context);
464
+ const original = { packageId, appNavs: [] };
465
+ let bundle = original;
466
+ loader.require = ((id) => id === "@peers-app/peers-sdk"
467
+ ? { registerContractResolver: contract_resolvers_1.registerContractResolver }
468
+ : bundle);
469
+ const load = (code = 'module.exports = require("fixture");') => loader._loadBundle({ packageId, name: "legacy" }, code, undefined, false, true);
470
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
471
+ try {
472
+ expect(await load()).toBe(original);
473
+ const before = await (0, tools_1.Tools)(context).list();
474
+ bundle = {
475
+ packageId,
476
+ tableDefinitions: [{}],
477
+ toolInstances: [{ tool: { toolId: (0, utils_1.newid)() }, toolFn: jest.fn() }],
478
+ };
479
+ expect(await load(`require("@peers-app/peers-sdk").registerContractResolver("${contractId}", 1, () => ({})); module.exports = require("fixture");`)).toBeUndefined();
480
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("Failed to evaluate"), expect.stringContaining("ships legacy executable tools"));
481
+ expect(await (0, tools_1.Tools)(context).list()).toEqual(before);
482
+ expect(context.tableContainer.registerTableDefinitions).not.toHaveBeenCalled();
483
+ expect((0, contract_resolvers_1.getContractResolver)(contractId, 1, packageId, context)).toBeUndefined();
484
+ expect(loader.packageInstances[packageId]).toBe(original);
485
+ }
486
+ finally {
487
+ warn.mockRestore();
488
+ await loader.dispose();
489
+ }
490
+ });
491
+ it.each([
492
+ "dependencies",
493
+ "preparation",
494
+ ])("unload invalidates a host load waiting on %s", async (boundary) => {
495
+ const packageId = (0, utils_1.newid)();
496
+ const contractId = (0, utils_1.newid)();
497
+ const context = {
498
+ dataContextId: (0, utils_1.newid)(),
499
+ tableContainer: { registerTableDefinition: jest.fn() },
500
+ };
501
+ const loader = new package_loader_1.PackageLoader(context);
502
+ const definition = {
503
+ packageId,
504
+ version: "1.0.0",
505
+ contracts: [
506
+ {
507
+ contractId,
508
+ version: 1,
509
+ name: "Pending",
510
+ description: "",
511
+ tables: [],
512
+ observables: [],
513
+ tools: [],
514
+ },
515
+ ],
516
+ consumes: [],
517
+ alsoImplements: new Map(),
518
+ assistants: [],
519
+ appNavs: [],
520
+ tableDefinitions: [],
521
+ };
522
+ loader.require = (() => definition);
523
+ let release;
524
+ let entered;
525
+ const waiting = new Promise((resolve) => {
526
+ entered = resolve;
527
+ });
528
+ const gate = new Promise((resolve) => {
529
+ release = resolve;
530
+ });
531
+ const hold = async () => {
532
+ entered();
533
+ await gate;
534
+ };
535
+ const dependencySpy = jest
536
+ .spyOn(consumedContracts, "resolveConsumedContracts")
537
+ .mockResolvedValue(undefined);
538
+ const preparationSpy = jest
539
+ .spyOn(loader, "correctPackageVersion")
540
+ .mockResolvedValue(undefined);
541
+ if (boundary === "dependencies")
542
+ dependencySpy.mockImplementationOnce(hold);
543
+ else
544
+ preparationSpy.mockImplementationOnce(hold);
545
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
546
+ const load = () => loader._loadBundle({ packageId, name: "Pending" }, 'module.exports.packageDefinition = require("fixture");', undefined, false, false);
547
+ try {
548
+ const installing = load();
549
+ await waiting;
550
+ const removing = loader.unloadPackage(packageId);
551
+ if (boundary === "dependencies")
552
+ await removing;
553
+ release();
554
+ await Promise.all([installing, removing]);
555
+ expect(loader.contractRegistry.getProviderPackageId(contractId, 1)).toBeUndefined();
556
+ expect(loader.isContractInstalled(packageId)).toBe(false);
557
+ await load();
558
+ expect(loader.contractRegistry.getProviderPackageId(contractId, 1)).toBe(packageId);
559
+ }
560
+ finally {
561
+ release();
562
+ dependencySpy.mockRestore();
563
+ preparationSpy.mockRestore();
564
+ warn.mockRestore();
565
+ await loader.dispose();
566
+ }
567
+ });
568
+ });
21
569
  describe("PackageLoader.correctPackageVersion", () => {
22
570
  const activePvId = "pv-active";
23
571
  const pkg = {
@@ -88,8 +636,10 @@ describe("PackageLoader.contractRegistry", () => {
88
636
  expect(loaderB.contractRegistry).toBeInstanceOf(registry_1.ContractRegistry);
89
637
  expect(loaderA.contractRegistry).not.toBe(loaderB.contractRegistry);
90
638
  });
91
- it("installs system contracts into each new registry", () => {
639
+ it("installs system contracts after explicit readiness", async () => {
92
640
  const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-system" });
641
+ expect(loader.contractRegistry.resolve(system_ids_1.logsContractId, 1)).toBeUndefined();
642
+ await loader.catalogReady;
93
643
  expect(loader.contractRegistry.getProviderPackageId(system_ids_1.logsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
94
644
  expect(loader.contractRegistry.resolve(system_ids_1.logsContractId, 1)?.name).toBe("System Logs");
95
645
  expect(loader.contractRegistry.getProviderPackageId(system_ids_1.deviceOperationsContractId, 1)).toBe(system_ids_1.peersSystemPackageId);
@@ -143,6 +693,51 @@ describe("PackageLoader.contractRegistry", () => {
143
693
  expect(resolver).toHaveBeenCalledWith({ dataContext });
144
694
  endpoint.dispose();
145
695
  });
696
+ it("checks retained identity evidence even for an unscoped legacy resolver", async () => {
697
+ const dataContext = { dataContextId: "ctx-legacy-quarantine" };
698
+ const loader = new package_loader_1.PackageLoader(dataContext);
699
+ const contractId = (0, utils_1.newid)();
700
+ const toolFn = jest.fn();
701
+ (0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
702
+ contractId,
703
+ version: 1,
704
+ tools: { read: { toolFn, tool: {} } },
705
+ }));
706
+ const endpoint = await loader.resolveContractEndpoint({
707
+ contractId,
708
+ version: 1,
709
+ context: {},
710
+ notify: async () => undefined,
711
+ });
712
+ const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
713
+ await (0, tools_1.Tools)(dataContext).save({
714
+ toolId,
715
+ contractId,
716
+ contractVersion: 1,
717
+ toolName: "read",
718
+ name: "read",
719
+ usageDescription: "",
720
+ code: "",
721
+ inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
722
+ outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
723
+ catalogState: "quarantined",
724
+ });
725
+ try {
726
+ await expect(endpoint.handleCall({
727
+ contractId,
728
+ version: 1,
729
+ member: "tool",
730
+ name: "read",
731
+ operation: "run",
732
+ args: [],
733
+ })).rejects.toThrow(/Quarantined/);
734
+ expect(toolFn).not.toHaveBeenCalled();
735
+ }
736
+ finally {
737
+ endpoint.dispose();
738
+ await loader.dispose();
739
+ }
740
+ });
146
741
  it("does not supply local identity to ordinary callers that omit it", async () => {
147
742
  const userId = (0, utils_1.newid)();
148
743
  const dataContext = {
@@ -252,7 +847,90 @@ describe("PackageLoader isolated artifacts", () => {
252
847
  (0, isolated_runtime_1.setIsolatedPackageRuntimeFactory)(() => runtime);
253
848
  return runtime;
254
849
  }
255
- it("installs a valid isolated artifact without evaluating provider source", async () => {
850
+ it.each([
851
+ "unload",
852
+ "dispose",
853
+ ])("%s invalidates isolated loads awaiting dev version correction", async (action) => {
854
+ const runtime = makeRuntime();
855
+ const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
856
+ let release;
857
+ let entered;
858
+ const gate = new Promise((resolve) => {
859
+ release = resolve;
860
+ });
861
+ const waiting = new Promise((resolve) => {
862
+ entered = resolve;
863
+ });
864
+ mockSignAndSave.mockImplementationOnce(async () => {
865
+ entered();
866
+ await gate;
867
+ });
868
+ const load = () => loader._loadBundle(pkg, (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact), selectedVersion({ version: "0.0.1", versionTag: "dev" }), true, true);
869
+ try {
870
+ const installing = load();
871
+ await waiting;
872
+ if (action === "unload")
873
+ await loader.unloadPackage(packageId);
874
+ else
875
+ await loader.dispose();
876
+ release();
877
+ await expect(installing).rejects.toThrow(/invalidated|disposed/);
878
+ expect(runtime.install).not.toHaveBeenCalled();
879
+ expect(runtime.replace).not.toHaveBeenCalled();
880
+ expect(runtime.isInstalled(packageId)).toBe(false);
881
+ expect(loader.isIsolatedInstalled(packageId)).toBe(false);
882
+ expect(loader.isContractInstalled(packageId)).toBe(false);
883
+ if (action === "unload") {
884
+ await load();
885
+ expect(runtime.isInstalled(packageId)).toBe(true);
886
+ }
887
+ }
888
+ finally {
889
+ release();
890
+ await loader.dispose();
891
+ }
892
+ });
893
+ it("does not restore activation bookkeeping when unload overlaps an isolated install", async () => {
894
+ const runtime = makeRuntime();
895
+ const loader = new package_loader_1.PackageLoader({ dataContextId: (0, utils_1.newid)() });
896
+ let release;
897
+ let entered;
898
+ const gate = new Promise((resolve) => {
899
+ release = resolve;
900
+ });
901
+ const waiting = new Promise((resolve) => {
902
+ entered = resolve;
903
+ });
904
+ const install = runtime.install.getMockImplementation();
905
+ if (!install)
906
+ throw new Error("Missing runtime install fixture");
907
+ runtime.install.mockImplementationOnce(async (next) => {
908
+ const result = await install(next);
909
+ entered();
910
+ await gate;
911
+ return result;
912
+ });
913
+ const installing = loader._loadBundle(pkg, (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact), selectedVersion(), false, true);
914
+ try {
915
+ await waiting;
916
+ await loader.unloadPackage(packageId);
917
+ release();
918
+ await expect(installing).rejects.toThrow(/invalidated/);
919
+ expect(loader.isIsolatedInstalled(packageId)).toBe(false);
920
+ expect(loader.isContractInstalled(packageId)).toBe(false);
921
+ expect(runtime.isInstalled(packageId)).toBe(false);
922
+ }
923
+ finally {
924
+ release();
925
+ await loader.dispose();
926
+ }
927
+ });
928
+ it.each([
929
+ undefined,
930
+ "sandbox",
931
+ "privileged",
932
+ ])("installs executionMode=%s without host evaluation", async (executionMode) => {
933
+ artifact.manifest.executionMode = executionMode;
256
934
  const runtime = makeRuntime();
257
935
  mockGet.mockResolvedValue(selectedVersion());
258
936
  mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
@@ -405,6 +1083,21 @@ describe("PackageLoader isolated artifacts", () => {
405
1083
  await expect(loader.loadPackage(pkg)).rejects.toThrow(/JSON is invalid/);
406
1084
  expect(globalThis.__shouldNotEvalOnHost).toBeUndefined();
407
1085
  });
1086
+ it.each([
1087
+ null,
1088
+ true,
1089
+ "unknown",
1090
+ ])("fails closed on malformed executionMode=%p", async (executionMode) => {
1091
+ const runtime = makeRuntime();
1092
+ mockGet.mockResolvedValue(selectedVersion());
1093
+ const malformed = structuredClone(artifact);
1094
+ Object.assign(malformed.manifest, { executionMode });
1095
+ mockGetFileContents.mockResolvedValue(Buffer.from(`PEERS_ISOLATED_PACKAGE_V1\n${JSON.stringify(malformed)}`));
1096
+ const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-bad-mode" });
1097
+ await expect(loader.loadPackage(pkg)).rejects.toThrow(/executionMode/);
1098
+ expect(runtime.install).not.toHaveBeenCalled();
1099
+ expect(globalThis.__shouldNotEvalOnHost).toBeUndefined();
1100
+ });
408
1101
  it("passes gateways and a trusted local identity resolver from its data context", () => {
409
1102
  const factory = jest.fn((_options) => ({
410
1103
  install: jest.fn(async () => ({ packageId, provided: [], consumes: [] })),