Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
8 changes: 0 additions & 8 deletions .env.tauth.example

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/frontend-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ jobs:
working-directory: frontend

- name: Run tests
run: npm test
run: npm test -- --fail-fast
working-directory: frontend
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ tools/
# Managed by gix gitignore workflow
.env
.env.*
!.env.*.example
.DS_Store
qodana.yaml
bin/
14 changes: 9 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ EasyMDE produces markdown, marked renders it to HTML, and DOMPurify sanitises th
- `GravityStore` persists notes in IndexedDB for offline-first behaviour; reconciliation applies backend snapshots.
- `createNoteRecord` validates note identifiers/markdown before writes so malformed payloads never hit storage.
- `GravityStore.setUserScope(userId)` switches the storage namespace so each Google account receives an isolated notebook.
- Runtime configuration loads from environment-specific JSON files under `data/`, selected according to the active hostname. Each profile now surfaces `authBaseUrl` so the frontend knows which TAuth origin to contact when requesting `/auth/nonce`, `/auth/google`, and `/auth/logout`.
- Authentication flows through Google Identity Services + TAuth: the browser loads `authBaseUrl/tauth.js`, fetches a nonce from `/auth/nonce`, exchanges Google credentials at `/auth/google`, and refreshes the session via `/auth/refresh`. The frontend never sends Google tokens to the Gravity backend; every API request simply carries the `app_session` cookie minted by TAuth and validated locally via HS256.
- Runtime configuration loads from environment-specific JSON files under `data/`, selected according to the active hostname. Each profile now surfaces `authBaseUrl` (API origin), `tauthScriptUrl` (TAuth CDN host), and `mprUiScriptUrl` (mpr-ui CDN host) so the frontend knows where to call `/auth/*` and where to load `tauth.js` + the auth UI bundle.
- Authentication flows through Google Identity Services + TAuth: the browser loads `tauth.js` from `tauthScriptUrl`, fetches a nonce from `/auth/nonce`, exchanges Google credentials at `/auth/google`, and refreshes the session via `/auth/refresh`. The frontend never sends Google tokens to the Gravity backend; every API request simply carries the `app_session` cookie minted by TAuth and validated locally via HS256.
- The backend records a canonical user table (`user_identities`) so each `(provider, subject)` pair (for example `google:1234567890`) maps to a stable Gravity `user_id`. That allows multiple login providers to point at the same notebook without rewriting note rows.

#### Frontend Dependencies
Expand Down Expand Up @@ -139,7 +139,9 @@ Profiles live under `frontend/data/runtime.config.<environment>.json` and are se
{
"environment": "development",
"backendBaseUrl": "http://localhost:8080",
"llmProxyUrl": "http://localhost:8081/v1/gravity/classify"
"llmProxyUrl": "http://localhost:8081/v1/gravity/classify",
"tauthScriptUrl": "https://tauth.mprlab.com/tauth.js",
"mprUiScriptUrl": "https://cdn.jsdelivr.net/gh/MarcoPoloResearchLab/mpr-ui@latest/mpr-ui.js"
}
```

Expand All @@ -148,15 +150,17 @@ Profiles live under `frontend/data/runtime.config.<environment>.json` and are se
{
"environment": "production",
"backendBaseUrl": "https://gravity-api.mprlab.com",
"llmProxyUrl": "https://llm-proxy.mprlab.com/v1/gravity/classify"
"llmProxyUrl": "https://llm-proxy.mprlab.com/v1/gravity/classify",
"tauthScriptUrl": "https://tauth.mprlab.com/tauth.js",
"mprUiScriptUrl": "https://cdn.jsdelivr.net/gh/MarcoPoloResearchLab/mpr-ui@latest/mpr-ui.js"
}
```

When serving from an alternate hostname, add a new profile or override the URLs explicitly before bootstrapping the Alpine application.

#### Authentication Contract

