| 1 | #!/usr/bin/env bash |
| 2 | # Prints the BOOTSTRAP_ADMINS list this deploy gives the Worker (the only thing it prints on stdout). |
| 3 | # Run by scripts/bootstrap.sh, which passes it to scripts/render-config.mjs as --admins. |
| 4 | # |
| 5 | # The emails in the list become site administrators when their account is created, or when an |
| 6 | # account still waiting for an invite signs in through /auth/access (src/lib/directory.ts). Admin |
| 7 | # status is then kept in D1, so the list only matters for accounts that don't exist yet. |
| 8 | # |
| 9 | # Where the list comes from, first match wins: |
| 10 | # 1. BOOTSTRAP_ADMINS in the environment, when it isn't empty (scripts/setup.sh asks for it). |
| 11 | # 2. BOOTSTRAP_ADMINS under vars in apps/diffinery/wrangler.jsonc, when it isn't empty (a copy |
| 12 | # that keeps its list in the file). The repository leaves it empty. |
| 13 | # 3. The list the deployed Worker has now, read from its plain-text vars: |
| 14 | # GET /accounts/{account_id}/workers/scripts/diffinery/settings |
| 15 | # https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/script_and_version_settings/methods/get/ |
| 16 | # so a deploy that doesn't set it (the Deploy workflow, a re-run of setup.sh) keeps the admins it had. |
| 17 | # 4. None: a brand-new instance without a list. It warns loudly: nobody becomes a site |
| 18 | # administrator until a deploy sets the list. |
| 19 | # If the Worker's settings can't be read (other than "no such Worker"), it stops instead of |
| 20 | # deploying an empty list in place of the one the Worker has. The emails are never printed to |
| 21 | # the log, only how many there are and where they came from. |
| 22 | # |
| 23 | # Requires: node; and CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, curl and jq for step 3. |
| 24 | set -euo pipefail |
| 25 | |
| 26 | ROOT="$(cd "$(dirname "$0")/.." && pwd)" |
| 27 | WORKER=diffinery |
| 28 | say() { echo "$*" >&2; } # stdout carries only the list |
| 29 | warn() { echo "::warning::$*" >&2; } |
| 30 | count() { tr ',' '\n' <<<"$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -c . || true; } |
| 31 | emit() { # emit LIST SOURCE |
| 32 | say "BOOTSTRAP_ADMINS: $(count "$1") address(es), from $2" |
| 33 | printf '%s\n' "$1" |
| 34 | exit 0 |
| 35 | } |
| 36 | none() { |
| 37 | warn "BOOTSTRAP_ADMINS is empty: $1, so nobody becomes a site administrator on first sign-in. Set BOOTSTRAP_ADMINS (comma-separated emails) and deploy again, for example: BOOTSTRAP_ADMINS=you@example.com scripts/setup.sh" |
| 38 | printf '\n' |
| 39 | exit 0 |
| 40 | } |
| 41 | trimmed() { sed 's/^[[:space:]]*//;s/[[:space:]]*$//' <<<"$1"; } |
| 42 | |
| 43 | # 1. The environment. |
| 44 | FROM_ENV="$(trimmed "${BOOTSTRAP_ADMINS:-}")" |
| 45 | [[ -z "$FROM_ENV" ]] || emit "$FROM_ENV" "the BOOTSTRAP_ADMINS environment variable" |
| 46 | |
| 47 | # 2. wrangler.jsonc. |
| 48 | FROM_FILE="$(node --input-type=module -e ' |
| 49 | import { readFileSync } from "node:fs"; |
| 50 | import { pathToFileURL } from "node:url"; |
| 51 | const { parseJsonc } = await import(pathToFileURL(process.argv[1]).href); |
| 52 | const cfg = parseJsonc(readFileSync(process.argv[2], "utf8")); |
| 53 | process.stdout.write(String(cfg.vars?.BOOTSTRAP_ADMINS ?? "")); |
| 54 | ' "$ROOT/scripts/jsonc.mjs" "$ROOT/apps/diffinery/wrangler.jsonc")" |
| 55 | FROM_FILE="$(trimmed "$FROM_FILE")" |
| 56 | [[ -z "$FROM_FILE" ]] || emit "$FROM_FILE" "apps/diffinery/wrangler.jsonc" |
| 57 | |
| 58 | # 3. The deployed Worker. |
| 59 | if [[ -z "${CLOUDFLARE_API_TOKEN:-}" || -z "${CLOUDFLARE_ACCOUNT_ID:-}" ]]; then |
| 60 | none "it isn't in the environment or wrangler.jsonc, and without CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID the deployed Worker's list can't be read" |
| 61 | fi |
| 62 | SETTINGS="$(curl -sS --retry 4 --retry-all-errors --retry-delay 3 "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/scripts/$WORKER/settings" -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" || true)" |
| 63 | if jq -e '.success == true' >/dev/null 2>&1 <<<"$SETTINGS"; then |
| 64 | LIVE="$(jq -r '[.result.bindings[]? | select(.type == "plain_text" and .name == "BOOTSTRAP_ADMINS") | .text][0] // empty' <<<"$SETTINGS")" |
| 65 | LIVE="$(trimmed "$LIVE")" |
| 66 | [[ -z "$LIVE" ]] || emit "$LIVE" "the deployed $WORKER Worker (kept as it is)" |
| 67 | none "it isn't in the environment, wrangler.jsonc or the deployed $WORKER Worker" |
| 68 | fi |
| 69 | if jq -e 'any(.errors[]?; .code == 10007)' >/dev/null 2>&1 <<<"$SETTINGS"; then |
| 70 | # 10007: no such Worker yet (the first deploy; wrangler's WORKER_NOT_FOUND_ERR_CODE). |
| 71 | none "it isn't in the environment or wrangler.jsonc, and the $WORKER Worker isn't deployed yet" |
| 72 | fi |
| 73 | ERRORS="$(jq -r '[.errors[]? | "\(.code): \(.message)"] | join("; ")' <<<"$SETTINGS" 2>/dev/null || true)" |
| 74 | echo "::error::Could not read the $WORKER Worker's settings to keep its BOOTSTRAP_ADMINS, so the deploy stops rather than replace the list with an empty one. The token needs Account → Workers Scripts → Edit. Or set BOOTSTRAP_ADMINS for this deploy. Cloudflare said: ${ERRORS:-no answer}" >&2 |
| 75 | exit 1 |