Gate Clip revision approval

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-09 19:55:54 +00:00
1 parent 24b1191ca2
commit 9bb1d3bb24
4 files changed
+16 -5

No files matched your search

+5 -1
View File
@@ -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();
});
+6 -2
View File
@@ -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/);
+3
View File
@@ -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");
}
+2 -2
View File
@@ -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))