Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/workflows/deploy-web-dev-bunny.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Eurosky fork: build the web app and deploy it to the DEV Bunny.net zone.
#
# Dev mirror of deploy-web-staging-bunny.yml, served at https://dev.mu.social.
# Unlike staging (which continuously tracks eurosky/fork), dev is manual-only:
# run the workflow from the Actions tab and pick any branch to put it live.
# Useful for trying out risky or experimental branches without disturbing
# staging. New file (not in upstream) -> no merge surface.
#
# Dev differs from staging only in build-time env (see the Build step): its
# own OAuth client identity (assertions come from the shared non-prod worker
# at oauth-staging.mu.social - see services/oauth/bunny/staging.ts), and its
# own throwaway Plausible domain. Search noindexing is handled at the Bunny
# pull zone (Edge Rule adds X-Robots-Tag: noindex, nofollow), not in the
# build.
#
# Required repo secrets (Settings -> Secrets and variables -> Actions):
# DEV_BUNNY_STORAGE_ZONE full base URL incl. zone, e.g.
# https://storage.bunnycdn.com/mu-dev
# DEV_BUNNY_STORAGE_PASSWORD storage zone password (AccessKey for uploads)
# DEV_BUNNY_PULLZONE_ID numeric dev pull zone id (cache purge)
# BUNNY_API_KEY account-level API key (shared with prod)
name: Deploy web dev (Bunny)

on:
# Manual only: Actions tab -> "Deploy web dev (Bunny)" -> Run workflow ->
# pick the branch to deploy. No push trigger; dev is an on-demand target.
workflow_dispatch: {}

# Never run two dev deploys at once; a newer run supersedes an in-flight one.
# Separate group from prod/staging so the three never queue on each other.
concurrency:
group: deploy-web-dev-bunny
cancel-in-progress: true

# Least privilege: only checks out + uploads to Bunny via Bunny secrets; never
# uses GITHUB_TOKEN to write to the repo.
permissions:
contents: read

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: package.json
cache: pnpm
# bunny_build.sh does: frozen install (CI) -> intl:build -> build-web ->
# static-path rewrite -> _redirects. CI=true is set by Actions, so it
# uses --frozen-lockfile.
- name: Build web
run: bash ./scripts/bunny_build.sh
env:
# Dev is its own atproto OAuth client: client_id derives from this
# base URL (gen-oauth-metadata.js emits the metadata into the build,
# so dev self-publishes it at /oauth-client-metadata.json).
# Client assertions come from the shared non-prod worker
# (services/oauth/bunny/staging.ts): one instance serves EVERY
# *.mu.social environment by deriving the client_id from the request
# Origin - adding version N needs no OAuth change at all. Prod stays
# on its own pinned worker at oauth.mu.social.
EXPO_PUBLIC_OAUTH_BASE_URL: https://dev.mu.social
EXPO_PUBLIC_OAUTH_ASSERTION_URL: https://oauth-staging.mu.social/client-assertion
# Dev routes AUTHED appview traffic to the Eurosky appview: the PDS
# resolves this DID's #bsky_appview service endpoint
# (https://api.eurosky.network) and proxies there via the
# atproto-proxy header. UNAUTHED traffic (logged-out browsing,
# brand.json services.publicApi/appView) stays on Bluesky's public
# API for now: the Eurosky appview does not resolve handles yet
# (handle lookups 404, profiles return handle.invalid), which would
# break logged-out views.
EXPO_PUBLIC_BLUESKY_PROXY_DID: did:web:api.eurosky.network
# Geolocation: same first-party endpoint as prod (services/geolocation/
# serves any origin), drives the regional moderation labelers.
EXPO_PUBLIC_ENABLE_GEOLOCATION: 'true'
EXPO_PUBLIC_GEOLOCATION_URL: https://ip.mu.social
EXPO_PUBLIC_ENABLE_LIVE_EVENTS: 'false'
EXPO_PUBLIC_ENABLE_APP_CONFIG: 'false'
# Product-analytics events otherwise POST to events.bsky.app. Disabled;
# GrowthBook flag fetching is unaffected (separate from this client).
EXPO_PUBLIC_ENABLE_METRICS: 'false'
# Deliberately NOT the prod Plausible site: dev.mu.social is
# unregistered in Plausible, so events are accepted and discarded and
# dev traffic never pollutes prod stats. (Blanking the value is not
# an option: bunny_build.sh's `:=` re-defaults empty to mu.social.)
EXPO_PUBLIC_PLAUSIBLE_DOMAIN: dev.mu.social
# First-party Bunny proxy for Plausible (see services/plausible/). Needs
# the scheme: the tracker posts to ${API_HOST}/api/event.
EXPO_PUBLIC_PLAUSIBLE_API_HOST: https://events.mu.social
# Shares the prod live-sports proxy (see services/footballData/); the
# endpoint is read-only and cached, so dev can reuse it.
EXPO_PUBLIC_FOOTBALLDATA_API_URL: https://sports.mu.social/v4
- name: Upload to Bunny + purge cache
run: bash ./scripts/bunny_upload.sh
env:
BUNNY_STORAGE_ZONE: ${{ secrets.DEV_BUNNY_STORAGE_ZONE }}
BUNNY_STORAGE_PASSWORD: ${{ secrets.DEV_BUNNY_STORAGE_PASSWORD }}
BUNNY_API_KEY: ${{ secrets.BUNNY_API_KEY }}
BUNNY_PULLZONE_ID: ${{ secrets.DEV_BUNNY_PULLZONE_ID }}
15 changes: 9 additions & 6 deletions .github/workflows/deploy-web-staging-bunny.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
# New file (not in upstream) -> no merge surface.
#
# Staging differs from prod only in build-time env (see the Build step):
# its own OAuth client identity + assertion worker, and a throwaway Plausible
# domain. Search noindexing is handled at the Bunny pull zone (Edge Rule adds
# its own OAuth client identity (assertions come from the shared non-prod
# worker at oauth-staging.mu.social, which accepts any *.mu.social subdomain -
# see services/oauth/bunny/staging.ts), and a throwaway Plausible domain.
# Search noindexing is handled at the Bunny pull zone (Edge Rule adds
# X-Robots-Tag: noindex, nofollow), not in the build.
#
# Required repo secrets (Settings -> Secrets and variables -> Actions):
Expand Down Expand Up @@ -54,10 +56,11 @@ jobs:
env:
# Staging is its own atproto OAuth client: client_id derives from this
# base URL (gen-oauth-metadata.js emits the metadata into the build,
# so staging self-publishes it at /oauth-client-metadata.json), and
# client assertions are signed by the staging worker instance - the
# prod worker pins prod's CLIENT_ID/ALLOWED_ORIGIN and would reject
# staging's requests. Same services/oauth/ script, second deployment.
# so staging self-publishes it at /oauth-client-metadata.json).
# Client assertions come from the shared non-prod worker
# (services/oauth/bunny/staging.ts): one instance serves EVERY
# *.mu.social environment by deriving the client_id from the request
# Origin. Prod stays on its own pinned worker at oauth.mu.social.
EXPO_PUBLIC_OAUTH_BASE_URL: https://staging.mu.social
EXPO_PUBLIC_OAUTH_ASSERTION_URL: https://oauth-staging.mu.social/client-assertion
# Geolocation: same first-party endpoint as prod (services/geolocation/
Expand Down
38 changes: 38 additions & 0 deletions services/oauth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ It is **stateless**: no DB, no KV, no token storage. The only state is one
secret - the client private key. User tokens and DPoP keys stay in the
browser (IndexedDB).

