@monster-orange/den 0.4.26 → 0.4.27

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.
@@ -917,8 +917,19 @@ export const DEN_SETTINGS = [
917
917
  category: "agents",
918
918
  label: "Hub",
919
919
  description:
920
- "Every agent gets Hub's tools: search Hub, publish docs and mocks there. Its team and project come from this install's token and the workspace's repos. Set hub.url and hub.token (a secret ref in this install's store) in config/local/user.yml.",
921
- keywords: ["hub", "den hub", "mcp", "team", "token", "docs", "mocks"],
920
+ "Connect this install to your company's Hub: sign in there and approve it. Every agent then gets Hub's tools: search Hub, publish docs and mocks there. Its team comes from this install's token, its project from the workspace's repos.",
921
+ keywords: [
922
+ "hub",
923
+ "den hub",
924
+ "connect",
925
+ "disconnect",
926
+ "mcp",
927
+ "team",
928
+ "company",
929
+ "token",
930
+ "docs",
931
+ "mocks",
932
+ ],
922
933
  scope: "user",
923
934
  control: { type: "custom", render: renderHub },
924
935
  keys: ["hub_allow_tools"],
@@ -1459,20 +1470,82 @@ export function rotationHtml(b) {
1459
1470
  );
1460
1471
  }
1461
1472
 
