@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.
- package/dist/contracts/__tests__/builder.test.js +0 -2
- package/dist/contracts/__tests__/contract-provider-router.test.js +51 -0
- package/dist/contracts/__tests__/contract-proxy.test.js +21 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +84 -0
- package/dist/contracts/__tests__/legacy-removal.test.d.ts +1 -0
- package/dist/contracts/__tests__/legacy-removal.test.js +90 -0
- package/dist/contracts/__tests__/system-contracts.test.js +88 -0
- package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog-inventory.test.js +44 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.js +446 -0
- package/dist/contracts/__tests__/tool-catalog.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog.test.js +649 -0
- package/dist/contracts/__tests__/workflow-context.test.d.ts +1 -0
- package/dist/contracts/__tests__/workflow-context.test.js +61 -0
- package/dist/contracts/builder.d.ts +9 -8
- package/dist/contracts/builder.js +34 -16
- package/dist/contracts/contract-provider-router.d.ts +12 -1
- package/dist/contracts/contract-provider-router.js +40 -2
- package/dist/contracts/contract-proxy.d.ts +20 -6
- package/dist/contracts/contract-proxy.js +23 -12
- package/dist/contracts/contract-resolvers.d.ts +22 -3
- package/dist/contracts/contract-resolvers.js +86 -7
- package/dist/contracts/extract.d.ts +7 -0
- package/dist/contracts/extract.js +3 -0
- package/dist/contracts/index.d.ts +11 -4
- package/dist/contracts/index.js +14 -2
- package/dist/contracts/registry.d.ts +3 -0
- package/dist/contracts/registry.js +2 -0
- package/dist/contracts/system/desktop-tools-contract.d.ts +5 -0
- package/dist/contracts/system/desktop-tools-contract.js +88 -0
- package/dist/contracts/system/host-tools-contract.d.ts +5 -0
- package/dist/contracts/system/host-tools-contract.js +76 -0
- package/dist/contracts/system/index.d.ts +3 -0
- package/dist/contracts/system/index.js +3 -0
- package/dist/contracts/system/tool-catalog-contract.d.ts +73 -0
- package/dist/contracts/system/tool-catalog-contract.js +70 -0
- package/dist/contracts/system-contracts.d.ts +3 -1
- package/dist/contracts/system-contracts.js +20 -6
- package/dist/contracts/tool-catalog-identity.d.ts +35 -0
- package/dist/contracts/tool-catalog-identity.js +88 -0
- package/dist/contracts/tool-catalog-migrations.d.ts +12 -0
- package/dist/contracts/tool-catalog-migrations.js +179 -0
- package/dist/contracts/tool-catalog-service.d.ts +30 -0
- package/dist/contracts/tool-catalog-service.js +287 -0
- package/dist/contracts/tool-catalog.d.ts +91 -0
- package/dist/contracts/tool-catalog.js +490 -0
- package/dist/contracts/types.d.ts +7 -5
- package/dist/data/messages.d.ts +3 -3
- package/dist/data/orm/sql.data-source.d.ts +5 -0
- package/dist/data/orm/sql.data-source.js +28 -10
- package/dist/data/orm/sql.data-source.test.d.ts +1 -1
- package/dist/data/orm/sql.data-source.test.js +160 -0
- package/dist/data/orm/table-container.d.ts +14 -0
- package/dist/data/orm/table-container.js +20 -0
- package/dist/data/tool-access-approvals.d.ts +6 -6
- package/dist/data/tool-catalog-fields.d.ts +31 -0
- package/dist/data/tool-catalog-fields.js +67 -0
- package/dist/data/tools.d.ts +38 -10
- package/dist/data/tools.js +31 -8
- package/dist/data/workflow-logs.d.ts +6 -2
- package/dist/data/workflow-logs.js +7 -5
- package/dist/data/workflow-runs.d.ts +6 -6
- package/dist/index.d.ts +0 -2
- package/dist/index.js +1 -3
- package/dist/package-installer/package-creator.d.ts +14 -5
- package/dist/package-installer/package-creator.js +92 -37
- package/dist/package-installer/package-creator.test.js +308 -23
- package/dist/package-installer/types.d.ts +8 -0
- package/dist/package-loader/contract-package-loader.d.ts +12 -3
- package/dist/package-loader/contract-package-loader.js +54 -50
- package/dist/package-loader/contract-package-loader.test.d.ts +1 -1
- package/dist/package-loader/contract-package-loader.test.js +172 -13
- package/dist/package-loader/host-authority-loading.test.js +16 -3
- package/dist/package-loader/isolated-artifact.d.ts +14 -2
- package/dist/package-loader/isolated-artifact.js +6 -0
- package/dist/package-loader/isolated-artifact.test.js +34 -0
- package/dist/package-loader/isolated-runtime.d.ts +14 -2
- package/dist/package-loader/isolated-table-gateway.d.ts +3 -1
- package/dist/package-loader/isolated-table-gateway.js +54 -5
- package/dist/package-loader/isolated-table-gateway.test.js +123 -0
- package/dist/package-loader/package-loader.d.ts +14 -2
- package/dist/package-loader/package-loader.js +184 -62
- package/dist/package-loader/package-loader.test.js +695 -2
- package/dist/package-loader/resolve-consumed-contracts.js +1 -1
- package/dist/system-ids.d.ts +6 -0
- package/dist/system-ids.js +7 -1
- package/dist/types/peers-package.d.ts +3 -2
- package/dist/types/workflow-run-context.d.ts +12 -2
- package/package.json +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -17
- package/dist/tools/tools-factory.d.ts +0 -5
- package/dist/tools/tools-factory.js +0 -34
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const system_ids_1 = require("../system-ids");
|
|
4
|
+
const utils_1 = require("../utils");
|
|
3
5
|
const package_creator_1 = require("./package-creator");
|
|
4
6
|
const package_dev_tools_1 = require("./package-dev-tools");
|
|
5
7
|
// --- Mocks for SDK internals ---
|
|
6
8
|
const mockSignAndSavePkg = jest.fn();
|
|
7
9
|
const mockGetPkg = jest.fn();
|
|
8
10
|
jest.mock("../data/packages", () => ({
|
|
9
|
-
Packages: () => ({
|
|
10
|
-
signAndSave: mockSignAndSavePkg,
|
|
11
|
-
get: mockGetPkg,
|
|
11
|
+
Packages: (dataContext) => ({
|
|
12
|
+
signAndSave: (...args) => mockSignAndSavePkg(dataContext, ...args),
|
|
13
|
+
get: (packageId) => mockGetPkg(dataContext, packageId),
|
|
12
14
|
}),
|
|
13
15
|
}));
|
|
14
16
|
const mockGroupDeviceVar = jest.fn();
|
|
@@ -36,6 +38,10 @@ jest.mock("./package-installer", () => ({
|
|
|
36
38
|
installPackageFromBundles: (...args) => mockInstallPackageFromBundles(...args),
|
|
37
39
|
getPackageInfo: (...args) => mockGetPackageInfo(...args),
|
|
38
40
|
}));
|
|
41
|
+
const { newKeys: realNewKeys } = jest.requireActual("../keys");
|
|
42
|
+
const generatedKeys = realNewKeys();
|
|
43
|
+
const sourceKeys = realNewKeys();
|
|
44
|
+
const targetKeys = realNewKeys();
|
|
39
45
|
// --- Test utilities ---
|
|
40
46
|
function makeMockDeps(files, shellExec) {
|
|
41
47
|
return {
|
|
@@ -67,6 +73,7 @@ function makeMockDeps(files, shellExec) {
|
|
|
67
73
|
}
|
|
68
74
|
function mockDataContext() {
|
|
69
75
|
return {
|
|
76
|
+
dataContextId: (0, utils_1.newid)(),
|
|
70
77
|
packageLoader: {
|
|
71
78
|
loadPackage: jest.fn().mockResolvedValue(undefined),
|
|
72
79
|
},
|
|
@@ -75,13 +82,10 @@ function mockDataContext() {
|
|
|
75
82
|
// --- Tests ---
|
|
76
83
|
beforeEach(() => {
|
|
77
84
|
jest.clearAllMocks();
|
|
78
|
-
mockGetPkg.mockResolvedValue(undefined);
|
|
79
|
-
mockPersistentVarsFindOne.mockResolvedValue(undefined);
|
|
80
|
-
mockNewKeys.mockReturnValue(
|
|
81
|
-
|
|
82
|
-
secretKey: "test-secret-key-xyz",
|
|
83
|
-
publicBoxKey: "test-box-key",
|
|
84
|
-
});
|
|
85
|
+
mockGetPkg.mockReset().mockResolvedValue(undefined);
|
|
86
|
+
mockPersistentVarsFindOne.mockReset().mockResolvedValue(undefined);
|
|
87
|
+
mockNewKeys.mockReturnValue(generatedKeys);
|
|
88
|
+
mockPersistentVarsSave.mockReset();
|
|
85
89
|
});
|
|
86
90
|
describe("unwrapAddOrUpdatePackageResult", () => {
|
|
87
91
|
it("accepts a legacy packageId string", () => {
|
|
@@ -204,7 +208,7 @@ describe("createPackage", () => {
|
|
|
204
208
|
cwd: "/packages/my-widget",
|
|
205
209
|
});
|
|
206
210
|
// Verify package record was created
|
|
207
|
-
expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({
|
|
211
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(dc, expect.objectContaining({
|
|
208
212
|
name: "My Widget",
|
|
209
213
|
disabled: false,
|
|
210
214
|
}), { restoreIfDeleted: true, saveAsSnapshot: true });
|
|
@@ -273,18 +277,18 @@ describe("createPackage", () => {
|
|
|
273
277
|
personalContext: personalCtx,
|
|
274
278
|
});
|
|
275
279
|
expect(deps.shell.exec).not.toHaveBeenCalled();
|
|
276
|
-
expect(mockGetPkg).toHaveBeenCalledWith("00mnkoyo7mizav6tseubjzr3a");
|
|
280
|
+
expect(mockGetPkg).toHaveBeenCalledWith(dc, "00mnkoyo7mizav6tseubjzr3a");
|
|
277
281
|
expect(mockNewKeys).toHaveBeenCalled();
|
|
278
282
|
expect(mockPersistentVarsSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
279
283
|
name: "packageSigningKey_00mnkoyo7mizav6tseubjzr3a",
|
|
280
284
|
isSecret: true,
|
|
281
|
-
value: { value:
|
|
282
|
-
}));
|
|
283
|
-
expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({
|
|
285
|
+
value: { value: generatedKeys.secretKey },
|
|
286
|
+
}), { restoreIfDeleted: true });
|
|
287
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(dc, expect.objectContaining({
|
|
284
288
|
packageId: "00mnkoyo7mizav6tseubjzr3a",
|
|
285
289
|
name: "Existing Pkg",
|
|
286
290
|
description: "Imported package",
|
|
287
|
-
publishPublicKey:
|
|
291
|
+
publishPublicKey: generatedKeys.publicKey,
|
|
288
292
|
}), { restoreIfDeleted: true, saveAsSnapshot: true });
|
|
289
293
|
expect(mockGroupDeviceVar).toHaveBeenCalledWith("packageLocalPath_00mnkoyo7mizav6tseubjzr3a", expect.any(Object));
|
|
290
294
|
expect(pvarSetter).toHaveBeenCalledWith("/custom/path");
|
|
@@ -324,11 +328,145 @@ describe("createPackage", () => {
|
|
|
324
328
|
});
|
|
325
329
|
expect(mockNewKeys).not.toHaveBeenCalled();
|
|
326
330
|
expect(mockPersistentVarsSave).not.toHaveBeenCalled();
|
|
327
|
-
expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({
|
|
331
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(dc, expect.objectContaining({
|
|
328
332
|
publishPublicKey: "already-established-public-key",
|
|
329
333
|
createdBy: "00m87fbwu96jzf8qnwllly0fd",
|
|
330
334
|
}), expect.any(Object));
|
|
331
335
|
});
|
|
336
|
+
describe("importing by name across contexts", () => {
|
|
337
|
+
const packageId = "00mnkoyo7mizav6tseubjzr3a";
|
|
338
|
+
let contexts;
|
|
339
|
+
let deps;
|
|
340
|
+
beforeEach(() => {
|
|
341
|
+
contexts = {
|
|
342
|
+
personal: mockDataContext(),
|
|
343
|
+
blankGroup: mockDataContext(),
|
|
344
|
+
group: mockDataContext(),
|
|
345
|
+
otherGroup: mockDataContext(),
|
|
346
|
+
};
|
|
347
|
+
const userContext = {
|
|
348
|
+
userId: contexts.personal.dataContextId,
|
|
349
|
+
userDataContext: contexts.personal,
|
|
350
|
+
groupDataContexts: new Map(Object.values(contexts)
|
|
351
|
+
.slice(1)
|
|
352
|
+
.map((dc) => [dc.dataContextId, dc])),
|
|
353
|
+
};
|
|
354
|
+
for (const dc of Object.values(contexts))
|
|
355
|
+
dc.userContext = userContext;
|
|
356
|
+
deps = makeMockDeps({ "/packages/existing-pkg/package.json": "{}" });
|
|
357
|
+
mockGetPackageInfo.mockResolvedValue({ packageId, description: "Imported package" });
|
|
358
|
+
mockGroupDeviceVar.mockReturnValue(jest.fn());
|
|
359
|
+
mockInstallPackageFromBundles.mockResolvedValue({ packageVersion: { packageId } });
|
|
360
|
+
mockPersistentVarsFindOne.mockResolvedValue({ value: { value: "unreadable-secret" } });
|
|
361
|
+
});
|
|
362
|
+
it.each([
|
|
363
|
+
["group", "personal", ["personal", "blankGroup", "group"]],
|
|
364
|
+
["group", "otherGroup", ["otherGroup", "personal", "blankGroup", "group"]],
|
|
365
|
+
["personal", "group", ["group", "personal"]],
|
|
366
|
+
])("reuses the key from %s when importing into %s", async (source, target, lookups) => {
|
|
367
|
+
mockGetPkg.mockImplementation(async (dc, id) => {
|
|
368
|
+
if (id !== packageId || dc === contexts[target])
|
|
369
|
+
return undefined;
|
|
370
|
+
if (dc === contexts[source]) {
|
|
371
|
+
return {
|
|
372
|
+
publishPublicKey: sourceKeys.publicKey,
|
|
373
|
+
name: "Source name",
|
|
374
|
+
description: "Source description",
|
|
375
|
+
createdBy: (0, utils_1.newid)(),
|
|
376
|
+
disabled: true,
|
|
377
|
+
activePackageVersionId: (0, utils_1.newid)(),
|
|
378
|
+
versionFollowRange: "^1.0.0",
|
|
379
|
+
followVersionTags: ["stable"],
|
|
380
|
+
updateUrl: "source-url",
|
|
381
|
+
remoteRepo: "source-repo",
|
|
382
|
+
signature: "source-signature",
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (dc === contexts.personal || dc === contexts.blankGroup) {
|
|
386
|
+
return { publishPublicKey: " \t " };
|
|
387
|
+
}
|
|
388
|
+
return undefined;
|
|
389
|
+
});
|
|
390
|
+
const result = await (0, package_creator_1.createPackage)(contexts[target], deps, {
|
|
391
|
+
name: "Existing Pkg",
|
|
392
|
+
personalContext: contexts.personal,
|
|
393
|
+
});
|
|
394
|
+
expect(mockGetPkg.mock.calls).toEqual(lookups.map((name) => [contexts[name], packageId]));
|
|
395
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(contexts[target], expect.objectContaining({
|
|
396
|
+
packageId,
|
|
397
|
+
name: "Existing Pkg",
|
|
398
|
+
description: "Imported package",
|
|
399
|
+
publishPublicKey: sourceKeys.publicKey,
|
|
400
|
+
createdBy: system_ids_1.peersRootUserId,
|
|
401
|
+
disabled: false,
|
|
402
|
+
activePackageVersionId: undefined,
|
|
403
|
+
versionFollowRange: undefined,
|
|
404
|
+
followVersionTags: undefined,
|
|
405
|
+
updateUrl: undefined,
|
|
406
|
+
remoteRepo: undefined,
|
|
407
|
+
signature: "",
|
|
408
|
+
}), { restoreIfDeleted: true, saveAsSnapshot: true });
|
|
409
|
+
expect(mockInstallPackageFromBundles).toHaveBeenCalledWith(contexts[target], deps, packageId, {
|
|
410
|
+
localPath: "/packages/existing-pkg",
|
|
411
|
+
});
|
|
412
|
+
expect(result.packageVersion).toEqual({ packageId });
|
|
413
|
+
expect(mockNewKeys).not.toHaveBeenCalled();
|
|
414
|
+
expect(mockPersistentVarsSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
415
|
+
value: { value: "unreadable-secret" },
|
|
416
|
+
description: expect.stringContaining(`v1:${packageId}:${sourceKeys.publicKey}`),
|
|
417
|
+
}));
|
|
418
|
+
});
|
|
419
|
+
it("keeps the target key without looking up a differing source key", async () => {
|
|
420
|
+
mockGetPkg.mockImplementation(async (dc) => ({
|
|
421
|
+
publishPublicKey: dc === contexts.otherGroup ? targetKeys.publicKey : sourceKeys.publicKey,
|
|
422
|
+
}));
|
|
423
|
+
await (0, package_creator_1.createPackage)(contexts.otherGroup, deps, {
|
|
424
|
+
name: "Existing Pkg",
|
|
425
|
+
personalContext: contexts.personal,
|
|
426
|
+
});
|
|
427
|
+
expect(mockGetPkg.mock.calls).toEqual([[contexts.otherGroup, packageId]]);
|
|
428
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(contexts.otherGroup, expect.objectContaining({ publishPublicKey: targetKeys.publicKey }), expect.any(Object));
|
|
429
|
+
expect(mockNewKeys).not.toHaveBeenCalled();
|
|
430
|
+
expect(mockPersistentVarsSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
431
|
+
value: { value: "unreadable-secret" },
|
|
432
|
+
description: expect.stringContaining(`v1:${packageId}:${targetKeys.publicKey}`),
|
|
433
|
+
}));
|
|
434
|
+
});
|
|
435
|
+
it.each([
|
|
436
|
+
"missing",
|
|
437
|
+
"mismatch",
|
|
438
|
+
"lookup-error",
|
|
439
|
+
"empty-target",
|
|
440
|
+
])("fails without installing or changing keys for %s", async (failure) => {
|
|
441
|
+
mockGetPkg.mockImplementation(async (dc) => {
|
|
442
|
+
if (dc === contexts.otherGroup) {
|
|
443
|
+
return failure === "empty-target" ? { publishPublicKey: "" } : undefined;
|
|
444
|
+
}
|
|
445
|
+
if (failure === "lookup-error")
|
|
446
|
+
throw new Error("source lookup failed");
|
|
447
|
+
return failure === "missing" ? undefined : { publishPublicKey: sourceKeys.publicKey };
|
|
448
|
+
});
|
|
449
|
+
if (failure === "mismatch") {
|
|
450
|
+
const { newKeys: realNewKeys } = jest.requireActual("../keys");
|
|
451
|
+
mockPersistentVarsFindOne.mockResolvedValue({ value: { value: realNewKeys().secretKey } });
|
|
452
|
+
}
|
|
453
|
+
await expect((0, package_creator_1.createPackage)(contexts.otherGroup, deps, {
|
|
454
|
+
name: "Existing Pkg",
|
|
455
|
+
personalContext: contexts.personal,
|
|
456
|
+
})).rejects.toThrow(failure === "mismatch"
|
|
457
|
+
? "signing key mismatch"
|
|
458
|
+
: failure === "lookup-error"
|
|
459
|
+
? "source lookup failed"
|
|
460
|
+
: "secret could not be hydrated");
|
|
461
|
+
if (failure === "empty-target") {
|
|
462
|
+
expect(mockGetPkg.mock.calls).toEqual([[contexts.otherGroup, packageId]]);
|
|
463
|
+
}
|
|
464
|
+
expect(mockSignAndSavePkg).not.toHaveBeenCalled();
|
|
465
|
+
expect(mockInstallPackageFromBundles).not.toHaveBeenCalled();
|
|
466
|
+
expect(mockNewKeys).not.toHaveBeenCalled();
|
|
467
|
+
expect(mockPersistentVarsSave).not.toHaveBeenCalled();
|
|
468
|
+
});
|
|
469
|
+
});
|
|
332
470
|
it("throws when directory exists but has no valid package", async () => {
|
|
333
471
|
const deps = makeMockDeps({});
|
|
334
472
|
const dc = mockDataContext();
|
|
@@ -410,13 +548,13 @@ describe("createPackage", () => {
|
|
|
410
548
|
expect(mockPersistentVarsSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
411
549
|
name: expect.stringMatching(/^packageSigningKey_[a-z0-9]{25}$/),
|
|
412
550
|
scope: "user",
|
|
413
|
-
value: { value:
|
|
551
|
+
value: { value: generatedKeys.secretKey },
|
|
414
552
|
isSecret: true,
|
|
415
553
|
description: expect.stringContaining("Signed Pkg"),
|
|
416
|
-
}));
|
|
554
|
+
}), { restoreIfDeleted: true });
|
|
417
555
|
// Verify publishPublicKey was set on the package record
|
|
418
|
-
expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({
|
|
419
|
-
publishPublicKey:
|
|
556
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(dc, expect.objectContaining({
|
|
557
|
+
publishPublicKey: generatedKeys.publicKey,
|
|
420
558
|
}), expect.any(Object));
|
|
421
559
|
});
|
|
422
560
|
it("skips key generation when personalContext is not provided", async () => {
|
|
@@ -441,7 +579,7 @@ describe("createPackage", () => {
|
|
|
441
579
|
// Should NOT save any pvar
|
|
442
580
|
expect(mockPersistentVarsSave).not.toHaveBeenCalled();
|
|
443
581
|
// publishPublicKey should be empty string
|
|
444
|
-
expect(mockSignAndSavePkg).toHaveBeenCalledWith(expect.objectContaining({
|
|
582
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(dc, expect.objectContaining({
|
|
445
583
|
publishPublicKey: "",
|
|
446
584
|
}), expect.any(Object));
|
|
447
585
|
});
|
|
@@ -454,9 +592,14 @@ describe("ensurePackageSigningKey", () => {
|
|
|
454
592
|
packageId: "00mnkoyo7mizav6tseubjzr3a",
|
|
455
593
|
packageName: "Test Pkg",
|
|
456
594
|
});
|
|
457
|
-
expect(publicKey).toBe(
|
|
595
|
+
expect(publicKey).toBe(generatedKeys.publicKey);
|
|
458
596
|
expect(mockNewKeys).toHaveBeenCalledTimes(1);
|
|
459
597
|
expect(mockPersistentVarsSave).toHaveBeenCalledTimes(1);
|
|
598
|
+
expect(mockPersistentVarsSave).toHaveBeenCalledWith(expect.objectContaining({
|
|
599
|
+
persistentVarId: (0, utils_1.deterministicPvarId)("packageSigningKey_00mnkoyo7mizav6tseubjzr3a"),
|
|
600
|
+
value: { value: generatedKeys.secretKey },
|
|
601
|
+
description: `Signing key for package "Test Pkg"\npeers-package-signing-key:v1:00mnkoyo7mizav6tseubjzr3a:${generatedKeys.publicKey}`,
|
|
602
|
+
}), { restoreIfDeleted: true });
|
|
460
603
|
});
|
|
461
604
|
it("reuses an established publishPublicKey without generating a new key", async () => {
|
|
462
605
|
const personalCtx = mockDataContext();
|
|
@@ -489,6 +632,148 @@ describe("ensurePackageSigningKey", () => {
|
|
|
489
632
|
expect(mockNewKeys).not.toHaveBeenCalled();
|
|
490
633
|
});
|
|
491
634
|
});
|
|
635
|
+
describe("signing key recovery after uninstall", () => {
|
|
636
|
+
const packageId = "00mnkoyo7mizav6tseubjzr3a";
|
|
637
|
+
const marker = `peers-package-signing-key:v1:${packageId}:${generatedKeys.publicKey}`;
|
|
638
|
+
let record;
|
|
639
|
+
let deps;
|
|
640
|
+
let personalContext;
|
|
641
|
+
let target;
|
|
642
|
+
beforeEach(() => {
|
|
643
|
+
personalContext = mockDataContext();
|
|
644
|
+
target = mockDataContext();
|
|
645
|
+
record = {
|
|
646
|
+
persistentVarId: (0, utils_1.deterministicPvarId)(`packageSigningKey_${packageId}`),
|
|
647
|
+
name: `packageSigningKey_${packageId}`,
|
|
648
|
+
scope: "user",
|
|
649
|
+
isSecret: true,
|
|
650
|
+
value: { value: "encrypted-secret~nonce" },
|
|
651
|
+
description: 'Signing key for package "Test Pkg"',
|
|
652
|
+
modifiedAt: 123,
|
|
653
|
+
};
|
|
654
|
+
mockPersistentVarsFindOne.mockImplementation(async () => record);
|
|
655
|
+
mockPersistentVarsSave.mockImplementation(async (saved) => {
|
|
656
|
+
record = saved;
|
|
657
|
+
});
|
|
658
|
+
deps = makeMockDeps({ "/packages/test-pkg/package.json": "{}" });
|
|
659
|
+
mockGetPackageInfo.mockResolvedValue({ packageId, name: "test-pkg" });
|
|
660
|
+
mockGroupDeviceVar.mockReturnValue(jest.fn());
|
|
661
|
+
mockInstallPackageFromBundles.mockResolvedValue({ packageVersion: { packageId } });
|
|
662
|
+
});
|
|
663
|
+
async function importPackage(resolveSigningPublicKey) {
|
|
664
|
+
return (0, package_creator_1.createPackage)(target, deps, {
|
|
665
|
+
name: "Test Pkg",
|
|
666
|
+
personalContext,
|
|
667
|
+
resolveSigningPublicKey,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
function expectNoWrites() {
|
|
671
|
+
expect(mockPersistentVarsSave).not.toHaveBeenCalled();
|
|
672
|
+
expect(mockNewKeys).not.toHaveBeenCalled();
|
|
673
|
+
expect(mockSignAndSavePkg).not.toHaveBeenCalled();
|
|
674
|
+
expect(mockGroupDeviceVar).not.toHaveBeenCalled();
|
|
675
|
+
expect(mockInstallPackageFromBundles).not.toHaveBeenCalled();
|
|
676
|
+
}
|
|
677
|
+
it("recovers a cached public key with no Packages row or decryption callback", async () => {
|
|
678
|
+
record.description += `\n${marker}`;
|
|
679
|
+
await importPackage();
|
|
680
|
+
expect(mockSignAndSavePkg).toHaveBeenCalledWith(target, expect.objectContaining({ publishPublicKey: generatedKeys.publicKey }), expect.any(Object));
|
|
681
|
+
expect(mockPersistentVarsSave).not.toHaveBeenCalled();
|
|
682
|
+
expect(mockNewKeys).not.toHaveBeenCalled();
|
|
683
|
+
});
|
|
684
|
+
it("recovers a legacy encrypted secret through the host and backfills only description once", async () => {
|
|
685
|
+
const original = { ...record };
|
|
686
|
+
const resolve = jest.fn(async () => generatedKeys.publicKey);
|
|
687
|
+
await importPackage(resolve);
|
|
688
|
+
expect(resolve).toHaveBeenCalledWith(original.value.value);
|
|
689
|
+
expect(mockPersistentVarsSave).toHaveBeenCalledWith({
|
|
690
|
+
...original,
|
|
691
|
+
description: `${original.description}\n${marker}`,
|
|
692
|
+
});
|
|
693
|
+
await importPackage(resolve);
|
|
694
|
+
expect(mockPersistentVarsSave).toHaveBeenCalledTimes(1);
|
|
695
|
+
expect(record.value).toBe(original.value);
|
|
696
|
+
expect(mockNewKeys).not.toHaveBeenCalled();
|
|
697
|
+
});
|
|
698
|
+
it("retains raw-secret hydration and backfills its public key without invoking the host", async () => {
|
|
699
|
+
record.value = { value: generatedKeys.secretKey };
|
|
700
|
+
const resolve = jest.fn();
|
|
701
|
+
await importPackage(resolve);
|
|
702
|
+
expect(resolve).not.toHaveBeenCalled();
|
|
703
|
+
expect(record.description).toContain(marker);
|
|
704
|
+
expect(record.value.value).toBe(generatedKeys.secretKey);
|
|
705
|
+
});
|
|
706
|
+
it.each([
|
|
707
|
+
"absent",
|
|
708
|
+
"throws",
|
|
709
|
+
"undefined",
|
|
710
|
+
"invalid",
|
|
711
|
+
])("fails safely when host recovery is %s", async (mode) => {
|
|
712
|
+
const resolve = mode === "absent"
|
|
713
|
+
? undefined
|
|
714
|
+
: jest.fn(async () => {
|
|
715
|
+
if (mode === "throws")
|
|
716
|
+
throw new Error(`sensitive ${record.value.value} ${generatedKeys.secretKey}`);
|
|
717
|
+
return mode === "invalid" ? "not-a-public-key" : undefined;
|
|
718
|
+
});
|
|
719
|
+
await expect(importPackage(resolve)).rejects.toThrow(`Package "Test Pkg" (${packageId}) has a signing secret pvar but no recoverable publishPublicKey, ` +
|
|
720
|
+
"and the secret could not be hydrated. Restore publishPublicKey or unlock the signing secret before importing.");
|
|
721
|
+
expectNoWrites();
|
|
722
|
+
});
|
|
723
|
+
it.each([
|
|
724
|
+
"established",
|
|
725
|
+
"cached",
|
|
726
|
+
])("uses the %s key when host decryption fails", async (source) => {
|
|
727
|
+
if (source === "cached")
|
|
728
|
+
record.description += `\n${marker}`;
|
|
729
|
+
else
|
|
730
|
+
mockGetPkg.mockResolvedValue({ publishPublicKey: generatedKeys.publicKey });
|
|
731
|
+
await importPackage(async () => {
|
|
732
|
+
throw new Error("decryption unavailable");
|
|
733
|
+
});
|
|
734
|
+
expect(record.description).toContain(marker);
|
|
735
|
+
expect(record.value.value).toBe("encrypted-secret~nonce");
|
|
736
|
+
expect(mockNewKeys).not.toHaveBeenCalled();
|
|
737
|
+
});
|
|
738
|
+
it.each([
|
|
739
|
+
"",
|
|
740
|
+
`peers-package-signing-key:v2:${packageId}:${generatedKeys.publicKey}`,
|
|
741
|
+
`peers-package-signing-key:v1:${(0, utils_1.newid)()}:${generatedKeys.publicKey}`,
|
|
742
|
+
`peers-package-signing-key:v1:${packageId}:invalid-key`,
|
|
743
|
+
`${marker}:extra`,
|
|
744
|
+
`prefix ${marker}`,
|
|
745
|
+
`${marker}\n${marker}`,
|
|
746
|
+
])("does not trust malformed or unrelated metadata: %s", async (description) => {
|
|
747
|
+
record.description = description;
|
|
748
|
+
await expect(importPackage()).rejects.toThrow("secret could not be hydrated");
|
|
749
|
+
expectNoWrites();
|
|
750
|
+
});
|
|
751
|
+
it("repairs malformed metadata after recovering a valid key", async () => {
|
|
752
|
+
record.description += `\npeers-package-signing-key:v1:${packageId}:invalid`;
|
|
753
|
+
await importPackage(async () => generatedKeys.publicKey);
|
|
754
|
+
expect(record.description).toBe(`Signing key for package "Test Pkg"\n${marker}`);
|
|
755
|
+
expect(record.value.value).toBe("encrypted-secret~nonce");
|
|
756
|
+
});
|
|
757
|
+
it.each([
|
|
758
|
+
"established/cache",
|
|
759
|
+
"raw/cache",
|
|
760
|
+
"host/cache",
|
|
761
|
+
"host/established",
|
|
762
|
+
])("rejects a %s mismatch before writes", async (source) => {
|
|
763
|
+
if (source.endsWith("cache"))
|
|
764
|
+
record.description += `\n${marker}`;
|
|
765
|
+
if (source.startsWith("established") || source.endsWith("established")) {
|
|
766
|
+
mockGetPkg.mockResolvedValue({ publishPublicKey: sourceKeys.publicKey });
|
|
767
|
+
}
|
|
768
|
+
if (source === "raw/cache")
|
|
769
|
+
record.value = { value: sourceKeys.secretKey };
|
|
770
|
+
const resolve = source.startsWith("host")
|
|
771
|
+
? async () => (source.endsWith("cache") ? sourceKeys.publicKey : generatedKeys.publicKey)
|
|
772
|
+
: undefined;
|
|
773
|
+
await expect(importPackage(resolve)).rejects.toThrow("signing key mismatch");
|
|
774
|
+
expectNoWrites();
|
|
775
|
+
});
|
|
776
|
+
});
|
|
492
777
|
describe("ensureTemplateCache", () => {
|
|
493
778
|
it("clones template when cache directory does not exist", async () => {
|
|
494
779
|
const deps = makeMockDeps({});
|
|
@@ -83,6 +83,14 @@ export interface ICreatePackageOpts {
|
|
|
83
83
|
* personal storage isn't available). The migration script backfills these.
|
|
84
84
|
*/
|
|
85
85
|
personalContext?: import("../context/data-context").DataContext;
|
|
86
|
+
/**
|
|
87
|
+
* Optional host-only recovery for an encrypted personal signing secret. Return
|
|
88
|
+
* only its Ed25519 public key, or `undefined` when recovery is unavailable.
|
|
89
|
+
* Called after raw hydration fails, including when a public key is already known
|
|
90
|
+
* so mismatches can be rejected. Errors are suppressed without logging secret
|
|
91
|
+
* material; existing or cached public keys remain usable when decryption fails.
|
|
92
|
+
*/
|
|
93
|
+
resolveSigningPublicKey?: (encryptedSecret: string) => Promise<string | undefined>;
|
|
86
94
|
}
|
|
87
95
|
/** Why a `git pull` of the cached package template did not succeed. */
|
|
88
96
|
export type TemplateCacheRefreshFailure = "offline" | "could-not-refresh";
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import type { DataContext } from "../context/data-context";
|
|
2
|
-
import
|
|
2
|
+
import { ContractRegistry } from "../contracts/registry";
|
|
3
3
|
import type { IPackageDefinitionResult } from "../contracts/types";
|
|
4
4
|
/**
|
|
5
5
|
* Installs a contract-based package into a DataContext.
|
|
6
6
|
*
|
|
7
7
|
* Handles packages that export an `IPackageDefinitionResult` from `definePackage()`.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Materializes catalog identities and supplemental metadata, publishes contract providers,
|
|
9
|
+
* saves package assistants and registers table definitions after successful preparation.
|
|
10
10
|
*
|
|
11
11
|
* When a contract version was previously frozen (promoted to stable), the
|
|
12
12
|
* installer preserves its stable status rather than re-registering it as dev.
|
|
13
|
+
* Without `opts.registry` or the context's PackageLoader registry, catalog rows
|
|
14
|
+
* are still written but no provider is published.
|
|
13
15
|
*/
|
|
14
16
|
export declare function installContractPackage(dataContext: DataContext, packageDefinition: IPackageDefinitionResult, opts?: {
|
|
17
|
+
/** Registry to publish into; defaults to the context's PackageLoader registry. */
|
|
15
18
|
registry?: ContractRegistry;
|
|
19
|
+
/** Complete fallible loader bookkeeping before provider publication. */
|
|
20
|
+
prepare?: () => Promise<void>;
|
|
21
|
+
/** Publish in-memory loader state in the same serialized activation turn. */
|
|
22
|
+
onActivated?: () => void;
|
|
23
|
+
/** Reject a load invalidated by unload, including after asynchronous preparation. */
|
|
24
|
+
assertCurrent?: () => void;
|
|
16
25
|
}): Promise<void>;
|
|
17
26
|
/**
|
|
18
27
|
* Attempts to extract an `IPackageDefinitionResult` from a bundle's module exports.
|
|
@@ -4,68 +4,72 @@ exports.installContractPackage = installContractPackage;
|
|
|
4
4
|
exports.extractPackageDefinition = extractPackageDefinition;
|
|
5
5
|
exports.hasPackageDefinition = hasPackageDefinition;
|
|
6
6
|
exports.finalizeContractsForPromotion = finalizeContractsForPromotion;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const registry_1 = require("../contracts/registry");
|
|
8
|
+
const tool_catalog_1 = require("../contracts/tool-catalog");
|
|
9
9
|
/**
|
|
10
10
|
* Installs a contract-based package into a DataContext.
|
|
11
11
|
*
|
|
12
12
|
* Handles packages that export an `IPackageDefinitionResult` from `definePackage()`.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Materializes catalog identities and supplemental metadata, publishes contract providers,
|
|
14
|
+
* saves package assistants and registers table definitions after successful preparation.
|
|
15
15
|
*
|
|
16
16
|
* When a contract version was previously frozen (promoted to stable), the
|
|
17
17
|
* installer preserves its stable status rather than re-registering it as dev.
|
|
18
|
+
* Without `opts.registry` or the context's PackageLoader registry, catalog rows
|
|
19
|
+
* are still written but no provider is published.
|
|
18
20
|
*/
|
|
19
21
|
async function installContractPackage(dataContext, packageDefinition, opts) {
|
|
20
22
|
const { packageId } = packageDefinition;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
const sharedRegistry = opts?.registry ?? dataContext.packageLoader?.contractRegistry;
|
|
24
|
+
// Without a context registry, providers go to a throwaway registry (none are
|
|
25
|
+
// published, as before) whose catalog is disposed with this call.
|
|
26
|
+
const registry = sharedRegistry ?? new registry_1.ContractRegistry();
|
|
27
|
+
const registrations = packageDefinition.contracts.map((contract) => {
|
|
28
|
+
const key = `${contract.contractId}@${contract.version}`;
|
|
29
|
+
const existing = registry.getDefinition(contract.contractId, contract.version);
|
|
30
|
+
const definition = existing && !existing.devTag && contract.devTag === "dev"
|
|
31
|
+
? { ...contract, devTag: undefined }
|
|
32
|
+
: contract;
|
|
33
|
+
return { definition, alsoImplements: packageDefinition.alsoImplements.get(key) ?? [] };
|
|
34
|
+
});
|
|
35
|
+
const catalog = registry.toolCatalog ?? new tool_catalog_1.ToolCatalog(dataContext, registry);
|
|
36
|
+
const prepare = async () => {
|
|
37
|
+
opts?.assertCurrent?.();
|
|
38
|
+
// Install assistants with packageId
|
|
39
|
+
const { Assistants } = await Promise.resolve().then(() => require("../data/assistants"));
|
|
40
|
+
for (const assistant of packageDefinition.assistants) {
|
|
41
|
+
const assistantWithPackageId = { ...assistant, packageId };
|
|
42
|
+
await Assistants(dataContext).save(assistantWithPackageId);
|
|
43
|
+
}
|
|
44
|
+
await opts?.prepare?.();
|
|
45
|
+
};
|
|
46
|
+
const activate = () => {
|
|
47
|
+
opts?.assertCurrent?.();
|
|
48
|
+
// Publish table definitions in the guarded activation turn, after the
|
|
49
|
+
// coordinator's final catalog writes. Preparation must leave live tables alone.
|
|
50
|
+
if (packageDefinition.tableDefinitions.length) {
|
|
51
|
+
dataContext.tableContainer.registerTableDefinitions(packageDefinition.tableDefinitions, {
|
|
52
|
+
overwrite: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Activate the new provider only after all fallible package-owned persistence
|
|
56
|
+
// has succeeded, so a failed update leaves the previous provider routable.
|
|
57
|
+
{
|
|
58
|
+
const result = registry.replaceBatch(packageId, registrations);
|
|
59
|
+
if (!result.valid) {
|
|
60
|
+
throw new Error(`Atomic contract registration failed for package ${packageId}: ${result.errors
|
|
61
|
+
.map((error) => error.message)
|
|
62
|
+
.join("; ")}`);
|
|
32
63
|
}
|
|
33
|
-
return { definition: effectiveContract, alsoImplements: alsoImpl };
|
|
34
|
-
})
|
|
35
|
-
: undefined;
|
|
36
|
-
const validation = registrations ? registry?.validateBatch(packageId, registrations) : undefined;
|
|
37
|
-
if (validation && !validation.valid) {
|
|
38
|
-
throw new Error(`Atomic contract registration failed for package ${packageId}: ${validation.errors
|
|
39
|
-
.map((error) => error.message)
|
|
40
|
-
.join("; ")}`);
|
|
41
|
-
}
|
|
42
|
-
// Install tool instances with packageId
|
|
43
|
-
for (const toolInstance of packageDefinition.toolInstances) {
|
|
44
|
-
(0, tools_2.registerTool)(toolInstance);
|
|
45
|
-
const toolWithPackageId = { ...toolInstance.tool, packageId };
|
|
46
|
-
await (0, tools_1.Tools)(dataContext).save(toolWithPackageId);
|
|
47
|
-
}
|
|
48
|
-
// Install table definitions
|
|
49
|
-
for (const tableDefinition of packageDefinition.tableDefinitions) {
|
|
50
|
-
dataContext.tableContainer.registerTableDefinition(tableDefinition, {
|
|
51
|
-
overwrite: true,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
// Install assistants with packageId
|
|
55
|
-
const { Assistants } = await Promise.resolve().then(() => require("../data/assistants"));
|
|
56
|
-
for (const assistant of packageDefinition.assistants) {
|
|
57
|
-
const assistantWithPackageId = { ...assistant, packageId };
|
|
58
|
-
await Assistants(dataContext).save(assistantWithPackageId);
|
|
59
|
-
}
|
|
60
|
-
// Activate the new provider only after all fallible package-owned persistence
|
|
61
|
-
// has succeeded, so a failed update leaves the previous provider routable.
|
|
62
|
-
if (registry && registrations) {
|
|
63
|
-
const result = registry.replaceBatch(packageId, registrations);
|
|
64
|
-
if (!result.valid) {
|
|
65
|
-
throw new Error(`Atomic contract registration failed for package ${packageId}: ${result.errors
|
|
66
|
-
.map((error) => error.message)
|
|
67
|
-
.join("; ")}`);
|
|
68
64
|
}
|
|
65
|
+
opts?.onActivated?.();
|
|
66
|
+
};
|
|
67
|
+
try {
|
|
68
|
+
await catalog.publish(packageId, registrations, prepare, activate, packageDefinition.toolMetadata ?? []);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (!sharedRegistry)
|
|
72
|
+
await catalog.dispose();
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import "../index";
|