diff --git a/apps/amethyst/Caddyfile b/apps/amethyst/Caddyfile index 58c1373..acde545 100644 --- a/apps/amethyst/Caddyfile +++ b/apps/amethyst/Caddyfile @@ -1,8 +1,10 @@ -{env.CLIENT_ADDRESS} { - handle /xrpc/* { - reverse_proxy aqua-api:3000 - } +{$CLIENT_ADDRESS} { + handle /xrpc/* { + reverse_proxy aqua-api:3000 + } - try_files {path} /index.html - file_server + handle { + try_files {path} /index.html + file_server + } } diff --git a/apps/amethyst/Dockerfile b/apps/amethyst/Dockerfile index 94a15aa..3aedf20 100644 --- a/apps/amethyst/Dockerfile +++ b/apps/amethyst/Dockerfile @@ -16,19 +16,26 @@ RUN corepack enable WORKDIR /app COPY package.json pnpm-workspace.yaml pnpm-lock.yaml turbo.json tsconfig.json ./ +COPY apps/amethyst/package.json ./apps/amethyst/package.json +COPY packages/lexicons/package.json ./packages/lexicons/package.json +COPY packages/tsconfig/package.json ./packages/tsconfig/package.json +COPY tools/lexicon-cli/package.json ./tools/lexicon-cli/package.json + +RUN pnpm install --frozen-lockfile --ignore-scripts + COPY packages ./packages COPY tools ./tools COPY lexicons ./lexicons +COPY vendor/atproto/lexicons/app/bsky/richtext/facet.json ./vendor/atproto/lexicons/app/bsky/richtext/facet.json COPY apps/amethyst ./apps/amethyst -RUN pnpm install --frozen-lockfile --ignore-scripts RUN pnpm rebuild esbuild unrs-resolver RUN pnpm lex:gen-server WORKDIR /app/apps/amethyst RUN pnpm run build:web -RUN node -e 'const fs=require("fs"); const host=process.env.CLIENT_ADDRESS || "'"${CLIENT_ADDRESS}"'"; const base=host.startsWith("http") ? host : `https://${host}`; const metadata={redirect_uris:[`${base}/auth/callback`],response_types:["code"],grant_types:["authorization_code","refresh_token"],scope:"atproto transition:generic",token_endpoint_auth_method:"none",application_type:"web",client_id:`${base}/client-metadata.json`,client_name:"teal",client_uri:base,dpop_bound_access_tokens:true}; fs.writeFileSync("/app/client-metadata.json", JSON.stringify(metadata, null, 2));' +RUN node -e 'const fs=require("fs"); const base=process.env.EXPO_PUBLIC_BASE_URL || "http://localhost:8081"; const metadata={redirect_uris:[`${base}/auth/callback`],response_types:["code"],grant_types:["authorization_code","refresh_token"],scope:"atproto transition:generic",token_endpoint_auth_method:"none",application_type:"web",client_id:`${base}/client-metadata.json`,client_name:"teal",client_uri:base,dpop_bound_access_tokens:true}; fs.writeFileSync("/app/client-metadata.json", JSON.stringify(metadata, null, 2));' FROM caddy:2.8-alpine diff --git a/apps/amethyst/package.json b/apps/amethyst/package.json index 8a5e4cf..b79c360 100644 --- a/apps/amethyst/package.json +++ b/apps/amethyst/package.json @@ -8,12 +8,12 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", - "build": "node ../../node_modules/expo/bin/cli export --output-dir ./build --platform all", - "build:web": "node ../../node_modules/expo/bin/cli export --output-dir ./build --platform web --clear", - "build:ios": "node ../../node_modules/expo/bin/cli export --output-dir ./build --platform ios --clear", + "build": "expo export --output-dir ./build --platform all", + "build:web": "expo export --output-dir ./build --platform web --clear", + "build:ios": "expo export --output-dir ./build --platform ios --clear", "test": "CI=1 node ../../node_modules/jest/bin/jest.js --config jest.lib.config.ts --runInBand --watchAll=false", "lexgen": "lex gen-server ./lexicons/generated/server/ ./lexicons/src/", - "install": "CI=1 EXPO_NO_TELEMETRY=1 node ../../node_modules/expo/bin/cli prebuild --no-install" + "install": "CI=1 EXPO_NO_TELEMETRY=1 expo prebuild --no-install" }, "jest": { "preset": "jest-expo", diff --git a/apps/aqua/Dockerfile b/apps/aqua/Dockerfile index 2a2e650..9f420c7 100644 --- a/apps/aqua/Dockerfile +++ b/apps/aqua/Dockerfile @@ -40,7 +40,7 @@ RUN apt-get update && apt-get install -y nodejs npm && rm -rf /var/lib/apt/lists RUN npm install -g pnpm@9.15.0 # Install dependencies and generate lexicons -RUN pnpm install +RUN pnpm install --ignore-scripts RUN cd tools/lexicon-cli && pnpm build RUN pnpm lex:gen --rust-only diff --git a/docs/development-oauth-tunnel.md b/docs/development-oauth-tunnel.md index 399bad6..82f7254 100644 --- a/docs/development-oauth-tunnel.md +++ b/docs/development-oauth-tunnel.md @@ -2,19 +2,25 @@ Use this when ATProto OAuth callback testing needs a stable public HTTPS hostname. Do not commit Cloudflare tunnel tokens or credentials. +Current stable development preview: + +```text +https://sigilyph.teal.fm +``` + ## One-Time Cloudflare Setup 1. Create a named Cloudflare Tunnel in the Cloudflare Zero Trust dashboard. -2. Add a public hostname for the tunnel, for example `teal-dev.example.com`. +2. Add a public hostname for the tunnel, for example `sigilyph.teal.fm`. 3. Route that hostname to the service URL `http://amethyst:80`. 4. Copy the generated tunnel token into a local shell or an uncommitted `.env` file as `CLOUDFLARED_TUNNEL_TOKEN`. ## Local Environment -Set the public host values before building Amethyst: +Set the public host values before building Amethyst. These values can live in your ignored `.env` file: ```bash -export TUNNEL_HOST=teal-dev.example.com +export TUNNEL_HOST=sigilyph.teal.fm export CLIENT_ADDRESS=:80 export EXPO_PUBLIC_BASE_URL=https://$TUNNEL_HOST export EXPO_PUBLIC_AQUA_URL=https://$TUNNEL_HOST @@ -26,23 +32,37 @@ The Amethyst Caddy image serves the web app and proxies `/xrpc/*` to Aqua, so th ## Build And Run ```bash -docker compose -f compose.dev.yml --profile named-tunnel build amethyst -docker compose -f compose.dev.yml --profile named-tunnel up amethyst aqua-api cadet postgres garnet cloudflared-named +pnpm tunnel:up +``` + +To stop the preview: + +```bash +pnpm tunnel:down +``` + +To inspect it: + +```bash +pnpm tunnel:status +pnpm tunnel:logs +pnpm tunnel:verify ``` Confirm the OAuth client metadata is served from the stable host: ```bash curl https://$TUNNEL_HOST/client-metadata.json +curl "https://$TUNNEL_HOST/xrpc/fm.teal.alpha.stats.getLatest?limit=1" ``` The metadata must include: ```json { - "redirect_uris": ["https://teal-dev.example.com/auth/callback"], - "client_id": "https://teal-dev.example.com/client-metadata.json", - "client_uri": "https://teal-dev.example.com" + "redirect_uris": ["https://sigilyph.teal.fm/auth/callback"], + "client_id": "https://sigilyph.teal.fm/client-metadata.json", + "client_uri": "https://sigilyph.teal.fm" } ``` @@ -53,4 +73,3 @@ The metadata must include: - Confirm the authorization server accepts `https://$TUNNEL_HOST/client-metadata.json`. - Confirm the callback returns to `https://$TUNNEL_HOST/auth/callback`. - Confirm the app restores the signed-in session after refresh. - diff --git a/package.json b/package.json index c8592c2..213f98e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,11 @@ "build": "pnpm turbo run build --filter='./packages/*' --filter='./apps/*'", "build:rust": "turbo run build:rust", "backfill": "./scripts/backfill-tap.sh", + "tunnel:up": "./scripts/dev-tunnel.sh up", + "tunnel:down": "./scripts/dev-tunnel.sh down", + "tunnel:status": "./scripts/dev-tunnel.sh status", + "tunnel:logs": "./scripts/dev-tunnel.sh logs", + "tunnel:verify": "./scripts/dev-tunnel.sh verify", "typecheck": "pnpm -r --filter='!./vendor/*' exec tsc --noEmit", "test": "turbo run test test:rust", "rust:fmt": "pnpm rust:fmt:services && pnpm rust:fmt:apps", diff --git a/packages/lexicons/lex-gen.sh b/packages/lexicons/lex-gen.sh index a2ec9db..a71f6c1 100755 --- a/packages/lexicons/lex-gen.sh +++ b/packages/lexicons/lex-gen.sh @@ -25,7 +25,7 @@ done # Generate lexicons echo "Generating lexicons from: $lexicon_paths" -node ../../node_modules/@atproto/lex-cli/dist/index.js gen-server ./src $lexicon_paths --yes +pnpm exec lex gen-server ./src $lexicon_paths --yes mkdir -p ./src/types/app/bsky/richtext cat > ./src/types/app/bsky/richtext/facet.ts <<'EOF' diff --git a/scripts/dev-tunnel.sh b/scripts/dev-tunnel.sh new file mode 100755 index 0000000..a036c55 --- /dev/null +++ b/scripts/dev-tunnel.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +COMPOSE=(docker compose -f "$ROOT_DIR/compose.dev.yml" --profile named-tunnel) + +load_local_env() { + local env_file="$ROOT_DIR/.env" + [[ -f "$env_file" ]] || return 0 + + local line key value + while IFS= read -r line; do + [[ -z "$line" || "$line" == \#* || "$line" != *=* ]] && continue + key="${line%%=*}" + key="${key//[[:space:]]/}" + value="${line#*=}" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + + case "$key" in + TUNNEL_HOST|CLIENT_ADDRESS|EXPO_PUBLIC_BASE_URL|EXPO_PUBLIC_AQUA_URL|EXPO_PUBLIC_DID_WEB|CLOUDFLARED_TUNNEL_TOKEN) + if [[ -z "${!key:-}" ]]; then + export "$key=$value" + fi + ;; + esac + done < "$env_file" +} + +load_local_env + +TUNNEL_HOST="${TUNNEL_HOST:-sigilyph.teal.fm}" +PUBLIC_ORIGIN="https://${TUNNEL_HOST}" + +usage() { + cat < + +Commands: + tunnel:up Build and start the stable Cloudflare tunnel preview + tunnel:down Stop the stable tunnel preview containers + tunnel:status Show compose service status + tunnel:logs Follow tunnel/app logs + tunnel:verify Verify public metadata and latest-play XRPC + +Required local env: + CLOUDFLARED_TUNNEL_TOKEN must be set in your shell or ignored .env file. + +Defaults: + TUNNEL_HOST=${TUNNEL_HOST} +USAGE +} + +require_token() { + if [[ -z "${CLOUDFLARED_TUNNEL_TOKEN:-}" ]]; then + echo "CLOUDFLARED_TUNNEL_TOKEN is missing. Add it to .env or export it locally." >&2 + exit 1 + fi +} + +export_preview_env() { + export CLIENT_ADDRESS="${CLIENT_ADDRESS:-:80}" + export EXPO_PUBLIC_BASE_URL="${EXPO_PUBLIC_BASE_URL:-$PUBLIC_ORIGIN}" + export EXPO_PUBLIC_AQUA_URL="${EXPO_PUBLIC_AQUA_URL:-$PUBLIC_ORIGIN}" + export EXPO_PUBLIC_DID_WEB="${EXPO_PUBLIC_DID_WEB:-did:web:${TUNNEL_HOST}}" +} + +curl_preview() { + local url="$1" + + if curl --fail --show-error --silent "$url" >/dev/null; then + return 0 + fi + + local ip + ip="$(dig +short "$TUNNEL_HOST" @1.1.1.1 | grep -E '^[0-9.]+$' | head -n 1 || true)" + if [[ -z "$ip" ]]; then + echo "Could not resolve $TUNNEL_HOST with the local resolver or Cloudflare DNS." >&2 + return 1 + fi + + echo "Local DNS has not caught up for $TUNNEL_HOST; retrying verification through $ip." >&2 + curl --fail --show-error --silent --resolve "$TUNNEL_HOST:443:$ip" "$url" >/dev/null +} + +verify_preview() { + echo "Verifying client metadata..." + curl_preview "$PUBLIC_ORIGIN/client-metadata.json" + echo "Verifying latest plays..." + curl_preview "$PUBLIC_ORIGIN/xrpc/fm.teal.alpha.stats.getLatest?limit=1" +} + +case "${1:-}" in + up) + require_token + export_preview_env + "${COMPOSE[@]}" up -d --build postgres garnet aqua-api cadet amethyst cloudflared-named + echo "Stable preview: $PUBLIC_ORIGIN" + verify_preview + ;; + down) + "${COMPOSE[@]}" down + ;; + status) + "${COMPOSE[@]}" ps + ;; + logs) + "${COMPOSE[@]}" logs -f amethyst aqua-api cadet cloudflared-named + ;; + verify) + verify_preview + ;; + *) + usage + exit 1 + ;; +esac diff --git a/services/cadet/Dockerfile b/services/cadet/Dockerfile index 0c87ebc..83cac3e 100644 --- a/services/cadet/Dockerfile +++ b/services/cadet/Dockerfile @@ -40,7 +40,7 @@ RUN apt-get update && apt-get install -y nodejs npm && rm -rf /var/lib/apt/lists RUN npm install -g pnpm@9.15.0 # Install dependencies and generate lexicons -RUN pnpm install +RUN pnpm install --ignore-scripts RUN cd tools/lexicon-cli && pnpm build RUN pnpm lex:gen --rust-only diff --git a/todo.md b/todo.md index a96a550..881683e 100644 --- a/todo.md +++ b/todo.md @@ -22,19 +22,18 @@ This file is the working handoff for the Teal-native Teal clone. Keep it updated - Missing Teal profiles fall back to public Bluesky profile data with an in-app disclaimer, and signed-in listeners can publish a Teal profile through the onboarding wizard. - Development and production Compose files include Amethyst, Aqua, Cadet, Satellite, Postgres, and Garnet. - Development Compose includes an optional Cloudflare Tunnel profile. -- Current temporary UI preview: `https://performing-readily-peace-payment.trycloudflare.com` - - This is an account-less Cloudflare quick tunnel. It remains available while the local tunnel process is running and its hostname will change after restart. - - The preview serves the current Amethyst export through a local static/proxy server on port 8787 and proxies `/xrpc/*` to the locally running Aqua API through the same public hostname. - - The current preview build embeds `EXPO_PUBLIC_BASE_URL=https://performing-readily-peace-payment.trycloudflare.com` and `EXPO_PUBLIC_AQUA_URL=https://performing-readily-peace-payment.trycloudflare.com`; `/client-metadata.json` serves a matching OAuth redirect. +- Current stable UI preview: `https://sigilyph.teal.fm` + - Cloudflare Tunnel `teal-dev-sigilyph` routes `sigilyph.teal.fm` to the Compose `amethyst:80` service. + - The ignored local `.env` has `TUNNEL_HOST=sigilyph.teal.fm`, matching `EXPO_PUBLIC_BASE_URL`, `EXPO_PUBLIC_AQUA_URL`, and `CLOUDFLARED_TUNNEL_TOKEN`. + - Use `pnpm tunnel:up`, `pnpm tunnel:down`, `pnpm tunnel:status`, `pnpm tunnel:logs`, and `pnpm tunnel:verify` for the stable preview. - The preview API is pointed at the OrbStack/Docker Postgres and Garnet services so it serves the existing indexed play corpus. - - OAuth callback testing still requires the stable-host work below. ## Next: Public Demo And OAuth -- [ ] Reserve a stable Cloudflare Tunnel hostname for development OAuth testing. Quick tunnels are useful for UI previews but their random hostnames change after restart. -- [ ] Route the stable public hostname to Amethyst and expose Aqua through a public HTTPS origin or a same-origin reverse proxy. -- [ ] Build Amethyst with `EXPO_PUBLIC_BASE_URL=https://` and `EXPO_PUBLIC_AQUA_URL=https://`. -- [ ] Serve `/client-metadata.json` with `redirect_uris=["https:///auth/callback"]`. +- [x] Reserve a stable Cloudflare Tunnel hostname for development OAuth testing: `sigilyph.teal.fm`. +- [x] Route the stable public hostname to Amethyst and expose Aqua through the same-origin Amethyst reverse proxy. +- [x] Build Amethyst with `EXPO_PUBLIC_BASE_URL=https://sigilyph.teal.fm` and `EXPO_PUBLIC_AQUA_URL=https://sigilyph.teal.fm`. +- [x] Serve `/client-metadata.json` with `redirect_uris=["https://sigilyph.teal.fm/auth/callback"]`. - [ ] Complete ATProto OAuth sign-in and callback QA through the stable public hostname. - [x] Document the stable tunnel token or named-tunnel setup without committing secrets.