1462
- /** HUB-1.5: Settings › Agents › Den Hub (b: GET /api/hub). */
1463
- export function hubHtml(b) {
1464
- if (!b || !b.configured) return "";
1473
+ /** HUB-1.7: what came back from Hub's redirect (?hub=…), when the server has no words for it. */
1474
+ const HUB_FLASH = {
1475
+ connected: [true, "Connected to Hub."],
1476
+ denied: [false, "Not connected: the request was declined on Hub."],
1477
+ expired: [
1478
+ false,
1479
+ "That connect link was used or too old: press Connect again.",
1480
+ ],
1481
+ failed: [false, "Connect didn't finish: press Connect again."],
1482
+ };
1483
+
1484
+ /**
1485
+ * HUB-1.5/1.7: Settings › Agents › Hub (b: GET /api/hub; flash: what the
1486
+ * last Connect came to). Not connected: Hub's address and Connect.
1487
+ * Connected: the company (a link), role, who signed in, this install,
1488
+ * Disconnect. A token Hub refuses offers Reconnect; a role that no longer
1489
+ * may connect says to ask an admin (never a retry).
1490
+ */
1491
+ export function hubHtml(b, flash) {
1492
+ b = b || { configured: false };
1493
+ const state = b.configured
1494
+ ? b.state || (b.connected ? "connected" : "no_token")
1495
+ : "no_token";
1496
+ const note = (t, attr) => `<p class="set-note" ${attr}>${escR(t)}</p>`;
1497
+ const flashHtml = flash
1498
+ ? `<p class="set-note hub-flash ${flash.ok ? "ok" : "warn"}" role="status" data-hub-flash="${flash.ok ? "ok" : "failed"}">${escR(flash.message)}</p>`
1499
+ : "";
1500
+ if (state === "bad_config")
1501
+ return (
1502
+ flashHtml +
1503
+ note(b.problem || "hub in user.yml is not valid", "data-hub-problem")
1504
+ );
1505
+ if (state === "no_token")
1506
+ return (
1507
+ flashHtml +
1508
+ `<form class="hub-connect" data-hub-connect>` +
1509
+ `<label for="hub-url">Hub's address</label> ` +
1510
+ `<input type="url" id="hub-url" class="den-mono" spellcheck="false" autocomplete="url" placeholder="https://hub.example.com" value="${escR(b.url || "")}">` +
1511
+ ` <button type="submit" class="btn sm primary" data-hub-action="connect">Connect</button>` +
1512
+ `</form>` +
1513
+ `<p class="set-note">Connect opens Hub. Sign in there and approve this install; you come back here connected. The token is kept in this install's secrets store, never shown.</p>` +
1514
+ (b.problem && b.url && !/^not connected yet/.test(b.problem)
1515
+ ? note(b.problem, "data-hub-problem")
1516
+ : "") +
1517
+ `<div class="rt-err" role="alert" hidden></div>`
1518
+ );
1519
+ const status = {
1520
+ connected: ["ok", "Connected"],
1521
+ rejected: ["warn", "Token not accepted"],
1522
+ forbidden: ["warn", "Not allowed"],
1523
+ unreachable: ["warn", "Can't reach Hub"],
1524
+ }[state] || ["warn", "Not connected"];
1525
+ const team = b.team
1526
+ ? b.teamUrl && /^https?:\/\//i.test(b.teamUrl)
1527
+ ? `<a href="${escR(b.teamUrl)}" target="_blank" rel="noopener noreferrer">${escR(b.team)}</a>`
1528
+ : escR(b.team)
1529
+ : "";
1465
1530
  const rows = [
1466
- ["URL", `<span class="den-mono">${escR(b.url || "not set")}</span>`],
1531
+ ["Hub", `<span class="den-mono">${escR(b.url || "")}</span>`],
1467
1532
  [
1468
1533
  "Status",
1469
- b.connected
1470
- ? `<span class="set-tag ok" data-hub-state="connected">Connected</span>`
1471
- : `<span class="set-tag warn" data-hub-state="down">Not connected</span>`,
1534
+ `<span class="set-tag ${status[0]}" data-hub-state="${escR(state)}">${status[1]}</span>`,
1472
1535
  ],
1473
- ...(b.team ? [["Team", escR(b.team)]] : []),
1536
+ ...(team ? [["Company", team]] : []),
1537
+ ...(b.role ? [["Role", escR(b.role)]] : []),
1538
+ ...(b.login ? [["Signed in as", escR(b.login)]] : []),
1474
1539
  ...(b.label ? [["This install", escR(b.label)]] : []),
1475
1540
  ];
1541
+ const why =
1542
+ state === "rejected"
1543
+ ? "Hub no longer accepts this install's token (revoked or expired). Reconnect signs you in again."
1544
+ : state === "forbidden"
1545
+ ? "Your role on Hub no longer allows Den installs. Ask a Hub admin to change your role, then Reconnect."
1546
+ : state === "unreachable"
1547
+ ? b.problem || "Hub didn't answer."
1548
+ : "";
1476
1549
  const tools =
1477
1550
  `<label class="hub-tools"><input type="checkbox" id="hub-allow-tools"${b.allowTools !== false ? " checked" : ""}> Agents use Hub's tools without asking</label>` +
1478
1551
  `<p class="set-note" data-hub-tools-note>${
@@ -1481,46 +1554,136 @@ export function hubHtml(b) {
1481
1554
  : "Agents ask before each Hub tool runs."
1482
1555
  }</p>`;
1483
1556
  return (
1557
+ flashHtml +
1484
1558
  `<dl class="hub-st" data-hub>` +
1485
1559
  rows.map(([k, v]) => `<dt>${escR(k)}</dt><dd>${v}</dd>`).join("") +
1486
1560
  `</dl>` +
1487
- tools +
1488
- (!b.connected && b.problem
1489
- ? `<p class="set-note" data-hub-problem>${escR(b.problem)}</p>`
1490
- : "")
1561
+ (why ? note(why, "data-hub-problem") : "") +
1562
+ `<div class="hub-acts">` +
1563
+ (state === "rejected"
1564
+ ? `<button type="button" class="btn sm primary" data-hub-action="connect">Reconnect</button> `
1565
+ : "") +
1566
+ `<button type="button" class="btn sm" data-hub-action="disconnect">Disconnect</button>` +
1567
+ `</div>` +
1568
+ `<div class="rt-err" role="alert" hidden></div>` +
1569
+ tools
1491
1570
  );
1492
1571
  }
1493
1572
 
1494
- /** HUB-1.5: the Den Hub row; with no hub.url, the row isn't shown at all. */
1573
+ /** Read and clear ?hub=… (set by Hub's redirect back through /hub/callback). */
1574
+ function takeHubFlash() {
1575
+ try {
1576
+ const u = new URL(location.href);
1577
+ const v = u.searchParams.get("hub");
1578
+ if (!v) return null;
1579
+ u.searchParams.delete("hub");
1580
+ history.replaceState(history.state, "", u.pathname + u.search + u.hash);
1581
+ return v;
1582
+ } catch {
1583
+ return null;
1584
+ }
1585
+ }
1586
+
1587
+ /** HUB-1.5/1.7: the Hub row: always shown, so a person can Connect. */
1495
1588
  function renderHub(host, ctx) {
1496
- const row = host.closest(".set-row");
1497
- if (row) row.hidden = true;
1589
+ return drawHub(host, ctx, takeHubFlash(), null);
1590
+ }
1591
+
1592
+ /** Draw the row; `back` is ?hub=… from Hub's redirect, `flash` words to show. */
1593
+ function drawHub(host, ctx, back, flash) {
1498
1594
  // Under an invalid config /api/hub answers 422: ask nothing, show nothing.
1499
- if (ctx.shell?.configInvalid?.() === true) return Promise.resolve();
1595
+ if (ctx.shell?.configInvalid?.() === true) {
1596
+ host.innerHTML = "";
1597
+ return Promise.resolve();
1598
+ }
1500
1599
  return ctx.shell
1501
1600
  .api("GET", "/api/hub")
1502
1601
  .then((b) => {
1503
- const html = hubHtml(b);
1504
- host.innerHTML = html;
1505
- if (row) row.hidden = !html;
1506
- const box = host.querySelector("#hub-allow-tools");
1507
- if (box)
1508
- box.addEventListener("change", () => {
1509
- box.disabled = true;
1510
- ctx.shell
1511
- .api("POST", "/api/settings", { hub_allow_tools: box.checked })
1512
- .then(() => renderHub(host, ctx))
1513
- .catch(() => {
1514
- box.checked = !box.checked;
1515
- box.disabled = false;
1516
- });
1517
- });
1602
+ let f = flash;
1603
+ if (!f && b && b.connect) f = b.connect;
1604
+ else if (!f && back && HUB_FLASH[back])
1605
+ f = { ok: HUB_FLASH[back][0], message: HUB_FLASH[back][1] };
1606
+ host.innerHTML = hubHtml(b, f);
1607
+ wireHub(host, ctx);
1518
1608
  })
1519
1609
  .catch(() => {
1520
1610
  host.innerHTML = "";
1521
1611
  });
1522
1612
  }
1523
1613
 
1614
+ function wireHub(host, ctx) {
1615
+ const err = (msg, retry) => {
1616
+ const e = host.querySelector(".rt-err");
1617
+ if (!e) return;
1618
+ e.hidden = !msg;
1619
+ e.textContent = msg || "";
1620
+ if (retry) {
1621
+ const btn = document.createElement("button");
1622
+ btn.type = "button";
1623
+ btn.className = "btn sm";
1624
+ btn.dataset.hubAction = "force";
1625
+ btn.textContent = "Remove it here anyway";
1626
+ btn.addEventListener("click", () => disconnect(true, btn));
1627
+ e.append(" ", btn);
1628
+ }
1629
+ };
1630
+ const busy = (on) =>
1631
+ host.querySelectorAll("button, input").forEach((x) => (x.disabled = on));
1632
+ const connect = () => {
1633
+ const typed = host.querySelector("#hub-url");
1634
+ busy(true);
1635
+ err("");
1636
+ ctx.shell
1637
+ .api("POST", "/api/hub/connect", typed ? { url: typed.value } : {})
1638
+ .then((r) => {
1639
+ // Off to Hub: sign in and approve there; Hub sends the person back.
1640
+ location.assign(r.location);
1641
+ })
1642
+ .catch((e) => {
1643
+ busy(false);
1644
+ err(e?.message || "Connect didn't start");
1645
+ });
1646
+ };
1647
+ const disconnect = (force, btn) => {
1648
+ busy(true);
1649
+ err("");
1650
+ ctx.shell
1651
+ .api("POST", "/api/hub/disconnect", force ? { force: true } : {})
1652
+ .then((r) => drawHub(host, ctx, null, { ok: true, message: r.message }))
1653
+ .catch((e) => {
1654
+ busy(false);
1655
+ if (btn) btn.remove();
1656
+ err(e?.message || "Not disconnected", !force);
1657
+ });
1658
+ };
1659
+ const form = host.querySelector("[data-hub-connect]");
1660
+ if (form)
1661
+ form.addEventListener("submit", (ev) => {
1662
+ ev.preventDefault();
1663
+ connect();
1664
+ });
1665
+ host
1666
+ .querySelectorAll('button[data-hub-action="connect"]')
1667
+ .forEach(
1668
+ (x) => x.type === "button" && x.addEventListener("click", connect),
1669
+ );
1670
+ host
1671
+ .querySelector('[data-hub-action="disconnect"]')
1672
+ ?.addEventListener("click", () => disconnect(false));
1673
+ const box = host.querySelector("#hub-allow-tools");
1674
+ if (box)
1675
+ box.addEventListener("change", () => {
1676
+ box.disabled = true;
1677
+ ctx.shell
1678
+ .api("POST", "/api/settings", { hub_allow_tools: box.checked })
1679
+ .then(() => drawHub(host, ctx, null, null))
1680
+ .catch(() => {
1681
+ box.checked = !box.checked;
1682
+ box.disabled = false;
1683
+ });
1684
+ });
1685
+ }
1686
+
1524
1687
  /** The Rotation row: this install's values, the overrides, + Add override. */
1525
1688
  function renderRotation(host, ctx) {
1526
1689
  const err = () => host.querySelector(".rt-err");
@@ -473,6 +473,29 @@
473
473
  min-width: 0;
474
474
  overflow-wrap: anywhere;
475
475
  }
476
+ /* HUB-1.7: Connect / Disconnect. */
477
+ .hub-connect {
478
+ display: flex;
479
+ flex-wrap: wrap;
480
+ gap: 6px 8px;
481
+ align-items: center;
482
+ }
483
+ .hub-connect input {
484
+ flex: 1 1 240px;
485
+ min-width: 0;
486
+ }
487
+ .hub-acts {
488
+ display: flex;
489
+ flex-wrap: wrap;
490
+ gap: 8px;
491
+ margin-top: 10px;
492
+ }
493
+ .hub-flash.ok {
494
+ color: var(--ok, inherit);
495
+ }
496
+ .hub-flash.warn {
497
+ color: var(--warn, inherit);
498
+ }
476
499
  .hub-tools {
477
500
  display: flex;
478
501
  gap: 8px;
@@ -309,6 +309,7 @@
309
309
  "lock.checking": "Checking…",
310
310
  "lock.note": "Den uses it only for sudo. Agents can't enter it.",
311
311
  "lock.saved": "Admin password kept for this session",
312
+ "lock.savedTo": "Admin password kept for this session and saved to {s}",
312
313
  "lock.saveFailed": "Kept for this session, but saving to {s} failed: {err}",
313
314
  "lock.wrong": "Incorrect password — try again",
314
315
  "lock.reqFailed": "Request failed — is the server running?",
@@ -1459,7 +1459,11 @@ function openLock() {
1459
1459
  s: sudo.vault.label || sudo.vault.backend,
1460
1460
  err: res.vaultError,
1461
1461
  })
1462
- : t("lock.saved"),
1462
+ : saveToVault
1463
+ ? t("lock.savedTo", {
1464
+ s: sudo.vault.label || sudo.vault.backend,
1465
+ })
1466
+ : t("lock.saved"),
1463
1467
  );
