@monster-orange/den 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +457 -0
  2. package/README.md +88 -158
  3. package/docs/distributions.md +216 -16
  4. package/docs/reference.md +177 -0
  5. package/package.json +3 -1
  6. package/src/shared/actor-token.ts +275 -0
  7. package/src/shared/agent-actor.ts +12 -2
  8. package/src/shared/agents/den-claude.ts +99 -1
  9. package/src/shared/agents/runtime.ts +7 -0
  10. package/src/shared/agents/runtimes.ts +9 -2
  11. package/src/shared/den-config.ts +52 -3
  12. package/src/shared/environment-manager/scope.ts +22 -0
  13. package/src/shared/environment-manager/service-runner.ts +94 -2
  14. package/src/shared/profile.ts +15 -4
  15. package/src/shared/sessions/client.ts +16 -2
  16. package/src/shared/sessions/den-pty.ts +10 -2
  17. package/src/shared/sessions/env.ts +13 -3
  18. package/src/shared/sessions/host.ts +56 -8
  19. package/src/shared/sessions/types.ts +8 -0
  20. package/src/shared/web-secret.ts +181 -0
  21. package/src/shared/web-session.ts +491 -0
  22. package/src/ui/cli/agent-ops.ts +843 -0
  23. package/src/ui/cli/agents-command.ts +97 -615
  24. package/src/ui/cli/flag-parser.ts +21 -0
  25. package/src/ui/cli/index.ts +127 -11
  26. package/src/ui/cli/sessions-command.ts +49 -5
  27. package/src/ui/cli/target-workspace.ts +105 -0
  28. package/src/ui/cli/web-command.ts +542 -14
  29. package/src/ui/mcp/api.ts +7 -0
  30. package/src/ui/mcp/server.ts +74 -6
  31. package/src/ui/server/agent-routes.ts +324 -0
  32. package/src/ui/server/audit.ts +81 -27
  33. package/src/ui/server/http-errors.ts +21 -3
  34. package/src/ui/server/index.ts +821 -66
  35. package/src/ui/server/public/agents-page.js +654 -0
  36. package/src/ui/server/public/agents-section.js +299 -0
  37. package/src/ui/server/public/agents.html +439 -0
  38. package/src/ui/server/public/appearance.js +3 -0
  39. package/src/ui/server/public/error-page.js +103 -0
  40. package/src/ui/server/public/header.js +45 -11
  41. package/src/ui/server/public/i18n/es.js +90 -1
  42. package/src/ui/server/public/i18n/ja.js +86 -1
  43. package/src/ui/server/public/local-dev.html +53 -0
  44. package/src/ui/server/public/logs.html +3 -0
  45. package/src/ui/server/public/panel.js +1 -1
  46. package/src/ui/server/public/release.html +8 -0
  47. package/src/ui/server/public/settings-page.js +5 -0
  48. package/src/ui/server/public/settings-registry.js +33 -0
  49. package/src/ui/server/public/shell.js +248 -5
  50. package/src/ui/server/public/topbar.js +51 -2
  51. package/src/ui/server/request-guard.ts +670 -0
  52. package/src/ui/server/route-classes.ts +317 -0
  53. package/src/ui/server/session-routes.ts +445 -0
  54. package/src/ui/server/shutdown-guard.ts +91 -0
  55. package/src/ui/server/sudo-store.ts +29 -3
  56. package/src/ui/server/ui-assets.ts +10 -1
  57. package/src/ui/server/websocket.ts +326 -0
  58. package/src/ui/server/workspace-routes.ts +13 -7
@@ -254,7 +254,7 @@
254
254
  // The lock chip and its popover (R8).
255
255
  "lock.locked": "Locked",
256
256
  "lock.unlocked": "Unlocked",
257
- "lock.title": "Unlock to start, stop and restart services",
257
+ "lock.title": "Unlock to start, stop and restart services and agents",
258
258
  "lock.nopw": "Passwordless sudo (NOPASSWD): no admin password needed",
259
259
  "lock.session": "Unlocked for this session",
260
260
  "lock.sessionDesc":
@@ -353,6 +353,24 @@
353
353
  "err.500.time": "Time",
354
354
  "err.500.id": "Request id",
355
355
  "err.500.version": "Version",
