Skip to content

Commit 033f909

Browse files
authored
Merge pull request #10 from eurosky-social/eurosky/oauth-worker
oauth worker and bunny deploy
2 parents b59f8b6 + f8c1ccd commit 033f909

10 files changed

Lines changed: 2251 additions & 0 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Eurosky fork: build the web app and deploy it to Bunny.net.
2+
#
3+
# Builds web-build/ via pages_build.sh, uploads it to the Bunny Edge Storage
4+
# zone, then purges the pull zone cache. New file (not in upstream) -> no merge
5+
# surface.
6+
#
7+
# Required repo secrets (Settings -> Secrets and variables -> Actions):
8+
# BUNNY_STORAGE_ZONE full base URL incl. zone, e.g.
9+
# https://storage.bunnycdn.com/eurosky-app
10+
# BUNNY_STORAGE_PASSWORD storage zone password (AccessKey for uploads)
11+
# BUNNY_API_KEY account-level API key (for the cache purge)
12+
# BUNNY_PULLZONE_ID numeric pull zone id (for the cache purge)
13+
name: Deploy web (Bunny)
14+
15+
on:
16+
# Manual run: Actions tab -> "Deploy web (Bunny)" -> Run workflow.
17+
workflow_dispatch: {}
18+
# To auto-deploy on push, uncomment and set the deploy branch:
19+
push:
20+
branches: [eurosky/fork]
21+
22+
# Never run two deploys at once; a newer run supersedes an in-flight one.
23+
concurrency:
24+
group: deploy-web-bunny
25+
cancel-in-progress: true
26+
27+
jobs:
28+
deploy:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v5
32+
- uses: pnpm/action-setup@v6
33+
- uses: actions/setup-node@v6
34+
with:
35+
node-version-file: package.json
36+
cache: pnpm
37+
# pages_build.sh does: frozen install (CI) -> intl:build -> build-web ->
38+
# static-path rewrite -> _redirects. CI=true is set by Actions, so it
39+
# uses --frozen-lockfile.
40+
- name: Build web
41+
run: bash ./pages_build.sh
42+
- name: Upload to Bunny + purge cache
43+
run: bash ./bunny_upload.sh
44+
env:
45+
BUNNY_STORAGE_ZONE: ${{ secrets.BUNNY_STORAGE_ZONE }}
46+
BUNNY_STORAGE_PASSWORD: ${{ secrets.BUNNY_STORAGE_PASSWORD }}
47+
BUNNY_API_KEY: ${{ secrets.BUNNY_API_KEY }}
48+
BUNNY_PULLZONE_ID: ${{ secrets.BUNNY_PULLZONE_ID }}

bunny_upload.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
# Eurosky fork: upload the static web build to a Bunny.net Edge Storage zone.
3+
#
4+
# Reads from the environment (or ./.env if not already set):
5+
# BUNNY_STORAGE_ZONE full base URL incl. zone name, e.g.
6+
# https://storage.bunnycdn.com/eurosky-app
7+
# BUNNY_STORAGE_PASSWORD storage zone password (sent as the AccessKey header)
8+
#
9+
# Usage: ./bunny_upload.sh [build-dir] (default: web-build)
10+
#
11+
# This only uploads files. It does not delete stale objects or purge the pull
12+
# zone cache - that comes with the CI step. Hashed assets are immutable so
13+
# leftover old chunks are harmless; index.html overwrites in place.
14+
set -euo pipefail
15+
16+
DIR="${1:-web-build}"
17+
18+
# Load .env only if the vars aren't already provided by the environment (CI).
19+
if [[ -z "${BUNNY_STORAGE_ZONE:-}" || -z "${BUNNY_STORAGE_PASSWORD:-}" ]]; then
20+
if [[ -f .env ]]; then
21+
set -a; . ./.env; set +a
22+
fi
23+
fi
24+
: "${BUNNY_STORAGE_ZONE:?set BUNNY_STORAGE_ZONE}"
25+
: "${BUNNY_STORAGE_PASSWORD:?set BUNNY_STORAGE_PASSWORD}"
26+
27+
BASE="${BUNNY_STORAGE_ZONE%/}" # strip any trailing slash
28+
[[ -d "$DIR" ]] || { echo "no $DIR/ - build first (./pages_build.sh)"; exit 1; }
29+
30+
total=$(find "$DIR" -type f | wc -l | tr -d ' ')
31+
echo "Uploading $total files from $DIR/ -> $BASE ..."
32+
33+
export BASE BUNNY_STORAGE_PASSWORD DIR
34+
35+
# Upload a single file; print only on failure. Bunny returns 201 on success.
36+
upload_one() {
37+
local file="$1"
38+
local rel="${file#"$DIR"/}"
39+
local code
40+
code=$(curl -sS -o /dev/null -w '%{http_code}' -X PUT \
41+
"$BASE/$rel" \
42+
-H "AccessKey: $BUNNY_STORAGE_PASSWORD" \
43+
--data-binary @"$file") || { echo "ERR $rel (curl failed)"; return 1; }
44+
if [[ "$code" != "201" && "$code" != "200" ]]; then
45+
echo "FAIL $code $rel"
46+
return 1
47+
fi
48+
}
49+
export -f upload_one
50+
51+
fails=0
52+
find "$DIR" -type f -print0 \
53+
| xargs -0 -P 8 -I{} bash -c 'upload_one "$@"' _ {} \
54+
|| fails=1
55+
56+
if [[ "$fails" != "0" ]]; then
57+
echo "Completed WITH ERRORS (see FAIL/ERR lines above)." >&2
58+
exit 1
59+
fi
60+
echo "Done: uploaded $total files to $BASE"
61+
62+
# Optional: purge the pull zone cache so the new build is served immediately.
63+
# Needs the account-level API key (NOT the storage password) and the pull
64+
# zone id. Skipped silently if either is missing (e.g. plain local upload).
65+
if [[ -n "${BUNNY_API_KEY:-}" && -n "${BUNNY_PULLZONE_ID:-}" ]]; then
66+
echo "Purging pull zone $BUNNY_PULLZONE_ID cache ..."
67+
pcode=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
68+
"https://api.bunny.net/pullzone/${BUNNY_PULLZONE_ID}/purgeCache" \
69+
-H "AccessKey: ${BUNNY_API_KEY}") || { echo "purge: curl failed" >&2; exit 1; }
70+
if [[ "$pcode" == "204" || "$pcode" == "200" ]]; then
71+
echo "Purged."
72+
else
73+
echo "purge returned HTTP $pcode" >&2
74+
exit 1
75+
fi
76+
else
77+
echo "(skipping cache purge: set BUNNY_API_KEY + BUNNY_PULLZONE_ID to enable)"
78+
fi

