Skip to content

Commit 2705892

Browse files
committed
Reject top-level JSON arrays in the project trust store shape check
The arktype object schema accepts arrays (they are typeof "object"), so a trust-store file containing a bare JSON array silently degraded to an empty-but-valid store instead of being flagged invalid. Explicit Array.isArray check restores the original rejection before validation.
1 parent d29669a commit 2705892

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

src/trust/project-trust.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ export async function readProjectTrustStore(
104104
logger.warn`project trust store is not valid JSON at ${path}: ${String(err)}`;
105105
return { state: "invalid", store: emptyStore() };
106106
}
107+
// arktype's plain object schema accepts arrays (Array.isArray(x) && typeof x
108+
// === "object"), so a top-level JSON array must be rejected explicitly before
109+
// validation — otherwise it degrades to an empty-but-"valid" store instead of
110+
// being flagged corrupt.
111+
if (Array.isArray(parsed)) {
112+
logger.warn`project trust store has an invalid shape at ${path}: expected object, got array`;
113+
return { state: "invalid", store: emptyStore() };
114+
}
107115
const validated = ProjectTrustRecordSchema(parsed);
108116
if (validated instanceof type.errors) {
109117
logger.warn`project trust store has an invalid shape at ${path}: ${validated.summary}`;

tests/unit/project-trust.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,42 @@ describe("project-trust", () => {
164164
}
165165
});
166166

167+
test("readProjectTrustStore: top-level JSON array is invalid", async () => {
168+
const { cwd, home, cleanup } = await scratch();
169+
try {
170+
const path = projectTrustPath(cwd, home);
171+
await mkdir(join(home, ".corbits", "trust"), { recursive: true });
172+
await writeFile(path, JSON.stringify([1, 2, 3]), "utf8");
173+
const result = await readProjectTrustStore(cwd, home);
174+
expect(result.state).toBe("invalid");
175+
expect(result.store).toEqual({ trustedPluginPaths: [], trustedMcpFingerprints: [] });
176+
} finally {
177+
await cleanup();
178+
}
179+
});
180+
181+
test("readProjectTrustStore: non-string repo field is invalid", async () => {
182+
const { cwd, home, cleanup } = await scratch();
183+
try {
184+
const path = projectTrustPath(cwd, home);
185+
await mkdir(join(home, ".corbits", "trust"), { recursive: true });
186+
await writeFile(
187+
path,
188+
JSON.stringify({
189+
repo: 7,
190+
trustedPluginPaths: [],
191+
trustedMcpFingerprints: [],
192+
}),
193+
"utf8",
194+
);
195+
const result = await readProjectTrustStore(cwd, home);
196+
expect(result.state).toBe("invalid");
197+
expect(result.store).toEqual({ trustedPluginPaths: [], trustedMcpFingerprints: [] });
198+
} finally {
199+
await cleanup();
200+
}
201+
});
202+
167203
test("readProjectTrustStore: partial file with only trustedPluginPaths stays valid", async () => {
168204
const { cwd, home, cleanup } = await scratch();
169205
try {

0 commit comments

Comments
 (0)