356
+ "err.421.title": "Wrong address",
357
+ "err.421.lead":
358
+ "This server answers only on its own addresses, so a page on another name can't reach it.",
359
+ "err.421.open": "Open on localhost",
360
+ "err.refused.title": "Request refused",
361
+ "err.signin.title": "Sign in to use {name}",
362
+ "err.signin.terminal": "Sign in to use the terminal",
363
+ "err.signin.agents": "Sign in to start agents",
364
+ "err.signin.sub": "This browser isn't signed in",
365
+ "err.signin.lead": "Run {cmd} and open the link.",
366
+ "err.signin.expiredTitle": "Sign-in link expired",
367
+ "err.signin.expired":
368
+ "That sign-in link expired or was used. Run {cmd} for a new one.",
369
+ "err.signin.local":
370
+ "A sign-in belongs to one address. This page is on {host}: open the same page on localhost, where this browser is signed in.",
371
+ "err.signin.why":
372
+ "There is no password: the link comes from a file only your user account can read.",
373
+ "err.signin.copy": "Copy command",
356
374
  "err.422.title": "Config is invalid",
357
375
  "err.422.sub": "The distribution config fails validation",
358
376
  "err.422.lead":
@@ -388,6 +406,7 @@
388
406
  "set.cli.cli_tabs": "Terminal tabs",
389
407
  "set.cli.log_window_lines": "Log pane lines",
390
408
  "set.cli.web_port": "Your web port",
409
+ "set.cli.signin": "Browser sign-in",
391
410
  "set.distribution.brand": "Product name",
392
411
  "set.distribution.github_org": "GitHub org",
393
412
  "set.distribution.orgs": "Org labels",
@@ -397,6 +416,74 @@
397
416
  "err.retry": "Retry",
398
417
  "err.copy": "Copy details",
399
418
  "err.copied": "Copied",
419
+ // The Workspaces page's Agents section (AG-4a, agents-section.js).
420
+ "ag.title": "Agents",
421
+ "ag.caption": "Agents in {ws}",
422
+ "ag.count": "{n} agents",
423
+ "ag.count.one": "{n} agent",
424
+ "ag.col.agent": "Agent",
425
+ "ag.col.runtime": "Runtime",
426
+ "ag.col.state": "State",
427
+ "ag.col.cwd": "Directory",
428
+ "ag.col.started": "Started",
429
+ "ag.col.actions": "Actions",
430
+ "ag.loading": "Loading agents…",
431
+ "ag.none": "No agents in this workspace yet",
432
+ "ag.personaOf": "persona {persona}",
433
+ "ag.state.running": "running",
434
+ "ag.state.stopping": "stopping…",
435
+ "ag.state.stopped": "stopped",
436
+ "ag.state.ended": "ended",
437
+ "ag.state.exited": "exited",
438
+ "ag.state.exitedCode": "exited ({code})",
439
+ "ag.stop": "Stop",
440
+ "ag.resume": "Resume",
441
+ "ag.stopName": "Stop {id}",
442
+ "ag.resumeName": "Resume {id}",
443
+ "ag.start": "+ Start agent",
444
+ "ag.persona": "Persona",
445
+ "ag.runtime": "Runtime",
446
+ "ag.runtime.auto": "The persona's",
447
+ "ag.unavailable": "unavailable",
448
+ "ag.go": "Start",
449
+ "ag.starting": "Starting…",
450
+ "ag.cancel": "Cancel",
451
+ "ag.started": "Started agent {id}",
452
+ "ag.stopped": "Stopped agent {id}",
453
+ "ag.resumed": "Resumed agent {id}",
454
+ // The Agents tab (AG-4a-term, agents-page.js).
455
+ "n.agents": "Agents",
456
+ "kind.agent": "Agent",
457
+ "ag.sub": "{n} sessions · {w} waiting · {scope}",
458
+ "ag.sub.one": "{n} session · {w} waiting · {scope}",
459
+ "ag.all": "all workspaces",
460
+ "ag.waiting": "waiting",
461
+ "ag.session": "session",
462
+ "ag.empty":
463
+ "Pick a session on the left to open its terminal. Open more to tile them (up to 4).",
464
+ "ag.none.scope": "No sessions or agents in {scope} yet",
465
+ "ag.detach": "Close pane",
466
+ "ag.detachName": "Close {id}'s pane (it keeps running)",
467
+ "ag.detached": "Closed {id}'s pane: it keeps running",
468
+ "ag.open": "Open",
469
+ "ag.openName": "Open {id} in Agents",
470
+ "ws.openAgents": "Open in Agents →",
471
+ "ag.signin": "Sign in to open terminals: run {cli} web login",
472
+ "ag.revoked": "token revoked",
473
+ "ag.revokedTip":
474
+ "Its token was revoked (web logout --all): stop and resume it, or start it again, to give it a new one",
475
+ "ag.ended":
476
+ "This session has ended. Its record stays until it is resumed or removed.",
477
+ "ag.exitedLine": "[the session exited{code}]",
478
+ "ag.lost": "[disconnected: {why}]",
479
+ "ag.reconnect": "Reconnect",
480
+ "ag.noBackend":
481
+ "Terminals need the optional PTY module (den-pty is unavailable): {cli} doctor says why",
482
+ "ag.noHost": "No session host is running: nothing has started yet",
483
+ "ag.ws": "Workspace",
484
+ "ag.pal": "Agents › {ws} / {name}",
485
+ "ag.palStart": "Start agent…",
486
+ "ag.cap": "Up to 4 panes: {id}'s closed to make room (it keeps running)",
400
487
  };