oauth-worker/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
.wrangler/
3+
dist/
4+
.dev.vars
5+
*.log
6+
.env

oauth-worker/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# eurosky-oauth-worker
2+
3+
Stateless Cloudflare Worker that signs Eurosky's `private_key_jwt` OAuth
4+
**client assertions**. It makes the Eurosky web app a *confidential* atproto
5+
OAuth client (unlimited session lifetime + 180-day refresh tokens) without
6+
putting the client private key in the browser.
7+
8+
It is **stateless**: no DB, no KV, no token storage. The only state is one
9+
secret — the client private key. User tokens and DPoP keys stay in the
10+
browser (IndexedDB), exactly as before.
11+
12+
## What it does
13+
14+
`POST` `{ header, payload }` (produced by `@atproto/oauth-client` in the app)
15+
→ validates strict invariants → signs ES256 → returns `{ jws }`.
16+
17+
It is an assertion *minter*, not a generic signing oracle. Every request must
18+
satisfy: `alg=ES256`, `kid` matches the key, `iss==sub==CLIENT_ID`, `aud` is
19+
an `https` URL, `jti` present, short lifetime (≤300s), sane `iat`/`exp`. The
20+
browser `Origin` is locked to `ALLOWED_ORIGIN`.
21+
22+
## Deploy
23+
24+
```bash
25+
cd eurosky-oauth-worker
26+
npm install
27+
28+
# 1. Set the private key secret. Paste the PRIVATE JWK JSON printed by
29+
# ../eurosky-social-app/scripts/gen-oauth-keypair.js (the block labelled
30+
# "PRIVATE JWK"). It is never committed and never leaves Cloudflare.
31+
npx wrangler secret put OAUTH_PRIVATE_JWK
32+
33+
# 2. Confirm vars in wrangler.toml match your domain:
34+
# CLIENT_ID = https://<domain>/oauth-client-metadata.json
35+
# ALLOWED_ORIGIN = https://<domain>
36+
# (current: eurosky.atmo.tools)
37+
38+
# 3. Deploy
39+
npx wrangler deploy
40+
```
41+
42+
## Wire the app to it
43+
44+
The app calls `OAUTH_ASSERTION_URL` (see
45+
`eurosky-social-app/src/config/oauth.ts`). Default:
46+
`https://oauth.eurosky.atmo.tools/client-assertion`.
47+
48+
Either bind that route to this Worker (uncomment the `[[routes]]` block in
49+
`wrangler.toml`, point DNS at it), or set
50+
`EXPO_PUBLIC_OAUTH_ASSERTION_URL` at app build time to the Worker's actual
51+
URL (e.g. its `*.workers.dev` URL).
52+
53+
Loopback/dev does NOT use this Worker — dev stays a public client.
54+
55+
## Key rotation
56+
57+
Generate a new keypair (`gen-oauth-keypair.js --force`), commit the new
58+
public JWKS, redeploy the web build (regenerates
59+
`oauth-client-metadata.json`), and `wrangler secret put OAUTH_PRIVATE_JWK`
60+
with the new private key. The shared `kid` keeps old/new distinguishable;
61+
publish both public keys briefly for a graceful rollover.

0 commit comments

Comments
 (0)