- **Browser responsibilities:** Gravity’s frontend loads `authBaseUrl/tauth.js`, asks `/auth/nonce` for a nonce, exchanges Google credentials at `/auth/google`, and retries requests via `/auth/refresh` when the backend returns `401`. All network calls simply include the `app_session` cookie; no Google tokens touch the Gravity API.
- **Browser responsibilities:** Gravity’s frontend loads `tauth.js` from `tauthScriptUrl`, asks `/auth/nonce` for a nonce, exchanges Google credentials at `/auth/google`, and retries requests via `/auth/refresh` when the backend returns `401`. All network calls simply include the `app_session` cookie; no Google tokens touch the Gravity API.
- **Backend responsibilities:** the API validates `app_session` with the shared HS256 signing secret and fixed `tauth` issuer, stores no refresh tokens, and trusts the canonical `user_id` resolved by the `user_identities` table. A one-time migration strips the legacy `google:` prefix from existing note rows and backfills the identity mapping automatically.
- **Logout propagation:** triggering **Sign out** in the UI invokes `/auth/logout`, revokes refresh tokens inside TAuth, and dispatches `gravity:auth-sign-out` so the browser returns to the anonymous notebook.
- **Future providers:** because every `(provider, subject)` pair maps to the same Gravity user, we can add Apple/email sign-in later without rewriting stored notes.
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ and are grouped by the date the work landed on `master`.
- TAuth session now delegates nonce issuance and credential exchange to auth-client helpers instead of local fetches (GN-423).
- Centralized environment config defaults and reused them across runtime config and test harnesses (GN-427).
- Runtime config now returns a frozen app config and callers pass it explicitly instead of shared globals (GN-427).
- Environment example files now live at `env.*.example`, keeping `.env*` files untracked while preserving copy-ready templates (GN-443).
- mpr-ui now loads from a runtime-configured script URL (`mprUiScriptUrl`) after tauth.js so login components always register (GN-444).
- Auth boot now fails fast when required helpers/components are missing and pre-initializes Google Identity Services before rendering the login button to avoid GSI warnings (GN-445).
- Signed-out visitors now see a landing page with a Google sign-in button; the Gravity interface requires authentication (GN-126).
- mpr-ui now loads from a static script tag and auth components mount after runtime config so attributes are applied before initialization (GN-436).
- Frontend now pulls mpr-ui assets from the `@latest` CDN tag so releases stay aligned (GN-437).

