Skip to content

Commit bae006a

Browse files
committed
Refuse path-trust migration from corrupt stores
1 parent 8682a9a commit bae006a

2 files changed

Lines changed: 31 additions & 7 deletions

File tree

src/trust/path-trust.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,13 @@ export async function revokePathPlugin(
189189
* (expand marketplaces, drop missing paths). Callers supply expansion so this
190190
* module stays free of the plugin loader. `onMigrated` fires only on the run
191191
* that seeds grants, so callers can surface the one-time event to the user.
192+
*
193+
* A corrupt store (`invalid`: unreadable, zero-byte, or malformed) refuses to
194+
* seed: re-granting from `pluginPaths` here would undo an explicit revoke the
195+
* moment the file becomes unreadable. Migration leaves the file untouched and
196+
* returns no grants, so path plugins load metadata-only until the user either
197+
* repairs the file (deleting it restores first-launch seeding) or re-consents
198+
* explicitly through add-by-path / enable.
192199
*/
193200
export async function migratePathTrustFromPluginPaths(
194201
pluginPaths: string[],
@@ -200,6 +207,10 @@ export async function migratePathTrustFromPluginPaths(
200207
if (existing.state === "valid") {
201208
return existing.store;
202209
}
210+
if (existing.state === "invalid") {
211+
logger.warn`refusing path-trust migration from a corrupt store at ${pathTrustPath(home)}: delete the file to re-seed from settings.pluginPaths or re-consent through add-by-path`;
212+
return emptyStore();
213+
}
203214
if (pluginPaths.length === 0) {
204215
return emptyStore();
205216
}

tests/unit/path-trust.test.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -240,39 +240,52 @@ describe("path-trust (global)", () => {
240240
}
241241
});
242242

243-
test("a zero-byte store file is invalid and migration re-seeds it", async () => {
243+
test("a zero-byte store file is invalid and migration refuses to seed it", async () => {
244244
const { home, cleanup } = await scratch();
245245
try {
246246
await writeStoreFile(home, "");
247247
expect((await readPathTrustStore(home)).state).toBe("invalid");
248248

249249
const plugin = join(home, "shared", "plugin");
250+
let resolveCalls = 0;
250251
const store = await migratePathTrustFromPluginPaths(
251252
[plugin],
252-
async () => [plugin],
253+
async (p) => {
254+
resolveCalls += 1;
255+
return [p];
256+
},
253257
home,
254258
);
255-
expect(isPathPluginTrusted(store, plugin)).toBe(true);
256-
expect((await readPathTrustStore(home)).state).toBe("valid");
259+
expect(isPathPluginTrusted(store, plugin)).toBe(false);
260+
expect(store.trustedPluginPaths).toEqual([]);
261+
expect(resolveCalls).toBe(0);
262+
expect((await readPathTrustStore(home)).state).toBe("invalid");
257263
} finally {
258264
await cleanup();
259265
}
260266
});
261267

262-
test("a corrupt store file is invalid, loads empty, and migration re-seeds it", async () => {
268+
test("a corrupt store file is invalid, loads empty, and migration refuses to seed it", async () => {
263269
const { home, cleanup } = await scratch();
264270
try {
265271
await writeStoreFile(home, "{not json");
266272
expect((await readPathTrustStore(home)).state).toBe("invalid");
267273
expect((await loadPathTrust(home)).trustedPluginPaths).toEqual([]);
268274

269275
const plugin = join(home, "shared", "plugin");
276+
let resolveCalls = 0;
270277
const store = await migratePathTrustFromPluginPaths(
271278
[plugin],
272-
async () => [plugin],
279+
async (p) => {
280+
resolveCalls += 1;
281+
return [p];
282+
},
273283
home,
274284
);
275-
expect(isPathPluginTrusted(store, plugin)).toBe(true);
285+
expect(isPathPluginTrusted(store, plugin)).toBe(false);
286+
expect(store.trustedPluginPaths).toEqual([]);
287+
expect(resolveCalls).toBe(0);
288+
expect((await readPathTrustStore(home)).state).toBe("invalid");
276289
} finally {
277290
await cleanup();
278291
}

0 commit comments

Comments
 (0)