diff --git a/docs/superpowers/plans/2026-08-11-ci-gated-coolify-deploy.md b/docs/superpowers/plans/2026-08-11-ci-gated-coolify-deploy.md index d9dda52..d754e4e 100644 --- a/docs/superpowers/plans/2026-08-11-ci-gated-coolify-deploy.md +++ b/docs/superpowers/plans/2026-08-11-ci-gated-coolify-deploy.md @@ -22,7 +22,7 @@ pre-correction code snippets must not be copied or executed; the merged files on correction that must be merged before Task 4 begins. Tasks 4–6 are the active merge/cutover runbook and incorporate the corrected contract directly: -- fixed webhook: `https://panel.codevena.dev/source/github/events/manual`; +- fixed webhook: `https://panel.codevena.dev/webhooks/source/github/events/manual`; - variables: add `COOLIFY_WEBHOOK_URL`; - secrets: use `COOLIFY_READ_TOKEN` and `COOLIFY_WEBHOOK_SECRET`, not `COOLIFY_API_TOKEN`; @@ -1722,15 +1722,17 @@ In Cloudflare Zero Trust: 1. create service token `GitHub Actions - AgoraHub Coolify deploy` expiring 2027-08-11; -2. create three more-specific self-hosted Access applications: the exact manual - webhook path `panel.codevena.dev/source/github/events/manual`, the exact path +2. create four more-specific self-hosted Access applications: the exact manual + webhook path `panel.codevena.dev/webhooks/source/github/events/manual`, the exact path for `panel.codevena.dev/api/v1/applications/z5eij4n8c4ubvxsmtpb507p2` (pre-queue branch verification), and - `panel.codevena.dev/api/v1/deployments/*` (polling); do not expose - `/api/v1/deploy` or any broader `/api/v1/*` path; -3. add a Service Auth policy to all three applications whose include rule is only - that service token; + `panel.codevena.dev/api/v1/deployments/*` (polling), plus an exact + `panel.codevena.dev/api/v1/deploy` application; do not expose any broader + `/api/v1/*` path; +3. add a Service Auth policy to the three automation applications whose include + rule is only that service token; give the exact `/api/v1/deploy` application + only an Everyone/Block policy; 4. verify an anonymous request to each application is denied by Cloudflare Access with an expected `3xx`, `401`, or `403`; retain enough response metadata to prove the denial came from Access and that no Coolify response @@ -1753,11 +1755,17 @@ Do not expose either one-time value in tool output or terminal history. - [ ] **Step 4: Create separate least-privilege Coolify credentials** On the AgoraHub application, create a high-entropy -`manual_webhook_secret_github` value. In Coolify API Tokens, create +`manual_webhook_secret_github` value. Because Coolify hides this field while an +official GitHub App is connected, create a temporary `read + write` API token, +PATCH only that application field over the internal `ssh dedicated` / +`localhost:8000` path, store the same value in GitHub, then revoke the temporary +token and prove it returns `401`; never grant it `deploy`, `root`, or +`read:sensitive`. In Coolify API Tokens, create `GitHub Actions - AgoraHub read` with exactly `read`. Verify the UI lists `read` and neither `deploy`, `write`, nor `root`. Transfer both values only through the -authenticated browser into GitHub's secret form; never place either value in a -shell argument, file, log, clipboard history, or Brain note. +controlled one-time handoff; never place either value in a shell argument, log, +clipboard history, or Brain note. Any temporary local handoff file must be mode +`0600` inside a mode-`0700` directory and securely removed after revocation. - [ ] **Step 5: Transfer API credentials directly into GitHub and set variables** @@ -1773,7 +1781,7 @@ Set variables: ```bash gh variable set COOLIFY_API_BASE_URL --repo Codevena/AgoraHub --body https://panel.codevena.dev/api/v1 -gh variable set COOLIFY_WEBHOOK_URL --repo Codevena/AgoraHub --body https://panel.codevena.dev/source/github/events/manual +gh variable set COOLIFY_WEBHOOK_URL --repo Codevena/AgoraHub --body https://panel.codevena.dev/webhooks/source/github/events/manual gh variable set COOLIFY_APP_UUID --repo Codevena/AgoraHub --body z5eij4n8c4ubvxsmtpb507p2 gh variable set COOLIFY_DEPLOY_BRANCH --repo Codevena/AgoraHub --body coolify-production ``` diff --git a/docs/superpowers/specs/2026-08-11-ci-gated-coolify-deploy-design.md b/docs/superpowers/specs/2026-08-11-ci-gated-coolify-deploy-design.md index 4ab6583..1694192 100644 --- a/docs/superpowers/specs/2026-08-11-ci-gated-coolify-deploy-design.md +++ b/docs/superpowers/specs/2026-08-11-ci-gated-coolify-deploy-design.md @@ -56,7 +56,7 @@ The correction below is incorporated throughout this document. The direct the live Coolify application UUID, branch, GitHub repository, and an explicit empty `watch_paths` value (`null` or blank string; omission fails closed); - it POSTs the exact-SHA payload to the fixed - `https://panel.codevena.dev/source/github/events/manual` endpoint with + `https://panel.codevena.dev/webhooks/source/github/events/manual` endpoint with `X-GitHub-Event: push` and `X-Hub-Signature-256`; - the webhook receives the two Cloudflare Access headers and the HMAC signature, but no Coolify bearer token; @@ -155,15 +155,18 @@ boundary. ### Two-layer API protection -Cloudflare Access gets three path-scoped self-hosted applications: the exact -manual webhook path `panel.codevena.dev/source/github/events/manual`, the exact -application path +Cloudflare Access gets four path-scoped self-hosted applications. Three allow +only the dedicated Service Auth token: the exact manual webhook path +`panel.codevena.dev/webhooks/source/github/events/manual`, the exact application path `panel.codevena.dev/api/v1/applications/z5eij4n8c4ubvxsmtpb507p2`, and -`panel.codevena.dev/api/v1/deployments/*` for status polling. None grants the -service token broader `/api/v1/*` access or exposes `/api/v1/deploy`. Their only -automation policy is Service Auth bound to a new token named -`GitHub Actions - AgoraHub Coolify deploy`. The existing human login policy for -the rest of `panel.codevena.dev` stays unchanged. +`panel.codevena.dev/api/v1/deployments/*` for status polling. A fourth exact +application for `panel.codevena.dev/api/v1/deploy` has only an Everyone/Block +policy, preventing API-token deploys from bypassing CI at the public edge. None +grants the service token broader `/api/v1/*` access. The three automation paths +use Service Auth bound to a new token named `GitHub Actions - AgoraHub Coolify +deploy`. The existing human login policy for the rest of `panel.codevena.dev` +stays unchanged; emergency API access remains possible only internally over the +documented SSH path. The AgoraHub application gets a new high-entropy `manual_webhook_secret_github`; the same one-time value is stored in GitHub as @@ -178,7 +181,7 @@ The GitHub repository stores: |---|---|---| | Variable | `COOLIFY_CD_ENABLED` | `false` during cutover, then permanently `true` | | Variable | `COOLIFY_API_BASE_URL` | `https://panel.codevena.dev/api/v1` | -| Variable | `COOLIFY_WEBHOOK_URL` | `https://panel.codevena.dev/source/github/events/manual` | +| Variable | `COOLIFY_WEBHOOK_URL` | `https://panel.codevena.dev/webhooks/source/github/events/manual` | | Variable | `COOLIFY_APP_UUID` | `z5eij4n8c4ubvxsmtpb507p2` | | Variable | `COOLIFY_DEPLOY_BRANCH` | `coolify-production` | | Secret | `COOLIFY_READ_TOKEN` | new Coolify read-only token | diff --git a/scripts/deploy-coolify.test.ts b/scripts/deploy-coolify.test.ts index e1ff37c..295cfe6 100644 --- a/scripts/deploy-coolify.test.ts +++ b/scripts/deploy-coolify.test.ts @@ -17,7 +17,7 @@ function enabledConfig(overrides: Partial = {}): EnabledDep githubRepository: "Codevena/AgoraHub", githubToken: "github-token", coolifyApiBaseUrl: "https://panel.codevena.dev/api/v1", - coolifyWebhookUrl: "https://panel.codevena.dev/source/github/events/manual", + coolifyWebhookUrl: "https://panel.codevena.dev/webhooks/source/github/events/manual", coolifyAppUuid: "z5eij4n8c4ubvxsmtpb507p2", coolifyDeployBranch: "coolify-production", coolifyReadToken: "coolify-read-token", @@ -61,6 +61,26 @@ describe("readDeployConfig", () => { expect(readDeployConfig({ COOLIFY_CD_ENABLED: "false" })).toEqual({ enabled: false }); }); + it("accepts the deployed Coolify manual webhook route", () => { + expect(readDeployConfig({ + COOLIFY_CD_ENABLED: "true", + VERIFIED_SHA: SHA, + GITHUB_REPOSITORY: "Codevena/AgoraHub", + GITHUB_TOKEN: "github-token", + COOLIFY_API_BASE_URL: "https://panel.codevena.dev/api/v1", + COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/webhooks/source/github/events/manual", + COOLIFY_APP_UUID: "z5eij4n8c4ubvxsmtpb507p2", + COOLIFY_DEPLOY_BRANCH: "coolify-production", + COOLIFY_READ_TOKEN: "coolify-read-token", + COOLIFY_WEBHOOK_SECRET: "webhook-secret", + CF_ACCESS_CLIENT_ID: "access-id", + CF_ACCESS_CLIENT_SECRET: "access-secret", + })).toMatchObject({ + enabled: true, + coolifyWebhookUrl: "https://panel.codevena.dev/webhooks/source/github/events/manual", + }); + }); + it("rejects a non-SHA verified revision before networking", () => { expect(() => readDeployConfig({ COOLIFY_CD_ENABLED: "true", @@ -68,7 +88,7 @@ describe("readDeployConfig", () => { GITHUB_REPOSITORY: "Codevena/AgoraHub", GITHUB_TOKEN: "github-token", COOLIFY_API_BASE_URL: "https://panel.codevena.dev/api/v1", - COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/source/github/events/manual", + COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/webhooks/source/github/events/manual", COOLIFY_APP_UUID: "z5eij4n8c4ubvxsmtpb507p2", COOLIFY_DEPLOY_BRANCH: "coolify-production", COOLIFY_READ_TOKEN: "coolify-read-token", @@ -97,7 +117,7 @@ describe("readDeployConfig", () => { GITHUB_REPOSITORY: "Codevena/AgoraHub", GITHUB_TOKEN: "github-token", COOLIFY_API_BASE_URL: "https://panel.codevena.dev/api/v1", - COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/source/github/events/manual", + COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/webhooks/source/github/events/manual", COOLIFY_APP_UUID: "z5eij4n8c4ubvxsmtpb507p2", COOLIFY_DEPLOY_BRANCH: "coolify-production", COOLIFY_READ_TOKEN: "coolify-read-token", @@ -114,7 +134,7 @@ describe("readDeployConfig", () => { it.each([ ["GITHUB_REPOSITORY", "someone/else"], ["COOLIFY_API_BASE_URL", "https://evil.example/api/v1"], - ["COOLIFY_WEBHOOK_URL", "https://evil.example/source/github/events/manual"], + ["COOLIFY_WEBHOOK_URL", "https://evil.example/webhooks/source/github/events/manual"], ["COOLIFY_APP_UUID", "anotherapp"], ["COOLIFY_DEPLOY_BRANCH", "main"], ])("rejects drifted approved target %s before networking", (name, value) => { @@ -124,7 +144,7 @@ describe("readDeployConfig", () => { GITHUB_REPOSITORY: "Codevena/AgoraHub", GITHUB_TOKEN: "github-app-token", COOLIFY_API_BASE_URL: "https://panel.codevena.dev/api/v1", - COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/source/github/events/manual", + COOLIFY_WEBHOOK_URL: "https://panel.codevena.dev/webhooks/source/github/events/manual", COOLIFY_APP_UUID: "z5eij4n8c4ubvxsmtpb507p2", COOLIFY_DEPLOY_BRANCH: "coolify-production", COOLIFY_READ_TOKEN: "coolify-read-token", @@ -192,7 +212,7 @@ describe("runCoolifyDeployment", () => { const webhookRequest = new Request(fetchImpl.mock.calls[5][0], fetchImpl.mock.calls[5][1]); const webhookBody = await webhookRequest.text(); expect(webhookRequest.url).toBe( - "https://panel.codevena.dev/source/github/events/manual" + "https://panel.codevena.dev/webhooks/source/github/events/manual" ); expect(webhookRequest.headers.get("authorization")).toBeNull(); expect(webhookRequest.headers.get("x-github-event")).toBe("push"); diff --git a/scripts/deploy-coolify.ts b/scripts/deploy-coolify.ts index f8491c2..366f153 100644 --- a/scripts/deploy-coolify.ts +++ b/scripts/deploy-coolify.ts @@ -42,7 +42,7 @@ const SHA_PATTERN = /^[0-9a-f]{40}$/i; const DEPLOYMENT_UUID_PATTERN = /^[a-z0-9_-]+$/i; const APPROVED_REPOSITORY = "Codevena/AgoraHub"; const APPROVED_API_BASE_URL = "https://panel.codevena.dev/api/v1"; -const APPROVED_WEBHOOK_URL = "https://panel.codevena.dev/source/github/events/manual"; +const APPROVED_WEBHOOK_URL = "https://panel.codevena.dev/webhooks/source/github/events/manual"; const APPROVED_APP_UUID = "z5eij4n8c4ubvxsmtpb507p2"; const APPROVED_DEPLOY_BRANCH = "coolify-production"; const DEFAULT_DEADLINE_MS = 20 * 60 * 1_000;