mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
Gate Clip revision approval
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
24b1191ca2
commit
9bb1d3bb24
4 files changed
+16
-5
No files matched your search
@@ -265,7 +265,7 @@ describe("clip routes", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects owner moderation, publish-status, and public-visibility patches", async () => {
|
||||
it("rejects owner moderation, publish-status, public-visibility, and approved-revision patches", async () => {
|
||||
const clipId = "33333333-3333-4333-8333-333333333333";
|
||||
const sourceCompanyId = "22222222-2222-4222-8222-222222222222";
|
||||
mockClipService.getClipById.mockResolvedValue({
|
||||
@@ -284,10 +284,14 @@ describe("clip routes", () => {
|
||||
const visibilityRes = await request(createApp())
|
||||
.patch("/api/clips/" + clipId)
|
||||
.send({ visibility: "public" });
|
||||
const approvedRevisionRes = await request(createApp())
|
||||
.patch("/api/clips/" + clipId)
|
||||
.send({ latestApprovedRevisionId: "44444444-4444-4444-8444-444444444444" });
|
||||
|
||||
expect(moderationRes.status).toBe(403);
|
||||
expect(publishRes.status).toBe(403);
|
||||
expect(visibilityRes.status).toBe(403);
|
||||
expect(approvedRevisionRes.status).toBe(403);
|
||||
expect(mockClipService.updateClip).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ describeEmbeddedPostgres("clipService", () => {
|
||||
expect(rows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("stores revisions append-only and serves exact revision manifests", async () => {
|
||||
it("stores revisions append-only while keeping public reads on the last approved revision", async () => {
|
||||
const companyId = await seedCompany();
|
||||
const { clip, revision } = await publishFixture(companyId);
|
||||
|
||||
@@ -211,9 +211,13 @@ describeEmbeddedPostgres("clipService", () => {
|
||||
expect(update.revision.revisionNumber).toBe(2);
|
||||
const firstRevision = await svc.getPublicRevision(clip.slug, 1);
|
||||
const secondRevision = await svc.getPublicDetail(clip.slug);
|
||||
const unapprovedRevision = await svc.getPublicRevision(clip.slug, update.revision.revisionNumber);
|
||||
|
||||
expect(update.clip.currentRevisionId).toBe(update.revision.id);
|
||||
expect(update.clip.latestApprovedRevisionId).toBe(revision.id);
|
||||
expect(firstRevision?.currentRevision?.manifestChecksum).toBe("sha256:manifest-v1");
|
||||
expect(secondRevision?.currentRevision?.manifestChecksum).toBe("sha256:manifest-v2");
|
||||
expect(secondRevision?.currentRevision?.manifestChecksum).toBe("sha256:manifest-v1");
|
||||
expect(unapprovedRevision).toBeNull();
|
||||
await expect(
|
||||
db.update(clipRevisions).set({ changeSummary: "mutated" }).where(eq(clipRevisions.id, revision.id)),
|
||||
).rejects.toThrow(/Failed query/);
|
||||
|
||||
@@ -649,6 +649,9 @@ export function clipRoutes(db: Db, storage?: StorageService) {
|
||||
if (req.body.moderationState !== undefined || req.body.moderationReason !== undefined) {
|
||||
throw forbidden("Clip moderation fields require platform moderation access");
|
||||
}
|
||||
if (req.body.latestApprovedRevisionId !== undefined) {
|
||||
throw forbidden("Approved revision promotion requires platform moderation access");
|
||||
}
|
||||
if (req.body.status === "published" && clip.status !== "published") {
|
||||
throw forbidden("Publishing clips requires platform moderation access");
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ export function clipService(db: Db) {
|
||||
const row = await getPublicClipBySlug(slug);
|
||||
if (!row) return null;
|
||||
const revision = await getRevisionByNumber(row.clip.id, revisionNumber);
|
||||
if (!revision || !isPubliclyReadableRevision(revision)) return null;
|
||||
if (!revision || revision.id !== row.clip.latestApprovedRevisionId || !isPubliclyReadableRevision(revision)) return null;
|
||||
const dependencies = await db.select().from(clipDependencies).where(eq(clipDependencies.revisionId, revision.id));
|
||||
return serializePublicClip({
|
||||
clip: row.clip,
|
||||
@@ -404,7 +404,7 @@ export function clipService(db: Db) {
|
||||
.update(clips)
|
||||
.set({
|
||||
currentRevisionId: revision.id,
|
||||
latestApprovedRevisionId: clip.status === "published" && clip.moderationState !== "blocked" ? revision.id : clip.latestApprovedRevisionId,
|
||||
latestApprovedRevisionId: clip.latestApprovedRevisionId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(clips.id, clip.id))
|
||||
|
||||
Reference in new issue
Block a user