### Fixed
- TAuth helper now loads from a dedicated CDN URL via `tauthScriptUrl`, and gHTTP no longer proxies `/tauth.js` while proxying `/me` to TAuth for session checks in the dev stack (GN-442).
- Dev docker compose now serves Gravity over HTTPS at computercat.tyemirov.net:4443 via gHTTP proxies for backend/TAuth endpoints, with updated dev runtime config and env templates (GN-441).
- Normalized development runtime config endpoints to swap loopback hosts for the active dev hostname and refreshed the TAuth env example for localhost defaults (GN-440).
- Sync queue now coalesces per note and resolves payloads from the latest stored note to avoid duplicate ops and offline failures (GN-439).
- Landing sign-in now sets mpr-ui auth base/login/logout/nonce attributes so nonce requests hit TAuth instead of the frontend origin (GN-433).
- Runtime config now accepts a Google client ID override so local GSI origins can match the correct project (GN-438).
Expand Down
17 changes: 17 additions & 0 deletions ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,23 @@ Each issue is formatted as `- [ ] [GN-<number>]`. When resolved it becomes -` [x
- [x] [GN-438] (P1) Allow runtime config to override the Google client ID so local dev origins can match the correct GSI project. (Resolved by requiring googleClientId in runtime config payloads/JSON, plumbing through the config builder, and updating tests.)
- [x] [GN-439] (P1) `sync.manager.test.js` fails during `make ci` with `offline` errors and duplicate sync operations (expected 1, observed 2) in the "coalesces repeated upserts" case; investigate and stabilize. (Resolved by coalescing per-note operations, deferring payload hydration to flush time, and adding regression coverage.)
- [x] [GN-439] (P1) `QuotaExceededError` in sync queue persistence when localStorage fills; redesign persistence to avoid localStorage quotas and coalesce pending sync operations. (Resolved by migrating notes/sync queue/sync metadata to IndexedDB with localStorage-only test mode and storage-full notifications.)
- [x] [GN-440] (P1) Local auth fails when TAuth tenant origin/cookie domain drift from the runtime-configured localhost URLs, causing tauth.js/nonce requests to miss or fail CORS. (Resolved by rewriting loopback runtime endpoints for non-loopback dev hosts and refreshing TAuth env defaults for localhost.)
- [x] [GN-441] (P1) Dev auth fails on computercat.tyemirov.net because gHTTP still serves only the frontend and runtime config points to localhost; proxy backend/TAuth through gHTTP HTTPS and update dev config/env defaults. (Resolved by adding gHTTP HTTPS env config + proxy routes, updating compose and runtime config defaults for computercat, and documenting the new dev stack.)
- [x] [GN-442] (P1) Load tauth.js from the CDN (not via gHTTP), remove the /tauth.js proxy route, and wire the runtime config/test harness to use a dedicated tauthScriptUrl for the helper. (Resolved by enforcing absolute CDN tauth.js URLs and proxying /me through gHTTP so auth boot hits TAuth.)
- [x] [GN-443] (P1) Keep `.env*` files untracked and rename example env files to `env.*.example` with updated setup docs.
- [x] [GN-444] (P1) Ensure the mpr-ui login component always registers by loading the bundle from a runtime-configured `mprUiScriptUrl` after tauth.js.
- [x] [GN-445] (P1) Make auth boot strict (no fallbacks) and pre-initialize GIS before rendering the mpr-ui login button to avoid GSI warnings.
- [ ] [GN-446] (P1) Adopt the mpr-ui config.yaml loader for auth wiring so login buttons render from declarative config and tauth.js only loads from the CDN.
- [x] [GN-447] (P1) Auth boot loop and missing avatar after login when TAuth init resolves late.
Resolved by waiting for TAuth init callbacks before dispatching auth state, aligning landing/app redirects, and adding Playwright E2E coverage for login → app user menu rendering.
- [x] [GN-448] (P0) Enter full screen menu item should appear before Sign out in the user menu.
(Resolved by adding the full screen action to the mpr-user menu items ahead of logout, wiring the menu action to the full screen toggle, and extending avatar menu coverage to assert ordering.)
- [x] [GN-449] (P0) Enter full screen must be a user menu item before Sign out (regression reported again).
(Resolved by removing the standalone fullscreen button, keeping the menu item before Sign out, and adding Playwright coverage for menu toggling + absence of the standalone control.)
- [x] [GN-450] (P1) CI fails because the Playwright login test reads mpr-ui assets from the gitignored tools folder.
(Resolved by removing local mpr-ui fixtures in the test harness, waiting for mpr-ui config application in landing tests, and propagating nonce error codes through the auth wrapper; make test/lint/ci pass.)
- [x] [GN-451] (P1) Fail fast on CI test runs after the first failure while keeping local runs exhaustive.
(Resolved by enabling CI-only fail-fast behavior in the test harness with immediate error output.)


## Maintenance (428–499)
Expand Down
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,29 +57,32 @@ Developers and curious tinkerers can find project structure, dependencies, and r

## Local Stack (Gravity + TAuth)

Run the full application locally (frontend, backend, and the new TAuth service) via Docker:
Run the full application locally (frontend, backend, TAuth, and the gHTTP reverse proxy) via Docker:

1. Copy the environment files and customize secrets as needed:
- `cp .env.gravity.example .env.gravity`
- `cp .env.tauth.example .env.tauth`
- `cp env.gravity.example .env.gravity`
- `cp env.tauth.example .env.tauth`
- `cp env.ghttp.example .env.ghttp`

Keep `GRAVITY_TAUTH_SIGNING_SECRET` in sync with `APP_JWT_SIGNING_KEY` so Gravity and TAuth agree on JWT signatures.

2. Start the stack with the development profile (local backend build): `docker compose --profile dev up --build`
2. Add TLS certificates for `computercat.tyemirov.net` and update `.env.ghttp` with the paths (the compose file mounts `./certs` at `/certs` inside the container).

3. Start the stack with the development profile (local backend build): `docker compose --profile dev up --build`

Switch to the docker profile (`docker compose --profile docker up`) to run everything from prebuilt GHCR images.

The compose file exposes:

- Frontend static assets at `http://localhost:8000`
- Gravity backend API at `http://localhost:8080`
- TAuth (nonce + Google exchange + `tauth.js`) at `http://localhost:8082`
- Frontend + proxied API at `https://computercat.tyemirov.net:4443` (gHTTP terminates TLS and proxies `/notes`, `/auth`, `/me`, and `/api` to the backend/TAuth containers)
- Gravity backend API at `http://localhost:8080` (container port published for direct access)
- TAuth (nonce + Google exchange) at `http://localhost:8082` (container port published for direct access)

