Spaces:
Sleeping
Sleeping
chore: add MIT LICENSE and production deploy assets
Browse files- LICENSE: MIT (Culture and Morality Lab, UMass Amherst). The site states
the tool is free and open source, so the repo now carries a matching
license. MIT is the standard for academic research software and maximizes
reuse and citation.
- deploy/reverse-proxy-worker.js: Cloudflare Worker that serves the Hugging
Face Space at the lab's custom domain (Spaces have no native custom-domain
support), keeping the domain in the address bar.
- deploy/PRODUCTION_RUNBOOK.md: full steps to move to lab-owned accounts
(Supabase, R2, HF Space, Groq), migrate data, and wire the domain.
- .gitignore: exclude personal .agents/ and .codex/ tooling.
- .gitignore +4 -0
- LICENSE +21 -0
- deploy/PRODUCTION_RUNBOOK.md +242 -0
- deploy/reverse-proxy-worker.js +81 -0
.gitignore
CHANGED
|
@@ -13,3 +13,7 @@ node_modules/
|
|
| 13 |
# os
|
| 14 |
.DS_Store
|
| 15 |
.env
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# os
|
| 14 |
.DS_Store
|
| 15 |
.env
|
| 16 |
+
|
| 17 |
+
# personal multi-agent tooling (not part of the project; .claude/ is the shared one)
|
| 18 |
+
.agents/
|
| 19 |
+
.codex/
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Culture and Morality Lab, University of Massachusetts Amherst
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
deploy/PRODUCTION_RUNBOOK.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Production Deployment Runbook
|
| 2 |
+
|
| 3 |
+
Moving the CCR Platform from the personal dev setup to the lab's own accounts,
|
| 4 |
+
and putting it on a custom domain. Written for a one-sitting migration: the code
|
| 5 |
+
is already unified in the lab GitHub repo, the app is feature-complete, and data
|
| 6 |
+
already lives on managed services (Postgres + object storage). This runbook
|
| 7 |
+
carries all of it over to lab-owned accounts.
|
| 8 |
+
|
| 9 |
+
Terminology: "the Space" is the Hugging Face Space that runs the app. "Custom
|
| 10 |
+
domain" is whatever the PI picks (examples below use `ccr.culturemoralitylab.org`;
|
| 11 |
+
swap in the chosen domain everywhere).
|
| 12 |
+
|
| 13 |
+
There are no em dashes in this file on purpose (project rule).
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 0. What is already done
|
| 18 |
+
|
| 19 |
+
- Code is unified in `Culture-and-Morality-Lab/ccr-platform` (public), with clean
|
| 20 |
+
commit history.
|
| 21 |
+
- License texts are staged in the repo root (`LICENSE-MIT`, `LICENSE-APACHE`);
|
| 22 |
+
one gets promoted to `LICENSE` once the PI picks.
|
| 23 |
+
- The app is one portable container (`Dockerfile`); the same image runs on any
|
| 24 |
+
host, so "deploy" is a git push to the Space.
|
| 25 |
+
- Data services are already in use on the dev instance (Supabase Postgres via
|
| 26 |
+
`DATABASE_URL`, Cloudflare R2 via the `CCR_S3_*` secrets), so this is a
|
| 27 |
+
copy-to-lab-accounts job, not new infrastructure.
|
| 28 |
+
|
| 29 |
+
## 1. Accounts to create (lab email, free)
|
| 30 |
+
|
| 31 |
+
| Service | Purpose | Cost |
|
| 32 |
+
| --- | --- | --- |
|
| 33 |
+
| Hugging Face (account or org) | Hosts the Space | Free (cpu-basic) |
|
| 34 |
+
| Supabase | Postgres database + Google sign-in | Free tier |
|
| 35 |
+
| Cloudflare | Domain registrar + DNS + R2 storage + the reverse-proxy Worker | Free (domain ~20/yr; R2 needs a card on file but is free at this scale) |
|
| 36 |
+
| Groq | AI item-drafting key | Free tier |
|
| 37 |
+
| GitHub | Already done (lab org repo exists) | Free |
|
| 38 |
+
|
| 39 |
+
Save every password and key in a password manager as you go. These are the lab's
|
| 40 |
+
institutional credentials, not personal logins.
|
| 41 |
+
|
| 42 |
+
## 2. Secrets sheet (fill privately, never commit)
|
| 43 |
+
|
| 44 |
+
The new Space needs these. Collect the values as you complete each section
|
| 45 |
+
below, then paste them into the Space in section 7.
|
| 46 |
+
|
| 47 |
+
| Name | Store | Value source |
|
| 48 |
+
| --- | --- | --- |
|
| 49 |
+
| `CCR_SESSION_SECRET` | Secret | Generate fresh: `python3 -c "import secrets; print(secrets.token_hex(32))"` (do NOT reuse the dev value) |
|
| 50 |
+
| `DATABASE_URL` | Secret | New Supabase project, Session pooler URI, with the DB password in it (section 3) |
|
| 51 |
+
| `SUPABASE_URL` | Secret | New Supabase project > Settings > API |
|
| 52 |
+
| `SUPABASE_ANON_KEY` | Secret | Same page (anon public key, NOT service_role) |
|
| 53 |
+
| `CCR_STORAGE` | Variable | `s3` |
|
| 54 |
+
| `CCR_S3_ENDPOINT` | Secret | New R2: `https://<account_id>.r2.cloudflarestorage.com` (section 5) |
|
| 55 |
+
| `CCR_S3_BUCKET` | Secret | New R2 bucket name |
|
| 56 |
+
| `CCR_S3_ACCESS_KEY_ID` | Secret | New R2 API token |
|
| 57 |
+
| `CCR_S3_SECRET_ACCESS_KEY` | Secret | New R2 API token |
|
| 58 |
+
| `GROQ_API_KEY` | Secret | New lab Groq key |
|
| 59 |
+
| `CCR_APP_URL` | Variable | `https://<custom-domain>` (section 8) |
|
| 60 |
+
| `CCR_COOKIE_SECURE` | Variable | `1` |
|
| 61 |
+
| `CCR_ANON_TTL_HOURS` | Variable | `24` |
|
| 62 |
+
| `ADMIN_EMAILS` | Variable | Comma-separated admin emails (confirm with PI) |
|
| 63 |
+
| `CCR_MAX_ROWS` | Variable | `50000` (matches the dev instance) |
|
| 64 |
+
|
| 65 |
+
Hugging Face keeps Variables and Secrets in two separate stores. A name defined
|
| 66 |
+
in BOTH puts the Space into CONFIG_ERROR before it builds. Put each name in one
|
| 67 |
+
store only, per the table.
|
| 68 |
+
|
| 69 |
+
## 3. Supabase project (database + sign-in)
|
| 70 |
+
|
| 71 |
+
1. New project under the lab's Supabase org. Match the region to the current
|
| 72 |
+
dev project (read it from the dev `DATABASE_URL`: the pooler host contains it,
|
| 73 |
+
e.g. `aws-0-us-east-1...` means US East). Generate a strong DB password and
|
| 74 |
+
save it.
|
| 75 |
+
2. Turn OFF "Enable Data API" (the app talks to Postgres directly and never uses
|
| 76 |
+
the REST API; leaving it off closes that surface). Leave "Enable automatic
|
| 77 |
+
RLS" on. Do NOT connect GitHub (the app manages its own schema).
|
| 78 |
+
3. Settings > API: copy the Project URL and the anon key into the secrets sheet
|
| 79 |
+
(`SUPABASE_URL`, `SUPABASE_ANON_KEY`).
|
| 80 |
+
4. Connect > Session pooler: copy the URI, insert the DB password, into
|
| 81 |
+
`DATABASE_URL`.
|
| 82 |
+
5. Google sign-in (do this before launch; the button is hidden without it):
|
| 83 |
+
- Google Cloud console (lab Google account) > OAuth consent screen (External)
|
| 84 |
+
> Credentials > Create OAuth client ID (Web application).
|
| 85 |
+
- Supabase > Authentication > Providers > Google > Enable. Copy the shown
|
| 86 |
+
callback (`https://<PROJECT_REF>.supabase.co/auth/v1/callback`) and add it
|
| 87 |
+
as an authorized redirect URI in the Google client. Paste the Google client
|
| 88 |
+
id and secret back into the Supabase Google form.
|
| 89 |
+
- Supabase > Authentication > URL Configuration: add the redirect URLs
|
| 90 |
+
`http://127.0.0.1:8000/api/auth/google/callback` and, once the domain is
|
| 91 |
+
live, `https://<custom-domain>/api/auth/google/callback`.
|
| 92 |
+
|
| 93 |
+
## 4. Migrate the database (carry accounts and data over)
|
| 94 |
+
|
| 95 |
+
Run BEFORE the new Space boots for the first time, so the app finds the data
|
| 96 |
+
already present rather than creating empty tables.
|
| 97 |
+
|
| 98 |
+
```sh
|
| 99 |
+
# Dump the app's tables from the current (dev) database. Use the dev
|
| 100 |
+
# DATABASE_URL from your local .env. --schema=public keeps it to the app's
|
| 101 |
+
# tables; --no-owner --no-acl avoids role mismatches between projects.
|
| 102 |
+
pg_dump "$DEV_DATABASE_URL" --schema=public --no-owner --no-acl -Fc -f ccr_public.dump
|
| 103 |
+
|
| 104 |
+
# Restore into the NEW Supabase project.
|
| 105 |
+
pg_restore --no-owner --no-acl --clean --if-exists \
|
| 106 |
+
-d "$NEW_DATABASE_URL" ccr_public.dump
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
If `pg_dump` version-mismatches against Supabase, use the Postgres client that
|
| 110 |
+
matches the server major version (or the `supabase db dump` CLI). The app's
|
| 111 |
+
startup auto-migration adds any missing columns after this, so a slightly older
|
| 112 |
+
dump still boots cleanly.
|
| 113 |
+
|
| 114 |
+
## 5. R2 object storage (uploaded files)
|
| 115 |
+
|
| 116 |
+
1. Cloudflare > R2 > create a bucket under the lab account (this is when R2 asks
|
| 117 |
+
for a card on file; the 10 GB free tier covers this use).
|
| 118 |
+
2. Create an R2 API token (Object Read and Write) scoped to that bucket. Put the
|
| 119 |
+
access key id, secret, endpoint, and bucket name into the secrets sheet.
|
| 120 |
+
3. Copy the existing files over (R2 is S3-compatible):
|
| 121 |
+
|
| 122 |
+
```sh
|
| 123 |
+
# From the dev R2 to the lab R2. Fill both endpoint/keys from each account.
|
| 124 |
+
aws s3 sync \
|
| 125 |
+
--endpoint-url "$DEV_S3_ENDPOINT" "s3://$DEV_BUCKET" ./r2-migrate-tmp
|
| 126 |
+
aws s3 sync \
|
| 127 |
+
--endpoint-url "$LAB_S3_ENDPOINT" ./r2-migrate-tmp "s3://$LAB_BUCKET"
|
| 128 |
+
# (or use rclone with two remotes: rclone sync dev:bucket lab:bucket)
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## 6. Domain purchase (on the PI call, card needed)
|
| 132 |
+
|
| 133 |
+
1. Cloudflare > Domain Registration > register the chosen domain (recommended:
|
| 134 |
+
buy at Cloudflare so domain, DNS, R2, and the Worker share one account).
|
| 135 |
+
Availability was last checked recently and can change daily, so register the
|
| 136 |
+
same day it is chosen.
|
| 137 |
+
2. If the domain is bought elsewhere, add it to Cloudflare and point its
|
| 138 |
+
nameservers at Cloudflare, so the Worker route in section 8 can attach.
|
| 139 |
+
|
| 140 |
+
## 7. Create the lab Space and add secrets
|
| 141 |
+
|
| 142 |
+
1. New Space under the lab Hugging Face account: SDK = Docker, hardware =
|
| 143 |
+
cpu-basic (free, same as dev). Give it the same repo.
|
| 144 |
+
2. Point the local repo's Space remote at the new Space and push:
|
| 145 |
+
|
| 146 |
+
```sh
|
| 147 |
+
git remote set-url hf https://huggingface.co/spaces/<lab-owner>/<space>
|
| 148 |
+
git push hf main # use the lab HF username + a WRITE token when prompted
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
3. Space > Settings > Variables and secrets: enter every row from the secrets
|
| 152 |
+
sheet (section 2), each in the store the table specifies.
|
| 153 |
+
4. The Space builds and boots. Because the database was restored in section 4,
|
| 154 |
+
accounts and data are already there.
|
| 155 |
+
|
| 156 |
+
## 8. Wire the custom domain (reverse proxy)
|
| 157 |
+
|
| 158 |
+
Hugging Face Spaces cannot serve a custom domain directly, so a small Cloudflare
|
| 159 |
+
Worker serves the Space at the lab domain and keeps the domain in the address bar.
|
| 160 |
+
|
| 161 |
+
1. Get the Space direct host: Space > Settings > Embed this Space > Direct URL,
|
| 162 |
+
of the form `https://<owner>-<space>.hf.space`.
|
| 163 |
+
2. Open `deploy/reverse-proxy-worker.js`, set `UPSTREAM_HOST` to that host
|
| 164 |
+
(without the `https://`).
|
| 165 |
+
3. Cloudflare > Workers & Pages > Create Worker > paste the file > Deploy.
|
| 166 |
+
4. That Worker > Settings > Domains & Routes > Add Custom Domain >
|
| 167 |
+
`ccr.<domain>` (Cloudflare provisions TLS automatically).
|
| 168 |
+
5. Set the Space Variable `CCR_APP_URL=https://ccr.<domain>` and confirm the
|
| 169 |
+
Supabase redirect URL from section 3.5 uses the same domain. Restart the
|
| 170 |
+
Space so it picks up `CCR_APP_URL`.
|
| 171 |
+
|
| 172 |
+
## 9. Launch checklist (verify before announcing)
|
| 173 |
+
|
| 174 |
+
- Site loads on the custom domain; the address bar stays on the domain while
|
| 175 |
+
navigating.
|
| 176 |
+
- Google sign-in completes and returns to the custom domain (not hf.space).
|
| 177 |
+
- Password sign-in works; an account created on the dev instance is present
|
| 178 |
+
(confirms the data migration).
|
| 179 |
+
- Upload a small corpus, pick a construct, run it, open results.
|
| 180 |
+
- AI drafting: "Draft with AI" tab works with the lab Groq key; the drafted
|
| 181 |
+
items appear, and a saved AI construct shows the "AI-generated, not validated"
|
| 182 |
+
label through picker, results, and the metadata download.
|
| 183 |
+
- `/guide` loads; `/admin` is reachable for the emails in `ADMIN_EMAILS`.
|
| 184 |
+
- Promote the license: `git mv LICENSE-MIT LICENSE && rm LICENSE-APACHE` (or the
|
| 185 |
+
reverse), commit, push. Needed because the site publicly says the tool is
|
| 186 |
+
open-source.
|
| 187 |
+
|
| 188 |
+
## 10. Cut over and retire the old instance
|
| 189 |
+
|
| 190 |
+
- Pause (do not delete) the old personal Space once the new one is verified, so
|
| 191 |
+
it is a fallback for a day or two.
|
| 192 |
+
- Keep the dev database and R2 until the new instance has run cleanly for a few
|
| 193 |
+
days, then retire them.
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## Optional, post-launch
|
| 198 |
+
|
| 199 |
+
### Keep the Space awake (free)
|
| 200 |
+
|
| 201 |
+
Free Spaces sleep after about 48 hours idle (first visitor then waits about a
|
| 202 |
+
minute). A free uptime pinger (for example UptimeRobot) hitting the domain every
|
| 203 |
+
few hours keeps it warm without paying. The durable fix is an always-on host
|
| 204 |
+
(next item).
|
| 205 |
+
|
| 206 |
+
### Always-on host (later)
|
| 207 |
+
|
| 208 |
+
The same container runs on any host (Fly, Railway, Hetzner, a UMass VM). Moving
|
| 209 |
+
there removes the sleep behavior AND gives native custom-domain support, so the
|
| 210 |
+
Worker in section 8 can be dropped. Not a launch blocker.
|
| 211 |
+
|
| 212 |
+
### One-push deploys (GitHub Action)
|
| 213 |
+
|
| 214 |
+
Auto-deploy the Space on every merge to `main`, so nobody needs the Space remote.
|
| 215 |
+
Add a GitHub Actions workflow that mirrors `main` to the Space, with an HF write
|
| 216 |
+
token stored as a GitHub Actions secret (`HF_TOKEN`):
|
| 217 |
+
|
| 218 |
+
```yaml
|
| 219 |
+
# .github/workflows/deploy-hf.yml
|
| 220 |
+
name: Deploy to Hugging Face Space
|
| 221 |
+
on:
|
| 222 |
+
push:
|
| 223 |
+
branches: [main]
|
| 224 |
+
jobs:
|
| 225 |
+
deploy:
|
| 226 |
+
runs-on: ubuntu-latest
|
| 227 |
+
steps:
|
| 228 |
+
- uses: actions/checkout@v4
|
| 229 |
+
with: { fetch-depth: 0 }
|
| 230 |
+
- name: Push to Space
|
| 231 |
+
env:
|
| 232 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 233 |
+
run: |
|
| 234 |
+
git push "https://lab:${HF_TOKEN}@huggingface.co/spaces/<lab-owner>/<space>" main
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
### Switch AI drafting to Claude Haiku (later)
|
| 238 |
+
|
| 239 |
+
Add `ANTHROPIC_API_KEY` as a Space secret and the app auto-selects Anthropic over
|
| 240 |
+
Groq (`CCR_GENERATION_PROVIDER` can force either). Groq stays as the free
|
| 241 |
+
fallback. The provenance stamp records which model drafted each construct, so
|
| 242 |
+
constructs drafted during the Groq period stay traceable.
|
deploy/reverse-proxy-worker.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Cloudflare Worker: reverse proxy that serves the CCR Platform Hugging Face
|
| 3 |
+
* Space at the lab's own domain (e.g. ccr.culturemoralitylab.org or
|
| 4 |
+
* www.psychologicaltextanalysis.org), so the address bar keeps the custom
|
| 5 |
+
* domain throughout. The Space host never appears to the visitor.
|
| 6 |
+
*
|
| 7 |
+
* Why a Worker: Hugging Face Spaces do not support custom domains on ANY tier
|
| 8 |
+
* (free or paid), so the domain has to be served in front of the Space. The
|
| 9 |
+
* Workers free tier (100k requests/day) covers lab traffic comfortably.
|
| 10 |
+
*
|
| 11 |
+
* SETUP
|
| 12 |
+
* 1. Set UPSTREAM_HOST below to the Space's direct host. Find it on the Space
|
| 13 |
+
* page: Settings > "Embed this Space" > Direct URL. It has the form
|
| 14 |
+
* https://<owner>-<space>.hf.space (lowercased, non-alphanumerics become
|
| 15 |
+
* hyphens), e.g. culture-and-morality-lab-ccr-platform.hf.space.
|
| 16 |
+
* 2. Cloudflare dashboard > Workers & Pages > Create Worker > paste this file
|
| 17 |
+
* > Deploy.
|
| 18 |
+
* 3. That Worker > Settings > Domains & Routes > Add Custom Domain > enter the
|
| 19 |
+
* subdomain (ccr.culturemoralitylab.org). Cloudflare provisions TLS
|
| 20 |
+
* automatically. The domain must already be in this Cloudflare account.
|
| 21 |
+
*
|
| 22 |
+
* PAIRS WITH (already on the migration runbook, deploy/PRODUCTION_RUNBOOK.md):
|
| 23 |
+
* - Space secret CCR_APP_URL = https://<your-custom-domain>
|
| 24 |
+
* (the app builds the Google sign-in return URL from this).
|
| 25 |
+
* - Supabase Auth > URL Configuration: add
|
| 26 |
+
* https://<your-custom-domain>/api/auth/google/callback to the redirect list.
|
| 27 |
+
*
|
| 28 |
+
* NOTES
|
| 29 |
+
* - redirect: "manual" is required. The app returns 3xx responses (the Google
|
| 30 |
+
* sign-in hand-off, and the post-login redirect back to "/"). Manual mode
|
| 31 |
+
* passes those to the browser instead of the Worker following them
|
| 32 |
+
* server-side, so sign-in works and app redirects stay on the custom domain.
|
| 33 |
+
* - The app's own redirects are relative ("/"), so they resolve against the
|
| 34 |
+
* custom domain automatically. The block below rewrites only the rare
|
| 35 |
+
* absolute redirect that points back at the Space host, as a safety net.
|
| 36 |
+
* - Host-only cookies (no Domain attribute) scope to the custom domain through
|
| 37 |
+
* the proxy with no Set-Cookie rewriting needed.
|
| 38 |
+
* - No websockets in the app; uploads are capped at 50 MB, under the Worker
|
| 39 |
+
* request-body limit.
|
| 40 |
+
*
|
| 41 |
+
* AFTER DEPLOY, verify once: load the site on the custom domain, sign in with
|
| 42 |
+
* Google (the address bar should return to the custom domain, not hf.space),
|
| 43 |
+
* upload a small corpus, run it. If Google sign-in lands on an error, re-check
|
| 44 |
+
* CCR_APP_URL and the Supabase redirect URL above.
|
| 45 |
+
*/
|
| 46 |
+
|
| 47 |
+
const UPSTREAM_HOST = "REPLACE-WITH-owner-space.hf.space";
|
| 48 |
+
|
| 49 |
+
export default {
|
| 50 |
+
async fetch(request) {
|
| 51 |
+
const incoming = new URL(request.url);
|
| 52 |
+
|
| 53 |
+
// Same path and query, sent to the Space host over HTTPS.
|
| 54 |
+
const target = new URL(request.url);
|
| 55 |
+
target.hostname = UPSTREAM_HOST;
|
| 56 |
+
target.protocol = "https:";
|
| 57 |
+
target.port = "";
|
| 58 |
+
|
| 59 |
+
// Clone onto the target URL (preserves method, headers, and body). The
|
| 60 |
+
// Host header is derived from the target URL, so the Space routes correctly.
|
| 61 |
+
const proxied = new Request(target, request);
|
| 62 |
+
const response = await fetch(proxied, { redirect: "manual" });
|
| 63 |
+
|
| 64 |
+
// Safety net: rewrite an absolute redirect back at the Space host onto the
|
| 65 |
+
// custom domain. App redirects are relative, so this rarely fires.
|
| 66 |
+
const location = response.headers.get("location");
|
| 67 |
+
if (location && location.startsWith(`https://${UPSTREAM_HOST}`)) {
|
| 68 |
+
const headers = new Headers(response.headers);
|
| 69 |
+
headers.set(
|
| 70 |
+
"location",
|
| 71 |
+
incoming.origin + location.slice(`https://${UPSTREAM_HOST}`.length),
|
| 72 |
+
);
|
| 73 |
+
return new Response(response.body, {
|
| 74 |
+
status: response.status,
|
| 75 |
+
statusText: response.statusText,
|
| 76 |
+
headers,
|
| 77 |
+
});
|
| 78 |
+
}
|
| 79 |
+
return response;
|
| 80 |
+
},
|
| 81 |
+
};
|