@legioncodeinc/hive 0.1.0

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 (190) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +262 -0
  3. package/dist/cli-commands.d.ts +41 -0
  4. package/dist/cli-commands.js +76 -0
  5. package/dist/cli-commands.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +46 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/daemon/dashboard/app.js +118 -0
  10. package/dist/daemon/dashboard/host.d.ts +90 -0
  11. package/dist/daemon/dashboard/host.js +172 -0
  12. package/dist/daemon/dashboard/host.js.map +1 -0
  13. package/dist/daemon/dashboard/web-assets.d.ts +88 -0
  14. package/dist/daemon/dashboard/web-assets.js +195 -0
  15. package/dist/daemon/dashboard/web-assets.js.map +1 -0
  16. package/dist/daemon/fleet-status.d.ts +15 -0
  17. package/dist/daemon/fleet-status.js +56 -0
  18. package/dist/daemon/fleet-status.js.map +1 -0
  19. package/dist/daemon/gate.d.ts +55 -0
  20. package/dist/daemon/gate.js +128 -0
  21. package/dist/daemon/gate.js.map +1 -0
  22. package/dist/daemon/proxy.d.ts +38 -0
  23. package/dist/daemon/proxy.js +126 -0
  24. package/dist/daemon/proxy.js.map +1 -0
  25. package/dist/daemon/registry.d.ts +31 -0
  26. package/dist/daemon/registry.js +120 -0
  27. package/dist/daemon/registry.js.map +1 -0
  28. package/dist/daemon/server.d.ts +41 -0
  29. package/dist/daemon/server.js +126 -0
  30. package/dist/daemon/server.js.map +1 -0
  31. package/dist/daemon/setup-auth.d.ts +37 -0
  32. package/dist/daemon/setup-auth.js +51 -0
  33. package/dist/daemon/setup-auth.js.map +1 -0
  34. package/dist/daemon/telemetry-proxy.d.ts +47 -0
  35. package/dist/daemon/telemetry-proxy.js +63 -0
  36. package/dist/daemon/telemetry-proxy.js.map +1 -0
  37. package/dist/dashboard/contracts.d.ts +235 -0
  38. package/dist/dashboard/contracts.js +43 -0
  39. package/dist/dashboard/contracts.js.map +1 -0
  40. package/dist/dashboard/web/app.d.ts +44 -0
  41. package/dist/dashboard/web/app.js +179 -0
  42. package/dist/dashboard/web/app.js.map +1 -0
  43. package/dist/dashboard/web/boot-route.d.ts +23 -0
  44. package/dist/dashboard/web/boot-route.js +29 -0
  45. package/dist/dashboard/web/boot-route.js.map +1 -0
  46. package/dist/dashboard/web/build-graph-button.d.ts +46 -0
  47. package/dist/dashboard/web/build-graph-button.js +81 -0
  48. package/dist/dashboard/web/build-graph-button.js.map +1 -0
  49. package/dist/dashboard/web/buzzing-screen.d.ts +30 -0
  50. package/dist/dashboard/web/buzzing-screen.js +135 -0
  51. package/dist/dashboard/web/buzzing-screen.js.map +1 -0
  52. package/dist/dashboard/web/folder-picker.d.ts +54 -0
  53. package/dist/dashboard/web/folder-picker.js +125 -0
  54. package/dist/dashboard/web/folder-picker.js.map +1 -0
  55. package/dist/dashboard/web/graph-layout.d.ts +101 -0
  56. package/dist/dashboard/web/graph-layout.js +0 -0
  57. package/dist/dashboard/web/graph-layout.js.map +1 -0
  58. package/dist/dashboard/web/harness-strip.d.ts +38 -0
  59. package/dist/dashboard/web/harness-strip.js +65 -0
  60. package/dist/dashboard/web/harness-strip.js.map +1 -0
  61. package/dist/dashboard/web/health-rail.d.ts +26 -0
  62. package/dist/dashboard/web/health-rail.js +61 -0
  63. package/dist/dashboard/web/health-rail.js.map +1 -0
  64. package/dist/dashboard/web/main.d.ts +22 -0
  65. package/dist/dashboard/web/main.js +51 -0
  66. package/dist/dashboard/web/main.js.map +1 -0
  67. package/dist/dashboard/web/needs-project.d.ts +51 -0
  68. package/dist/dashboard/web/needs-project.js +69 -0
  69. package/dist/dashboard/web/needs-project.js.map +1 -0
  70. package/dist/dashboard/web/page-frame.d.ts +95 -0
  71. package/dist/dashboard/web/page-frame.js +98 -0
  72. package/dist/dashboard/web/page-frame.js.map +1 -0
  73. package/dist/dashboard/web/pages/coming-soon.d.ts +23 -0
  74. package/dist/dashboard/web/pages/coming-soon.js +19 -0
  75. package/dist/dashboard/web/pages/coming-soon.js.map +1 -0
  76. package/dist/dashboard/web/pages/dashboard.d.ts +32 -0
  77. package/dist/dashboard/web/pages/dashboard.js +205 -0
  78. package/dist/dashboard/web/pages/dashboard.js.map +1 -0
  79. package/dist/dashboard/web/pages/graph.d.ts +72 -0
  80. package/dist/dashboard/web/pages/graph.js +362 -0
  81. package/dist/dashboard/web/pages/graph.js.map +1 -0
  82. package/dist/dashboard/web/pages/harnesses.d.ts +65 -0
  83. package/dist/dashboard/web/pages/harnesses.js +210 -0
  84. package/dist/dashboard/web/pages/harnesses.js.map +1 -0
  85. package/dist/dashboard/web/pages/health.d.ts +22 -0
  86. package/dist/dashboard/web/pages/health.js +91 -0
  87. package/dist/dashboard/web/pages/health.js.map +1 -0
  88. package/dist/dashboard/web/pages/lifecycle-panel.d.ts +60 -0
  89. package/dist/dashboard/web/pages/lifecycle-panel.js +187 -0
  90. package/dist/dashboard/web/pages/lifecycle-panel.js.map +1 -0
  91. package/dist/dashboard/web/pages/logs.d.ts +55 -0
  92. package/dist/dashboard/web/pages/logs.js +311 -0
  93. package/dist/dashboard/web/pages/logs.js.map +1 -0
  94. package/dist/dashboard/web/pages/memories.d.ts +35 -0
  95. package/dist/dashboard/web/pages/memories.js +439 -0
  96. package/dist/dashboard/web/pages/memories.js.map +1 -0
  97. package/dist/dashboard/web/pages/projects.d.ts +38 -0
  98. package/dist/dashboard/web/pages/projects.js +325 -0
  99. package/dist/dashboard/web/pages/projects.js.map +1 -0
  100. package/dist/dashboard/web/pages/roi-chart.d.ts +52 -0
  101. package/dist/dashboard/web/pages/roi-chart.js +133 -0
  102. package/dist/dashboard/web/pages/roi-chart.js.map +1 -0
  103. package/dist/dashboard/web/pages/roi.d.ts +93 -0
  104. package/dist/dashboard/web/pages/roi.js +375 -0
  105. package/dist/dashboard/web/pages/roi.js.map +1 -0
  106. package/dist/dashboard/web/pages/settings.d.ts +103 -0
  107. package/dist/dashboard/web/pages/settings.js +420 -0
  108. package/dist/dashboard/web/pages/settings.js.map +1 -0
  109. package/dist/dashboard/web/pages/sync.d.ts +59 -0
  110. package/dist/dashboard/web/pages/sync.js +280 -0
  111. package/dist/dashboard/web/pages/sync.js.map +1 -0
  112. package/dist/dashboard/web/panels.d.ts +178 -0
  113. package/dist/dashboard/web/panels.js +471 -0
  114. package/dist/dashboard/web/panels.js.map +1 -0
  115. package/dist/dashboard/web/primitives.d.ts +80 -0
  116. package/dist/dashboard/web/primitives.js +208 -0
  117. package/dist/dashboard/web/primitives.js.map +1 -0
  118. package/dist/dashboard/web/readiness-splash.d.ts +39 -0
  119. package/dist/dashboard/web/readiness-splash.js +201 -0
  120. package/dist/dashboard/web/readiness-splash.js.map +1 -0
  121. package/dist/dashboard/web/registry.d.ts +96 -0
  122. package/dist/dashboard/web/registry.js +99 -0
  123. package/dist/dashboard/web/registry.js.map +1 -0
  124. package/dist/dashboard/web/router.d.ts +48 -0
  125. package/dist/dashboard/web/router.js +76 -0
  126. package/dist/dashboard/web/router.js.map +1 -0
  127. package/dist/dashboard/web/scope-context.d.ts +155 -0
  128. package/dist/dashboard/web/scope-context.js +266 -0
  129. package/dist/dashboard/web/scope-context.js.map +1 -0
  130. package/dist/dashboard/web/service-icons.d.ts +33 -0
  131. package/dist/dashboard/web/service-icons.js +80 -0
  132. package/dist/dashboard/web/service-icons.js.map +1 -0
  133. package/dist/dashboard/web/setup-gate.d.ts +116 -0
  134. package/dist/dashboard/web/setup-gate.js +272 -0
  135. package/dist/dashboard/web/setup-gate.js.map +1 -0
  136. package/dist/dashboard/web/sidebar.d.ts +63 -0
  137. package/dist/dashboard/web/sidebar.js +91 -0
  138. package/dist/dashboard/web/sidebar.js.map +1 -0
  139. package/dist/dashboard/web/use-fleet-telemetry.d.ts +111 -0
  140. package/dist/dashboard/web/use-fleet-telemetry.js +278 -0
  141. package/dist/dashboard/web/use-fleet-telemetry.js.map +1 -0
  142. package/dist/dashboard/web/wire.d.ts +1937 -0
  143. package/dist/dashboard/web/wire.js +1837 -0
  144. package/dist/dashboard/web/wire.js.map +1 -0
  145. package/dist/errors.d.ts +5 -0
  146. package/dist/errors.js +11 -0
  147. package/dist/errors.js.map +1 -0
  148. package/dist/install/registry.d.ts +35 -0
  149. package/dist/install/registry.js +110 -0
  150. package/dist/install/registry.js.map +1 -0
  151. package/dist/lock.d.ts +10 -0
  152. package/dist/lock.js +87 -0
  153. package/dist/lock.js.map +1 -0
  154. package/dist/service/commands.d.ts +15 -0
  155. package/dist/service/commands.js +49 -0
  156. package/dist/service/commands.js.map +1 -0
  157. package/dist/service/index.d.ts +38 -0
  158. package/dist/service/index.js +174 -0
  159. package/dist/service/index.js.map +1 -0
  160. package/dist/service/platform.d.ts +37 -0
  161. package/dist/service/platform.js +67 -0
  162. package/dist/service/platform.js.map +1 -0
  163. package/dist/service/templates.d.ts +10 -0
  164. package/dist/service/templates.js +120 -0
  165. package/dist/service/templates.js.map +1 -0
  166. package/dist/shared/constants.d.ts +15 -0
  167. package/dist/shared/constants.js +19 -0
  168. package/dist/shared/constants.js.map +1 -0
  169. package/dist/shared/daemon-routing.d.ts +11 -0
  170. package/dist/shared/daemon-routing.js +38 -0
  171. package/dist/shared/daemon-routing.js.map +1 -0
  172. package/dist/shared/fleet-readiness.d.ts +18 -0
  173. package/dist/shared/fleet-readiness.js +9 -0
  174. package/dist/shared/fleet-readiness.js.map +1 -0
  175. package/dist/shared/fleet-telemetry.d.ts +68 -0
  176. package/dist/shared/fleet-telemetry.js +61 -0
  177. package/dist/shared/fleet-telemetry.js.map +1 -0
  178. package/dist/shared/lifecycle-flags.d.ts +34 -0
  179. package/dist/shared/lifecycle-flags.js +39 -0
  180. package/dist/shared/lifecycle-flags.js.map +1 -0
  181. package/dist/shared/memory-types.d.ts +55 -0
  182. package/dist/shared/memory-types.js +75 -0
  183. package/dist/shared/memory-types.js.map +1 -0
  184. package/dist/shared/service-status.d.ts +82 -0
  185. package/dist/shared/service-status.js +97 -0
  186. package/dist/shared/service-status.js.map +1 -0
  187. package/dist/telemetry/emit.d.ts +197 -0
  188. package/dist/telemetry/emit.js +196 -0
  189. package/dist/telemetry/emit.js.map +1 -0
  190. package/package.json +50 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The server-side PORTAL LANDING GATE — PRD-003a, implementing ADR-0004's precedence.
