Replace the Schulcloud token without a restart
A token lasts 30 days and only a browser login yields one — the account is federated, so the server cannot mint it. Replacing it meant editing .env and recreating the container, every month. `schulcloud token set` (a hidden prompt, or piped input) and a /token page both send it to PUT /api/token. The server checks it with Schulcloud first — well-formed, unexpired, still logged in, the same account — then swaps it into the config every request reads, restarts the keepalive and saves it in STATE_DIR, a new volume, with mode 0600. At startup the newer of the saved token and TSC_JWT_COOKIE wins, unless they belong to different accounts. A refused paste changes nothing, and the token is never logged. The keepalive's pings carry a generation, so a 401 for the old token that arrives after a swap cannot stop the new cycle. `schulcloud token`, whoami and the log report the expiry and warn a week ahead. Found on the way: a host that is off for more than two hours loses the session however long the token has left — this machine lost it overnight — which is what the always-on Pi is for. 174 tests. Smoke 72/72 on the local instance, and a real swap verified end to end there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
30
CLAUDE.md
30
CLAUDE.md
@@ -39,9 +39,10 @@ index.
|
||||
read-only with respect to Schulcloud. Run `smoke` after touching `src/core/`,
|
||||
`src/mcp/` or `src/http/` — the unit tests cover only pure functions.
|
||||
|
||||
Run smoke **both ways**: with `DATABASE_URL` set (69 checks, index-backed) and
|
||||
without (67 checks, live-only). The degradation path is a supported mode, not a
|
||||
fallback nobody exercises.
|
||||
Run smoke **both ways**: with `DATABASE_URL` set (74 checks, index-backed) and
|
||||
without (72 checks, live-only). The degradation path is a supported mode, not a
|
||||
fallback nobody exercises. Every Schulcloud check fails with 401 when the live
|
||||
session has lapsed — check the container's keepalive log before suspecting code.
|
||||
|
||||
Store tests need a database and skip without one:
|
||||
`TEST_DATABASE_URL=postgresql://… npm test`. They use a real Postgres on
|
||||
@@ -74,6 +75,11 @@ bin/cli.ts ──HTTP──────────┘ cli/{config,client,sync
|
||||
- `legacy-files.ts` — the file manager ("Dateien": Persönliche, Kurs-, Team-,
|
||||
Geteilte Dateien) as one path tree, parsed from the legacy client's pages.
|
||||
A separate store from files-storage; the `fs_*` tools and `/api/fs` sit on it.
|
||||
- `session-token.ts` — the Schulcloud token, replaceable at runtime: checked
|
||||
with `GET /me` (same `userId`), swapped into `config.jwt`, saved to
|
||||
`STATE_DIR`. **Read `config.jwt` at the moment of use; never keep a copy.**
|
||||
- **`http/`** — besides `/mcp` and `/api`: `/token`, a page that PUTs a fresh
|
||||
token to `/api/token`. docs/AUTH.md says why it exists.
|
||||
- **`store/`** — crawl generations, identity diffs, `german` + `pg_trgm` FTS.
|
||||
`Store.open` returns `undefined` when Postgres is down; callers degrade.
|
||||
- **`indexer/`** — crawl → persist → mirror bytes → extract text → index.
|
||||
@@ -105,7 +111,8 @@ permission, so `getFileManagerPage` allows only the listing routes, by pattern
|
||||
widen that pattern only with a route you have read the handler of. A pre-signed
|
||||
download URL is fetched with **no** credentials: it names another host, and
|
||||
neither the bearer nor the `jwt` cookie may go with it. `refresh_index` and `POST /api/refresh` write only to the Pi's
|
||||
own index and mirror — every upstream call they make is still a GET.
|
||||
own index and mirror, and `PUT /api/token` only to the server's own token — every
|
||||
upstream call they make is still a GET.
|
||||
|
||||
**Filenames from Schulcloud are untrusted paths.** Course titles, card titles
|
||||
and filenames are all user-supplied upstream, and both the server's mirror and
|
||||
@@ -121,6 +128,8 @@ user explicitly asking for one and understanding this.
|
||||
**Never log or echo secrets.** `TSC_JWT_COOKIE` grants full read access to the
|
||||
account; `MCP_AUTH_TOKEN` guards the endpoint. Neither belongs in
|
||||
logs, error messages, or tool output. `.env` is git-ignored — keep it that way.
|
||||
One more counts as a secret: a token replaced at runtime, which lives only in
|
||||
`STATE_DIR`, mode 0600.
|
||||
|
||||
**Live behaviour beats upstream source.** The clones in `vendor/` track `main`
|
||||
and may be ahead of what is deployed. When they disagree with the instance, the
|
||||
@@ -260,7 +269,10 @@ These cost real time to discover; `docs/API.md` has the full list with evidence.
|
||||
- **`exp` (30 days) is not the session lifetime.** The binding limit is a Valkey
|
||||
whitelist entry with a `JWT_TIMEOUT_SECONDS` TTL (7200s; live value at
|
||||
`GET /api/v3/config/public`) that every authenticated request re-sets.
|
||||
`src/keepalive.ts` holds it open — don't remove it.
|
||||
`src/keepalive.ts` holds it open — don't remove it. It cannot hold it across
|
||||
downtime: a host off for more than two hours loses the session (seen when a
|
||||
dev machine was off overnight), which is why the deployment is an always-on
|
||||
Pi and why a fresh token can be swapped in without a restart.
|
||||
- **A Schulportal tab left open revokes our token.** The `jwt` cookie *is* the
|
||||
browser's session token, same `jti`. The front end runs a client-side timer
|
||||
(reset only on route change, never from the server TTL) and calls
|
||||
@@ -316,6 +328,8 @@ bundle (2.1.272), not its docs:
|
||||
|
||||
## Environment
|
||||
|
||||
`.env` holds `TSC_URL`, `TSC_JWT_COOKIE`, `MCP_AUTH_TOKEN`. See `.env.example`
|
||||
for the full set and `docs/AUTH.md` for refreshing the JWT. `npm run probe`
|
||||
reports both clocks: days until hard expiry and seconds of idle budget left.
|
||||
`.env` holds `TSC_URL`, `TSC_JWT_COOKIE`, `MCP_AUTH_TOKEN`; docker-compose sets
|
||||
`STATE_DIR`. See `.env.example` for the
|
||||
full set and `docs/AUTH.md` for refreshing the JWT — `schulcloud token set`,
|
||||
no restart. `npm run probe` and `schulcloud token` report the clocks: days until
|
||||
hard expiry and the session budget.
|
||||
|
||||
Reference in New Issue
Block a user