Runtime configuration files under `frontend/data/` now include `authBaseUrl`, so the browser can discover which TAuth origin to contact for `/auth/nonce`, `/auth/google`, and `/auth/logout` once the frontend wiring lands. Update `frontend/data/runtime.config.production.json` if your deployment uses a different TAuth hostname.
Auth settings now live in `frontend/config.yaml`, which maps each allowed `window.location.origin` to its TAuth base URL, Google client ID, tenant ID, and `/auth/*` paths (plus optional login button styling). Update `frontend/config.yaml` whenever origins or auth settings change. Runtime configuration files under `frontend/data/` remain the source of truth for backend and LLM proxy endpoints; update `frontend/data/runtime.config.production.json` and `frontend/data/runtime.config.development.json` when those API hosts change.

### Authentication Contract

- Gravity no longer exchanges Google credentials itself. The browser loads `https://<tauth-origin>/tauth.js`, fetches a nonce from `/auth/nonce`, and lets TAuth exchange the Google credential at `/auth/google`.
- Gravity no longer exchanges Google credentials itself. The browser loads `tauth.js` from the TAuth CDN, applies `frontend/config.yaml` to discover the TAuth base URL, fetches a nonce from `/auth/nonce`, and lets TAuth exchange the Google credential at `/auth/google`.
- TAuth mints two cookies: `app_session` (short-lived HS256 JWT) and `app_refresh` (long-lived refresh token). Every request from the UI includes `app_session` automatically, so the Gravity backend validates the JWT using `GRAVITY_TAUTH_SIGNING_SECRET` and the fixed `tauth` issuer. No bearer tokens or local storage is used.
- To keep the multi-tenant TAuth flow working, the backend’s CORS preflight now whitelists the `X-TAuth-Tenant` header (in addition to `Authorization`, `Content-Type`, etc.), so browsers can send the tenant hint while relying on cookie authentication.
- When a request returns `401`, the browser calls `/auth/refresh` on the TAuth origin; a fresh `app_session` cookie is minted and the original request is retried.
Expand Down
8 changes: 6 additions & 2 deletions config.tauth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ server:
database_url: "${TAUTH_DATABASE_URL}"
enable_cors: "${TAUTH_ENABLE_CORS}"
cors_allowed_origins:
- ${TAUTH_CORS_ORIGIN_1}
- "${TAUTH_CORS_ORIGIN_1}"
- "${TAUTH_CORS_ORIGIN_2}"
- "${TAUTH_CORS_ORIGIN_3}"
cors_allowed_origin_exceptions:
- "${TAUTH_CORS_EXCEPTION_1}"
enable_tenant_header_override: ${TAUTH_ENABLE_TENANT_HEADER_OVERRIDE}
Expand All @@ -12,7 +14,9 @@ tenants:
- id: ${TAUTH_TENANT_ID_GRAVITY}
display_name: ${TAUTH_TENANT_DISPLAY_NAME_GRAVITY}
tenant_origins:
- ${TAUTH_CORS_ORIGIN_1}
- "${TAUTH_CORS_ORIGIN_1}"
- "${TAUTH_CORS_ORIGIN_2}"
- "${TAUTH_CORS_ORIGIN_3}"
google_web_client_id: "${TAUTH_TENANT_GOOGLE_WEB_CLIENT_ID_GRAVITY}"
jwt_signing_key: "${TAUTH_TENANT_JWT_SIGNING_KEY_GRAVITY}"
cookie_domain: "${TAUTH_COOKIE_DOMAIN}"
Expand Down
14 changes: 8 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ services:
condition: service_started
gravity-tauth:
condition: service_started
working_dir: /srv/gravity/frontend
command: ["--directory", "/srv/gravity/frontend", "8000"]
env_file:
- .env.ghttp
volumes:
- .:/srv/gravity:ro
- /media/share/Drive/exchange/certs/computercat:/certs:ro
ports:
- "8000:8000"
- "4443:8443"
restart: unless-stopped

