@peers-app/peers-sdk 0.25.19 → 0.25.21
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/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/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/package-loader.test.js +21 -1
- package/package.json +1 -1
|
@@ -27,30 +27,39 @@ export declare function ensureTemplateCache(deps: IPackageInstallerDeps, templat
|
|
|
27
27
|
/**
|
|
28
28
|
* Resolve (or create) the Ed25519 publish public key for a package.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Never rotates an established identity. Checks the Packages key, the public key
|
|
31
|
+
* cached in the personal signing pvar's description, and any key recoverable from
|
|
32
|
+
* its secret for agreement before writing. Hosts may decrypt legacy secrets via
|
|
33
|
+
* `resolveSigningPublicKey`; plaintext secrets never need to leave the host.
|
|
34
|
+
* Lazily caches recovered public keys without changing the stored secret value.
|
|
35
|
+
* Generates keys only when both the secret and every public-key source are missing.
|
|
33
36
|
*
|
|
34
|
-
* @throws When
|
|
37
|
+
* @throws When recovered public keys disagree, or a secret has no recoverable public key.
|
|
35
38
|
*/
|
|
36
39
|
export declare function ensurePackageSigningKey(opts: {
|
|
37
40
|
personalContext?: DataContext;
|
|
38
41
|
packageId: string;
|
|
39
42
|
packageName: string;
|
|
40
43
|
existingPublishPublicKey?: string;
|
|
44
|
+
/** Host-only recovery of a public key from an encrypted personal signing secret. */
|
|
45
|
+
resolveSigningPublicKey?: ICreatePackageOpts["resolveSigningPublicKey"];
|
|
41
46
|
}): Promise<string>;
|
|
42
47
|
/**
|
|
43
48
|
* Import an existing on-disk Peers package directory into the current data context.
|
|
44
49
|
*
|
|
45
50
|
* Validates the manifest packageId, creates/updates the Packages record, persists
|
|
46
51
|
* `packageLocalPath_*`, provisions a signing key when missing, then installs the
|
|
47
|
-
* local dev bundles.
|
|
52
|
+
* local dev bundles. When the target Packages row is absent, reuses the first
|
|
53
|
+
* nonblank public key for the same packageId from the user's personal context
|
|
54
|
+
* or groups. Other contexts' package metadata is not copied.
|
|
48
55
|
*/
|
|
49
56
|
export declare function importExistingPackage(dataContext: DataContext, deps: IPackageInstallerDeps, opts: {
|
|
50
57
|
location: string;
|
|
51
58
|
personalContext?: DataContext;
|
|
52
59
|
/** Display name fallback when the manifest name is a slug. */
|
|
53
60
|
displayName?: string;
|
|
61
|
+
/** Host-only recovery of a public key from an encrypted personal signing secret. */
|
|
62
|
+
resolveSigningPublicKey?: ICreatePackageOpts["resolveSigningPublicKey"];
|
|
54
63
|
}): Promise<ICreatePackageResult>;
|
|
55
64
|
/**
|
|
56
65
|
* Create a new Peers package from the package template.
|
|
@@ -106,63 +106,96 @@ async function ensureTemplateCache(deps, templateRepo) {
|
|
|
106
106
|
await deps.shell.exec(`git clone ${templateRepo} ${cachePath}`);
|
|
107
107
|
return { cachePath };
|
|
108
108
|
}
|
|
109
|
+
const SIGNING_KEY_MARKER_PREFIX = "peers-package-signing-key:";
|
|
110
|
+
function validSigningPublicKey(value) {
|
|
111
|
+
return typeof value === "string" && (0, keys_1.derivePublicBoxKey)(value) ? value : undefined;
|
|
112
|
+
}
|
|
113
|
+
function cachedSigningPublicKey(description, packageId) {
|
|
114
|
+
const markers = (description || "")
|
|
115
|
+
.split(/\r?\n/)
|
|
116
|
+
.filter((line) => line.startsWith(SIGNING_KEY_MARKER_PREFIX));
|
|
117
|
+
if (markers.length !== 1)
|
|
118
|
+
return undefined;
|
|
119
|
+
const match = /^peers-package-signing-key:v1:([^:]+):([^:]+)$/.exec(markers[0]);
|
|
120
|
+
return match?.[1] === packageId ? validSigningPublicKey(match[2]) : undefined;
|
|
121
|
+
}
|
|
122
|
+
function signingKeyDescription(description, packageId, publicKey) {
|
|
123
|
+
const text = description
|
|
124
|
+
.split(/\r?\n/)
|
|
125
|
+
.filter((line) => !line.startsWith(SIGNING_KEY_MARKER_PREFIX))
|
|
126
|
+
.join("\n");
|
|
127
|
+
return `${text}${text ? "\n" : ""}${SIGNING_KEY_MARKER_PREFIX}v1:${packageId}:${publicKey}`;
|
|
128
|
+
}
|
|
109
129
|
/**
|
|
110
130
|
* Resolve (or create) the Ed25519 publish public key for a package.
|
|
111
131
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
132
|
+
* Never rotates an established identity. Checks the Packages key, the public key
|
|
133
|
+
* cached in the personal signing pvar's description, and any key recoverable from
|
|
134
|
+
* its secret for agreement before writing. Hosts may decrypt legacy secrets via
|
|
135
|
+
* `resolveSigningPublicKey`; plaintext secrets never need to leave the host.
|
|
136
|
+
* Lazily caches recovered public keys without changing the stored secret value.
|
|
137
|
+
* Generates keys only when both the secret and every public-key source are missing.
|
|
115
138
|
*
|
|
116
|
-
* @throws When
|
|
139
|
+
* @throws When recovered public keys disagree, or a secret has no recoverable public key.
|
|
117
140
|
*/
|
|
118
141
|
async function ensurePackageSigningKey(opts) {
|
|
119
142
|
const { personalContext, packageId, packageName, existingPublishPublicKey } = opts;
|
|
120
143
|
const establishedKey = existingPublishPublicKey?.trim() || "";
|
|
121
|
-
if (personalContext)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
144
|
+
if (!personalContext)
|
|
145
|
+
return establishedKey;
|
|
146
|
+
const keyName = `${package_author_signing_1.PACKAGE_SIGNING_KEY_PREFIX}${packageId}`;
|
|
147
|
+
const table = (0, persistent_vars_1.PersistentVars)(personalContext);
|
|
148
|
+
const existingKey = await table.findOne({ name: keyName });
|
|
149
|
+
const rawSecret = existingKey?.value?.value;
|
|
150
|
+
const cachedKey = cachedSigningPublicKey(existingKey?.description, packageId);
|
|
151
|
+
let derivedKey;
|
|
152
|
+
if (typeof rawSecret === "string" && rawSecret.length > 0) {
|
|
153
|
+
try {
|
|
154
|
+
derivedKey = validSigningPublicKey((0, keys_1.hydrateKeys)(rawSecret).publicKey);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Encrypted-at-rest secrets cannot be hydrated directly by the SDK.
|
|
158
|
+
}
|
|
159
|
+
if (!derivedKey && opts.resolveSigningPublicKey) {
|
|
126
160
|
try {
|
|
127
|
-
|
|
128
|
-
if (establishedKey && derivedPublicKey !== establishedKey) {
|
|
129
|
-
throw new Error(`Package signing key mismatch for "${packageName}" (${packageId}): ` +
|
|
130
|
-
"stored secret does not match Packages.publishPublicKey. " +
|
|
131
|
-
"Refusing to rotate the established key.");
|
|
132
|
-
}
|
|
133
|
-
return establishedKey || derivedPublicKey;
|
|
161
|
+
derivedKey = validSigningPublicKey(await opts.resolveSigningPublicKey(rawSecret));
|
|
134
162
|
}
|
|
135
|
-
catch
|
|
136
|
-
|
|
137
|
-
throw err;
|
|
138
|
-
}
|
|
139
|
-
// Secret is likely encrypted at rest — cannot derive here without the user key.
|
|
140
|
-
if (establishedKey) {
|
|
141
|
-
return establishedKey;
|
|
142
|
-
}
|
|
143
|
-
throw new Error(`Package "${packageName}" (${packageId}) has a signing secret pvar but no ` +
|
|
144
|
-
"publishPublicKey, and the secret could not be hydrated. " +
|
|
145
|
-
"Restore publishPublicKey or replace the unreadable key before importing.");
|
|
163
|
+
catch {
|
|
164
|
+
// Decryption is best-effort. Never expose ciphertext, plaintext, or host errors.
|
|
146
165
|
}
|
|
147
166
|
}
|
|
148
167
|
}
|
|
149
|
-
|
|
150
|
-
|
|
168
|
+
const publicKey = establishedKey || cachedKey || derivedKey;
|
|
169
|
+
if ([establishedKey, cachedKey, derivedKey].some((key) => key && key !== publicKey)) {
|
|
170
|
+
throw new Error(`Package signing key mismatch for "${packageName}" (${packageId}): ` +
|
|
171
|
+
"Packages.publishPublicKey, cached public key, and stored secret must agree. " +
|
|
172
|
+
"Refusing to rotate the established key.");
|
|
151
173
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
174
|
+
if (publicKey) {
|
|
175
|
+
if (existingKey && validSigningPublicKey(publicKey)) {
|
|
176
|
+
const description = signingKeyDescription(existingKey.description || "", packageId, publicKey);
|
|
177
|
+
if (description !== existingKey.description) {
|
|
178
|
+
// Keep the ciphertext identical so PersistentVarsTable.save does not encrypt it again.
|
|
179
|
+
await table.save({ ...existingKey, description });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return publicKey;
|
|
183
|
+
}
|
|
184
|
+
if (rawSecret !== undefined && rawSecret !== null && rawSecret !== "") {
|
|
185
|
+
throw new Error(`Package "${packageName}" (${packageId}) has a signing secret pvar but no ` +
|
|
186
|
+
"recoverable publishPublicKey, and the secret could not be hydrated. " +
|
|
187
|
+
"Restore publishPublicKey or unlock the signing secret before importing.");
|
|
154
188
|
}
|
|
155
189
|
const keys = (0, keys_1.newKeys)();
|
|
156
|
-
|
|
157
|
-
await (0, persistent_vars_1.PersistentVars)(personalContext).save({
|
|
190
|
+
await table.save({
|
|
158
191
|
persistentVarId: (0, utils_1.deterministicPvarId)(keyName),
|
|
159
192
|
name: keyName,
|
|
160
193
|
scope: "user",
|
|
161
194
|
value: { value: keys.secretKey },
|
|
162
195
|
isSecret: true,
|
|
163
|
-
description: `Signing key for package "${packageName}"`,
|
|
196
|
+
description: signingKeyDescription(`Signing key for package "${packageName}"`, packageId, keys.publicKey),
|
|
164
197
|
modifiedAt: Date.now(),
|
|
165
|
-
});
|
|
198
|
+
}, { restoreIfDeleted: true });
|
|
166
199
|
return keys.publicKey;
|
|
167
200
|
}
|
|
168
201
|
async function registerPackageRecord(dataContext, opts) {
|
|
@@ -195,7 +228,9 @@ async function persistPackageLocalPath(dataContext, deps, packageId, location) {
|
|
|
195
228
|
*
|
|
196
229
|
* Validates the manifest packageId, creates/updates the Packages record, persists
|
|
197
230
|
* `packageLocalPath_*`, provisions a signing key when missing, then installs the
|
|
198
|
-
* local dev bundles.
|
|
231
|
+
* local dev bundles. When the target Packages row is absent, reuses the first
|
|
232
|
+
* nonblank public key for the same packageId from the user's personal context
|
|
233
|
+
* or groups. Other contexts' package metadata is not copied.
|
|
199
234
|
*/
|
|
200
235
|
async function importExistingPackage(dataContext, deps, opts) {
|
|
201
236
|
const info = await (0, package_installer_1.getPackageInfo)(deps, opts.location);
|
|
@@ -204,12 +239,30 @@ async function importExistingPackage(dataContext, deps, opts) {
|
|
|
204
239
|
}
|
|
205
240
|
const packageId = info.packageId;
|
|
206
241
|
const existing = await (0, packages_1.Packages)(dataContext).get(packageId);
|
|
242
|
+
let existingPublishPublicKey = existing?.publishPublicKey;
|
|
243
|
+
const userContext = dataContext.userContext;
|
|
244
|
+
if (!existing && userContext) {
|
|
245
|
+
for (const sourceContext of [
|
|
246
|
+
userContext.userDataContext,
|
|
247
|
+
...userContext.groupDataContexts.values(),
|
|
248
|
+
]) {
|
|
249
|
+
if (sourceContext.dataContextId === dataContext.dataContextId)
|
|
250
|
+
continue;
|
|
251
|
+
const sourcePackage = await (0, packages_1.Packages)(sourceContext).get(packageId);
|
|
252
|
+
const sourcePublicKey = sourcePackage?.publishPublicKey?.trim();
|
|
253
|
+
if (sourcePublicKey) {
|
|
254
|
+
existingPublishPublicKey = sourcePublicKey;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
207
259
|
const packageName = opts.displayName || info.name || packageId;
|
|
208
260
|
const publishPublicKey = await ensurePackageSigningKey({
|
|
209
261
|
personalContext: opts.personalContext,
|
|
210
262
|
packageId,
|
|
211
263
|
packageName,
|
|
212
|
-
existingPublishPublicKey
|
|
264
|
+
existingPublishPublicKey,
|
|
265
|
+
resolveSigningPublicKey: opts.resolveSigningPublicKey,
|
|
213
266
|
});
|
|
214
267
|
await registerPackageRecord(dataContext, {
|
|
215
268
|
packageId,
|
|
@@ -248,6 +301,7 @@ async function createPackage(dataContext, deps, opts) {
|
|
|
248
301
|
location,
|
|
249
302
|
personalContext: opts.personalContext,
|
|
250
303
|
displayName: opts.name,
|
|
304
|
+
resolveSigningPublicKey: opts.resolveSigningPublicKey,
|
|
251
305
|
});
|
|
252
306
|
}
|
|
253
307
|
await (0, package_dev_tools_1.assertPackageDevTools)(deps);
|
|
@@ -313,6 +367,7 @@ async function createPackage(dataContext, deps, opts) {
|
|
|
313
367
|
personalContext: opts.personalContext,
|
|
314
368
|
packageId,
|
|
315
369
|
packageName: opts.name,
|
|
370
|
+
resolveSigningPublicKey: opts.resolveSigningPublicKey,
|
|
316
371
|
});
|
|
317
372
|
await registerPackageRecord(dataContext, {
|
|
318
373
|
packageId,
|
|
@@ -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";
|
|
@@ -72,6 +72,8 @@ export interface IIsolatedPackageNetworkCredential {
|
|
|
72
72
|
*
|
|
73
73
|
* Honored at install the same way `suggestedAccessLevel` is: there is no
|
|
74
74
|
* separate approval step yet. The Packages screen displays the declaration.
|
|
75
|
+
* This constrains the System HTTP bridge in both execution modes. Privileged
|
|
76
|
+
* providers can also use Node's network APIs directly, outside this policy.
|
|
75
77
|
*/
|
|
76
78
|
export interface IIsolatedPackageNetwork {
|
|
77
79
|
allowedHosts: string[];
|
|
@@ -152,11 +154,21 @@ export interface IIsolatedActionAlias {
|
|
|
152
154
|
export interface IIsolatedPackageManifest {
|
|
153
155
|
packageId: string;
|
|
154
156
|
version: string;
|
|
157
|
+
/**
|
|
158
|
+
* Provider execution backend. Omission selects the SES sandbox. Privileged
|
|
159
|
+
* providers run as trusted code in a separate Node process with the host's
|
|
160
|
+
* OS account, environment, working directory, and unrestricted OS access;
|
|
161
|
+
* this does not elevate to root/admin and does not apply to UI code.
|
|
162
|
+
*/
|
|
163
|
+
executionMode?: "sandbox" | "privileged";
|
|
155
164
|
provides: IIsolatedProvidedContract[];
|
|
156
165
|
consumes: IIsolatedConsumedContract[];
|
|
157
166
|
/** Package-owned table schemas. Contract references decide public exposure. */
|
|
158
167
|
tables?: IIsolatedPackageTable[];
|
|
159
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* System HTTP allowlist and credentials. Omit to deny requests through that bridge.
|
|
170
|
+
* Privileged providers retain direct OS/network access.
|
|
171
|
+
*/
|
|
160
172
|
network?: IIsolatedPackageNetwork;
|
|
161
173
|
appNavs?: IAppNav[];
|
|
162
174
|
/** Private checked targets, independent of the frozen Voice Skill catalog. */
|
|
@@ -164,7 +176,7 @@ export interface IIsolatedPackageManifest {
|
|
|
164
176
|
}
|
|
165
177
|
/**
|
|
166
178
|
* Installable isolated package: declarative manifest plus provider source
|
|
167
|
-
* that
|
|
179
|
+
* that runs only inside a supervised sandbox worker or privileged Node process.
|
|
168
180
|
*/
|
|
169
181
|
export interface IIsolatedPackageArtifact {
|
|
170
182
|
manifest: IIsolatedPackageManifest;
|
|
@@ -587,10 +587,16 @@ function validateIsolatedPackageArtifact(artifact) {
|
|
|
587
587
|
"network",
|
|
588
588
|
"appNavs",
|
|
589
589
|
"actionAliases",
|
|
590
|
+
"executionMode",
|
|
590
591
|
], "manifest");
|
|
591
592
|
if (typeof providerSource !== "string" || providerSource.trim() === "") {
|
|
592
593
|
throw new Error("Package artifact.providerSource must be a non-empty string");
|
|
593
594
|
}
|
|
595
|
+
if (manifest.executionMode !== undefined &&
|
|
596
|
+
manifest.executionMode !== "sandbox" &&
|
|
597
|
+
manifest.executionMode !== "privileged") {
|
|
598
|
+
throw new Error('manifest.executionMode must be "sandbox" or "privileged"');
|
|
599
|
+
}
|
|
594
600
|
assertPeerId(manifest.packageId, "packageId");
|
|
595
601
|
if (typeof manifest.version !== "string" || manifest.version.trim() === "") {
|
|
596
602
|
throw new Error("package version is required");
|
|
@@ -40,6 +40,40 @@ function makeArtifact(overrides = {}) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
describe("isolated package artifact", () => {
|
|
43
|
+
test.each([
|
|
44
|
+
undefined,
|
|
45
|
+
"sandbox",
|
|
46
|
+
"privileged",
|
|
47
|
+
])("round-trips executionMode=%s without changing the legacy envelope", (executionMode) => {
|
|
48
|
+
const { artifact } = makeArtifact();
|
|
49
|
+
if (executionMode !== undefined)
|
|
50
|
+
artifact.manifest.executionMode = executionMode;
|
|
51
|
+
const bundle = (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact);
|
|
52
|
+
const parsed = (0, isolated_artifact_1.parseIsolatedPackageArtifact)(bundle);
|
|
53
|
+
expect(parsed.manifest.executionMode).toBe(executionMode);
|
|
54
|
+
expect((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(parsed)).toBe(bundle);
|
|
55
|
+
if (executionMode === undefined)
|
|
56
|
+
expect(bundle).not.toContain("executionMode");
|
|
57
|
+
});
|
|
58
|
+
test.each([
|
|
59
|
+
null,
|
|
60
|
+
true,
|
|
61
|
+
false,
|
|
62
|
+
1,
|
|
63
|
+
"",
|
|
64
|
+
"node",
|
|
65
|
+
"Privileged",
|
|
66
|
+
{},
|
|
67
|
+
[],
|
|
68
|
+
])("rejects malformed executionMode=%p at validation and parse time", (executionMode) => {
|
|
69
|
+
const { artifact } = makeArtifact();
|
|
70
|
+
Object.assign(artifact.manifest, { executionMode });
|
|
71
|
+
const bundle = `${isolated_artifact_1.ISOLATED_PACKAGE_MAGIC}\n${JSON.stringify(artifact)}\n`;
|
|
72
|
+
expect((0, isolated_artifact_1.looksLikeIsolatedPackageArtifact)(bundle)).toBe(true);
|
|
73
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/executionMode/);
|
|
74
|
+
expect(() => (0, isolated_artifact_1.parseIsolatedPackageArtifact)(bundle)).toThrow(/executionMode/);
|
|
75
|
+
expect(() => (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)).toThrow(/executionMode/);
|
|
76
|
+
});
|
|
43
77
|
test("round-trips a generic authority request and rejects unknown modes", () => {
|
|
44
78
|
const { artifact } = makeArtifact();
|
|
45
79
|
const tool = artifact.manifest.provides[0].tools[0];
|
|
@@ -252,7 +252,12 @@ describe("PackageLoader isolated artifacts", () => {
|
|
|
252
252
|
(0, isolated_runtime_1.setIsolatedPackageRuntimeFactory)(() => runtime);
|
|
253
253
|
return runtime;
|
|
254
254
|
}
|
|
255
|
-
it(
|
|
255
|
+
it.each([
|
|
256
|
+
undefined,
|
|
257
|
+
"sandbox",
|
|
258
|
+
"privileged",
|
|
259
|
+
])("installs executionMode=%s without host evaluation", async (executionMode) => {
|
|
260
|
+
artifact.manifest.executionMode = executionMode;
|
|
256
261
|
const runtime = makeRuntime();
|
|
257
262
|
mockGet.mockResolvedValue(selectedVersion());
|
|
258
263
|
mockGetFileContents.mockResolvedValue(Buffer.from((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)));
|
|
@@ -405,6 +410,21 @@ describe("PackageLoader isolated artifacts", () => {
|
|
|
405
410
|
await expect(loader.loadPackage(pkg)).rejects.toThrow(/JSON is invalid/);
|
|
406
411
|
expect(globalThis.__shouldNotEvalOnHost).toBeUndefined();
|
|
407
412
|
});
|
|
413
|
+
it.each([
|
|
414
|
+
null,
|
|
415
|
+
true,
|
|
416
|
+
"unknown",
|
|
417
|
+
])("fails closed on malformed executionMode=%p", async (executionMode) => {
|
|
418
|
+
const runtime = makeRuntime();
|
|
419
|
+
mockGet.mockResolvedValue(selectedVersion());
|
|
420
|
+
const malformed = structuredClone(artifact);
|
|
421
|
+
Object.assign(malformed.manifest, { executionMode });
|
|
422
|
+
mockGetFileContents.mockResolvedValue(Buffer.from(`PEERS_ISOLATED_PACKAGE_V1\n${JSON.stringify(malformed)}`));
|
|
423
|
+
const loader = new package_loader_1.PackageLoader({ dataContextId: "ctx-bad-mode" });
|
|
424
|
+
await expect(loader.loadPackage(pkg)).rejects.toThrow(/executionMode/);
|
|
425
|
+
expect(runtime.install).not.toHaveBeenCalled();
|
|
426
|
+
expect(globalThis.__shouldNotEvalOnHost).toBeUndefined();
|
|
427
|
+
});
|
|
408
428
|
it("passes gateways and a trusted local identity resolver from its data context", () => {
|
|
409
429
|
const factory = jest.fn((_options) => ({
|
|
410
430
|
install: jest.fn(async () => ({ packageId, provided: [], consumes: [] })),
|