@legioncodeinc/hive 0.2.1 → 0.3.1

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 (132) hide show
  1. package/README.md +2 -2
  2. package/assets/brand/activeloop-full-mark-logo-on-dark.svg +209 -0
  3. package/assets/brand/activeloop-full-mark-logo.svg +208 -0
  4. package/assets/brand/divider-major.svg +1 -0
  5. package/assets/brand/divider-minor.svg +1 -0
  6. package/assets/brand/doctor-mark.svg +1 -0
  7. package/assets/brand/hive-mark.svg +1 -0
  8. package/assets/brand/hive-wordmark-black.svg +1 -0
  9. package/assets/brand/hive-wordmark-on-dark.svg +1 -0
  10. package/assets/brand/legion-logo-dark.svg +16 -0
  11. package/assets/brand/legion-logo-light.svg +16 -0
  12. package/assets/brand/nectar-mark.svg +1 -0
  13. package/assets/logos/fonts/Inter-Italic-VariableFont_opsz_wght.ttf +0 -0
  14. package/assets/logos/fonts/Inter-VariableFont_opsz_wght.ttf +0 -0
  15. package/assets/logos/fonts/JetBrainsMono-Bold.woff2 +0 -0
  16. package/assets/logos/fonts/JetBrainsMono-Medium.woff2 +0 -0
  17. package/assets/logos/fonts/JetBrainsMono-Regular.woff2 +0 -0
  18. package/assets/logos/fonts/JetBrainsMono-SemiBold.woff2 +0 -0
  19. package/assets/logos/honeycomb-memory-cluster.svg +17 -0
  20. package/assets/styles.css +11 -0
  21. package/assets/tokens/base.css +76 -0
  22. package/assets/tokens/colors.css +111 -0
  23. package/assets/tokens/fonts.css +32 -0
  24. package/assets/tokens/spacing.css +48 -0
  25. package/assets/tokens/typography.css +38 -0
  26. package/dist/daemon/dashboard/app.js +26 -25
  27. package/dist/daemon/dashboard/host.d.ts +7 -0
  28. package/dist/daemon/dashboard/host.js +16 -0
  29. package/dist/daemon/dashboard/host.js.map +1 -1
  30. package/dist/daemon/dashboard/web-assets.d.ts +2 -0
  31. package/dist/daemon/dashboard/web-assets.js +19 -0
  32. package/dist/daemon/dashboard/web-assets.js.map +1 -1
  33. package/dist/daemon/gate.d.ts +2 -2
  34. package/dist/daemon/gate.js +15 -4
  35. package/dist/daemon/gate.js.map +1 -1
  36. package/dist/daemon/installer/bin-resolver.d.ts +34 -0
  37. package/dist/daemon/installer/bin-resolver.js +107 -0
  38. package/dist/daemon/installer/bin-resolver.js.map +1 -0
  39. package/dist/daemon/installer/config.d.ts +63 -0
  40. package/dist/daemon/installer/config.js +74 -0
  41. package/dist/daemon/installer/config.js.map +1 -0
  42. package/dist/daemon/installer/detection.d.ts +20 -0
  43. package/dist/daemon/installer/detection.js +73 -0
  44. package/dist/daemon/installer/detection.js.map +1 -0
  45. package/dist/daemon/installer/funnel-telemetry.d.ts +54 -0
  46. package/dist/daemon/installer/funnel-telemetry.js +134 -0
  47. package/dist/daemon/installer/funnel-telemetry.js.map +1 -0
  48. package/dist/daemon/installer/index.d.ts +12 -0
  49. package/dist/daemon/installer/index.js +10 -0
  50. package/dist/daemon/installer/index.js.map +1 -0
  51. package/dist/daemon/installer/install-state.d.ts +56 -0
  52. package/dist/daemon/installer/install-state.js +159 -0
  53. package/dist/daemon/installer/install-state.js.map +1 -0
  54. package/dist/daemon/installer/manifest-snapshot.json +25 -0
  55. package/dist/daemon/installer/manifest.d.ts +47 -0
  56. package/dist/daemon/installer/manifest.js +103 -0
  57. package/dist/daemon/installer/manifest.js.map +1 -0
  58. package/dist/daemon/installer/products.d.ts +33 -0
  59. package/dist/daemon/installer/products.js +42 -0
  60. package/dist/daemon/installer/products.js.map +1 -0
  61. package/dist/daemon/installer/routes.d.ts +43 -0
  62. package/dist/daemon/installer/routes.js +201 -0
  63. package/dist/daemon/installer/routes.js.map +1 -0
  64. package/dist/daemon/installer/security.d.ts +33 -0
  65. package/dist/daemon/installer/security.js +80 -0
  66. package/dist/daemon/installer/security.js.map +1 -0
  67. package/dist/daemon/installer/spawn.d.ts +49 -0
  68. package/dist/daemon/installer/spawn.js +63 -0
  69. package/dist/daemon/installer/spawn.js.map +1 -0
  70. package/dist/daemon/installer/token.d.ts +23 -0
  71. package/dist/daemon/installer/token.js +56 -0
  72. package/dist/daemon/installer/token.js.map +1 -0
  73. package/dist/daemon/server.d.ts +6 -0
  74. package/dist/daemon/server.js +7 -0
  75. package/dist/daemon/server.js.map +1 -1
  76. package/dist/dashboard/web/app.js +42 -20
  77. package/dist/dashboard/web/app.js.map +1 -1
  78. package/dist/dashboard/web/boot-route.d.ts +11 -7
  79. package/dist/dashboard/web/boot-route.js +12 -6
  80. package/dist/dashboard/web/boot-route.js.map +1 -1
  81. package/dist/dashboard/web/main.js +2 -1
  82. package/dist/dashboard/web/main.js.map +1 -1
  83. package/dist/dashboard/web/onboarding/advanced-picker.d.ts +16 -0
  84. package/dist/dashboard/web/onboarding/advanced-picker.js +59 -0
  85. package/dist/dashboard/web/onboarding/advanced-picker.js.map +1 -0
  86. package/dist/dashboard/web/onboarding/contracts.d.ts +188 -0
  87. package/dist/dashboard/web/onboarding/contracts.js +161 -0
  88. package/dist/dashboard/web/onboarding/contracts.js.map +1 -0
  89. package/dist/dashboard/web/onboarding/health-view.d.ts +17 -0
  90. package/dist/dashboard/web/onboarding/health-view.js +79 -0
  91. package/dist/dashboard/web/onboarding/health-view.js.map +1 -0
  92. package/dist/dashboard/web/onboarding/install-card.d.ts +28 -0
  93. package/dist/dashboard/web/onboarding/install-card.js +171 -0
  94. package/dist/dashboard/web/onboarding/install-card.js.map +1 -0
  95. package/dist/dashboard/web/onboarding/login-step.d.ts +29 -0
  96. package/dist/dashboard/web/onboarding/login-step.js +104 -0
  97. package/dist/dashboard/web/onboarding/login-step.js.map +1 -0
  98. package/dist/dashboard/web/onboarding/onboarding-client.d.ts +52 -0
  99. package/dist/dashboard/web/onboarding/onboarding-client.js +133 -0
  100. package/dist/dashboard/web/onboarding/onboarding-client.js.map +1 -0
  101. package/dist/dashboard/web/onboarding/onboarding-hero.d.ts +24 -0
  102. package/dist/dashboard/web/onboarding/onboarding-hero.js +70 -0
  103. package/dist/dashboard/web/onboarding/onboarding-hero.js.map +1 -0
  104. package/dist/dashboard/web/onboarding/onboarding-screen.d.ts +43 -0
  105. package/dist/dashboard/web/onboarding/onboarding-screen.js +193 -0
  106. package/dist/dashboard/web/onboarding/onboarding-screen.js.map +1 -0
  107. package/dist/dashboard/web/onboarding/onboarding-selection-store.d.ts +20 -0
  108. package/dist/dashboard/web/onboarding/onboarding-selection-store.js +76 -0
  109. package/dist/dashboard/web/onboarding/onboarding-selection-store.js.map +1 -0
  110. package/dist/dashboard/web/onboarding/product-copy.d.ts +40 -0
  111. package/dist/dashboard/web/onboarding/product-copy.js +70 -0
  112. package/dist/dashboard/web/onboarding/product-copy.js.map +1 -0
  113. package/dist/dashboard/web/onboarding/use-install-dwell.d.ts +27 -0
  114. package/dist/dashboard/web/onboarding/use-install-dwell.js +42 -0
  115. package/dist/dashboard/web/onboarding/use-install-dwell.js.map +1 -0
  116. package/dist/dashboard/web/onboarding/use-onboarding-token.d.ts +14 -0
  117. package/dist/dashboard/web/onboarding/use-onboarding-token.js +41 -0
  118. package/dist/dashboard/web/onboarding/use-onboarding-token.js.map +1 -0
  119. package/dist/dashboard/web/route-daemon-owner.d.ts +7 -0
  120. package/dist/dashboard/web/route-daemon-owner.js +15 -0
  121. package/dist/dashboard/web/route-daemon-owner.js.map +1 -0
  122. package/dist/dashboard/web/wire.d.ts +1 -1
  123. package/dist/shared/onboarding-types.d.ts +79 -0
  124. package/dist/shared/onboarding-types.js +12 -0
  125. package/dist/shared/onboarding-types.js.map +1 -0
  126. package/dist/telemetry/emit.d.ts +30 -3
  127. package/dist/telemetry/emit.js +25 -2
  128. package/dist/telemetry/emit.js.map +1 -1
  129. package/dist/telemetry/onboarding-session-ledger.d.ts +31 -0
  130. package/dist/telemetry/onboarding-session-ledger.js +78 -0
  131. package/dist/telemetry/onboarding-session-ledger.js.map +1 -0
  132. package/package.json +6 -2