1464
1468
  settleSudo(true);
1465
1469
  return;
@@ -392,6 +392,20 @@ export const ROUTES: readonly RouteEntry[] = [
392
392
  "/api/hub",
393
393
  "HUB-1.5: Den Hub: its URL, connected or not, the team and the install's label (never the token)",
394
394
  ),
395
+ M(
396
+ ["POST"],
397
+ "/api/hub/connect",
398
+ "HUB-1.7: start Connect to Hub: a PKCE verifier and single-use state kept here; answers Hub's /connect URL",
399
+ ),
400
+ M(
401
+ ["POST"],
402
+ "/api/hub/disconnect",
403
+ "HUB-1.7: revoke this install's Hub token on Hub, then remove it here",
404
+ ),
405
+ R(
406
+ "/hub/callback",
407
+ "HUB-1.7: Hub sends the person back here. Sign-in free: the session cookie is SameSite=Strict, so Hub's redirect doesn't carry it. It acts only for a state this server issued to a signed-in Connect (single-use, 10 min), with the PKCE verifier only this server holds; anything else changes nothing",
408
+ ),
395
409
  M(
396
410
  ["PUT"],
397
411
  "/api/agents/rotation",
@@ -488,6 +502,9 @@ export const SIGNIN_FREE: readonly string[] = [
488
502
  // SEC-2a: approve this computer with a code, from the signed-out page.
489
503
  "/api/signin/code",
490
504
  "/api/signin/code/:id",
505
+ // HUB-1.7: Hub's redirect back (no session cookie on it): the state
506
+ // this server issued is the proof (hub-connect.ts).
507
+ "/hub/callback",
491
508
  ];
492
509
 
493
510
  /**