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:
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Runtime configuration, read once from the environment.
|
||||
* Runtime configuration, read once from the environment — except `jwt`, which
|
||||
* can be replaced while the server runs.
|
||||
*
|
||||
* The two Schulcloud values are named after the browser artefacts they come
|
||||
* from (`TSC_URL`, `TSC_JWT_COOKIE`) so that copying a fresh token out of
|
||||
@@ -11,10 +12,16 @@ import { resolve } from 'node:path';
|
||||
export interface Config {
|
||||
/** Instance base URL, no trailing slash, e.g. `https://schulcloud-thueringen.de`. */
|
||||
baseUrl: string;
|
||||
/** Raw JWT from the instance's `jwt` cookie. Sent as `Authorization: Bearer`. */
|
||||
/**
|
||||
* Raw JWT from the instance's `jwt` cookie. Sent as `Authorization: Bearer`.
|
||||
* Replaced at runtime by core/session-token.ts, so read it at the moment of
|
||||
* use and never keep a copy.
|
||||
*/
|
||||
jwt: string;
|
||||
/** Shared secret callers must present to this MCP server. Unused in stdio mode. */
|
||||
authToken: string | undefined;
|
||||
/** Where state that must survive a restart is kept: a replaced session token. Unset = memory only. */
|
||||
stateDir: string | undefined;
|
||||
port: number;
|
||||
bindHost: string;
|
||||
/** Hard ceiling on how many bytes `download_file` will pull from the instance. */
|
||||
@@ -82,6 +89,7 @@ export function loadConfig(): Config {
|
||||
baseUrl: required('TSC_URL').replace(/\/+$/, ''),
|
||||
jwt: required('TSC_JWT_COOKIE'),
|
||||
authToken: process.env.MCP_AUTH_TOKEN?.trim() || undefined,
|
||||
stateDir: process.env.STATE_DIR?.trim() ? resolve(process.env.STATE_DIR.trim()) : undefined,
|
||||
port: int('PORT', 8080),
|
||||
bindHost: process.env.BIND_HOST?.trim() || '0.0.0.0',
|
||||
maxDownloadBytes: int('MAX_DOWNLOAD_BYTES', 25 * 1024 * 1024),
|
||||
|
||||
Reference in New Issue
Block a user