feat(settings): hub auth-policy panel — T4/T5 security parity in the GUI
Some checks failed
Security / Security check (push) Failing after 1s

Settings → Security now shows the hub's effective auth policy via the
new read-only AuthStatus RPC: active token validator (static / jwt-rs256
with issuer, audience, JWKS source), anonymous-access warning, per-token
cards with scope grants, env-var presence and rate limits, plus a
localized admin-denied story for non-admin tokens. Live-reloads on
endpoint change.

Also fixes a batch of fai→chain rename leftovers this panel's
verification uncovered: hub_auth_token.dart and registry_token.dart
read/wrote ~/.fai/ while the hub reads ~/.chain/ (stored registry
tokens never reached the hub), today_story_loader + tools/today used
~/.fai/today, chain_log legacy ~/.fai/logs migration removed per the
no-legacy-recognisers decision, and UI strings still advertised the
retired .fai bundle extension.

Includes 5 widget tests for the panel, an integration-test screenshot
harness (auth_policy_shots_test.dart, guide-shots style), and DE+EN
l10n. flutter analyze clean, 58 tests green.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-07-15 03:19:33 +02:00
parent c6da5025ce
commit efaa089454
18 changed files with 1208 additions and 74 deletions

View file

@ -1468,7 +1468,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get registryCredentialsBlurb =>
'Token used to download .fai modules from a registry behind a signin wall (Forgejo, GitHub-private). Stored at ~/.chain/registry-token, mode 0600. The CHAIN_REGISTRY_TOKEN env var still wins when set.';
'Token used to download .chain modules from a registry behind a signin wall (Forgejo, GitHub-private). Stored at ~/.chain/registry-token, mode 0600. The CHAIN_REGISTRY_TOKEN env var still wins when set.';
@override
String registryTokenStatusConfigured(int chars) {
@ -1506,6 +1506,77 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Could not save: $error';
}
@override
String get authPolicyHeader => 'HUB ACCESS POLICY';
@override
String get authPolicyBlurb =>
'How the hub checks incoming calls: the active validator, the tokens and their scope grants. Secrets stay in environment variables — only their names appear here.';
@override
String get authPolicyValidatorStatic => 'Validator: static token list';
@override
String get authPolicyValidatorJwt =>
'Validator: JWT (RS256) via an external identity provider';
@override
String get authPolicyAnonymous =>
'No tokens configured — the hub accepts anonymous calls. Fine for local work; configure tokens in ~/.chain/config.yaml for production.';
@override
String get authPolicyNeedsAdmin =>
'This view needs a token with the admin scope. Store it above under “Hub authentication” and reload.';
@override
String get authPolicyHubTooOld =>
'The connected hub does not know this view yet — it is older than Studio. Update the hub (chain update apply) and reload.';
@override
String get authPolicyRetry => 'Retry';
@override
String get authPolicyReload => 'Reload tokens';
@override
String authPolicyReloadDone(int n) {
return 'Reloaded — $n tokens active.';
}
@override
String get authPolicyEditHint =>
'The policy is edited in ~/.chain/config.yaml (auth: section). After a change or token rotation, reload here — the hub applies it without a restart.';
@override
String authPolicyEnvSet(String env) {
return 'Environment variable $env is set';
}
@override
String authPolicyEnvMissing(String env) {
return 'Environment variable $env is MISSING — the token is unusable';
}
@override
String authPolicyRateLimit(int n) {
return '$n/min';
}
@override
String get authPolicyJwtKeySource => 'Key source';
@override
String get authPolicyJwtAudience => 'Audience (aud)';
@override
String get authPolicyJwtIssuer => 'Issuer (iss)';
@override
String get authPolicyJwtScopeClaim => 'Scope claim';
@override
String get authPolicyNotChecked => 'not checked';
@override
String get hubAuthTokenHeader => 'HUB AUTHENTICATION';
@ -2651,22 +2722,22 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String addSourceIntro(String capability) {
return '`$capability` is not in the public store. Point the hub at a `.fai` bundle URL or a local bundle path; the hub downloads, verifies (sha256 + signature) and installs it.';
return '`$capability` is not in the public store. Point the hub at a `.chain` bundle URL or a local bundle path; the hub downloads, verifies (sha256 + signature) and installs it.';
}
@override
String get addSourceField => 'URL or path to .fai bundle';
String get addSourceField => 'URL or path to .chain bundle';
@override
String get addSourceHint =>
'https://git.flemming.ai/your-org/your-module/releases/download/v0.1.0/foo-0.1.0.fai';
'https://git.flemming.ai/your-org/your-module/releases/download/v0.1.0/foo-0.1.0.chain';
@override
String get addSourceHowItWorksTitle => 'How private modules work';
@override
String get addSourceHowItWorksBody =>
'A module is a directory with a module.yaml + the WASM artifact. To share it: pack it (`chain pack <dir>`) and host the resulting `.fai` bundle anywhere (your own Forgejo / GitHub / S3). The hub installs by URL and verifies the signature against its trust store.\n\nDeveloping locally? Use the CLI — Studio cannot install from an unpacked directory (yet):';
'A module is a directory with a module.yaml + the WASM artifact. To share it: pack it (`chain pack <dir>`) and host the resulting `.chain` bundle anywhere (your own Forgejo / GitHub / S3). The hub installs by URL and verifies the signature against its trust store.\n\nDeveloping locally? Use the CLI — Studio cannot install from an unpacked directory (yet):';
@override
String get addSourceCliExample => 'chain install --link /path/to/module';