Constrain Clip trust state writes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-09 19:17:30 +00:00
1 parent 0992001898
commit 24d056623d
3 files changed
+68 -2

No files matched your search

+1 -1
View File
@@ -42,7 +42,7 @@ export const createClipCreatorProfileSchema = z.object({
bio: z.string().trim().max(1_000).nullable().optional(),
avatarUrl: z.string().url().nullable().optional(),
websiteUrl: z.string().url().nullable().optional(),
verificationState: z.string().trim().min(1).max(48).optional(),
verificationState: z.literal("unverified").optional(),
reputationSummary: recordSchema.optional(),
});
+55
View File
@@ -179,6 +179,61 @@ describe("clip routes", () => {
expect(mockCompanyPortabilityService.previewExport).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";
mockClipService.getClipById.mockResolvedValue({
id: clipId,
slug: "support-triage",
sourceCompanyId,
});
mockClipService.createRevision.mockResolvedValue({
clip: { id: clipId, slug: "support-triage" },
revision: { id: "44444444-4444-4444-8444-444444444444", revisionNumber: 2 },
});
const res = await request(createApp())
.post("/api/clips/" + clipId + "/revisions")
.send({
manifestChecksum: "sha256:manifest-v2",
artifactChecksum: "sha256:artifact-v2",
manifestPayload: { schema: "paperclip.clip/v1" },
securityReviewState: "security_reviewed",
verificationState: "passed",
});
expect(res.status).toBe(201);
expect(mockClipService.createRevision).toHaveBeenCalledWith(
clipId,
expect.objectContaining({
securityReviewState: "unreviewed",
verificationState: "not_run",
}),
);
});
it("rejects owner moderation and publish-status patches", async () => {
const clipId = "33333333-3333-4333-8333-333333333333";
const sourceCompanyId = "22222222-2222-4222-8222-222222222222";
mockClipService.getClipById.mockResolvedValue({
id: clipId,
slug: "support-triage",
sourceCompanyId,
status: "pending_review",
});
const moderationRes = await request(createApp())
.patch("/api/clips/" + clipId)
.send({ moderationState: "normal" });
const publishRes = await request(createApp())
.patch("/api/clips/" + clipId)
.send({ status: "published" });
expect(moderationRes.status).toBe(403);
expect(publishRes.status).toBe(403);
expect(mockClipService.updateClip).not.toHaveBeenCalled();
});
it("uses the clip source company when building import previews", async () => {
const destinationCompanyId = "11111111-1111-4111-8111-111111111111";
const sourceCompanyId = "22222222-2222-4222-8222-222222222222";
+12 -1
View File
@@ -605,7 +605,12 @@ export function clipRoutes(db: Db, storage?: StorageService) {
}
assertCompanyAccess(req, clip.sourceCompanyId);
if (!consumeClipRateLimit(req, res, "update")) return;
const result = await svc.createRevision(clip.id, req.body);
const revisionInput = {
...req.body,
securityReviewState: "unreviewed" as const,
verificationState: "not_run" as const,
};
const result = await svc.createRevision(clip.id, revisionInput);
const actor = getActorInfo(req);
await logActivity(db, {
companyId: clip.sourceCompanyId,
@@ -632,6 +637,12 @@ export function clipRoutes(db: Db, storage?: StorageService) {
return;
}
assertCompanyAccess(req, clip.sourceCompanyId);
if (req.body.moderationState !== undefined || req.body.moderationReason !== undefined) {
throw forbidden("Clip moderation fields require platform moderation access");
}
if (req.body.status === "published" && clip.status !== "published") {
throw forbidden("Publishing clips requires platform moderation access");
}
const actorInfo = getActorInfo(req);
const updated = await svc.updateClip(clip.id, req.body, {
actorType: actorInfo.actorType,