## Two scripts: prod (pinned) vs staging/dev (wildcard subdomain)

- `bunny/index.ts` - **prod**. Pins one exact `CLIENT_ID` + `ALLOWED_ORIGIN`
pair via env. Deployed at `oauth.mu.social`, serves only `mu.social`.
- `bunny/staging.ts` - **every non-prod environment**. Accepts any https
SUBDOMAIN of `ALLOWED_PARENT_DOMAIN` (e.g. `*.mu.social`) and derives the
client_id from the validated request Origin as
`<origin>/oauth-client-metadata.json` - the exact URL each environment
self-publishes its metadata at. Deployed at `oauth-staging.mu.social`.
The apex is deliberately rejected, so this instance can never mint under
prod's identity. Spinning up `v10.mu.social` needs NO OAuth change: point
its build's `EXPO_PUBLIC_OAUTH_ASSERTION_URL` at the staging worker, done.

Both are the same strict reconstructing minter and share the same keypair
(the committed public JWKS is inlined into every build's metadata). The
security model is identical too: the Origin check is defense-in-depth, not
authentication, in both variants (any non-browser caller sets Origin freely);
PKCE + pinned redirect_uri + DPoP do the real work. The split just keeps
prod's config maximally rigid while making new dev versions free.

## What it does

`POST` `{ header, payload }` (produced by `@atproto/oauth-client` in the app)
Expand Down Expand Up @@ -46,6 +66,14 @@ that whole class of clock-skew failures.
`https://<domain>/oauth-client-metadata.json`
- `ALLOWED_ORIGIN` - exact browser Origin, e.g. `https://<domain>`

For the staging/dev worker, deploy `bunny/staging.ts` the same way (hostname
e.g. `oauth-staging.<your-domain>`) with:

- `OAUTH_PRIVATE_JWK` (**Environment SECRET**) - same secret as prod
- `ALLOWED_PARENT_DOMAIN` - apex whose https subdomains are accepted,
e.g. `mu.social` (no `CLIENT_ID`/`ALLOWED_ORIGIN` - both derive from
the request Origin)

## Wire the app to it

The app calls `OAUTH_ASSERTION_URL` (see `../src/config/oauth.ts`). Default:
Expand Down Expand Up @@ -79,3 +107,13 @@ WORKER_URL=http://localhost:8000 node test.mjs

`CLIENT_ID` / `ALLOWED_ORIGIN` default to the `mu.social` config and can be
overridden via env to match the target instance.

`test-staging.mjs` is the equivalent suite for `bunny/staging.ts`: the same
minter guardrails plus the wildcard origin gate (subdomains accepted with
per-origin derived identities; apex, lookalike domains, http, explicit ports,
and cross-environment identities rejected). `PARENT_DOMAIN` defaults to
`mu.social`.

```bash
WORKER_URL=https://oauth-staging.mu.social node test-staging.mjs
```
Loading
Loading