Spaces:
Sleeping
Sleeping
fix(auth): trim whitespace from CCR_APP_URL to prevent sign-in issues
Browse filesdocs: update deployment instructions and clarify CCR_APP_URL usage
test: add test for app_url to handle pasted whitespace in CCR_APP_URL
chore: update reverse proxy worker with correct upstream host
- CHANGELOG.md +4 -0
- DEPLOY.md +32 -17
- _to_delete/git-index.lock.stale +0 -0
- _to_delete/git-index.lock.stale2 +0 -0
- backend/app/auth_google.py +9 -2
- backend/tests/test_google_auth.py +8 -0
- deploy/reverse-proxy-worker.js +1 -1
CHANGELOG.md
CHANGED
|
@@ -64,6 +64,10 @@ Format: [Keep a Changelog](https://keepachangelog.com/). User-visible changes on
|
|
| 64 |
- Landing page links the PI's name (Mohammad Atari) to his website.
|
| 65 |
|
| 66 |
### Fixed
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
- Existing local databases no longer 500 after schema additions: additive
|
| 68 |
SQLite auto-migration adds missing columns at startup (Alembic replaces this in
|
| 69 |
Phase 2 with Postgres).
|
|
|
|
| 64 |
- Landing page links the PI's name (Mohammad Atari) to his website.
|
| 65 |
|
| 66 |
### Fixed
|
| 67 |
+
- Google sign-in no longer breaks when CCR_APP_URL is pasted with a trailing
|
| 68 |
+
newline or space: the app trims the value before building the sign-in return
|
| 69 |
+
URL. Untrimmed, the newline reached Supabase as %0A inside the redirect, so
|
| 70 |
+
the redirect never matched the allow list and browsers flagged the link.
|
| 71 |
- Existing local databases no longer 500 after schema additions: additive
|
| 72 |
SQLite auto-migration adds missing columns at startup (Alembic replaces this in
|
| 73 |
Phase 2 with Postgres).
|
DEPLOY.md
CHANGED
|
@@ -1,17 +1,23 @@
|
|
| 1 |
-
# Deploying the
|
| 2 |
|
| 3 |
The Space builds from this repo's Dockerfile. One-time setup lives in the
|
| 4 |
-
Space settings; after that, deploys are just `git push
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
## Space settings (Settings > Variables and secrets)
|
| 7 |
|
| 8 |
Hugging Face keeps **Variables** and **Secrets** in two separate stores, and a
|
| 9 |
name defined in BOTH puts the Space into `CONFIG_ERROR` ("Collision on
|
| 10 |
variables and secrets names") before it even builds. Add each key below to one
|
| 11 |
-
store only
|
| 12 |
for a duplicated name first, not a bad value.
|
| 13 |
|
| 14 |
-
Secrets (credentials
|
| 15 |
|
| 16 |
| Secret | Value |
|
| 17 |
| ------------------ | ----------------------------------------------------------- |
|
|
@@ -20,17 +26,23 @@ Secrets (credentials — encrypted, write-only once set):
|
|
| 20 |
| SUPABASE_ANON_KEY | from the same page (anon public key, NOT service_role) |
|
| 21 |
| DATABASE_URL | Supabase session-pooler URI (see persistent storage below) |
|
| 22 |
|
| 23 |
-
Variables (non-sensitive tuning
|
| 24 |
|
| 25 |
| Variable | Value |
|
| 26 |
| ---------------------- | --------------------------------------------------- |
|
| 27 |
-
| CCR_APP_URL | https://
|
| 28 |
| CCR_COOKIE_SECURE | 1 |
|
| 29 |
| CCR_MAX_ROWS | 20000 (global row ceiling; code default is 100000) |
|
| 30 |
| CCR_MAX_UPLOAD_BYTES | optional; code default is 52428800 (50 MB) |
|
| 31 |
| CCR_ANON_MAX_BYTES | optional; code default is 5242880 (5 MB) |
|
| 32 |
|
| 33 |
-
`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
with rows and tokens, not file bytes, and on 2 vCPU it is *time*, not memory,
|
| 35 |
that runs out first. Measured on the cpu-basic Space shape (2 vCPU / 16 GB):
|
| 36 |
upload + parse peaks at roughly 5x file size, so even a 50 MB corpus costs
|
|
@@ -43,7 +55,7 @@ unauthenticated request cannot make the server parse a large file only to
|
|
| 43 |
reject it at row 201.
|
| 44 |
|
| 45 |
Embedding throughput at 2 threads, batch 64 (see `scripts/bench_models.py` to
|
| 46 |
-
re-measure on the actual host
|
| 47 |
|
| 48 |
| model | ~15-word rows | ~60-word rows | ~250-word rows |
|
| 49 |
| ------------- | ------------- | ------------- | -------------- |
|
|
@@ -53,7 +65,7 @@ re-measure on the actual host — these are derated estimates, not Space-measure
|
|
| 53 |
At `CCR_MAX_ROWS=20000` that is ~25 s to ~3 min for MiniLM, but up to ~40 min
|
| 54 |
for E5 Large on long documents. Jobs that long are also *fragile*: a Space
|
| 55 |
restart marks any running job failed (`recover_orphaned_jobs`), so worst-case
|
| 56 |
-
job duration
|
| 57 |
|
| 58 |
Retention (CCR_ANON_TTL_HOURS=24) and model pre-warm are already defaults in
|
| 59 |
the Dockerfile.
|
|
@@ -75,7 +87,9 @@ students that URL; no files need to be shared out of band.
|
|
| 75 |
Google provider form.
|
| 76 |
4. Authentication > URL Configuration: add BOTH redirect URLs:
|
| 77 |
- http://127.0.0.1:8000/api/auth/google/callback
|
| 78 |
-
- https://
|
|
|
|
|
|
|
| 79 |
5. Project Settings > API: copy the Project URL and anon key into the Space
|
| 80 |
secrets (and your local .env).
|
| 81 |
|
|
@@ -83,13 +97,14 @@ students that URL; no files need to be shared out of band.
|
|
| 83 |
|
| 84 |
```bash
|
| 85 |
git push origin main # GitHub
|
| 86 |
-
git push
|
| 87 |
```
|
| 88 |
|
| 89 |
-
The
|
| 90 |
-
the password when prompted (or a credential helper).
|
|
|
|
| 91 |
|
| 92 |
-
## Caveats of the
|
| 93 |
|
| 94 |
- ~~Ephemeral disk~~ RESOLVED (2026-08-06, verified): the Space now runs with
|
| 95 |
`DATABASE_URL` (Supabase Postgres) and the `CCR_STORAGE=s3` R2 secrets set,
|
|
@@ -97,9 +112,9 @@ the password when prompted (or a credential helper).
|
|
| 97 |
restarts. The paragraph below ("Persistent storage") documents that setup
|
| 98 |
for anyone redeploying from scratch. Without those secrets, the old caveat
|
| 99 |
applies: SQLite + local files reset on every rebuild.
|
| 100 |
-
- The Space
|
| 101 |
-
|
| 102 |
-
the
|
| 103 |
|
| 104 |
## Persistent storage (make accounts/data survive restarts)
|
| 105 |
|
|
|
|
| 1 |
+
# Deploying the lab instance (Hugging Face Space)
|
| 2 |
|
| 3 |
The Space builds from this repo's Dockerfile. One-time setup lives in the
|
| 4 |
+
Space settings; after that, deploys are just `git push prod main`.
|
| 5 |
+
|
| 6 |
+
This repo deploys to the Space `Culture-and-Morality-Lab/ccr-platform`, which
|
| 7 |
+
is served to the public at https://psychologicaltextanalysis.com through the
|
| 8 |
+
Cloudflare Worker in `deploy/reverse-proxy-worker.js` (Spaces cannot hold a
|
| 9 |
+
custom domain on any tier). Account-by-account setup and the migration from the
|
| 10 |
+
personal dev stack live in `deploy/PRODUCTION_RUNBOOK.md`.
|
| 11 |
|
| 12 |
## Space settings (Settings > Variables and secrets)
|
| 13 |
|
| 14 |
Hugging Face keeps **Variables** and **Secrets** in two separate stores, and a
|
| 15 |
name defined in BOTH puts the Space into `CONFIG_ERROR` ("Collision on
|
| 16 |
variables and secrets names") before it even builds. Add each key below to one
|
| 17 |
+
store only - if the Space reports a config error after a settings change, look
|
| 18 |
for a duplicated name first, not a bad value.
|
| 19 |
|
| 20 |
+
Secrets (credentials - encrypted, write-only once set):
|
| 21 |
|
| 22 |
| Secret | Value |
|
| 23 |
| ------------------ | ----------------------------------------------------------- |
|
|
|
|
| 26 |
| SUPABASE_ANON_KEY | from the same page (anon public key, NOT service_role) |
|
| 27 |
| DATABASE_URL | Supabase session-pooler URI (see persistent storage below) |
|
| 28 |
|
| 29 |
+
Variables (non-sensitive tuning - visible in settings, safe to edit):
|
| 30 |
|
| 31 |
| Variable | Value |
|
| 32 |
| ---------------------- | --------------------------------------------------- |
|
| 33 |
+
| CCR_APP_URL | https://psychologicaltextanalysis.com |
|
| 34 |
| CCR_COOKIE_SECURE | 1 |
|
| 35 |
| CCR_MAX_ROWS | 20000 (global row ceiling; code default is 100000) |
|
| 36 |
| CCR_MAX_UPLOAD_BYTES | optional; code default is 52428800 (50 MB) |
|
| 37 |
| CCR_ANON_MAX_BYTES | optional; code default is 5242880 (5 MB) |
|
| 38 |
|
| 39 |
+
Paste `CCR_APP_URL` with no trailing space or newline. The app builds the
|
| 40 |
+
Google sign-in return URL from it, so a stray newline arrives at Supabase as
|
| 41 |
+
`%0A` inside `redirect_to`: the redirect stops matching the allow list and
|
| 42 |
+
browsers flag the link as dangerous. The app trims the value since 2026-08-17,
|
| 43 |
+
but older deployments and any other URL variable still take it literally.
|
| 44 |
+
|
| 45 |
+
`CCR_MAX_ROWS` is the limit that actually bounds a run - embedding cost scales
|
| 46 |
with rows and tokens, not file bytes, and on 2 vCPU it is *time*, not memory,
|
| 47 |
that runs out first. Measured on the cpu-basic Space shape (2 vCPU / 16 GB):
|
| 48 |
upload + parse peaks at roughly 5x file size, so even a 50 MB corpus costs
|
|
|
|
| 55 |
reject it at row 201.
|
| 56 |
|
| 57 |
Embedding throughput at 2 threads, batch 64 (see `scripts/bench_models.py` to
|
| 58 |
+
re-measure on the actual host - these are derated estimates, not Space-measured):
|
| 59 |
|
| 60 |
| model | ~15-word rows | ~60-word rows | ~250-word rows |
|
| 61 |
| ------------- | ------------- | ------------- | -------------- |
|
|
|
|
| 65 |
At `CCR_MAX_ROWS=20000` that is ~25 s to ~3 min for MiniLM, but up to ~40 min
|
| 66 |
for E5 Large on long documents. Jobs that long are also *fragile*: a Space
|
| 67 |
restart marks any running job failed (`recover_orphaned_jobs`), so worst-case
|
| 68 |
+
job duration - not row count alone - is the number to keep in view.
|
| 69 |
|
| 70 |
Retention (CCR_ANON_TTL_HOURS=24) and model pre-warm are already defaults in
|
| 71 |
the Dockerfile.
|
|
|
|
| 87 |
Google provider form.
|
| 88 |
4. Authentication > URL Configuration: add BOTH redirect URLs:
|
| 89 |
- http://127.0.0.1:8000/api/auth/google/callback
|
| 90 |
+
- https://psychologicaltextanalysis.com/api/auth/google/callback
|
| 91 |
+
Each entry must match what the app sends byte for byte, so keep the custom
|
| 92 |
+
domain (not the hf.space host) here once the Worker is live.
|
| 93 |
5. Project Settings > API: copy the Project URL and anon key into the Space
|
| 94 |
secrets (and your local .env).
|
| 95 |
|
|
|
|
| 97 |
|
| 98 |
```bash
|
| 99 |
git push origin main # GitHub
|
| 100 |
+
git push prod main # Hugging Face Space (rebuilds + redeploys)
|
| 101 |
```
|
| 102 |
|
| 103 |
+
The prod remote has no stored token; use the lab HF username and a WRITE token
|
| 104 |
+
as the password when prompted (or a credential helper). Pushing to origin alone
|
| 105 |
+
changes nothing on the deployed instance.
|
| 106 |
|
| 107 |
+
## Caveats of the deployed instance
|
| 108 |
|
| 109 |
- ~~Ephemeral disk~~ RESOLVED (2026-08-06, verified): the Space now runs with
|
| 110 |
`DATABASE_URL` (Supabase Postgres) and the `CCR_STORAGE=s3` R2 secrets set,
|
|
|
|
| 112 |
restarts. The paragraph below ("Persistent storage") documents that setup
|
| 113 |
for anyone redeploying from scratch. Without those secrets, the old caveat
|
| 114 |
applies: SQLite + local files reset on every rebuild.
|
| 115 |
+
- The lab Space runs on cpu-upgrade hardware, so it does not sleep. On the
|
| 116 |
+
free cpu-basic tier a Space sleeps after ~48 h idle and the first visit
|
| 117 |
+
wakes it (~1 min), which is still the behaviour of the personal dev Space.
|
| 118 |
|
| 119 |
## Persistent storage (make accounts/data survive restarts)
|
| 120 |
|
_to_delete/git-index.lock.stale
ADDED
|
File without changes
|
_to_delete/git-index.lock.stale2
ADDED
|
File without changes
|
backend/app/auth_google.py
CHANGED
|
@@ -48,8 +48,15 @@ def _supabase_url() -> str:
|
|
| 48 |
|
| 49 |
def app_url() -> str:
|
| 50 |
"""Public base URL of THIS app (redirect target). Local default matches
|
| 51 |
-
the dev server; deployments set CCR_APP_URL.
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
|
| 55 |
def begin() -> tuple[str, str]:
|
|
|
|
| 48 |
|
| 49 |
def app_url() -> str:
|
| 50 |
"""Public base URL of THIS app (redirect target). Local default matches
|
| 51 |
+
the dev server; deployments set CCR_APP_URL.
|
| 52 |
+
|
| 53 |
+
strip() before rstrip(): a value pasted into a hosting dashboard can carry
|
| 54 |
+
a trailing newline. Untrimmed it rides into redirect_to as %0A, so the URL
|
| 55 |
+
no longer matches the Supabase redirect allow list AND browsers flag the
|
| 56 |
+
link (a raw newline in a URL parameter reads as header injection). Seen in
|
| 57 |
+
production 2026-08-17.
|
| 58 |
+
"""
|
| 59 |
+
return os.environ.get("CCR_APP_URL", "http://127.0.0.1:8000").strip().rstrip("/")
|
| 60 |
|
| 61 |
|
| 62 |
def begin() -> tuple[str, str]:
|
backend/tests/test_google_auth.py
CHANGED
|
@@ -41,6 +41,14 @@ def test_login_redirects_to_supabase_with_pkce(client, google_env):
|
|
| 41 |
assert auth_google.VERIFIER_COOKIE in resp.cookies
|
| 42 |
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
def test_callback_creates_user_and_signs_in(client, google_env, monkeypatch):
|
| 45 |
monkeypatch.setattr(
|
| 46 |
auth_google, "exchange",
|
|
|
|
| 41 |
assert auth_google.VERIFIER_COOKIE in resp.cookies
|
| 42 |
|
| 43 |
|
| 44 |
+
def test_app_url_ignores_pasted_whitespace(monkeypatch):
|
| 45 |
+
"""A dashboard-pasted CCR_APP_URL can carry a trailing newline. Untrimmed
|
| 46 |
+
it reaches Supabase as %0A inside redirect_to, so the redirect never
|
| 47 |
+
matches the allow list (production, 2026-08-17)."""
|
| 48 |
+
monkeypatch.setenv("CCR_APP_URL", " https://example.org/\n")
|
| 49 |
+
assert auth_google.app_url() == "https://example.org"
|
| 50 |
+
|
| 51 |
+
|
| 52 |
def test_callback_creates_user_and_signs_in(client, google_env, monkeypatch):
|
| 53 |
monkeypatch.setattr(
|
| 54 |
auth_google, "exchange",
|
deploy/reverse-proxy-worker.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
| 44 |
* CCR_APP_URL and the Supabase redirect URL above.
|
| 45 |
*/
|
| 46 |
|
| 47 |
-
const UPSTREAM_HOST = "
|
| 48 |
|
| 49 |
export default {
|
| 50 |
async fetch(request) {
|
|
|
|
| 44 |
* CCR_APP_URL and the Supabase redirect URL above.
|
| 45 |
*/
|
| 46 |
|
| 47 |
+
const UPSTREAM_HOST = "culture-and-morality-lab-ccr-platform.hf.space";
|
| 48 |
|
| 49 |
export default {
|
| 50 |
async fetch(request) {
|