3
+ *
4
+ * On landing on ANY non-exempt route hive evaluates ONE ordered precedence, health then auth,
5
+ * before deciding what to serve:
6
+ *
7
+ * 1. Fleet not healthy (per the EXISTING `isFleetReady()` projection, `fleet-status.ts`) →
8
+ * redirect to `/buzzing` (g-AC-3). Checked FIRST: an unhealthy fleet makes every other screen
9
+ * useless, so auth is never even evaluated in this branch.
10
+ * 2. Else not logged in (the EXISTING proxied honeycomb `/setup/state` `authenticated` bit,
11
+ * `setup-auth.ts`) → redirect to `/login` (g-AC-4). A fetch failure/timeout reads as "not
12
+ * logged in" (l-AC-6) — never a fail-soft into the dashboard.
13
+ * 3. Else serve the requested route as-is (g-AC-5 / g-AC-6): `next()` falls through to the
14
+ * more specific route/catch-all registered after this middleware in `server.ts`.
15
+ *
16
+ * `/buzzing` and `/login` are a FIXED exemption set checked BEFORE the precedence above, so they
17
+ * are always served directly and the redirect can never loop (g-AC-7 / g-AC-8 / g-AC-9): the only
18
+ * two redirect destinations are themselves exempt from producing another redirect.
19
+ *
20
+ * This middleware also bypasses hive's own infra: `/health` (this daemon's own liveness, not a
21
+ * portal screen), the bundled asset routes (`/app.js`, `/styles.css`, the logo, `/fonts/*` — these
22
+ * must load even when the shell just redirected the BROWSER to `/buzzing` or `/login`, since the
23
+ * exempt screens are the same SPA bundle), and the `/api/*` / `/setup/*` data-plane proxy (that is
24
+ * an existing same-origin API surface, not a page navigation the gate should ever intercept or
25
+ * redirect — g-AC-11's "no open redirect" posture, plus l-AC-2's "all `/setup/*` traffic stays
26
+ * same-origin", both depend on the proxy handling its own requests untouched).
27
+ *
28
+ * SECURITY (g-AC-11, open-redirect defense): every redirect this middleware issues targets a
29
+ * FIXED, hard-coded literal (`/buzzing` or `/login`) — never a value derived from user input (no
30
+ * `?next=`, no `Referer`, no request path echoed back). This makes an open redirect structurally
31
+ * impossible here: there is no code path where an attacker-controlled string reaches `c.redirect`.
32
+ */
33
+ import type { MiddlewareHandler } from "hono";
34
+ import { type FetchImpl as FleetFetchImpl } from "./fleet-status.js";
35
+ import { type SetupAuthFetchImpl } from "./setup-auth.js";
36
+ /** The only two gate-exempt SCREENS (g-AC-7 / g-AC-8): always served directly, never redirected. */
37
+ export declare const GATE_EXEMPT_ROUTES: readonly ["/buzzing", "/login"];
38
+ export interface CreatePortalGateOptions {
39
+ /** The fetch used for the health check (defaults to the global `fetch`; a test injects a fake). */
40
+ readonly fleetStatusFetch?: FleetFetchImpl;
41
+ /** Override doctor's status URL (defaults to the fixed loopback constant). */
42
+ readonly doctorStatusUrl?: string;
43
+ /** The fetch used for the auth check (defaults to the global `fetch`; a test injects a fake). */
44
+ readonly setupAuthFetch?: SetupAuthFetchImpl;
45
+ /** Override the doctor registry file path the auth check resolves honeycomb's base from. */
46
+ readonly registryPath?: string;
47
+ }
48
+ /**
49
+ * Build the portal landing gate middleware (g-AC-1..11). Register it FIRST on hive's Hono app
50
+ * (`app.use("*", createPortalGate(...))`) so it runs ahead of every other route. It never renders
51
+ * a response itself for the passing case — it calls `next()` and lets the routes registered after
52
+ * it (the asset routes, `/health`, `/api/fleet-status`, the BFF proxy, and finally the SPA shell
53
+ * catch-all) serve the request, exactly as they do today for the paths this gate bypasses.
54
+ */
55
+ export declare function createPortalGate(options?: CreatePortalGateOptions): MiddlewareHandler;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The server-side PORTAL LANDING GATE — PRD-003a, implementing ADR-0004's precedence.
3
+ *
4
+ * On landing on ANY non-exempt route hive evaluates ONE ordered precedence, health then auth,
5
+ * before deciding what to serve:
6
+ *
7
+ * 1. Fleet not healthy (per the EXISTING `isFleetReady()` projection, `fleet-status.ts`) →
8
+ * redirect to `/buzzing` (g-AC-3). Checked FIRST: an unhealthy fleet makes every other screen
9
+ * useless, so auth is never even evaluated in this branch.
10
+ * 2. Else not logged in (the EXISTING proxied honeycomb `/setup/state` `authenticated` bit,
11
+ * `setup-auth.ts`) → redirect to `/login` (g-AC-4). A fetch failure/timeout reads as "not
12
+ * logged in" (l-AC-6) — never a fail-soft into the dashboard.
13
+ * 3. Else serve the requested route as-is (g-AC-5 / g-AC-6): `next()` falls through to the
14
+ * more specific route/catch-all registered after this middleware in `server.ts`.
15
+ *
16
+ * `/buzzing` and `/login` are a FIXED exemption set checked BEFORE the precedence above, so they
17
+ * are always served directly and the redirect can never loop (g-AC-7 / g-AC-8 / g-AC-9): the only
18
+ * two redirect destinations are themselves exempt from producing another redirect.
19
+ *
20
+ * This middleware also bypasses hive's own infra: `/health` (this daemon's own liveness, not a
21
+ * portal screen), the bundled asset routes (`/app.js`, `/styles.css`, the logo, `/fonts/*` — these
22
+ * must load even when the shell just redirected the BROWSER to `/buzzing` or `/login`, since the
23
+ * exempt screens are the same SPA bundle), and the `/api/*` / `/setup/*` data-plane proxy (that is
24
+ * an existing same-origin API surface, not a page navigation the gate should ever intercept or
25
+ * redirect — g-AC-11's "no open redirect" posture, plus l-AC-2's "all `/setup/*` traffic stays
26
+ * same-origin", both depend on the proxy handling its own requests untouched).
27
+ *
28
+ * SECURITY (g-AC-11, open-redirect defense): every redirect this middleware issues targets a
29
+ * FIXED, hard-coded literal (`/buzzing` or `/login`) — never a value derived from user input (no
30
+ * `?next=`, no `Referer`, no request path echoed back). This makes an open redirect structurally
31
+ * impossible here: there is no code path where an attacker-controlled string reaches `c.redirect`.
32
+ */
33
+ import { DOCTOR_STATUS_URL } from "../shared/constants.js";
34
+ import { isFleetReady } from "../shared/fleet-readiness.js";
35
+ import { fetchFleetStatus } from "./fleet-status.js";
36
+ import { fetchSetupAuthenticated } from "./setup-auth.js";
37
+ /** The fixed redirect target for an unhealthy fleet (g-AC-3). A hard-coded literal, never derived. */
38
+ const BUZZING_ROUTE = "/buzzing";
39
+ /** The fixed redirect target for a logged-out operator (g-AC-4). A hard-coded literal, never derived. */
40
+ const LOGIN_ROUTE = "/login";
41
+ /** The only two gate-exempt SCREENS (g-AC-7 / g-AC-8): always served directly, never redirected. */
42
+ export const GATE_EXEMPT_ROUTES = [BUZZING_ROUTE, LOGIN_ROUTE];
43
+ /**
44
+ * hive's OWN liveness route + the bundled SPA asset routes — never a page navigation to gate.
45
+ * `/health` is deliberately NOT in this set (see {@link isInfraPath}): PRD-005b claims that same
46
+ * literal path for the operator-facing `/health` page, so it needs content-negotiated handling
47
+ * rather than a blanket exemption.
48
+ */
49
+ const GATE_EXEMPT_INFRA_PATHS = new Set(["/app.js", "/styles.css", "/honeycomb-memory-cluster.svg"]);
50
+ /** Path PREFIXES that are data-plane traffic (the BFF proxy, fonts), never a gated page route. */
51
+ const GATE_EXEMPT_INFRA_PREFIXES = ["/api/", "/setup/", "/fonts/"];
52
+ /**
53
+ * hive's own machine-liveness path. Ambiguous on purpose (PRD-005b): a health-probe/monitoring
54
+ * caller (doctor's own `/health` probe, an ops tool) never sends `Accept: text/html`, while a
55
+ * browser navigating to the new `/health` OPERATOR page always does. `server.ts`'s `/health`
56
+ * handler makes the SAME distinction so the two behaviors stay in lockstep.
57
+ */
58
+ const LIVENESS_PATH = "/health";
59
+ /** True iff `accept` prefers HTML — the signal that distinguishes a page navigation from a machine probe. */
60
+ function prefersHtml(accept) {
61
+ return accept.includes("text/html");
62
+ }
63
+ /**
64
+ * True for hive's own infra/asset/proxy surface — bypasses the gate entirely (see module doc).
65
+ * `/health` is infra ONLY when the caller is not asking for HTML (a liveness probe); an
66
+ * HTML-accepting request to `/health` is treated as a normal page route so it gets the same
67
+ * buzzing/login precedence as every other SPA route (PRD-005b).
68
+ */
69
+ function isInfraPath(pathname, accept) {
70
+ if (pathname === LIVENESS_PATH)
71
+ return !prefersHtml(accept);
72
+ if (GATE_EXEMPT_INFRA_PATHS.has(pathname))
73
+ return true;
74
+ return GATE_EXEMPT_INFRA_PREFIXES.some((prefix) => pathname.startsWith(prefix));
75
+ }
76
+ /** True for the two gate-exempt screens (`/buzzing`, `/login`) — always served directly. */
77
+ function isExemptRoute(pathname) {
78
+ return GATE_EXEMPT_ROUTES.includes(pathname);
79
+ }
80
+ /**
81
+ * Build the portal landing gate middleware (g-AC-1..11). Register it FIRST on hive's Hono app
82
+ * (`app.use("*", createPortalGate(...))`) so it runs ahead of every other route. It never renders
83
+ * a response itself for the passing case — it calls `next()` and lets the routes registered after
84
+ * it (the asset routes, `/health`, `/api/fleet-status`, the BFF proxy, and finally the SPA shell
85
+ * catch-all) serve the request, exactly as they do today for the paths this gate bypasses.
86
+ */
87
+ export function createPortalGate(options = {}) {
88
+ const fleetStatusFetch = options.fleetStatusFetch ?? fetch;
89
+ const doctorStatusUrl = options.doctorStatusUrl ?? DOCTOR_STATUS_URL;
90
+ const setupAuthFetch = options.setupAuthFetch ?? fetch;
91
+ const registryPath = options.registryPath;
92
+ return async (c, next) => {
93
+ const pathname = new URL(c.req.url).pathname;
94
+ const accept = c.req.header("accept") ?? "";
95
+ // hive's own infra/asset/proxy surface: never a page navigation, never gated.
96
+ if (isInfraPath(pathname, accept)) {
97
+ await next();
98
+ return undefined;
99
+ }
100
+ // g-AC-7 / g-AC-8: the exemption set is checked BEFORE the precedence, so `/buzzing` and
101
+ // `/login` are ALWAYS served directly — this is what guarantees the redirect below can never
102
+ // loop (g-AC-9): the only two redirect targets are themselves exempt from producing another.
103
+ if (isExemptRoute(pathname)) {
104
+ await next();
105
+ return undefined;
106
+ }
107
+ // g-AC-3: health first, before auth is even evaluated. Reuses the SAME `isFleetReady()`
108
+ // predicate `/buzzing`'s own readiness view will read (PRD-002a), so "healthy" means one
109
+ // thing across the gate and the screen it redirects to.
110
+ const fleetStatus = await fetchFleetStatus(fleetStatusFetch, doctorStatusUrl);
111
+ if (!isFleetReady(fleetStatus)) {
112
+ return c.redirect(BUZZING_ROUTE, 302);
113
+ }
114
+ // g-AC-4 / l-AC-4 / l-AC-6: auth second. A fetch failure/timeout resolves to `false` inside
115
+ // `fetchSetupAuthenticated`, so a transient proxy fault falls to `/login`, never the dashboard.
116
+ // The incoming request's abort signal is threaded through so a client disconnect aborts the
117
+ // upstream `/setup/state` fetch instead of pinning it (an abort also reads as fail-closed).
118
+ const authenticated = await fetchSetupAuthenticated(setupAuthFetch, { registryPath, signal: c.req.raw.signal });
119
+ if (!authenticated) {
120
+ return c.redirect(LOGIN_ROUTE, 302);
121
+ }
122
+ // g-AC-5 / g-AC-6 / g-AC-10: healthy + authenticated — serve the requested route as-is (the
123
+ // SPA shell catch-all defaults `/` to the dashboard; nothing here re-derives a fallback path).
124
+ await next();
125
+ return undefined;
126
+ };
127
+ }
128
+ //# sourceMappingURL=gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/daemon/gate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * hive server-side API proxy: the BFF (backend-for-frontend) federation layer.
3
+ *
4
+ * The dashboard browser talks ONLY to hive's own origin (same-origin `/api/*` and
5
+ * `/setup/*`). hive's SERVER resolves the daemon that owns each request from doctor's
6
+ * registry, then fetches that daemon over loopback and streams the response straight back.
7
+ * This replaces the earlier client-side federation (the browser fetching each workload
8
+ * daemon's origin directly via `/api/daemon-bases`), which required the workload daemons to
9
+ * emit CORS headers and exposed their ports to a browser context.
10
+ *
11
+ * Auth model: TRANSPARENT PASS-THROUGH. hive forwards the browser's own request headers
12
+ * (session headers + any auth) verbatim to the workload daemon; it stores no credential of its
13
+ * own. This preserves honeycomb's existing loopback + local-mode + session-header posture and
14
+ * keeps team/hybrid auth working without hive becoming an auth authority.
15
+ *
16
+ * SECURITY (SSRF): the daemon base is resolved through {@link resolveDaemonBases}, which drops
17
+ * any non-loopback `healthUrl` from the registry and only ever hands back loopback origins. The
18
+ * proxy re-checks the resolved base with {@link isLoopbackBaseUrl} (defense in depth) and pins
19
+ * `redirect: "error"` so a workload daemon cannot 3xx-redirect the proxied fetch to an
20
+ * off-loopback origin after the initial URL passed the loopback gate.
21
+ */
22
+ import type { Context } from "hono";
23
+ /** The injectable fetch surface (the global in prod; a mock in unit tests). */
24
+ export type ProxyFetch = (input: string, init: RequestInit) => Promise<Response>;
25
+ export interface CreateApiProxyOptions {
26
+ /** Override the doctor registry file path the daemon bases are resolved from. */
27
+ readonly registryPath?: string;
28
+ /** The fetch implementation used to reach the workload daemon (defaults to the global `fetch`). */
29
+ readonly fetchImpl?: ProxyFetch;
30
+ }
31
+ /**
32
+ * Build the same-origin API proxy handler. Register it on hive's Hono app for `/api/*` and
33
+ * `/setup/*` (AFTER the hive-owned routes like `/health` and `/api/fleet-status`, so those
34
+ * win). It resolves the owning daemon per request, forwards method + headers + body over
35
+ * loopback, and streams the upstream response back. Any failure (network error, non-loopback
36
+ * base, redirect) degrades to a fail-soft 502 so one daemon being down never blanks the page.
37
+ */
38
+ export declare function createApiProxy(options?: CreateApiProxyOptions): (c: Context) => Promise<Response>;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * hive server-side API proxy: the BFF (backend-for-frontend) federation layer.
3
+ *
4
+ * The dashboard browser talks ONLY to hive's own origin (same-origin `/api/*` and
5
+ * `/setup/*`). hive's SERVER resolves the daemon that owns each request from doctor's
6
+ * registry, then fetches that daemon over loopback and streams the response straight back.
7
+ * This replaces the earlier client-side federation (the browser fetching each workload
8
+ * daemon's origin directly via `/api/daemon-bases`), which required the workload daemons to
9
+ * emit CORS headers and exposed their ports to a browser context.
10
+ *
11
+ * Auth model: TRANSPARENT PASS-THROUGH. hive forwards the browser's own request headers
12
+ * (session headers + any auth) verbatim to the workload daemon; it stores no credential of its
13
+ * own. This preserves honeycomb's existing loopback + local-mode + session-header posture and
14
+ * keeps team/hybrid auth working without hive becoming an auth authority.
15
+ *
16
+ * SECURITY (SSRF): the daemon base is resolved through {@link resolveDaemonBases}, which drops
17
+ * any non-loopback `healthUrl` from the registry and only ever hands back loopback origins. The
18
+ * proxy re-checks the resolved base with {@link isLoopbackBaseUrl} (defense in depth) and pins
19
+ * `redirect: "error"` so a workload daemon cannot 3xx-redirect the proxied fetch to an
20
+ * off-loopback origin after the initial URL passed the loopback gate.
21
+ */
22
+ import { isLoopbackBaseUrl, resolveEndpointOwner } from "../shared/daemon-routing.js";
23
+ import { resolveDaemonBases } from "./registry.js";
24
+ /**
25
+ * Request headers that must NOT be forwarded to the upstream daemon. `host` is dropped so fetch
26
+ * sets it from the target origin; the rest are hop-by-hop headers (RFC 7230 §6.1) that are
27
+ * meaningless across a proxy, plus `content-length` which fetch recomputes from the forwarded body.
28
+ */
29
+ const STRIPPED_REQUEST_HEADERS = new Set([
30
+ "host",
31
+ "connection",
32
+ "keep-alive",
33
+ "proxy-authenticate",
34
+ "proxy-authorization",
35
+ "te",
36
+ "trailer",
37
+ "transfer-encoding",
38
+ "upgrade",
39
+ "content-length"
40
+ ]);
41
+ /**
42
+ * Response headers that must NOT be forwarded back to the browser. Hop-by-hop headers plus the
43
+ * framing headers fetch has already resolved: `content-encoding`/`content-length` would be wrong
44
+ * because fetch decompresses the upstream body before we re-stream it.
45
+ */
46
+ const STRIPPED_RESPONSE_HEADERS = new Set([
47
+ "connection",
48
+ "keep-alive",
49
+ "proxy-authenticate",
50
+ "proxy-authorization",
51
+ "te",
52
+ "trailer",
53
+ "transfer-encoding",
54
+ "upgrade",
55
+ "content-encoding",
56
+ "content-length"
57
+ ]);
58
+ /** The fail-soft result for an unreachable/failed upstream: a non-2xx JSON body the wire degrades to empty. */
59
+ function unreachableResponse(daemon) {
60
+ return new Response(JSON.stringify({ error: "unreachable", daemon }), {
61
+ status: 502,
62
+ headers: { "content-type": "application/json" }
63
+ });
64
+ }
65
+ function forwardRequestHeaders(source) {
66
+ const headers = new Headers();
67
+ source.forEach((value, key) => {
68
+ if (!STRIPPED_REQUEST_HEADERS.has(key.toLowerCase()))
69
+ headers.set(key, value);
70
+ });
71
+ return headers;
72
+ }
73
+ function forwardResponseHeaders(source) {
74
+ const headers = new Headers();
75
+ source.forEach((value, key) => {
76
+ if (!STRIPPED_RESPONSE_HEADERS.has(key.toLowerCase()))
77
+ headers.set(key, value);
78
+ });
79
+ return headers;
80
+ }
81
+ /**
82
+ * Build the same-origin API proxy handler. Register it on hive's Hono app for `/api/*` and
83
+ * `/setup/*` (AFTER the hive-owned routes like `/health` and `/api/fleet-status`, so those
84
+ * win). It resolves the owning daemon per request, forwards method + headers + body over
85
+ * loopback, and streams the upstream response back. Any failure (network error, non-loopback
86
+ * base, redirect) degrades to a fail-soft 502 so one daemon being down never blanks the page.
87
+ */
88
+ export function createApiProxy(options = {}) {
89
+ const fetchImpl = options.fetchImpl ?? fetch;
90
+ return async (c) => {
91
+ const incoming = c.req.raw;
92
+ const requestUrl = new URL(incoming.url);
93
+ const endpointPath = requestUrl.pathname;
94
+ const owner = resolveEndpointOwner(endpointPath);
95
+ const base = resolveDaemonBases({ registryPath: options.registryPath })[owner];
96
+ // resolveDaemonBases only ever returns loopback origins; this re-check is defense in depth so a
97
+ // future change to base resolution can never turn the proxy into an off-loopback SSRF primitive.
98
+ if (!isLoopbackBaseUrl(base))
99
+ return unreachableResponse(owner);
100
+ const target = `${base}${endpointPath}${requestUrl.search}`;
101
+ const method = incoming.method;
102
+ const hasBody = method !== "GET" && method !== "HEAD";
103
+ // Buffer the (small, JSON) request body rather than streaming it, so no `duplex: "half"` dance
104
+ // is needed. GET/HEAD carry no body. The response body is streamed through (below) so SSE tails
105
+ // and large payloads are not buffered.
106
+ const body = hasBody ? await incoming.arrayBuffer() : undefined;
107
+ try {
108
+ const upstream = await fetchImpl(target, {
109
+ method,
110
+ headers: forwardRequestHeaders(incoming.headers),
111
+ ...(body !== undefined ? { body } : {}),
112
+ redirect: "error"
113
+ });
114
+ return new Response(upstream.body, {
115
+ status: upstream.status,
116
+ statusText: upstream.statusText,
117
+ headers: forwardResponseHeaders(upstream.headers)
118
+ });
119
+ }
120
+ catch {
121
+ // Network error, refused connection, or a blocked redirect: fail soft per daemon.
122
+ return unreachableResponse(owner);
123
+ }
124
+ };
125
+ }
126
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/daemon/proxy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type DaemonBases, type DaemonName } from "../shared/daemon-routing.js";
2
+ export declare const DOCTOR_REGISTRY_PATH: string;
3
+ export interface ResolveDaemonBasesOptions {
4
+ readonly registryPath?: string;
5
+ readonly readFile?: (path: string) => string;
6
+ }
7
+ export declare function baseUrlFromHealthUrl(healthUrl: string): string | null;
8
+ export declare function parseDoctorRegistry(raw: string): Partial<Record<DaemonName, string>>;
9
+ /**
10
+ * Parse the RAW list of registered service names from a doctor registry file body (PRD-004a
11
+ * bz-AC-1/bz-AC-2). Unlike {@link parseDoctorRegistry} (which narrows to the two daemons
12
+ * hive's BFF proxy forwards to, `honeycomb`/`nectar`), this returns EVERY registered name —
13
+ * including `hive` itself or any future peer — so `/buzzing` and the health rail can render one
14
+ * tile/pill per service that doctor says should exist, even one hive's proxy never routes
15
+ * to. A corrupt/absent registry degrades to an empty list (never a throw), matching every other
16
+ * reader of this file.
17
+ */
18
+ export declare function parseRegisteredServiceNames(raw: string): readonly string[];
19
+ /** Options for {@link resolveRegisteredServiceNames}. */
20
+ export interface ResolveRegisteredServiceNamesOptions {
21
+ readonly registryPath?: string;
22
+ readonly readFile?: (path: string) => string;
23
+ }
24
+ /**
25
+ * Read the FULL list of registered service names from doctor's registry file (PRD-004a
26
+ * bz-AC-1/bz-AC-2, PRD-005a hr-AC-1). A missing/unreadable/corrupt registry resolves to an empty
27
+ * list rather than throwing, so a cold box with no registry yet still serves `/buzzing` (it simply
28
+ * shows no tiles until the fleet-status/SSE feed enumerates services some other way).
29
+ */
30
+ export declare function resolveRegisteredServiceNames(options?: ResolveRegisteredServiceNamesOptions): readonly string[];
31
+ export declare function resolveDaemonBases(options?: ResolveDaemonBasesOptions): DaemonBases;
@@ -0,0 +1,120 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { z } from "zod";
4
+ import { HONEYCOMB_HOME_DIR } from "../shared/constants.js";
5
+ import { isLoopbackBaseUrl, normalizeDaemonBases } from "../shared/daemon-routing.js";
6
+ export const DOCTOR_REGISTRY_PATH = join(HONEYCOMB_HOME_DIR, "doctor.daemons.json");
7
+ const RegistryEntrySchema = z.object({
8
+ name: z.string().min(1),
9
+ healthUrl: z.string().url(),
10
+ pidPath: z.string().min(1)
11
+ });
12
+ const DoctorRegistrySchema = z.object({
13
+ daemons: z.array(RegistryEntrySchema).catch([])
14
+ });
15
+ function daemonName(name) {
16
+ if (name === "honeycomb" || name === "nectar")
17
+ return name;
18
+ return null;
19
+ }
20
+ export function baseUrlFromHealthUrl(healthUrl) {
21
+ try {
22
+ const url = new URL(healthUrl);
23
+ // Reject any registry entry naming a non-loopback host BEFORE it can become a daemon base:
24
+ // every workload daemon binds loopback only, so a `healthUrl` pointing elsewhere is either a
25
+ // corrupt registry or a tampered one, and must never become a base the server-side proxy
26
+ // (`src/daemon/proxy.ts`) would forward request bodies (including captured session/memory
27
+ // content) to.
28
+ if (!isLoopbackBaseUrl(url.origin))
29
+ return null;
30
+ const pathname = url.pathname.endsWith("/health") ? url.pathname.slice(0, -"/health".length) : url.pathname;
31
+ const normalizedPath = pathname === "/" ? "" : pathname.replace(/\/$/, "");
32
+ return `${url.origin}${normalizedPath}`;
33
+ }
34
+ catch {
35
+ // zod normally rejects invalid URLs first; this keeps the exported helper total for tests and callers.
36
+ return null;
37
+ }
38
+ }
39
+ export function parseDoctorRegistry(raw) {
40
+ let parsedJson;
41
+ try {
42
+ parsedJson = JSON.parse(raw);
43
+ }
44
+ catch {
45
+ // A corrupt registry must not prevent hive from serving. Defaults remain available.
46
+ return {};
47
+ }
48
+ const parsed = DoctorRegistrySchema.safeParse(parsedJson);
49
+ if (!parsed.success)
50
+ return {};
51
+ const bases = {};
52
+ for (const entry of parsed.data.daemons) {
53
+ const name = daemonName(entry.name);
54
+ if (name === null)
55
+ continue;
56
+ const baseUrl = baseUrlFromHealthUrl(entry.healthUrl);
57
+ if (baseUrl !== null)
58
+ bases[name] = baseUrl;
59
+ }
60
+ return bases;
61
+ }
62
+ /**
63
+ * Parse the RAW list of registered service names from a doctor registry file body (PRD-004a
64
+ * bz-AC-1/bz-AC-2). Unlike {@link parseDoctorRegistry} (which narrows to the two daemons
65
+ * hive's BFF proxy forwards to, `honeycomb`/`nectar`), this returns EVERY registered name —
66
+ * including `hive` itself or any future peer — so `/buzzing` and the health rail can render one
67
+ * tile/pill per service that doctor says should exist, even one hive's proxy never routes
68
+ * to. A corrupt/absent registry degrades to an empty list (never a throw), matching every other
69
+ * reader of this file.
70
+ */
71
+ export function parseRegisteredServiceNames(raw) {
72
+ let parsedJson;
73
+ try {
74
+ parsedJson = JSON.parse(raw);
75
+ }
76
+ catch {
77
+ return [];
78
+ }
79
+ const parsed = DoctorRegistrySchema.safeParse(parsedJson);
80
+ if (!parsed.success)
81
+ return [];
82
+ // De-duplicate defensively (a hand-edited registry could repeat a name); preserve first-seen order.
83
+ const seen = new Set();
84
+ const names = [];
85
+ for (const entry of parsed.data.daemons) {
86
+ if (seen.has(entry.name))
87
+ continue;
88
+ seen.add(entry.name);
89
+ names.push(entry.name);
90
+ }
91
+ return names;
92
+ }
93
+ /**
94
+ * Read the FULL list of registered service names from doctor's registry file (PRD-004a
95
+ * bz-AC-1/bz-AC-2, PRD-005a hr-AC-1). A missing/unreadable/corrupt registry resolves to an empty
96
+ * list rather than throwing, so a cold box with no registry yet still serves `/buzzing` (it simply
97
+ * shows no tiles until the fleet-status/SSE feed enumerates services some other way).
98
+ */
99
+ export function resolveRegisteredServiceNames(options = {}) {
100
+ const registryPath = options.registryPath ?? DOCTOR_REGISTRY_PATH;
101
+ const readFile = options.readFile ?? ((path) => readFileSync(path, "utf8"));
102
+ try {
103
+ return parseRegisteredServiceNames(readFile(registryPath));
104
+ }
105
+ catch {
106
+ return [];
107
+ }
108
+ }
109
+ export function resolveDaemonBases(options = {}) {
110
+ const registryPath = options.registryPath ?? DOCTOR_REGISTRY_PATH;
111
+ const readFile = options.readFile ?? ((path) => readFileSync(path, "utf8"));
112
+ try {
113
+ return normalizeDaemonBases(parseDoctorRegistry(readFile(registryPath)));
114
+ }
115
+ catch {
116
+ // Missing or unreadable registry means no daemon has registered yet. Use documented loopback defaults.
117
+ return normalizeDaemonBases();
118
+ }
119
+ }
120
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/daemon/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { Hono } from "hono";
3
+ import { type LockPaths } from "../lock.js";
4
+ import { type FetchImpl } from "./fleet-status.js";
5
+ import { type ProxyFetch } from "./proxy.js";
6
+ import { type TelemetryFetch } from "./telemetry-proxy.js";
7
+ import type { SetupAuthFetchImpl } from "./setup-auth.js";
8
+ export interface CreateHiveOptions {
9
+ readonly host?: string;
10
+ readonly port?: number;
11
+ readonly now?: () => number;
12
+ readonly registryPath?: string;
13
+ readonly fleetStatusFetch?: FetchImpl;
14
+ readonly doctorStatusUrl?: string;
15
+ /** The fetch used by the API proxy to reach workload daemons over loopback (defaults to the global `fetch`). */
16
+ readonly proxyFetch?: ProxyFetch;
17
+ /** The fetch used by the portal gate's auth check (`/setup/state`, defaults to the global `fetch`). */
18
+ readonly setupAuthFetch?: SetupAuthFetchImpl;
19
+ /** Override doctor's SSE events URL the telemetry relay connects to (defaults to the fixed loopback constant). */
20
+ readonly doctorEventsUrl?: string;
21
+ /** The fetch used by the telemetry relay to reach doctor's SSE stream (defaults to the global `fetch`). */
22
+ readonly telemetryStreamFetch?: TelemetryFetch;
23
+ }
24
+ export interface StartHiveOptions extends CreateHiveOptions {
25
+ readonly lockPaths?: Partial<LockPaths>;
26
+ readonly serveFn?: ServeFunction;
27
+ }
28
+ export interface HiveInstance {
29
+ readonly app: Hono;
30
+ readonly host: string;
31
+ readonly port: number;
32
+ readonly startedAt: number;
33
+ }
34
+ export interface StartedHive extends HiveInstance {
35
+ readonly lockPaths: LockPaths;
36
+ stop(): Promise<void>;
37
+ }
38
+ type ServeFunction = typeof serve;
39
+ export declare function createHive(options?: CreateHiveOptions): HiveInstance;
40
+ export declare function startHive(options?: StartHiveOptions): StartedHive;
41
+ export {};