Fix Clip dependency and path validation gaps

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-09 20:04:59 +00:00
1 parent 9bb1d3bb24
commit f38235b51b
4 files changed
+40 -2

No files matched your search

@@ -292,6 +292,10 @@ describe("clip manifest foundation", () => {
expect(first.dependencies.adapters.map((entry) => entry.type)).toEqual(["claude_local", "codex_local"]);
expect(first.dependencies.skills.map((entry) => entry.key)).toEqual(["triage"]);
expect(first.dependencies.secrets.map((entry) => entry.key)).toContain("GH_TOKEN");
expect(first.dependencies.secrets.find((entry) => entry.key === "GH_TOKEN")).toMatchObject({
requirement: "required",
sourceRefs: ["agents.support", "envInputs.0"],
});
expect(first.dependencies.permissions.map((entry) => entry.capability)).toContain("github.issues.write");
expect(first.dependencies.runtime).toMatchObject({
browser: true,
+5
View File
@@ -894,6 +894,11 @@ function addMapEntry<T extends { sourceRefs: Set<string> }>(map: Map<string, T>,
map.set(key, value);
return;
}
const existingRequirement = existing as T & { requirement?: "required" | "optional" };
const nextRequirement = (value as T & { requirement?: "required" | "optional" }).requirement;
if (existingRequirement.requirement === "optional" && nextRequirement === "required") {
existingRequirement.requirement = "required";
}
for (const sourceRef of value.sourceRefs) {
existing.sourceRefs.add(sourceRef);
}
+19
View File
@@ -232,6 +232,25 @@ describe("clip routes", () => {
expect(mockCompanyPortabilityService.previewExport).not.toHaveBeenCalled();
});
it("rejects malformed clipId path params before clip database lookups", async () => {
const revisionRes = await request(createApp())
.post("/api/clips/not-a-uuid/revisions")
.send({
manifestChecksum: "sha256:manifest-v2",
artifactChecksum: "sha256:artifact-v2",
manifestPayload: { schema: "paperclip.clip/v1" },
});
const patchRes = await request(createApp())
.patch("/api/clips/not-a-uuid")
.send({ title: "Updated title" });
expect(revisionRes.status).toBe(400);
expect(revisionRes.body).toEqual({ error: "Invalid clipId path parameter." });
expect(patchRes.status).toBe(400);
expect(patchRes.body).toEqual({ error: "Invalid clipId path parameter." });
expect(mockClipService.getClipById).not.toHaveBeenCalled();
});
it("strips owner-supplied trust states when creating revisions", async () => {
const clipId = "33333333-3333-4333-8333-333333333333";
const sourceCompanyId = "22222222-2222-4222-8222-222222222222";
+12 -2
View File
@@ -75,6 +75,14 @@ function getClipCompanyIdParam(req: Request) {
return companyId;
}
function getClipIdParam(req: Request) {
const clipId = req.params.clipId as string;
if (!isUuidLike(clipId)) {
throw badRequest("Invalid clipId path parameter.");
}
return clipId;
}
function evictExpiredClipRateLimitHits(cutoff: number, now: number) {
if (now - clipRateLimitLastEvictionAt < CLIP_RATE_LIMIT_EVICTION_INTERVAL_MS) return;
clipRateLimitLastEvictionAt = now;
@@ -607,7 +615,8 @@ export function clipRoutes(db: Db, storage?: StorageService) {
});
router.post("/clips/:clipId/revisions", validate(createClipRevisionSchema), async (req, res) => {
const clip = await svc.getClipById(req.params.clipId as string);
const clipId = getClipIdParam(req);
const clip = await svc.getClipById(clipId);
if (!clip) {
res.status(404).json({ error: "Clip not found" });
return;
@@ -640,7 +649,8 @@ export function clipRoutes(db: Db, storage?: StorageService) {
});
router.patch("/clips/:clipId", validate(updateClipSchema), async (req, res) => {
const clip = await svc.getClipById(req.params.clipId as string);
const clipId = getClipIdParam(req);
const clip = await svc.getClipById(clipId);
if (!clip) {
res.status(404).json({ error: "Clip not found" });
return;