mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
Constrain Clip trust state writes
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0992001898
commit
24d056623d
3 files changed
+68
-2
No files matched your search
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user