diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json index 05b6a33d8e..6becd7e0a1 100644 --- a/apps/gittensory-ui/public/openapi.json +++ b/apps/gittensory-ui/public/openapi.json @@ -14687,6 +14687,36 @@ } ] } + }, + "/v1/repos/{owner}/{repo}/focus-manifest/refresh": { + "post": { + "responses": { + "200": { + "description": "Refresh the persisted focus manifest cache from the repo file", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "nullable": true + } + } + } + } + }, + "403": { + "description": "Insufficient role" + } + }, + "security": [ + { + "GittensoryBearer": [] + }, + { + "GittensorySessionCookie": [] + } + ] + } } }, "servers": [ diff --git a/src/api/routes.ts b/src/api/routes.ts index a8732a98f6..970a27a4d3 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -1582,7 +1582,21 @@ export function createApp() { const repoForbidden = await requireSessionRepoAccess(c, identity, fullName, repo); if (repoForbidden) return repoForbidden; } - const manifest = await loadRepoFocusManifest(c.env, fullName, { refresh: c.req.query("refresh") === "true" }); + const manifest = await loadRepoFocusManifest(c.env, fullName); + return c.json({ repoFullName: fullName, manifest, policy: compileFocusManifestPolicy(manifest) }); + }); + + app.post("/v1/repos/:owner/:repo/focus-manifest/refresh", async (c) => { + const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`; + const forbidden = await requireAppRole(c, ["maintainer", "owner", "operator"]); + if (forbidden) return forbidden; + const identity = await authenticateRequestIdentity(c); + const repo = await getRepository(c.env, fullName); + if (identity?.kind === "session") { + const repoForbidden = await requireSessionRepoAccess(c, identity, fullName, repo); + if (repoForbidden) return repoForbidden; + } + const manifest = await loadRepoFocusManifest(c.env, fullName, { refresh: true }); return c.json({ repoFullName: fullName, manifest, policy: compileFocusManifestPolicy(manifest) }); }); diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts index be69bb505b..76920ef8b1 100644 --- a/src/openapi/spec.ts +++ b/src/openapi/spec.ts @@ -393,6 +393,14 @@ export function buildOpenApiSpec() { 403: { description: "Insufficient role" }, }, }); + registry.registerPath({ + method: "post", + path: "/v1/repos/{owner}/{repo}/focus-manifest/refresh", + responses: { + 200: { description: "Refresh the persisted focus manifest cache from the repo file", content: { "application/json": { schema: z.record(z.string(), z.unknown()) } } }, + 403: { description: "Insufficient role" }, + }, + }); registry.registerPath({ method: "put", path: "/v1/repos/{owner}/{repo}/focus-manifest", diff --git a/test/unit/routes-focus-manifest.test.ts b/test/unit/routes-focus-manifest.test.ts index d5d926d032..73a833a6dd 100644 --- a/test/unit/routes-focus-manifest.test.ts +++ b/test/unit/routes-focus-manifest.test.ts @@ -149,16 +149,39 @@ describe("focus-manifest route auth", () => { }); }); - it("bypasses cached manifest when refresh=true", async () => { + it("does not refresh cached manifests from GET query parameters", async () => { const app = createApp(); const env = createTestEnv({ GITTENSORY_DRIFT_ISSUE_REPO: "JSONbored/gittensory" }); const headers = apiHeaders(env); - const first = await app.request(FOCUS_MANIFEST_PATH, { headers }, env); - expect(first.status).toBe(200); - const refreshed = await app.request(`${FOCUS_MANIFEST_PATH}?refresh=true`, { headers }, env); + const putResponse = await app.request( + FOCUS_MANIFEST_PATH, + { method: "PUT", headers, body: JSON.stringify({ wantedPaths: ["private-cache/"] }) }, + env, + ); + expect(putResponse.status).toBe(200); + + const response = await app.request(`${FOCUS_MANIFEST_PATH}?refresh=true`, { headers }, env); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ + manifest: { present: true, source: "api_record", wantedPaths: ["private-cache/"] }, + }); + }); + + it("refreshes cached manifests from an unsafe POST endpoint", async () => { + const app = createApp(); + const env = createTestEnv({ GITTENSORY_DRIFT_ISSUE_REPO: "JSONbored/gittensory" }); + const headers = apiHeaders(env); + const putResponse = await app.request( + FOCUS_MANIFEST_PATH, + { method: "PUT", headers, body: JSON.stringify({ wantedPaths: ["private-cache/"] }) }, + env, + ); + expect(putResponse.status).toBe(200); + + const refreshed = await app.request(`${FOCUS_MANIFEST_PATH}/refresh`, { method: "POST", headers }, env); expect(refreshed.status).toBe(200); await expect(refreshed.json()).resolves.toMatchObject({ - manifest: { present: true, wantedPaths: expect.arrayContaining(["apps/gittensory-ui/"]) }, + manifest: { present: true, source: "repo_file", wantedPaths: expect.arrayContaining(["apps/gittensory-ui/"]) }, }); });