gravity-frontend-docker:
Expand All @@ -27,12 +28,13 @@ services:
condition: service_started
gravity-tauth:
condition: service_started
working_dir: /srv/gravity/frontend
command: ["--directory", "/srv/gravity/frontend", "8000"]
env_file:
- .env.ghttp
volumes:
- .:/srv/gravity:ro
- /media/share/Drive/exchange/certs/computercat:/certs:ro
ports:
- "8000:8000"
- "4443:8443"
restart: unless-stopped

gravity-backend-dev:
Expand Down
1 change: 1 addition & 0 deletions down.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docker compose --env-file .env.gravity --profile dev down
16 changes: 16 additions & 0 deletions env.ghttp.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# gHTTP HTTPS + reverse proxy configuration for the Gravity dev stack.
# Copy to .env.ghttp and update certificate paths for your machine.
GHTTP_SERVE_DIRECTORY=/srv/gravity/frontend
GHTTP_SERVE_PORT=8443
GHTTP_SERVE_LOGGING_TYPE=JSON
GHTTP_SERVE_TLS_CERTIFICATE=/certs/computercat.tyemirov.net.pem
GHTTP_SERVE_TLS_PRIVATE_KEY=/certs/computercat.tyemirov.net-key.pem
# Proxy backend + TAuth endpoints through ghttp so the browser stays on HTTPS.
# Routes: /notes (Gravity backend), /auth/* + /me (TAuth).
# tauth.js stays CDN-hosted and is not proxied here.
# For dev profile use gravity-backend-dev; for docker profile use gravity-backend-docker
GHTTP_SERVE_PROXIES=/notes=http://gravity-backend-dev:8080,/auth=http://gravity-tauth:8082,/me=http://gravity-tauth:8082
#
# Static file routes (served automatically from GHTTP_SERVE_DIRECTORY):
# / -> index.html (public landing page with Google sign-in, redirects to /app.html if authenticated)
# /app.html -> app.html (authenticated app page, redirects to / if not authenticated)
File renamed without changes.
27 changes: 27 additions & 0 deletions env.tauth.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Gravity tenant
TAUTH_TENANT_ID_GRAVITY=gravity
TAUTH_TENANT_DISPLAY_NAME_GRAVITY="Gravity Notes"
TAUTH_TENANT_GOOGLE_WEB_CLIENT_ID_GRAVITY=qqq.apps.googleusercontent.com
TAUTH_TENANT_JWT_SIGNING_KEY_GRAVITY=qqq
TAUTH_TENANT_SESSION_COOKIE_NAME_GRAVITY=app_session_gravity
TAUTH_TENANT_REFRESH_COOKIE_NAME_GRAVITY=app_refresh_gravity

# Cookie domain (blank => host-only cookie for the requesting origin)
TAUTH_COOKIE_DOMAIN=computercat.tyemirov.net

# TAuth server
TAUTH_CONFIG_FILE=/config/config.yml
TAUTH_LISTEN_ADDR=:8082
TAUTH_DATABASE_URL=sqlite:///data/tauth.db
TAUTH_ENABLE_CORS=true
TAUTH_CORS_EXCEPTION_1=https://accounts.google.com
TAUTH_CORS_ORIGIN_1=https://computercat.tyemirov.net:4443
TAUTH_CORS_ORIGIN_2=https://computercat.tyemirov.net
TAUTH_CORS_ORIGIN_3=https://gravity.mprlab.com
TAUTH_ENABLE_TENANT_HEADER_OVERRIDE=true

# Shared
# Set to true for local dev behind gHTTP reverse proxy (gHTTP terminates TLS,
# proxies to TAuth over HTTP). Set to false for production where TAuth
# terminates TLS directly or sits behind a proxy that forwards X-Forwarded-Proto.
TAUTH_ALLOW_INSECURE_HTTP=true
Loading
Loading