Protect Clip catalog integrity

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-09 19:39:17 +00:00
1 parent c0b194ed4a
commit e10d403e5e
2 files changed
+28 -8

No files matched your search

+20 -7
View File
@@ -147,15 +147,15 @@ function publicClipFixture(input: { clipId: string; slug?: string }) {
};
}
function createApp() {
function createApp(actor: Record<string, unknown> = {
type: "board",
source: "local_implicit",
userId: "board",
}) {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
(req as any).actor = {
type: "board",
source: "local_implicit",
userId: "board",
};
(req as any).actor = actor;
next();
});
app.use("/api", clipRoutes({} as any));
@@ -212,7 +212,7 @@ describe("clip routes", () => {
);
});
it("rejects owner moderation and publish-status patches", async () => {
it("rejects owner moderation, publish-status, and public-visibility patches", async () => {
const clipId = "33333333-3333-4333-8333-333333333333";
const sourceCompanyId = "22222222-2222-4222-8222-222222222222";
mockClipService.getClipById.mockResolvedValue({
@@ -228,12 +228,25 @@ describe("clip routes", () => {
const publishRes = await request(createApp())
.patch("/api/clips/" + clipId)
.send({ status: "published" });
const visibilityRes = await request(createApp())
.patch("/api/clips/" + clipId)
.send({ visibility: "public" });
expect(moderationRes.status).toBe(403);
expect(publishRes.status).toBe(403);
expect(visibilityRes.status).toBe(403);
expect(mockClipService.updateClip).not.toHaveBeenCalled();
});
it("rejects anonymous applied import telemetry", async () => {
const res = await request(createApp({ type: "none", source: "none" }))
.post("/api/public/clips/support-triage/import-telemetry")
.send({ status: "applied" });
expect(res.status).toBe(403);
expect(mockClipService.recordImportTelemetry).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";
+8 -1
View File
@@ -643,6 +643,9 @@ export function clipRoutes(db: Db, storage?: StorageService) {
if (req.body.status === "published" && clip.status !== "published") {
throw forbidden("Publishing clips requires platform moderation access");
}
if (req.body.visibility === "public" && clip.visibility !== "public") {
throw forbidden("Public clip visibility requires platform moderation access");
}
const actorInfo = getActorInfo(req);
const updated = await svc.updateClip(clip.id, req.body, {
actorType: actorInfo.actorType,
@@ -751,7 +754,11 @@ export function clipRoutes(db: Db, storage?: StorageService) {
}
if (!consumeClipRateLimit(req, res, "import")) return;
const actor = actorForClip(req);
const clip = await svc.recordImportTelemetry(req.params.slug as string, req.body, actor);
const telemetryStatus = req.body.status ?? "previewed";
if (actor.actorType === "anonymous" && telemetryStatus !== "previewed") {
throw forbidden("Authenticated actor required for applied import telemetry");
}
const clip = await svc.recordImportTelemetry(req.params.slug as string, { ...req.body, status: telemetryStatus }, actor);
await logActivity(db, {
companyId: req.body.destinationCompanyId ?? clip.sourceCompanyId,
actorType: actor.actorType === "agent" ? "agent" : actor.actorType === "user" ? "user" : "system",