401
488
  // The languages the UI has a catalog for, each named in itself (a name is
402
489
  // never translated). The server's list is ui-language.ts's UI_LANGUAGES.
@@ -479,6 +566,9 @@
479
566
  settings:
480
567
  '<circle cx="12" cy="12" r="3"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/>',
481
568
  page: '<rect x="5" y="4" width="14" height="16" rx="2"/><path d="M9 9h6M9 13h6"/>',
569
+ // The prototype's Agents icon: a terminal window with a prompt.
570
+ agents:
571
+ '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M7 10l3 2-3 2M12 15h5"/>',
482
572
  edge: '<path d="M12 3l8 4v5c0 4.5-3.4 8.3-8 9-4.6-.7-8-4.5-8-9V7z"/><path d="M8 12h8M12 8v8"/>',
483
573
  };
484
574
  function icon(name) {
@@ -1146,6 +1236,17 @@
1146
1236
  order: 15,
1147
1237
  href: "/graph",
1148
1238
  },
1239
+ // The Agents tab (AG-4a-term, AG spec §5d): sessions and terminal
1240
+ // panes, after Graph as the prototype orders them.
1241
+ {
1242
+ id: "agents",
1243
+ label: "n.agents",
1244
+ icon: "agents",
1245
+ group: "develop",
1246
+ order: 17,
1247
+ href: "/agents",
1248
+ keywords: ["sessions", "terminal"],
1249
+ },
1149
1250
  {
1150
1251
  id: "services",
1151
1252
  label: "n.services",
@@ -3011,6 +3112,19 @@
3011
3112
  : typeof data === "string" && data.trim()
3012
3113
  ? data.trim().slice(0, 300)
3013
3114
  : t("api.failed", { what: what }) + " (" + r.status + ")";
3115
+ // SEC-1: a sign-in refusal in the page's language, with its command.
3116
+ if (
3117
+ data &&
3118
+ data.code === "signin_required" &&
3119
+ (data.scope === "agents" || data.scope === "terminal")
3120
+ ) {
3121
+ var cmd = /`([^`]+)`/.exec(String(data.error || ""));
3122
+ msg =
3123
+ (data.scope === "agents"
3124
+ ? t("err.signin.agents")
3125
+ : t("err.signin.terminal")) +
3126
+ (cmd ? ". " + t("err.signin.lead", { cmd: cmd[1] }) : "");
3127
+ }
3014
3128
  throw apiError(msg, {
3015
3129
  status: r.status,
3016
3130
  code: data && typeof data === "object" ? data.code : null,
@@ -3247,12 +3361,43 @@
3247
3361
  }
3248
3362
  }
3249
3363
 
3364
+ // The config fails validation (0.4.1): the server marks every page it
3365
+ // serves while it does (data-config-invalid on #den-shell), and the first
3366
+ // /api answer of 422 config_invalid marks it too. From then on the shell
3367
+ // and its modules start none of the polls and streams that read the
3368
+ // config (health, edge, workspaces, applications, the org config, …):
3369
+ // each would only answer 422 again. A reload once fixed starts them.
3370
+ var configBad = host.dataset.configInvalid === "1";
3371
+ // SEC-1b: a refusal page (421 "Wrong address", or signed out): the server
3372
+ // marks it (data-api-off), since every /api call from it would answer
3373
+ // the same refusal. The shell starts none of its polls and streams, the
3374
+ // same gate as an invalid config, and the status chip reads unknown.
3375
+ var apiOffPage = host.dataset.apiOff === "1";
3376
+ function apiOff() {
3377
+ return apiOffPage;
3378
+ }
3379
+ function configInvalid() {
3380
+ return configBad || apiOffPage;
3381
+ }
3382
+ function markConfigInvalid() {
3383
+ if (configBad) return;
3384
+ configBad = true;
3385
+ try {
3386
+ window.dispatchEvent(new CustomEvent("den:config-invalid"));
3387
+ } catch (e) {
3388
+ /* no CustomEvent */
3389
+ }
3390
+ }
3391
+
3250
3392
  // R26d: the server unreachable.
3251
3393
  var server = { up: true, delay: 0, timer: null, tick: null, nextAt: 0 };
3252
3394
  var rawFetch =
3253
3395
  typeof window.fetch === "function" ? window.fetch.bind(window) : null;
3254
3396
  function sameOrigin(input) {
3255
- var u = typeof input === "string" ? input : input && input.url;
3397
+ var u =
3398
+ typeof input === "string"
3399
+ ? input
3400
+ : input && (typeof input.url === "string" ? input.url : input.href);
3256
3401
  if (!u) return false;
3257
3402
  if (u.charAt(0) === "/" && u.charAt(1) !== "/") return true;
3258
3403
  try {
@@ -3319,13 +3464,102 @@
3319
3464
  hideBanner("net");
3320
3465
  serverState(true);
3321
3466
  }
3467
+ // SEC-1a: every same-origin call that changes something (POST, PUT,
3468
+ // PATCH, DELETE), through denShell.api() or a page's own fetch(), carries
3469
+ // X-Den-CSRF with the token GET /api/session hands out. The token is per
3470
+ // server start: a 403 `csrf` (the server restarted) fetches a fresh one
3471
+ // and retries once. Never sent to another origin.
3472
+ var CSRF_METHODS = { POST: 1, PUT: 1, PATCH: 1, DELETE: 1 };
3473
+ var csrf = { token: null, pending: null };
3474
+ function csrfToken(fresh) {
3475
+ if (fresh) csrf.token = null;
3476
+ if (csrf.token) return Promise.resolve(csrf.token);
3477
+ if (!csrf.pending)
3478
+ csrf.pending = rawFetch("/api/session", {
3479
+ cache: "no-store",
3480
+ credentials: "same-origin",
3481
+ })
3482
+ .then(function (r) {
3483
+ return r.ok ? r.json() : null;
3484
+ })
3485
+ .then(
3486
+ function (b) {
3487
+ csrf.pending = null;
3488
+ csrf.token = b && typeof b.csrf === "string" ? b.csrf : null;
3489
+ return csrf.token;
3490
+ },
3491
+ function () {
3492
+ csrf.pending = null;
3493
+ return null;
3494
+ },
3495
+ );
3496
+ return csrf.pending;
3497
+ }
3498
+ function isRequest(input) {
3499
+ return typeof Request !== "undefined" && input instanceof Request;
3500
+ }
3501
+ function methodOf(input, init) {
3502
+ var m = (init && init.method) || (isRequest(input) && input.method);
3503
+ return String(m || "GET").toUpperCase();
3504
+ }
3505
+ function withCsrf(input, init, token) {
3506
+ var headers = new Headers(
3507
+ (init && init.headers) || (isRequest(input) ? input.headers : undefined),
3508
+ );
3509
+ headers.set("X-Den-CSRF", token);
3510
+ return Object.assign({}, init || {}, { headers: headers });
3511
+ }
3512
+ function csrfFetch(input, init) {
3513
+ // A body that can be sent twice (not a stream) may be retried.
3514
+ var body = init && init.body;
3515
+ var again =
3516
+ !isRequest(input) &&
3517
+ (body == null ||
3518
+ typeof body === "string" ||
3519
+ (typeof FormData !== "undefined" && body instanceof FormData) ||
3520
+ (typeof URLSearchParams !== "undefined" &&
3521
+ body instanceof URLSearchParams) ||
3522
+ (typeof Blob !== "undefined" && body instanceof Blob));
3523
+ var send = function (token) {
3524
+ return rawFetch(input, token ? withCsrf(input, init, token) : init);
3525
+ };
3526
+ return csrfToken(false).then(function (token) {
3527
+ return send(token).then(function (r) {
3528
+ if (!again || !r || r.status !== 403) return r;
3529
+ return r
3530
+ .clone()
3531
+ .json()
3532
+ .then(
3533
+ function (b) {
3534
+ if (!b || b.code !== "csrf") return r;
3535
+ return csrfToken(true).then(function (fresh) {
3536
+ return fresh ? send(fresh) : r;
3537
+ });
3538
+ },
3539
+ function () {
3540
+ return r;
3541
+ },
3542
+ );
3543
+ });
3544
+ });
3545
+ }
3322
3546
  if (rawFetch && !window.fetch.__denShell) {
3323
3547
  var wrapped = function (input, init) {
3324
- var p = rawFetch(input, init);
3548
+ var p =
3549
+ CSRF_METHODS[methodOf(input, init)] && sameOrigin(input)
3550
+ ? csrfFetch(input, init)
3551
+ : rawFetch(input, init);
3325
3552
  if (!p || typeof p.then !== "function") return p;
3326
3553
  return p.then(
3327
3554
  function (r) {
3328
3555
  if (!server.up && sameOrigin(input)) serverBack();
3556
+ if (r && r.status === 422 && !configBad && sameOrigin(input))
3557
+ r.clone()
3558
+ .json()
3559
+ .then(function (b) {
3560
+ if (b && b.code === "config_invalid") markConfigInvalid();
3561
+ })
3562
+ .catch(function () {});
3329
3563
  return r;
3330
3564
  },
3331
3565
  function (err) {
@@ -3337,6 +3571,8 @@
3337
3571
  };
3338
3572
  wrapped.__denShell = true;
3339
3573
  window.fetch = wrapped;
3574
+ // The token is in hand before the first click needs it.
3575
+ if (!apiOffPage) csrfToken(false);
3340
3576
  }
3341
3577
 
3342
3578
  // ── the UI language (R19): a user pref ───────────────────────────────
@@ -3401,6 +3637,13 @@
3401
3637
  serverUp: function () {
3402
3638
  return server.up;
3403
3639
  },
3640
+ /**
3641
+ * 0.4.1: true once the config is known to fail validation, or on a
3642
+ * refusal page (apiOff): the config polls and streams don't start.
3643
+ */
3644
+ configInvalid: configInvalid,
3645
+ /** SEC-1b: a refusal page (421, signed out): no /api call would answer. */
3646
+ apiOff: apiOff,
3404
3647
  /** The browser-tab icon (appearance.js: the active theme's, per mode). */
3405
3648
  setFavicon: setFavicon,
3406
3649
  /** The org part of the scope: "/ ORG <title>", static (one org). */
@@ -3480,7 +3723,7 @@
3480
3723
  checkOrphansSoon();
3481
3724
 
3482
3725
  // The server's prefs win over the local copy.
3483
- if (typeof fetch === "function")
3726
+ if (typeof fetch === "function" && !apiOff())
3484
3727
  fetch("/api/settings")
3485
3728
  .then(function (r) {
3486
3729
  return r.ok ? r.json() : null;
@@ -3502,7 +3745,7 @@
3502
3745
  .catch(function () {});
3503
3746
 
3504
3747
  // The Edge page shows once the edge is configured (no Docker asked).
3505
- if (typeof fetch === "function")
3748
+ if (typeof fetch === "function" && !configInvalid())
3506
3749
  fetch("/api/edge?probe=1")
3507
3750
  .then(function (r) {
3508
3751
  return r.ok ? r.json() : null;
@@ -411,7 +411,7 @@ export function runOf(url, base = "http://localhost/") {
411
411
  const LS_LAST_RUN = "__DEN_CLI__-last-run";
412
412
  const LS_LOCK = "__DEN_CLI__-lock";
413
413
  const SS_PENDING = "__DEN_CLI__-pending-action";
414
- const KIND = ["page", "service", "app", "action", "setting"];
414
+ const KIND = ["page", "service", "app", "agent", "action", "setting"];
415
415
  const kindLabel = (k) =>
416
416
  KIND.includes(k)
417
417
  ? t(`kind.${k}`)
@@ -507,7 +507,9 @@ let serverDown = false;
507
507
  function paintChip() {
508
508
  const chip = $("den-status");
509
509
  if (!chip) return;
510
- if (serverDown) {
510
+ // R26d, and SEC-1b's refusal pages (421, signed out: sh().apiOff), where
511
+ // no health is asked for: unknown, neutral, never "Checking…" forever.
512
+ if (serverDown || sh()?.apiOff?.() === true) {
511
513
  chip.classList.remove("bad");
512
514
  chip.setAttribute("aria-busy", "false");
513
515
  chip.title = t("status.unknownTip");
@@ -779,6 +781,25 @@ function openLogs(svc) {
779
781
 
780
782
  // ── command palette (R17) ───────────────────────────────────────────────────
781
783
 
784
+ /**
785
+ * The sessions and agents in view (GET /api/sessions), asked when the
786
+ * palette opens: each is "Agents › <ws> / <name>", which opens its pane on
787
+ * the Agents page (AG-4a-term).
788
+ */
789
+ let palSessions = [];
790
+ function askPaletteSessions(redraw) {
791
+ if (sh()?.apiOff?.()) return;
792
+ fetch("/api/sessions")
793
+ .then((r) => (r.ok ? r.json() : null))
794
+ .then((d) => {
795
+ palSessions = Array.isArray(d?.sessions) ? d.sessions : [];
796
+ redraw();
797
+ })
798
+ .catch(() => {
799
+ /* no sessions to offer */
800
+ });
801
+ }
802
+
782
803
  /** Every palette row: pages, apps, services, their actions, then commands. */
783
804
  function paletteItems() {
784
805
  const s = sh();
@@ -846,6 +867,26 @@ function paletteItems() {
846
867
  run: () => openLogs(svcOf(id)),
847
868
  });
848
869
  }
870
+ for (const x of palSessions) {
871
+ const name = x.kind === "agent" ? (x.persona ?? x.label) : x.label;
872
+ items.push({
873
+ key: `agent:${x.id}`,
874
+ kind: "agent",
875
+ label: t("ag.pal", { ws: x.workspace, name }),
876
+ sub: x.kind === "agent" && x.persona !== x.label ? x.label : undefined,
877
+ dot: x.state === "running" ? "" : "off",
878
+ keywords: [x.workspace, x.label, x.persona ?? "", "agents", "terminal"],
879
+ run: () => go(`/agents?open=${encodeURIComponent(x.id)}`),
880
+ });
881
+ }
882
+ if (s?.nav?.().some((n) => n.id === "agents"))
883
+ items.push({
884
+ key: "agent:start",
885
+ kind: "action",
886
+ label: t("ag.palStart"),
887
+ keywords: ["agents", "start", "persona"],
888
+ run: () => go("/agents?start=1"),
889
+ });
849
890
  for (const c of s?.commands?.() ?? [])
850
891
  items.push({
851
892
  key: `cmd:${c.id}`,
@@ -955,6 +996,9 @@ function openPalette() {
955
996
  };
956
997
  draw();
957
998
  input.focus();
999
+ askPaletteSessions(() => {
1000
+ if (overlay?.kind === "palette") draw();
1001
+ });
958
1002
  }
959
1003
 
960
1004
  // ── Runs chip (R7) ──────────────────────────────────────────────────────────
@@ -1153,6 +1197,9 @@ async function getJson(url, fallback, init) {
1153
1197
  */
1154
1198
  let vaultAsked = null;
1155
1199
  function checkVault() {
1200
+ // 0.4.1: it reads the config's secrets refs; an invalid one answers 422.
1201
+ if (window.denShell?.configInvalid?.() === true)
1202
+ return Promise.resolve(lock.vault);
1156
1203
  vaultAsked ??= getJson("/api/sudo-password/vault-status", {
1157
1204
  backend: "none",
1158
1205
  label: "",
@@ -1178,6 +1225,8 @@ function checkVault() {
1178
1225
  }
1179
1226
 
1180
1227
  async function initLock() {
1228
+ // A refusal page (SEC-1b): /api would answer the same refusal.
1229
+ if (sh()?.apiOff?.() === true) return;
1181
1230
  try {
1182
1231
  if (sessionStorage.getItem(LS_LOCK) === "1") {
1183
1232
  lock.unlocked = true;