@@ -0,0 +1,33 @@
1
+ /**
2
+ * PRD-009a: the hard-coded product allowlist and per-product registration verbs (is-AC-3/13).
3
+ *
4
+ * This is the single source of truth for which slugs exist, their npm bin names, and the OWN
5
+ * registration verb each product runs post-install, exactly as honeycomb's `install.sh` does today
6
+ * (`doctor install-service`, `honeycomb install`, `nectar install`). The install endpoint only ever
7
+ * acts on a slug present in {@link INSTALLABLE_PRODUCTS}; `hive` is deliberately absent (it is the
8
+ * running daemon, never a portal install target).
9
+ */
10
+ import type { InstallableProduct, ProductSlug } from "../../shared/onboarding-types.js";
11
+ /** The four known product slugs (is-AC-3). The install allowlist is the installable subset below. */
12
+ export declare const PRODUCT_SLUGS: readonly ["honeycomb", "doctor", "hive", "nectar"];
13
+ /**
14
+ * The npm package name for each slug, used as LOCAL DETECTION evidence (the folder name under the
15
+ * global node_modules). This is deliberately independent of the manifest so detection works before
16
+ * any manifest is fetched (is-AC-1); the manifest remains the sole authority for INSTALL targets.
17
+ */
18
+ export declare const PRODUCT_PACKAGES: Record<ProductSlug, string>;
19
+ /** The three portal-installable products; `hive` is excluded (not installable, 400). */
20
+ export declare const INSTALLABLE_PRODUCTS: readonly ["doctor", "honeycomb", "nectar"];
21
+ /** Per-product static facts: the npm bin name and the argv of its own post-install registration verb. */
22
+ export interface ProductProfile {
23
+ /** The npm bin name (equals the slug for every fleet product). */
24
+ readonly binName: string;
25
+ /** The registration verb argv run after a successful npm install (is-AC-13). */
26
+ readonly registrationVerb: readonly string[];
27
+ }
28
+ /** True for one of the four known slugs (is-AC-3 allowlist membership). */
29
+ export declare function isProductSlug(value: string): value is ProductSlug;
30
+ /** True for a portal-installable product (`hive` is excluded). */
31
+ export declare function isInstallableProduct(value: string): value is InstallableProduct;
32
+ /** The static profile (bin name + registration verb) for an installable product. */
33
+ export declare function productProfile(product: InstallableProduct): ProductProfile;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * PRD-009a: the hard-coded product allowlist and per-product registration verbs (is-AC-3/13).
3
+ *
4
+ * This is the single source of truth for which slugs exist, their npm bin names, and the OWN
5
+ * registration verb each product runs post-install, exactly as honeycomb's `install.sh` does today
6
+ * (`doctor install-service`, `honeycomb install`, `nectar install`). The install endpoint only ever
7
+ * acts on a slug present in {@link INSTALLABLE_PRODUCTS}; `hive` is deliberately absent (it is the
8
+ * running daemon, never a portal install target).
9
+ */
10
+ /** The four known product slugs (is-AC-3). The install allowlist is the installable subset below. */
11
+ export const PRODUCT_SLUGS = ["honeycomb", "doctor", "hive", "nectar"];
12
+ /**
13
+ * The npm package name for each slug, used as LOCAL DETECTION evidence (the folder name under the
14
+ * global node_modules). This is deliberately independent of the manifest so detection works before
15
+ * any manifest is fetched (is-AC-1); the manifest remains the sole authority for INSTALL targets.
16
+ */
17
+ export const PRODUCT_PACKAGES = {
18
+ honeycomb: "@legioncodeinc/honeycomb",
19
+ doctor: "@legioncodeinc/doctor",
20
+ hive: "@legioncodeinc/hive",
21
+ nectar: "@legioncodeinc/nectar"
22
+ };
23
+ /** The three portal-installable products; `hive` is excluded (not installable, 400). */
24
+ export const INSTALLABLE_PRODUCTS = ["doctor", "honeycomb", "nectar"];
25
+ const PROFILES = {
26
+ doctor: { binName: "doctor", registrationVerb: ["install-service"] },
27
+ honeycomb: { binName: "honeycomb", registrationVerb: ["install"] },
28
+ nectar: { binName: "nectar", registrationVerb: ["install"] }
29
+ };
30
+ /** True for one of the four known slugs (is-AC-3 allowlist membership). */
31
+ export function isProductSlug(value) {
32
+ return PRODUCT_SLUGS.includes(value);
33
+ }
34
+ /** True for a portal-installable product (`hive` is excluded). */
35
+ export function isInstallableProduct(value) {
36
+ return INSTALLABLE_PRODUCTS.includes(value);
37
+ }
38
+ /** The static profile (bin name + registration verb) for an installable product. */
39
+ export function productProfile(product) {
40
+ return PROFILES[product];
41
+ }
42
+ //# sourceMappingURL=products.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"products.js","sourceRoot":"","sources":["../../../src/daemon/installer/products.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,qGAAqG;AACrG,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAC;AAEhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAgC;IAC3D,SAAS,EAAE,0BAA0B;IACrC,MAAM,EAAE,uBAAuB;IAC/B,IAAI,EAAE,qBAAqB;IAC3B,MAAM,EAAE,uBAAuB;CAChC,CAAC;AAEF,wFAAwF;AACxF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,CAAU,CAAC;AAU/E,MAAM,QAAQ,GAA+C;IAC3D,MAAM,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC,iBAAiB,CAAC,EAAE;IACpE,SAAS,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC,SAAS,CAAC,EAAE;IAClE,MAAM,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC,SAAS,CAAC,EAAE;CAC7D,CAAC;AAEF,2EAA2E;AAC3E,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAQ,aAAmC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC9D,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAQ,oBAA0C,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACrE,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,cAAc,CAAC,OAA2B;IACxD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * PRD-009a: the `/api/onboarding/*` installer surface and its service factory.
3
+ *
4
+ * These Hono routes are registered on hive's app BEFORE the generic `/api/*` BFF proxy (the same
5
+ * registration-order discipline as `/api/fleet-status` and `/api/telemetry/stream`), so hive itself
6
+ * answers them rather than proxying to a workload daemon. Every route runs the three-check guard
7
+ * (`security.ts`): Host, Origin, and the one-time token. Manifest resolution, detection, the install
8
+ * state machine, and the token store are all reached through the injectable config, so a test never
9
+ * hits the network, real npm, or the real filesystem.
10
+ */
11
+ import type { Hono } from "hono";
12
+ import type { EmitDeps } from "../../telemetry/emit.js";
13
+ import { type InstallStateStore } from "./install-state.js";
14
+ import { type InstallerConfig } from "./config.js";
15
+ import { type ManifestResolver } from "./manifest.js";
16
+ import { type TokenStore } from "./token.js";
17
+ import { type FunnelTelemetry } from "./funnel-telemetry.js";
18
+ import { type SetupAuthFetchImpl } from "../setup-auth.js";
19
+ import { type FetchImpl as FleetFetchImpl } from "../fleet-status.js";
20
+ /** Service options: the installer config seams plus the fleet-status inputs the health check reuses. */
21
+ export interface InstallerServiceOptions extends Partial<InstallerConfig> {
22
+ /** The fetch used by the health check's `fetchFleetStatus` (defaults to the global `fetch`). */
23
+ readonly fleetStatusFetch?: FleetFetchImpl;
24
+ /** Override doctor's status URL for the health check (defaults to the fixed loopback constant). */
25
+ readonly doctorStatusUrl?: string;
26
+ /** Fetch seam for `/setup/state` auth observation (`login_completed`, PRD-009c). */
27
+ readonly setupAuthFetch?: SetupAuthFetchImpl;
28
+ /** Injectable telemetry deps for funnel emission (tests record POST bodies). */
29
+ readonly funnelEmitDeps?: EmitDeps;
30
+ /** Override onboarding session ledger dir (tests). */
31
+ readonly funnelStateDir?: string;
32
+ }
33
+ /** The assembled installer service: a route registrar plus its state (exposed for tests). */
34
+ export interface InstallerService {
35
+ register(app: Hono): void;
36
+ readonly config: InstallerConfig;
37
+ readonly store: InstallStateStore;
38
+ readonly tokenStore: TokenStore;
39
+ readonly manifest: ManifestResolver;
40
+ readonly funnel: FunnelTelemetry;
41
+ }
42
+ /** Build the installer service with a memoized npm-prefix resolver over the injected seams. */
43
+ export declare function createInstallerService(options?: InstallerServiceOptions): InstallerService;
@@ -0,0 +1,201 @@
1
+ /**
2
+ * PRD-009a: the `/api/onboarding/*` installer surface and its service factory.
3
+ *
4
+ * These Hono routes are registered on hive's app BEFORE the generic `/api/*` BFF proxy (the same
5
+ * registration-order discipline as `/api/fleet-status` and `/api/telemetry/stream`), so hive itself
6
+ * answers them rather than proxying to a workload daemon. Every route runs the three-check guard
7
+ * (`security.ts`): Host, Origin, and the one-time token. Manifest resolution, detection, the install
8
+ * state machine, and the token store are all reached through the injectable config, so a test never
9
+ * hits the network, real npm, or the real filesystem.
10
+ */
11
+ import { z } from "zod";
12
+ import { DOCTOR_STATUS_URL } from "../../shared/constants.js";
13
+ import { createInstallStateStore } from "./install-state.js";
14
+ import { createInstallerConfig } from "./config.js";
15
+ import { createManifestResolver } from "./manifest.js";
16
+ import { createTokenStore } from "./token.js";
17
+ import { detectFleet, installedVersion } from "./detection.js";
18
+ import { isInstallableProduct } from "./products.js";
19
+ import { resolveNpmPrefixViaCli } from "./bin-resolver.js";
20
+ import { guardInstallerRequest } from "./security.js";
21
+ import { createFunnelTelemetry, OnboardingEventBodySchema } from "./funnel-telemetry.js";
22
+ import { fetchSetupAuthenticated } from "../setup-auth.js";
23
+ import { fetchFleetStatus, isFleetReady } from "../fleet-status.js";
24
+ const InstallBodySchema = z.object({ product: z.string() });
25
+ function jsonError(c, status, error) {
26
+ return c.json({ error }, status);
27
+ }
28
+ async function readJsonBody(c) {
29
+ try {
30
+ return await c.req.json();
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ /** Build the installer service with a memoized npm-prefix resolver over the injected seams. */
37
+ export function createInstallerService(options = {}) {
38
+ const base = createInstallerConfig(options);
39
+ // Memoize `npm prefix -g` so it runs at most once per daemon session (is-AC: prefix cached).
40
+ let prefixPromise = null;
41
+ const config = {
42
+ ...base,
43
+ resolveNpmPrefix: () => {
44
+ if (prefixPromise === null) {
45
+ prefixPromise = options.resolveNpmPrefix ? options.resolveNpmPrefix() : resolveNpmPrefixViaCli(config);
46
+ }
47
+ return prefixPromise;
48
+ }
49
+ };
50
+ const funnel = createFunnelTelemetry({
51
+ config,
52
+ emitDeps: options.funnelEmitDeps,
53
+ stateDir: options.funnelStateDir
54
+ });
55
+ const store = createInstallStateStore(config, {
56
+ onInstallStarted: (product) => funnel.recordProductInstallStarted(product),
57
+ onInstallCompleted: (product) => funnel.recordProductInstallCompleted(product),
58
+ onInstallFailed: (product, stage) => funnel.recordProductInstallFailed(product, stage)
59
+ });
60
+ const tokenStore = createTokenStore(config);
61
+ const manifest = createManifestResolver(config);
62
+ const fleetStatusFetch = options.fleetStatusFetch ?? fetch;
63
+ const setupAuthFetch = options.setupAuthFetch ?? fetch;
64
+ const doctorStatusUrl = options.doctorStatusUrl ?? DOCTOR_STATUS_URL;
65
+ const register = (app) => {
66
+ // 1) Detection (is-AC-1/2). Token required only while a session is active (is-AC-10 carve-out).
67
+ app.get("/api/onboarding/detect", async (c) => {
68
+ const rejection = guardInstallerRequest(c, tokenStore, "detect");
69
+ if (rejection !== null)
70
+ return rejection;
71
+ return c.json(await detectFleet(config, store));
72
+ });
73
+ // 2) Install start (is-AC-3/4/5/15/16). Server resolves the target; the request carries only a slug.
74
+ app.post("/api/onboarding/install", async (c) => {
75
+ const rejection = guardInstallerRequest(c, tokenStore, "always");
76
+ if (rejection !== null)
77
+ return rejection;
78
+ const body = await readJsonBody(c);
79
+ const parsed = InstallBodySchema.safeParse(body);
80
+ if (!parsed.success)
81
+ return jsonError(c, 400, "invalid_body");
82
+ // is-AC-3: only the four slugs; `hive` is not installable. Either way a 400 with no spawn.
83
+ const product = parsed.data.product;
84
+ if (!isInstallableProduct(product))
85
+ return jsonError(c, 400, "invalid_product");
86
+ // is-AC-4/5: resolve `packageName@version` server-side, refusing rather than falling to @latest.
87
+ const resolution = await manifest.resolve(product);
88
+ if (resolution.kind === "unpublished")
89
+ return jsonError(c, 409, "unpublished");
90
+ if (resolution.kind === "manifest_unresolved")
91
+ return jsonError(c, 409, "manifest_unresolved");
92
+ // is-AC-15: already installed at the pinned version -> short-circuit, never spawn npm.
93
+ const snapshot = store.detectState(product);
94
+ const detectedVersion = await installedVersion(config, product);
95
+ if (snapshot.status === "installed" || detectedVersion === resolution.version) {
96
+ return c.json({ product, state: "installed" }, 200);
97
+ }
98
+ // is-AC-16: begin (or attach to an in-flight install); either way the wire state is in_progress.
99
+ store.begin(product, {
100
+ packageName: resolution.packageName,
101
+ version: resolution.version,
102
+ target: resolution.target
103
+ });
104
+ return c.json({ product, state: "install_in_progress" }, 202);
105
+ });
106
+ // 3) SSE progress (is-AC-11/12/14). Mirrors the telemetry-proxy relay discipline: a streamed
107
+ // body, no buffering beyond the current event, tied to the request's abort signal.
108
+ app.get("/api/onboarding/install/:product/events", (c) => {
109
+ const rejection = guardInstallerRequest(c, tokenStore, "always");
110
+ if (rejection !== null)
111
+ return rejection;
112
+ const product = c.req.param("product");
113
+ if (!isInstallableProduct(product))
114
+ return jsonError(c, 400, "invalid_product");
115
+ const signal = c.req.raw.signal;
116
+ const stream = new ReadableStream({
117
+ start(controller) {
118
+ const encoder = new TextEncoder();
119
+ let closed = false;
120
+ const subscriber = {
121
+ send(event) {
122
+ if (closed)
123
+ return;
124
+ try {
125
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
126
+ }
127
+ catch {
128
+ closed = true;
129
+ }
130
+ },
131
+ close() {
132
+ if (closed)
133
+ return;
134
+ closed = true;
135
+ try {
136
+ controller.close();
137
+ }
138
+ catch {
139
+ // The stream may already be closed by a client disconnect; nothing to do.
140
+ }
141
+ }
142
+ };
143
+ const unsubscribe = store.subscribe(product, subscriber);
144
+ const onAbort = () => {
145
+ unsubscribe();
146
+ subscriber.close();
147
+ };
148
+ // is-AC-14: a client disconnect removes this subscriber but the install continues.
149
+ if (signal.aborted)
150
+ onAbort();
151
+ else
152
+ signal.addEventListener("abort", onAbort, { once: true });
153
+ }
154
+ });
155
+ return new Response(stream, {
156
+ status: 200,
157
+ headers: {
158
+ "content-type": "text/event-stream",
159
+ "cache-control": "no-store",
160
+ connection: "keep-alive"
161
+ }
162
+ });
163
+ });
164
+ // 4) Health check (is-AC-18): reuse the existing readiness projection, do not re-derive it.
165
+ app.get("/api/onboarding/health", async (c) => {
166
+ const rejection = guardInstallerRequest(c, tokenStore, "always");
167
+ if (rejection !== null)
168
+ return rejection;
169
+ const status = await fetchFleetStatus(fleetStatusFetch, doctorStatusUrl);
170
+ const ready = isFleetReady(status);
171
+ funnel.observeHealthReady(ready);
172
+ const authenticated = await fetchSetupAuthenticated(setupAuthFetch, { signal: c.req.raw.signal });
173
+ funnel.observeAuthenticated(authenticated);
174
+ return c.json({ ready, status });
175
+ });
176
+ // 5) Completion (is-AC-10): invalidate the token (delete the file + set the memory flag), 204.
177
+ app.post("/api/onboarding/complete", (c) => {
178
+ const rejection = guardInstallerRequest(c, tokenStore, "always");
179
+ if (rejection !== null)
180
+ return rejection;
181
+ tokenStore.invalidate();
182
+ return c.body(null, 204);
183
+ });
184
+ // 6) Funnel events (PRD-009c): validate token + closed UI event set, emit through chokepoint.
185
+ app.post("/api/onboarding/event", async (c) => {
186
+ const rejection = guardInstallerRequest(c, tokenStore, "always");
187
+ if (rejection !== null)
188
+ return rejection;
189
+ const body = await readJsonBody(c);
190
+ const parsed = OnboardingEventBodySchema.safeParse(body);
191
+ if (!parsed.success)
192
+ return jsonError(c, 400, "invalid_body");
193
+ funnel.recordUiEvent(parsed.data);
194
+ const authenticated = await fetchSetupAuthenticated(setupAuthFetch, { signal: c.req.raw.signal });
195
+ funnel.observeAuthenticated(authenticated);
196
+ return c.body(null, 202);
197
+ });
198
+ };
199
+ return { register, config, store, tokenStore, manifest, funnel };
200
+ }
201
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../../src/daemon/installer/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * PRD-009a: the three non-negotiable installer mitigations (is-AC-7/8/9/10).
3
+ *
4
+ * A loopback endpoint that shells out to `npm install` is a drive-by target: any page the operator
5
+ * visits can `fetch` or form-POST at `127.0.0.1:3853`. Every installer request therefore passes:
6
+ * - HOST validation (is-AC-8, DNS-rebinding defense): a rebound hostname that resolves to
7
+ * 127.0.0.1 still fails because its `Host` header is not the portal's own host.
8
+ * - ORIGIN validation (is-AC-7): a foreign Origin is rejected 403; a missing Origin on a
9
+ * state-changing (non-GET) request is rejected too.
10
+ * - TOKEN validation (is-AC-9): the one-time bootstrap token, constant-time compared. State-
11
+ * changing endpoints always require it; read-only detection requires it only while a session is
12
+ * active, staying available token-free after completion for the re-entry short-circuit (is-AC-10).
13
+ */
14
+ import type { Context } from "hono";
15
+ import type { TokenStore } from "./token.js";
16
+ /** The request header carrying the onboarding token (the SSE path uses the `t` query param instead). */
17
+ export declare const TOKEN_HEADER: "x-onboarding-token";
18
+ /** Whether detection-style read endpoints require the token unconditionally or only while active. */
19
+ export type TokenMode = "always" | "detect";
20
+ /** True iff the `Host` header is the portal's own host (is-AC-8). */
21
+ export declare function hostAllowed(host: string | undefined): boolean;
22
+ /**
23
+ * Origin policy (is-AC-7): a present Origin must be the portal's own; a missing Origin is allowed
24
+ * only on a safe (GET) request and rejected on any state-changing method.
25
+ */
26
+ export declare function originAllowed(method: string, origin: string | undefined): boolean;
27
+ /** Read the presented token from the header, falling back to the `t` query param only for EventSource. */
28
+ export declare function extractToken(c: Context): string | null;
29
+ /**
30
+ * Run the full guard for an installer request. Returns a rejection {@link Response} to short-circuit
31
+ * the handler, or `null` when the request passed all three checks. Never logs or echoes the token.
32
+ */
33
+ export declare function guardInstallerRequest(c: Context, tokenStore: TokenStore, tokenMode: TokenMode): Response | null;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * PRD-009a: the three non-negotiable installer mitigations (is-AC-7/8/9/10).
3
+ *
4
+ * A loopback endpoint that shells out to `npm install` is a drive-by target: any page the operator
5
+ * visits can `fetch` or form-POST at `127.0.0.1:3853`. Every installer request therefore passes:
6
+ * - HOST validation (is-AC-8, DNS-rebinding defense): a rebound hostname that resolves to
7
+ * 127.0.0.1 still fails because its `Host` header is not the portal's own host.
8
+ * - ORIGIN validation (is-AC-7): a foreign Origin is rejected 403; a missing Origin on a
9
+ * state-changing (non-GET) request is rejected too.
10
+ * - TOKEN validation (is-AC-9): the one-time bootstrap token, constant-time compared. State-
11
+ * changing endpoints always require it; read-only detection requires it only while a session is
12
+ * active, staying available token-free after completion for the re-entry short-circuit (is-AC-10).
13
+ */
14
+ import { HIVE_HOST, HIVE_PORT } from "../../shared/constants.js";
15
+ /** The portal's own hosts (is-AC-8). Only these `Host` header values are accepted. */
16
+ const ALLOWED_HOSTS = new Set([`${HIVE_HOST}:${HIVE_PORT}`, `localhost:${HIVE_PORT}`]);
17
+ /** The portal's own origins (is-AC-7). Only these `Origin` header values are accepted. */
18
+ const ALLOWED_ORIGINS = new Set([`http://${HIVE_HOST}:${HIVE_PORT}`, `http://localhost:${HIVE_PORT}`]);
19
+ /** The request header carrying the onboarding token (the SSE path uses the `t` query param instead). */
20
+ export const TOKEN_HEADER = "x-onboarding-token";
21
+ /** True iff the `Host` header is the portal's own host (is-AC-8). */
22
+ export function hostAllowed(host) {
23
+ return host !== undefined && ALLOWED_HOSTS.has(host);
24
+ }
25
+ /**
26
+ * Origin policy (is-AC-7): a present Origin must be the portal's own; a missing Origin is allowed
27
+ * only on a safe (GET) request and rejected on any state-changing method.
28
+ */
29
+ export function originAllowed(method, origin) {
30
+ if (origin === undefined)
31
+ return method === "GET" || method === "HEAD";
32
+ return ALLOWED_ORIGINS.has(origin);
33
+ }
34
+ function forbidden() {
35
+ return new Response(JSON.stringify({ error: "forbidden" }), {
36
+ status: 403,
37
+ headers: { "content-type": "application/json" }
38
+ });
39
+ }
40
+ function unauthorized() {
41
+ return new Response(JSON.stringify({ error: "unauthorized" }), {
42
+ status: 401,
43
+ headers: { "content-type": "application/json" }
44
+ });
45
+ }
46
+ function allowsQueryToken(c) {
47
+ const path = new URL(c.req.url).pathname;
48
+ return c.req.method === "GET" && /^\/api\/onboarding\/install\/[^/]+\/events$/.test(path);
49
+ }
50
+ /** Read the presented token from the header, falling back to the `t` query param only for EventSource. */
51
+ export function extractToken(c) {
52
+ const header = c.req.header(TOKEN_HEADER);
53
+ if (header !== undefined && header.length > 0)
54
+ return header;
55
+ if (!allowsQueryToken(c))
56
+ return null;
57
+ const query = c.req.query("t");
58
+ return query !== undefined && query.length > 0 ? query : null;
59
+ }
60
+ /**
61
+ * Run the full guard for an installer request. Returns a rejection {@link Response} to short-circuit
62
+ * the handler, or `null` when the request passed all three checks. Never logs or echoes the token.
63
+ */
64
+ export function guardInstallerRequest(c, tokenStore, tokenMode) {
65
+ if (!hostAllowed(c.req.header("host")))
66
+ return forbidden();
67
+ if (!originAllowed(c.req.method, c.req.header("origin")))
68
+ return forbidden();
69
+ const provided = extractToken(c);
70
+ if (tokenMode === "always") {
71
+ if (!tokenStore.requireValid(provided))
72
+ return unauthorized();
73
+ return null;
74
+ }
75
+ // "detect": token required only while an onboarding session is active (is-AC-10 carve-out).
76
+ if (tokenStore.isActive() && !tokenStore.requireValid(provided))
77
+ return unauthorized();
78
+ return null;
79
+ }
80
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.js","sourceRoot":"","sources":["../../../src/daemon/installer/security.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * PRD-009a: the argv-safe child-process seam (is-AC-6).
3
+ *
4
+ * Every npm invocation and every product registration verb runs through {@link SpawnFn}, whose
5
+ * signature is structurally injection-proof: the command and its arguments are ALWAYS an argv
6
+ * array, so there is no code path that can concatenate request-derived data into a shell string.
7
+ * The default node implementation ({@link createNodeSpawn}) passes `shell: false` explicitly.
8
+ *
9
+ * Windows footgun this design side-steps: `npm.cmd` cannot be spawned with `shell:false` on
10
+ * Node >= 20 (EINVAL). Callers therefore never spawn a `.cmd`; they spawn `process.execPath`
11
+ * with a resolved `*.js` entry as the first argv element (see `bin-resolver.ts`), so no `.cmd`
12
+ * and no shell is ever involved.
13
+ */
14
+ import type { ChildProcess } from "node:child_process";
15
+ /** The bounded tail we keep of each stream (is-AC-17: a bounded stderr excerpt, ~2 KB). */
16
+ export declare const SPAWN_TAIL_LIMIT = 2048;
17
+ /** The terminal outcome of a spawned process: its exit code plus bounded stdout/stderr tails. */
18
+ export interface SpawnOutcome {
19
+ /** The process exit code, or `null` when it was terminated by a signal without a code. */
20
+ readonly code: number | null;
21
+ readonly stdoutTail: string;
22
+ readonly stderrTail: string;
23
+ }
24
+ /** Optional streaming hooks + an abort signal for a single spawn. */
25
+ export interface SpawnInvocationOptions {
26
+ readonly signal?: AbortSignal;
27
+ /** Called for each stdout chunk (used to derive observable stage milestones, is-AC-12). */
28
+ readonly onStdout?: (chunk: string) => void;
29
+ /** Called for each stderr chunk. */
30
+ readonly onStderr?: (chunk: string) => void;
31
+ }
32
+ /**
33
+ * The injectable spawn surface. `command` + `args` are an argv array by construction; a shell
34
+ * string is not expressible. Resolves with the terminal {@link SpawnOutcome}; rejects only on a
35
+ * spawn-level error (ENOENT, EINVAL), which callers translate into a `failed` stage.
36
+ */
37
+ export type SpawnFn = (command: string, args: readonly string[], options?: SpawnInvocationOptions) => Promise<SpawnOutcome>;
38
+ /** The low-level node spawn seam, injected by tests to assert `shell:false` + the argv array. */
39
+ export type RawSpawn = (command: string, args: readonly string[], options: {
40
+ readonly shell: false;
41
+ readonly signal?: AbortSignal;
42
+ readonly env?: NodeJS.ProcessEnv;
43
+ }) => ChildProcess;
44
+ /**
45
+ * Build the default {@link SpawnFn} over node's `child_process.spawn`, pinned to `shell: false`.
46
+ * A test injects `rawSpawn` to assert the argv array and the disabled shell without touching a
47
+ * real process.
48
+ */
49
+ export declare function createNodeSpawn(rawSpawn?: RawSpawn): SpawnFn;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * PRD-009a: the argv-safe child-process seam (is-AC-6).
3
+ *
4
+ * Every npm invocation and every product registration verb runs through {@link SpawnFn}, whose
5
+ * signature is structurally injection-proof: the command and its arguments are ALWAYS an argv
6
+ * array, so there is no code path that can concatenate request-derived data into a shell string.
7
+ * The default node implementation ({@link createNodeSpawn}) passes `shell: false` explicitly.
8
+ *
9
+ * Windows footgun this design side-steps: `npm.cmd` cannot be spawned with `shell:false` on
10
+ * Node >= 20 (EINVAL). Callers therefore never spawn a `.cmd`; they spawn `process.execPath`
11
+ * with a resolved `*.js` entry as the first argv element (see `bin-resolver.ts`), so no `.cmd`
12
+ * and no shell is ever involved.
13
+ */
14
+ import { spawn as nodeSpawn } from "node:child_process";
15
+ import { delimiter, dirname } from "node:path";
16
+ /** The bounded tail we keep of each stream (is-AC-17: a bounded stderr excerpt, ~2 KB). */
17
+ export const SPAWN_TAIL_LIMIT = 2048;
18
+ /**
19
+ * The child env: the daemon's env with the SPAWNED BINARY's directory prepended to PATH. Under a
20
+ * service manager (launchd/systemd) the daemon inherits a minimal PATH without node's bin dir,
21
+ * and npm >= 9 no longer prepends node's directory when running lifecycle scripts — so a package
22
+ * postinstall invoking plain `node` dies with `sh: node: command not found` (exit 127). The
23
+ * command here is always `process.execPath`, so prepending its dir puts the RIGHT node first.
24
+ */
25
+ function childEnv(command) {
26
+ const basePath = process.env.PATH ?? "";
27
+ return { ...process.env, PATH: `${dirname(command)}${delimiter}${basePath}` };
28
+ }
29
+ /** Keep only the last `limit` characters of `current + chunk` so a chatty child cannot grow memory. */
30
+ function appendTail(current, chunk, limit) {
31
+ const next = current + chunk;
32
+ return next.length > limit ? next.slice(next.length - limit) : next;
33
+ }
34
+ /**
35
+ * Build the default {@link SpawnFn} over node's `child_process.spawn`, pinned to `shell: false`.
36
+ * A test injects `rawSpawn` to assert the argv array and the disabled shell without touching a
37
+ * real process.
38
+ */
39
+ export function createNodeSpawn(rawSpawn = nodeSpawn) {
40
+ return (command, args, options = {}) => new Promise((resolve, reject) => {
41
+ let stdoutTail = "";
42
+ let stderrTail = "";
43
+ // The whole point of is-AC-6: an argv array and `shell: false`, never a shell string.
44
+ const child = rawSpawn(command, [...args], { shell: false, signal: options.signal, env: childEnv(command) });
45
+ child.stdout?.on("data", (data) => {
46
+ const chunk = String(data);
47
+ stdoutTail = appendTail(stdoutTail, chunk, SPAWN_TAIL_LIMIT);
48
+ options.onStdout?.(chunk);
49
+ });
50
+ child.stderr?.on("data", (data) => {
51
+ const chunk = String(data);
52
+ stderrTail = appendTail(stderrTail, chunk, SPAWN_TAIL_LIMIT);
53
+ options.onStderr?.(chunk);
54
+ });
55
+ child.on("error", (error) => {
56
+ reject(error);
57
+ });
58
+ child.on("close", (code) => {
59
+ resolve({ code, stdoutTail, stderrTail });
60
+ });
61
+ });
62
+ }
63
+ //# sourceMappingURL=spawn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawn.js","sourceRoot":"","sources":["../../../src/daemon/installer/spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE/C,2FAA2F;AAC3F,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAqCrC;;;;;;GAMG;AACH,SAAS,QAAQ,CAAC,OAAe;IAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;IACxC,OAAO,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,GAAG,QAAQ,EAAE,EAAE,CAAC;AAChF,CAAC;AAED,uGAAuG;AACvG,SAAS,UAAU,CAAC,OAAe,EAAE,KAAa,EAAE,KAAa;IAC/D,MAAM,IAAI,GAAG,OAAO,GAAG,KAAK,CAAC;IAC7B,OAAO,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,WAAqB,SAAgC;IACnF,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,EAAE,EAAE,EAAE,CACrC,IAAI,OAAO,CAAe,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC5C,IAAI,UAAU,GAAG,EAAE,CAAC;QACpB,IAAI,UAAU,GAAG,EAAE,CAAC;QAEpB,sFAAsF;QACtF,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAE7G,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAa,EAAE,EAAE;YACzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;YAC3B,UAAU,GAAG,UAAU,CAAC,UAAU,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC;YAC7D,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,IAAa,EAAE,EAAE;YACzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;YAC3B,UAAU,GAAG,UAAU,CAAC,UAAU,EAAE,KAAK,EAAE,gBAAgB,CAAC,CAAC;YAC7D,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC;QAC5B,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAY,EAAE,EAAE;YACjC,MAAM,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAmB,EAAE,EAAE;YACxC,OAAO,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * PRD-009a: the daemon side of the one-time onboarding token contract (is-AC-9/10).
3
+ *
4
+ * The bootstrap mints the token and writes it to `~/.honeycomb/hive/onboarding-token` at mode
5
+ * 0600 BEFORE the daemon starts (PRD-009d). The daemon reads it LAZILY at request time, not at
6
+ * startup, because on re-entry the daemon may already be running when a fresh bootstrap writes a
7
+ * new token. Comparison is constant time (`crypto.timingSafeEqual`). Completion invalidates the
8
+ * token: the file is deleted and an in-memory flag is set, so state-changing endpoints refuse all
9
+ * requests until a fresh bootstrap mints a new one (is-AC-10). The token is never logged, never
10
+ * returned, and never placed in an error body.
11
+ */
12
+ import type { InstallerConfig } from "./config.js";
13
+ /** The token gate: whether onboarding is active, whether a presented token is valid, and invalidation. */
14
+ export interface TokenStore {
15
+ /** True while a token file exists and has not been invalidated (an active onboarding session). */
16
+ isActive(): boolean;
17
+ /** True iff `provided` matches the on-disk token in constant time and the token is not invalidated. */
18
+ requireValid(provided: string | null | undefined): boolean;
19
+ /** Invalidate the token: delete the file and set the in-memory flag (idempotent). */
20
+ invalidate(): void;
21
+ }
22
+ /** Build the token store over the injected config seams. */
23
+ export declare function createTokenStore(config: InstallerConfig): TokenStore;