Secrets and rotation
The secret inventory, pnpm ops:push-secrets, rotating the relay key and Slack token, and when to require secrets.
Secret inventory
| Secret | Lives in | Used by |
|---|---|---|
NOTIFY_API_KEY | Worker secret; operator store worker.env; sender env ZUDO_SLACK_NOTIFY_API_KEY | Worker, and every sender |
SLACK_BOT_TOKEN | Worker secret; operator store worker.env | Worker only |
SLACK_TARGETS | Worker secret; operator store worker.env | Worker only |
ZUDO_SLACK_NOTIFY_URL | Operator store sender.env | Senders (not sensitive) |
CLOUDFLARE_API_TOKEN | GitHub repository secret | Deploy workflows only |
CLOUDFLARE_ACCOUNT_ID | GitHub repository secret | Deploy workflows only |
Three homes, no others:
Cloudflare Worker secrets: the runtime values.
GitHub repository secrets: Cloudflare deploy credentials only. Never the Slack token or relay key.
The operator store at
$DROPBOX_: the source you push from,ROOT/ env/ zudo- slack- notify/ credentials/andworker. env credentials/.sender. env
Senders never hold SLACK_BOT_TOKEN or a Cloudflare token.
pnpm ops:push-secrets
pnpm ops:push-secrets runs scripts/.
Reads
$DROPBOX_. UseROOT/ env/ zudo- slack- notify/ credentials/ worker. env --env-file <path>to read another file; ifDROPBOX_ROOTis unset and no--env-fileis given, it exits with an error. The file isKEY=valuelines; comments, blank lines, anexportprefix, and one pair of surrounding quotes are accepted.Uploads only the non-empty values of
NOTIFY_API_KEY,SLACK_BOT_TOKEN, andSLACK_TARGETS, in a singlewrangler secret bulkrun againstapp/. The temporary file it hands to wrangler is private and is removed afterwards.Never prints values. Output is one line per name:
NAME: set,NAME: skipped (empty), orNAME: would set (dry run). Validation errors name the key and the rule, never the value.Validates shape first, with the same checks the Worker applies at runtime (it imports them from
app/): the key is 32 to 256 printable ASCII characters without whitespace, the token issrc/ notification. ts xoxb-followed by 5 to 495 letters, digits, or dashes, and the targets are a JSON object of at most 16 KiB with 1 to 100 aliases, each lowercase kebab-case (^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$, at most 64 characters) and mapped to a channel ID matching^[CGD][A-Z0-9]{8,63}$.If any non-empty value is invalid, it uploads nothing at all. It also fails if every value is empty.
--dry-runvalidates and reports which names it would upload, without uploading.
Exit codes: 0 success (or a valid dry run), 1 a value failed validation, nothing was left to upload, or wrangler failed, 2 a usage problem (unknown argument, missing --env-file path, unreadable env file, no DROPBOX_ROOT).
pnpm ops:push-secrets --dry-run
pnpm ops:push-secrets
pnpm ops:push-secrets --env-file ./worker.local.envEmpty means skip, not delete
A blank value in worker.env leaves the deployed secret untouched, and the script prints the wrangler secret delete command for each skipped name. To revoke one, delete it explicitly from app/:
cd app
pnpm exec wrangler secret delete SLACK_BOT_TOKENDeleting a secret makes the Worker answer 503 server_misconfigured again.
Rotate the relay key
Generate a new key:
openssl rand -hex 32.Update
NOTIFY_API_KEYinworker.envandZUDO_SLACK_NOTIFY_API_KEYinsender.env.Run
pnpm ops:push-secrets.
The old key stops working as soon as the new secret is live, so update every sender file around the same time. Until they match, senders get 401 unauthorized (CLI exit 1).
Rotate the Slack token
In the Slack app dashboard, reinstall the app or regenerate the token and copy the new
xoxb-value.Update
SLACK_BOT_TOKENinworker.env.Run
pnpm ops:push-secrets.
No sender change is needed. A revoked or stale token shows up as slack_rejected with invalid_auth, token_revoked, or token_expired in the message.
Change targets
Edit SLACK_TARGETS in worker.env and push. Invite the bot to any new channel first, or sends fail with not_in_channel. An alias removed from the map returns 403 target_not_allowed.
When to add [secrets] required
app/ deliberately has no [secrets] required block yet. The first deploy happens before the Slack bot token exists, so the Worker must deploy without it and fail closed. Once Slack is wired and all three secrets exist on the deployed Worker, add:
[secrets]
required = ["NOTIFY_API_KEY", "SLACK_BOT_TOKEN", "SLACK_TARGETS"]so a later deploy cannot silently ship without them. Keep the top-level scalars above every [table] in the file.
The SLACK_WIRED smoke variable
The post-deploy smoke test posts to / without credentials and inspects the status. The GitHub repository variable SLACK_WIRED selects what is acceptable:
SLACK_WIRED | Unauthenticated POST answer |
|---|---|
not true (default) | 401 passes, and 503 with code server_misconfigured is also tolerated |
true | Only 401 passes; any 503 fails |
Any success response fails in both modes, since an unauthenticated POST must never succeed. deploy-app.yml passes the repository variable to the script as SLACK_WIRED.
Set it to true after the first delivered message, as described in Send the first message. After that, a 503 means a secret went missing and the smoke run fails.