@celestea/studio 2.7.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 (240) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app.d.ts +111 -0
  3. package/dist/app.js +219 -0
  4. package/dist/auth/api-token.d.ts +76 -0
  5. package/dist/auth/api-token.js +172 -0
  6. package/dist/auth/htpasswd.d.ts +22 -0
  7. package/dist/auth/htpasswd.js +61 -0
  8. package/dist/auth/index.d.ts +14 -0
  9. package/dist/auth/index.js +14 -0
  10. package/dist/auth/page.d.ts +19 -0
  11. package/dist/auth/page.js +73 -0
  12. package/dist/auth/rate-limit.d.ts +27 -0
  13. package/dist/auth/rate-limit.js +47 -0
  14. package/dist/auth/token.d.ts +49 -0
  15. package/dist/auth/token.js +112 -0
  16. package/dist/config.d.ts +78 -0
  17. package/dist/config.js +72 -0
  18. package/dist/deployment.d.ts +92 -0
  19. package/dist/deployment.js +154 -0
  20. package/dist/fake-runtime-adapter.d.ts +37 -0
  21. package/dist/fake-runtime-adapter.js +276 -0
  22. package/dist/handlers/auth.d.ts +24 -0
  23. package/dist/handlers/auth.js +155 -0
  24. package/dist/handlers/common.d.ts +76 -0
  25. package/dist/handlers/common.js +127 -0
  26. package/dist/handlers/config-shape.d.ts +76 -0
  27. package/dist/handlers/config-shape.js +227 -0
  28. package/dist/handlers/config.d.ts +26 -0
  29. package/dist/handlers/config.js +161 -0
  30. package/dist/handlers/context-shape.d.ts +33 -0
  31. package/dist/handlers/context-shape.js +59 -0
  32. package/dist/handlers/dialog.d.ts +22 -0
  33. package/dist/handlers/dialog.js +292 -0
  34. package/dist/handlers/exec.d.ts +22 -0
  35. package/dist/handlers/exec.js +131 -0
  36. package/dist/handlers/fs-read.d.ts +63 -0
  37. package/dist/handlers/fs-read.js +159 -0
  38. package/dist/handlers/fs.d.ts +57 -0
  39. package/dist/handlers/fs.js +153 -0
  40. package/dist/handlers/grants-shape.d.ts +45 -0
  41. package/dist/handlers/grants-shape.js +130 -0
  42. package/dist/handlers/grants.d.ts +21 -0
  43. package/dist/handlers/grants.js +267 -0
  44. package/dist/handlers/health.d.ts +37 -0
  45. package/dist/handlers/health.js +139 -0
  46. package/dist/handlers/index.d.ts +40 -0
  47. package/dist/handlers/index.js +85 -0
  48. package/dist/handlers/permissions.d.ts +9 -0
  49. package/dist/handlers/permissions.js +183 -0
  50. package/dist/handlers/plugins.d.ts +18 -0
  51. package/dist/handlers/plugins.js +23 -0
  52. package/dist/handlers/prompts.d.ts +12 -0
  53. package/dist/handlers/prompts.js +120 -0
  54. package/dist/handlers/providers.d.ts +12 -0
  55. package/dist/handlers/providers.js +178 -0
  56. package/dist/handlers/questions.d.ts +23 -0
  57. package/dist/handlers/questions.js +115 -0
  58. package/dist/handlers/session-model.d.ts +38 -0
  59. package/dist/handlers/session-model.js +105 -0
  60. package/dist/handlers/session-move.d.ts +22 -0
  61. package/dist/handlers/session-move.js +188 -0
  62. package/dist/handlers/session-tools.d.ts +19 -0
  63. package/dist/handlers/session-tools.js +75 -0
  64. package/dist/handlers/sessions.d.ts +32 -0
  65. package/dist/handlers/sessions.js +242 -0
  66. package/dist/handlers/usage.d.ts +26 -0
  67. package/dist/handlers/usage.js +77 -0
  68. package/dist/handlers/worker.d.ts +12 -0
  69. package/dist/handlers/worker.js +82 -0
  70. package/dist/handlers/workspaces.d.ts +12 -0
  71. package/dist/handlers/workspaces.js +88 -0
  72. package/dist/harness.test-util.d.ts +101 -0
  73. package/dist/harness.test-util.js +176 -0
  74. package/dist/index.d.ts +33 -0
  75. package/dist/index.js +33 -0
  76. package/dist/main.d.ts +13 -0
  77. package/dist/main.js +35 -0
  78. package/dist/plugins.d.ts +105 -0
  79. package/dist/plugins.js +97 -0
  80. package/dist/question-registry.d.ts +124 -0
  81. package/dist/question-registry.js +212 -0
  82. package/dist/question-rows.d.ts +32 -0
  83. package/dist/question-rows.js +43 -0
  84. package/dist/replay/compare.d.ts +47 -0
  85. package/dist/replay/compare.js +89 -0
  86. package/dist/replay/e2e-replay.d.ts +46 -0
  87. package/dist/replay/e2e-replay.js +91 -0
  88. package/dist/replay/expect-compact.d.ts +31 -0
  89. package/dist/replay/expect-compact.js +92 -0
  90. package/dist/replay/fixtures.d.ts +52 -0
  91. package/dist/replay/fixtures.js +53 -0
  92. package/dist/replay/host.d.ts +32 -0
  93. package/dist/replay/host.js +62 -0
  94. package/dist/replay/index.d.ts +16 -0
  95. package/dist/replay/index.js +16 -0
  96. package/dist/replay/probes.d.ts +55 -0
  97. package/dist/replay/probes.js +225 -0
  98. package/dist/replay/report.d.ts +13 -0
  99. package/dist/replay/report.js +103 -0
  100. package/dist/replay/session-e2e.d.ts +44 -0
  101. package/dist/replay/session-e2e.js +91 -0
  102. package/dist/routes.d.ts +47 -0
  103. package/dist/routes.js +59 -0
  104. package/dist/runtime/attachments-llm.d.ts +13 -0
  105. package/dist/runtime/attachments-llm.js +37 -0
  106. package/dist/runtime/boot-recovery.d.ts +32 -0
  107. package/dist/runtime/boot-recovery.js +71 -0
  108. package/dist/runtime/context-snapshot.d.ts +59 -0
  109. package/dist/runtime/context-snapshot.js +113 -0
  110. package/dist/runtime/engine-grants.d.ts +121 -0
  111. package/dist/runtime/engine-grants.js +338 -0
  112. package/dist/runtime/engine-permissions.d.ts +29 -0
  113. package/dist/runtime/engine-permissions.js +92 -0
  114. package/dist/runtime/engine-plugins.d.ts +189 -0
  115. package/dist/runtime/engine-plugins.js +292 -0
  116. package/dist/runtime/engine-profile.d.ts +28 -0
  117. package/dist/runtime/engine-profile.js +88 -0
  118. package/dist/runtime/engine-session.d.ts +9 -0
  119. package/dist/runtime/engine-session.js +9 -0
  120. package/dist/runtime/fallback-contract.d.ts +32 -0
  121. package/dist/runtime/fallback-contract.js +10 -0
  122. package/dist/runtime/fallback-host.d.ts +134 -0
  123. package/dist/runtime/fallback-host.js +299 -0
  124. package/dist/runtime/host-autowake.d.ts +79 -0
  125. package/dist/runtime/host-autowake.js +86 -0
  126. package/dist/runtime/image-downgrade.d.ts +49 -0
  127. package/dist/runtime/image-downgrade.js +90 -0
  128. package/dist/runtime/inbox-message.d.ts +11 -0
  129. package/dist/runtime/inbox-message.js +20 -0
  130. package/dist/runtime/index.d.ts +17 -0
  131. package/dist/runtime/index.js +17 -0
  132. package/dist/runtime/ledger-view.d.ts +40 -0
  133. package/dist/runtime/ledger-view.js +56 -0
  134. package/dist/runtime/llm-assembly.d.ts +66 -0
  135. package/dist/runtime/llm-assembly.js +114 -0
  136. package/dist/runtime/offline-llm.d.ts +71 -0
  137. package/dist/runtime/offline-llm.js +132 -0
  138. package/dist/runtime/provider-target.d.ts +10 -0
  139. package/dist/runtime/provider-target.js +10 -0
  140. package/dist/runtime/question-host.d.ts +33 -0
  141. package/dist/runtime/question-host.js +41 -0
  142. package/dist/runtime/question-view.d.ts +48 -0
  143. package/dist/runtime/question-view.js +76 -0
  144. package/dist/runtime/real-runtime-adapter.d.ts +95 -0
  145. package/dist/runtime/real-runtime-adapter.js +553 -0
  146. package/dist/runtime/recovery-audit.d.ts +81 -0
  147. package/dist/runtime/recovery-audit.js +95 -0
  148. package/dist/runtime/recovery-view.d.ts +36 -0
  149. package/dist/runtime/recovery-view.js +63 -0
  150. package/dist/runtime/session-compose.d.ts +235 -0
  151. package/dist/runtime/session-compose.js +350 -0
  152. package/dist/runtime/session-grants.d.ts +36 -0
  153. package/dist/runtime/session-grants.js +69 -0
  154. package/dist/runtime/session-lifecycle.d.ts +40 -0
  155. package/dist/runtime/session-lifecycle.js +86 -0
  156. package/dist/runtime/session-publisher.d.ts +40 -0
  157. package/dist/runtime/session-publisher.js +48 -0
  158. package/dist/runtime/session-release.d.ts +53 -0
  159. package/dist/runtime/session-release.js +80 -0
  160. package/dist/runtime/test-util.d.ts +78 -0
  161. package/dist/runtime/test-util.js +162 -0
  162. package/dist/runtime/watchdog-view.d.ts +33 -0
  163. package/dist/runtime/watchdog-view.js +45 -0
  164. package/dist/runtime/worker-bridge.d.ts +76 -0
  165. package/dist/runtime/worker-bridge.js +156 -0
  166. package/dist/runtime/worker-live.d.ts +22 -0
  167. package/dist/runtime/worker-live.js +25 -0
  168. package/dist/runtime/worker-recovery.d.ts +35 -0
  169. package/dist/runtime/worker-recovery.js +64 -0
  170. package/dist/runtime/worker-table.d.ts +58 -0
  171. package/dist/runtime/worker-table.js +70 -0
  172. package/dist/runtime-adapter.d.ts +421 -0
  173. package/dist/runtime-adapter.js +76 -0
  174. package/dist/serial-queue.d.ts +16 -0
  175. package/dist/serial-queue.js +22 -0
  176. package/dist/server.d.ts +44 -0
  177. package/dist/server.js +119 -0
  178. package/dist/settings.d.ts +23 -0
  179. package/dist/settings.js +31 -0
  180. package/dist/sse.d.ts +67 -0
  181. package/dist/sse.js +186 -0
  182. package/dist/static.d.ts +20 -0
  183. package/dist/static.js +120 -0
  184. package/dist/store/builtin-sections.d.ts +52 -0
  185. package/dist/store/builtin-sections.js +118 -0
  186. package/dist/store/celestea-home.d.ts +12 -0
  187. package/dist/store/celestea-home.js +11 -0
  188. package/dist/store/fs-json.d.ts +51 -0
  189. package/dist/store/fs-json.js +119 -0
  190. package/dist/store/grants-audit.d.ts +84 -0
  191. package/dist/store/grants-audit.js +109 -0
  192. package/dist/store/grants-service.d.ts +44 -0
  193. package/dist/store/grants-service.js +41 -0
  194. package/dist/store/grants-tokens.d.ts +62 -0
  195. package/dist/store/grants-tokens.js +105 -0
  196. package/dist/store/grants.d.ts +113 -0
  197. package/dist/store/grants.js +238 -0
  198. package/dist/store/index.d.ts +36 -0
  199. package/dist/store/index.js +36 -0
  200. package/dist/store/mode.d.ts +35 -0
  201. package/dist/store/mode.js +41 -0
  202. package/dist/store/permissions.d.ts +61 -0
  203. package/dist/store/permissions.js +139 -0
  204. package/dist/store/prompts-compose.d.ts +56 -0
  205. package/dist/store/prompts-compose.js +62 -0
  206. package/dist/store/prompts-template.d.ts +26 -0
  207. package/dist/store/prompts-template.js +99 -0
  208. package/dist/store/prompts.d.ts +142 -0
  209. package/dist/store/prompts.js +273 -0
  210. package/dist/store/provider-probe.d.ts +81 -0
  211. package/dist/store/provider-probe.js +154 -0
  212. package/dist/store/providers.d.ts +97 -0
  213. package/dist/store/providers.js +219 -0
  214. package/dist/store/result.d.ts +29 -0
  215. package/dist/store/result.js +30 -0
  216. package/dist/store/session-id.d.ts +119 -0
  217. package/dist/store/session-id.js +220 -0
  218. package/dist/store/session-meta.d.ts +41 -0
  219. package/dist/store/session-meta.js +66 -0
  220. package/dist/store/session-ops.d.ts +109 -0
  221. package/dist/store/session-ops.js +264 -0
  222. package/dist/store/session-tools.d.ts +42 -0
  223. package/dist/store/session-tools.js +69 -0
  224. package/dist/store/sessions.d.ts +162 -0
  225. package/dist/store/sessions.js +307 -0
  226. package/dist/store/validate.d.ts +13 -0
  227. package/dist/store/validate.js +38 -0
  228. package/dist/store/workspaces.d.ts +67 -0
  229. package/dist/store/workspaces.js +228 -0
  230. package/dist/user-questions.d.ts +58 -0
  231. package/dist/user-questions.js +157 -0
  232. package/dist/version.d.ts +14 -0
  233. package/dist/version.js +52 -0
  234. package/package.json +38 -0
  235. package/webdist/assets/index-Bngs7gUm.js +98 -0
  236. package/webdist/assets/index-Bu6ci_rN.css +1 -0
  237. package/webdist/assets/katex-CCbS2qAY.js +257 -0
  238. package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
  239. package/webdist/build-meta.json +7 -0
  240. package/webdist/index.html +261 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mcd0LUO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/app.d.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Hono application factory — P4.
3
+ *
4
+ * Wiring order is contract order:
5
+ * 1. compose the studio context (store plugins + bus + injected runtime);
6
+ * 2. register all 39 contract endpoints and assert full coverage;
7
+ * 3. `/api/*` fallback = 404 JSON (an unknown API path must NEVER fall
8
+ * through to the static/SPA handler);
9
+ * 4. static files + SPA fallback from the read-only Vite build.
10
+ *
11
+ * The engine is injected: `opts.runtime` is a `RuntimeAdapter` (or a factory
12
+ * over the composed stores, which is what the REAL adapter needs to resolve
13
+ * session directories AND the provider target). With nothing injected the app
14
+ * mounts the real runtime (`runtime/`) wired to the real LLM, so the default
15
+ * deployment is the engine over a live provider, not a fake; the P4 fake stays
16
+ * available to tests through `harness.test-util.ts`.
17
+ *
18
+ * W743 (W732 A1): the engine assembly itself lives in `createStudioEngine()`
19
+ * below — the ONE factory — and `defaultRuntime()` is just the production
20
+ * binding of its injected values. The real-engine test harness
21
+ * (`runtime/test-util.ts`) calls the same function, so "the tests run the real
22
+ * engine" now also means "the tests run the real grants boundary and the real
23
+ * usage ledger".
24
+ */
25
+ import { Hono } from "hono";
26
+ import type { Llm } from "@celestea/core";
27
+ import { type Profile } from "@celestea/runtime";
28
+ import { type RegisteredRoute } from "./routes.js";
29
+ import { type StudioConfig } from "./config.js";
30
+ import { type EngineFactory, type StudioServices } from "./plugins.js";
31
+ import type { EngineProfile, RuntimeAdapter } from "./runtime-adapter.js";
32
+ import type { StoreServices } from "./plugins.js";
33
+ import type { DisclosureOptions } from "./runtime/engine-plugins.js";
34
+ export interface StudioAppOptions {
35
+ cwd?: string;
36
+ env?: NodeJS.ProcessEnv;
37
+ config?: StudioConfig;
38
+ /** Engine seam (or a factory over the stores); defaults to the REAL runtime. */
39
+ runtime?: RuntimeAdapter | EngineFactory;
40
+ /** Deterministic clock for session dir suffixes / trash stamps. */
41
+ now?: () => number;
42
+ }
43
+ export interface StudioApp {
44
+ app: Hono;
45
+ routes: RegisteredRoute[];
46
+ services: StudioServices;
47
+ /** Contract ids bound by handlers (39 on success). */
48
+ endpointIds: string[];
49
+ }
50
+ /**
51
+ * Everything ONE engine build needs, resolved from the composed stores.
52
+ *
53
+ * W743 (closes W732 A1): this is the SINGLE engine assembly. The production app
54
+ * and the real-engine test harness both call it, so the two can no longer drift
55
+ * apart — the only thing a caller injects is PATHS, ENV and the LLM/profile
56
+ * seam; grants, the usage ledger, the worker receipt dir, session resolution and
57
+ * the three per-session profile hooks are assembled here exactly once.
58
+ *
59
+ * Note on placement: the factory cannot live in `packages/runtime` because it
60
+ * builds the HOST's adapter (`real-runtime-adapter.ts`, an L3 module) and needs
61
+ * the composed stores — `packages/*` must never depend on `apps/*`
62
+ * (ARCHITECTURE.md §1, K1/K2). It therefore stays in the L3 composition root and
63
+ * every other assembly (tests included) reuses THIS function.
64
+ *
65
+ * The dependency is a FUNCTION of `stores` because the engine factory runs
66
+ * INSIDE `composeStudio`: the test harness only learns its throwaway data dir
67
+ * once the workspace it registered is mounted.
68
+ */
69
+ export interface StudioEngineInput {
70
+ /** The data file the host ACTUALLY composed (`<data dir>/workspaces.json`). */
71
+ workspacesFile: string;
72
+ /** Process environment (provider keys, tool roots, resource caps, grants). */
73
+ env: NodeJS.ProcessEnv;
74
+ /** Startup engine profile. */
75
+ profile: EngineProfile;
76
+ /** Provider row the profile came from, recorded as the ledger's `provider`. */
77
+ providerLabel: string | null;
78
+ /** Late-bound host services (per-session prompt assembly; see [HostRef]). */
79
+ host: HostRef;
80
+ /**
81
+ * W806 (P0): explicit dynamic-disclosure activation for the composed
82
+ * sessions. Absent = the static mode baseline (the default face).
83
+ */
84
+ disclosure?: DisclosureOptions;
85
+ /**
86
+ * LLM seam override. Absent = the live provider assembled from the profile
87
+ * (production); the real-engine tests inject the deterministic OFFLINE engine.
88
+ */
89
+ llm?: (profile: Profile) => Llm;
90
+ }
91
+ /** Resolves the injected values of one engine build from the composed stores. */
92
+ export type StudioEngineDeps = (stores: StoreServices) => StudioEngineInput;
93
+ /**
94
+ * The ONE engine factory. The factory form is what makes the production path
95
+ * possible — providers.json is composed before the engine, so the startup
96
+ * profile is resolved from the operator's provider registry — and it is also
97
+ * what the test harness reuses. Worker receipts land under
98
+ * `<data dir>/worker-results`.
99
+ */
100
+ export declare function createStudioEngine(deps: StudioEngineDeps): EngineFactory;
101
+ /**
102
+ * W729: the host services exist only AFTER `composeStudio` ran, but the engine
103
+ * factory runs INSIDE it (the adapter is built while the stores are mounted).
104
+ * The per-session prompt hook is therefore late-bound through this ref; it is
105
+ * only ever called while composing a NAMED session, which happens on the first
106
+ * turn / activate — long after startup filled the ref in.
107
+ */
108
+ export interface HostRef {
109
+ services: StudioServices | null;
110
+ }
111
+ export declare function createStudioApp(opts?: StudioAppOptions): StudioApp;
package/dist/app.js ADDED
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Hono application factory — P4.
3
+ *
4
+ * Wiring order is contract order:
5
+ * 1. compose the studio context (store plugins + bus + injected runtime);
6
+ * 2. register all 39 contract endpoints and assert full coverage;
7
+ * 3. `/api/*` fallback = 404 JSON (an unknown API path must NEVER fall
8
+ * through to the static/SPA handler);
9
+ * 4. static files + SPA fallback from the read-only Vite build.
10
+ *
11
+ * The engine is injected: `opts.runtime` is a `RuntimeAdapter` (or a factory
12
+ * over the composed stores, which is what the REAL adapter needs to resolve
13
+ * session directories AND the provider target). With nothing injected the app
14
+ * mounts the real runtime (`runtime/`) wired to the real LLM, so the default
15
+ * deployment is the engine over a live provider, not a fake; the P4 fake stays
16
+ * available to tests through `harness.test-util.ts`.
17
+ *
18
+ * W743 (W732 A1): the engine assembly itself lives in `createStudioEngine()`
19
+ * below — the ONE factory — and `defaultRuntime()` is just the production
20
+ * binding of its injected values. The real-engine test harness
21
+ * (`runtime/test-util.ts`) calls the same function, so "the tests run the real
22
+ * engine" now also means "the tests run the real grants boundary and the real
23
+ * usage ledger".
24
+ */
25
+ import { Hono } from "hono";
26
+ import { dirname, join } from "node:path";
27
+ import { createUsageLedgerFile } from "@celestea/runtime";
28
+ import { API_ENDPOINT_COUNT, routeTable } from "./routes.js";
29
+ import { loadStudioConfig } from "./config.js";
30
+ import { apiTokenMiddleware, registerTokenBootstrap } from "./auth/api-token.js";
31
+ import { composeStudio } from "./plugins.js";
32
+ import { registerHandlers } from "./handlers/index.js";
33
+ import { assembleSystemPromptFor } from "./handlers/config-shape.js";
34
+ import { registerStatic } from "./static.js";
35
+ import { DEFAULT_SESSION_MODE } from "./store/mode.js";
36
+ import { sessionWorkspaceOf } from "./store/sessions.js";
37
+ import { readSessionMeta } from "./store/session-meta.js";
38
+ import { createSessionGrants } from "./runtime/session-grants.js";
39
+ import { grantsEnv } from "./store/grants-service.js";
40
+ import { createRealRuntimeAdapter, startupEngineProfile } from "./runtime/index.js";
41
+ import { recoverActiveSessionOnBoot } from "./runtime/boot-recovery.js";
42
+ import { RecoveryAuditWriter } from "./runtime/recovery-audit.js";
43
+ import { observeWorkerTableOnBoot } from "./runtime/worker-recovery.js";
44
+ import { workerTablePath } from "./runtime/worker-table.js";
45
+ /**
46
+ * The ONE engine factory. The factory form is what makes the production path
47
+ * possible — providers.json is composed before the engine, so the startup
48
+ * profile is resolved from the operator's provider registry — and it is also
49
+ * what the test harness reuses. Worker receipts land under
50
+ * `<data dir>/worker-results`.
51
+ */
52
+ export function createStudioEngine(deps) {
53
+ return (stores) => {
54
+ const input = deps(stores);
55
+ const dataDir = dirname(input.workspacesFile);
56
+ const resultsDir = join(dataDir, "worker-results");
57
+ return createRealRuntimeAdapter({
58
+ // E §2.3 P0 ①: the studio's OWN table — never the DSH fleet's (R2-1/B6).
59
+ workerRegistryPath: workerTablePath({ env: input.env, dataDir }),
60
+ // W787 (§5.2③): the audit channel of a degraded session log. The BOOT
61
+ // observer builds its own writer on the same append-only file (the two
62
+ // lines are written at different moments and never interleave a record).
63
+ recoveryAudit: new RecoveryAuditWriter({ dataDir, env: input.env }),
64
+ profile: input.profile,
65
+ env: input.env,
66
+ resultsDir,
67
+ // W516: every instance reads its session's grants at compose time. The env
68
+ // is pinned to the workspaces file the host ACTUALLY composed, so the
69
+ // fail-closed root rules resolve the same data dir (grants-service.ts).
70
+ grants: createSessionGrants({ dataDir, env: grantsEnv(input.env, input.workspacesFile) }),
71
+ // W728 §3 P0: ONE append-only usage ledger per process (`<data dir>`),
72
+ // shared by every session instance; `CELESTEA_USAGE_LEDGER=off` disables.
73
+ ledgerFile: createUsageLedgerFile({ dataDir, env: input.env }),
74
+ // E §4 P1 (W785): `fallbacks.json` + `fallbacks-audit.jsonl` live next to
75
+ // the ledger, so the fallback wiring needs the same data dir.
76
+ dataDir,
77
+ providerLabel: input.providerLabel,
78
+ ...(input.disclosure === undefined ? {} : { disclosure: input.disclosure }),
79
+ ...(input.llm === undefined ? {} : { llm: input.llm }),
80
+ resolveSession: (id) => {
81
+ const resolved = stores.sessions.resolve(id);
82
+ // W768: the workspace travels with the session target, so the tools'
83
+ // cwd/root and the prompt's `{{workspace}}` come from THIS resolution.
84
+ return resolved.ok
85
+ ? { sessionId: id, dir: resolved.value.dir, workspace: sessionWorkspaceOf(resolved.value) }
86
+ : null;
87
+ },
88
+ // W513: the session-level model override is applied to that session's own
89
+ // instance (it no longer rewrites a global engine profile).
90
+ sessionModel: (id) => sessionMetaAt(stores, id)?.model ?? null,
91
+ // W729 (§5.1 #4, R3): the session's mode is fixed at creation and the
92
+ // PROMPT assembly is therefore per instance, not per process. Both hooks
93
+ // read `session.json` of the session being composed, so a standard and an
94
+ // execution session in the same process get their own system prompt.
95
+ sessionMode: (id) => sessionMetaAt(stores, id)?.mode ?? null,
96
+ sessionSystemPrompt: (id) => sessionPromptAt(input.host, id),
97
+ // W804: the OPTIMISTIC per-model modality gate. null = the model is not
98
+ // configured anywhere => allow image input (the section 7.6 downgrade is
99
+ // the fallback for a wrong guess); a configured row without "image" is the
100
+ // only thing that disables the read_image gate.
101
+ modelInputModalities: (modelId) => {
102
+ for (const provider of stores.providers.rows()) {
103
+ const model = provider.models.find((m) => m.id === modelId);
104
+ if (model !== undefined)
105
+ return model.input_modalities ?? null;
106
+ }
107
+ return null;
108
+ },
109
+ });
110
+ };
111
+ }
112
+ /** The production engine: [createStudioEngine] over the REAL provider registry. */
113
+ function defaultRuntime(config, env, host) {
114
+ return createStudioEngine((stores) => {
115
+ const startup = startupEngineProfile(stores.providers, env, config.apiKeyEnv);
116
+ return {
117
+ workspacesFile: config.paths.workspacesFile,
118
+ env,
119
+ profile: startup.profile,
120
+ providerLabel: startup.target.provider_id,
121
+ host,
122
+ };
123
+ });
124
+ }
125
+ /** `session.json` of one session (null when the id does not resolve). */
126
+ function sessionMetaAt(stores, id) {
127
+ const resolved = stores.sessions.resolve(id);
128
+ return resolved.ok ? readSessionMeta(resolved.value.dir) : null;
129
+ }
130
+ /**
131
+ * W729/K8: every named session's prompt is assembled for THAT session. A session
132
+ * without `session.json.mode` gets the DEFAULT (standard) mode's text — the
133
+ * pre-W729 behaviour for the mode dimension — and its **own** identity variables.
134
+ *
135
+ * W768 (amends K8's "keeps the primed base prompt"): the primed base prompt was
136
+ * assembled ONCE at startup against whichever session was active then, so every
137
+ * other session received a prompt naming a workspace it is not in. With the
138
+ * tools now running in the session's own workspace, inheriting another session's
139
+ * prompt is precisely the "prompt says A, `pwd` says B" bug — so the assembly is
140
+ * per session for ALL sessions, and the mode gate no longer decides it.
141
+ */
142
+ function sessionPromptAt(host, id) {
143
+ if (host.services === null)
144
+ return null;
145
+ return assembleSystemPromptFor(host.services, id);
146
+ }
147
+ /** Every contract endpoint must be bound exactly once, or startup fails. */
148
+ function assertCoverage(routes, ids) {
149
+ const bound = new Set(ids);
150
+ const missing = routes.filter((r) => !bound.has(r.id)).map((r) => r.id);
151
+ if (missing.length > 0)
152
+ throw new Error(`unbound contract endpoints: ${missing.join(", ")}`);
153
+ if (ids.length !== API_ENDPOINT_COUNT) {
154
+ throw new Error(`expected ${API_ENDPOINT_COUNT} contract endpoints, got ${ids.length}`);
155
+ }
156
+ }
157
+ /**
158
+ * Hand the engine the BASE system prompt the HOST assembles (prompt registry +
159
+ * settings override). The retired backend did this inside `build_gen`; here the engine is
160
+ * primed once at startup and the next composed generation picks it up.
161
+ *
162
+ * W729: this primes the DEFAULT (detached) generation only. A named session's
163
+ * prompt is assembled per instance through the composer's `sessionSystemPrompt`
164
+ * hook, so priming can never leak one session's mode into another's instance.
165
+ */
166
+ function primeEnginePrompt(services, env) {
167
+ // The BASE (detached) generation is always the DEFAULT mode, whatever mode the
168
+ // session left active in workspaces.json happens to declare.
169
+ //
170
+ // W782: `env` is threaded through so the primed prompt states the unit name
171
+ // this process's own environment declares, not a compile-time guess.
172
+ services.runtime.primeSystemPrompt?.(assembleSystemPromptFor(services, null, DEFAULT_SESSION_MODE, env));
173
+ }
174
+ export function createStudioApp(opts = {}) {
175
+ const env = opts.env ?? process.env;
176
+ const config = opts.config ?? loadStudioConfig({ cwd: opts.cwd, env });
177
+ // Filled in right after composition; the engine reads it lazily (see HostRef).
178
+ const host = { services: null };
179
+ const runtime = opts.runtime ?? defaultRuntime(config, env, host);
180
+ const services = composeStudio({ config, runtime, env, now: opts.now });
181
+ host.services = services;
182
+ const table = routeTable();
183
+ const app = new Hono();
184
+ // H-security: a configured token gates every /api/* request except
185
+ // /api/health. No token = the historical nginx-delegated path, unchanged
186
+ // (the listener is loopback; a non-loopback bind is refused in server.ts).
187
+ if (config.authToken !== null) {
188
+ app.use("/api/*", apiTokenMiddleware(config.authToken));
189
+ // The browser never sends an Authorization header; this one-shot bootstrap
190
+ // sets the HttpOnly cookie the middleware also accepts. Registered BEFORE
191
+ // registerStatic's catch-all.
192
+ registerTokenBootstrap(app, config.authToken);
193
+ }
194
+ primeEnginePrompt(services, env);
195
+ // W787 (§5.2③): the ONE boot audit channel — the checkpoint repair and the
196
+ // worker-table observation both report into `<data dir>/recovery-audit.jsonl`.
197
+ const bootAudit = new RecoveryAuditWriter({ dataDir: dirname(config.paths.workspacesFile), env });
198
+ // E §1.3 P0 ③: close the turn the previous process died inside — BEFORE any
199
+ // instance of the active session is composed, because composing one replays the
200
+ // log and takes its turn counter from it. A clean log, a missing checkpoint or
201
+ // an unresolvable active session are all no-ops (fail-safe).
202
+ recoverActiveSessionOnBoot({ workspaces: services.workspaces, sessions: services.sessions, audit: bootAudit });
203
+ // E §2.3 P0 ③: OBSERVE the persisted worker table (dead owner / missing host
204
+ // session) and record it. Never re-dispatch: that is P2, behind an explicit
205
+ // switch, and it needs the tool side-effect table first (§5.1).
206
+ const dataDir = dirname(config.paths.workspacesFile);
207
+ observeWorkerTableOnBoot({
208
+ path: workerTablePath({ env, dataDir }),
209
+ knownHost: (sid) => services.sessions.resolve(sid).ok,
210
+ resultsDir: join(dataDir, "worker-results"),
211
+ audit: bootAudit,
212
+ });
213
+ const endpointIds = registerHandlers(app, services, table);
214
+ assertCoverage(table.routes, endpointIds);
215
+ // Unknown API paths are 404 JSON, never the SPA (frozen static contract).
216
+ app.all("/api/*", (c) => c.json({ error: "not found" }, 404));
217
+ registerStatic(app, config.paths.staticRoot);
218
+ return { app, routes: table.routes, services, endpointIds };
219
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * H-security — the minimal self-contained API token (fail-closed).
3
+ *
4
+ * Why this exists: `handlers/auth.ts` delegates the browser gate to nginx
5
+ * (`auth_request` -> `/auth/check`), and the backend is deliberately reachable
6
+ * from localhost with no cookie. That is safe ONLY while the listener is on the
7
+ * loopback. The CLI's `--bind` can move it to `0.0.0.0`, where the SAME backend
8
+ * exposes `POST /api/exec` (arbitrary shell as this user), `GET /api/fs/list`
9
+ * (any directory) and every agent endpoint to anyone who can reach the port.
10
+ *
11
+ * Rules, all "never silently allow":
12
+ * 1. a NON-loopback bind is REFUSED unless a token is configured;
13
+ * 2. when a token IS configured, every `/api/*` request except `/api/health`
14
+ * must present it — as `Authorization: Bearer`, `x-celestea-token`, or the
15
+ * cookie `GET /auth/token?token=…` sets — else 401.
16
+ *
17
+ * The browser problem (why the cookie exists): a browser never sends an
18
+ * `Authorization` header, so a token-only gate leaves the UI unusable (every
19
+ * fetch 401). `/auth/token` is the standard one-shot bootstrap: a correct token
20
+ * sets an HttpOnly cookie and 302s to `/`; the middleware then accepts the
21
+ * cookie. The cookie value is an HMAC of the token, never the plaintext token,
22
+ * so the raw secret is not stored client-side.
23
+ *
24
+ * The loopback path is untouched (no token -> no middleware, no bootstrap), so
25
+ * the existing nginx deployment and every existing test keep working.
26
+ */
27
+ import type { Hono, MiddlewareHandler } from "hono";
28
+ /** Env var carrying the token (the `--token` flag overrides it). */
29
+ export declare const ENV_AUTH_TOKEN = "CELESTEA_AUTH_TOKEN";
30
+ /** The token header (in addition to `Authorization: Bearer <token>`). */
31
+ export declare const AUTH_TOKEN_HEADER = "x-celestea-token";
32
+ /** The one API path that stays public (liveness probes, the UI's boot check). */
33
+ export declare const AUTH_EXEMPT_PATH = "/api/health";
34
+ /** The browser bootstrap path (NOT under /api, so the token gate never blocks it). */
35
+ export declare const AUTH_TOKEN_PATH = "/auth/token";
36
+ /** The HttpOnly cookie the bootstrap sets; distinct from the login cookie. */
37
+ export declare const AUTH_TOKEN_COOKIE = "celestea_api_token";
38
+ /** 30 days, the same lifetime the login cookie uses. */
39
+ export declare const AUTH_TOKEN_COOKIE_MAX_AGE: number;
40
+ /** true only for an address that cannot be reached from another host. */
41
+ export declare function isLoopbackBind(bind: string): boolean;
42
+ /** The configured token, or null when unset/blank. */
43
+ export declare function readAuthToken(env?: NodeJS.ProcessEnv): string | null;
44
+ /** The token a request presents, from either accepted header (or null). */
45
+ export declare function presentedToken(authHeader: string | undefined, tokenHeader: string | undefined): string | null;
46
+ /** Constant-time string comparison (a length mismatch is a plain false). */
47
+ export declare function tokensMatch(presented: string, expected: string): boolean;
48
+ /**
49
+ * The cookie value for a token. An HMAC keyed by the token, so the cookie the
50
+ * browser stores is NOT the raw secret (a leaked cookie does not reveal the
51
+ * token, and the token never appears in a page or a cookie jar verbatim).
52
+ */
53
+ export declare function tokenCookieValue(token: string): string;
54
+ /** true when the presented cookie value is this token's cookie value. */
55
+ export declare function cookieMatches(presented: string | null | undefined, token: string): boolean;
56
+ /** The refusal when a non-loopback bind has no token. */
57
+ export declare class InsecureBindError extends Error {
58
+ readonly name = "InsecureBindError";
59
+ constructor(bind: string);
60
+ }
61
+ /** Refuse a non-loopback bind that has no token (throws [InsecureBindError]). */
62
+ export declare function assertBindIsSafe(bind: string, token: string | null): void;
63
+ /**
64
+ * The token middleware: every `/api/*` request except `/api/health` must
65
+ * present the token as a header OR the bootstrap cookie, else 401. `/auth/*`
66
+ * and the static UI are not under `/api/*` and stay reachable.
67
+ */
68
+ export declare function apiTokenMiddleware(token: string): MiddlewareHandler;
69
+ /** The `Set-Cookie` value: HttpOnly + SameSite=Strict + Path=/ (+Secure over TLS). */
70
+ export declare function apiTokenCookie(token: string, secure: boolean): string;
71
+ /**
72
+ * `GET /auth/token?token=<secret>`: correct -> set the cookie and 302 to `/`;
73
+ * wrong/missing -> 401 with NO cookie. Not a contract endpoint (a security-layer
74
+ * route, registered outside the frozen route table).
75
+ */
76
+ export declare function registerTokenBootstrap(app: Hono, token: string): void;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * H-security — the minimal self-contained API token (fail-closed).
3
+ *
4
+ * Why this exists: `handlers/auth.ts` delegates the browser gate to nginx
5
+ * (`auth_request` -> `/auth/check`), and the backend is deliberately reachable
6
+ * from localhost with no cookie. That is safe ONLY while the listener is on the
7
+ * loopback. The CLI's `--bind` can move it to `0.0.0.0`, where the SAME backend
8
+ * exposes `POST /api/exec` (arbitrary shell as this user), `GET /api/fs/list`
9
+ * (any directory) and every agent endpoint to anyone who can reach the port.
10
+ *
11
+ * Rules, all "never silently allow":
12
+ * 1. a NON-loopback bind is REFUSED unless a token is configured;
13
+ * 2. when a token IS configured, every `/api/*` request except `/api/health`
14
+ * must present it — as `Authorization: Bearer`, `x-celestea-token`, or the
15
+ * cookie `GET /auth/token?token=…` sets — else 401.
16
+ *
17
+ * The browser problem (why the cookie exists): a browser never sends an
18
+ * `Authorization` header, so a token-only gate leaves the UI unusable (every
19
+ * fetch 401). `/auth/token` is the standard one-shot bootstrap: a correct token
20
+ * sets an HttpOnly cookie and 302s to `/`; the middleware then accepts the
21
+ * cookie. The cookie value is an HMAC of the token, never the plaintext token,
22
+ * so the raw secret is not stored client-side.
23
+ *
24
+ * The loopback path is untouched (no token -> no middleware, no bootstrap), so
25
+ * the existing nginx deployment and every existing test keep working.
26
+ */
27
+ import { createHmac, timingSafeEqual } from "node:crypto";
28
+ import { cookieValue } from "./token.js";
29
+ /** Env var carrying the token (the `--token` flag overrides it). */
30
+ export const ENV_AUTH_TOKEN = "CELESTEA_AUTH_TOKEN";
31
+ /** The token header (in addition to `Authorization: Bearer <token>`). */
32
+ export const AUTH_TOKEN_HEADER = "x-celestea-token";
33
+ /** The one API path that stays public (liveness probes, the UI's boot check). */
34
+ export const AUTH_EXEMPT_PATH = "/api/health";
35
+ /** The browser bootstrap path (NOT under /api, so the token gate never blocks it). */
36
+ export const AUTH_TOKEN_PATH = "/auth/token";
37
+ /** The HttpOnly cookie the bootstrap sets; distinct from the login cookie. */
38
+ export const AUTH_TOKEN_COOKIE = "celestea_api_token";
39
+ /** 30 days, the same lifetime the login cookie uses. */
40
+ export const AUTH_TOKEN_COOKIE_MAX_AGE = 30 * 24 * 60 * 60;
41
+ /** The host part of a `host` or `host:port` bind string (IPv6 aware). */
42
+ function hostOf(bind) {
43
+ const raw = bind.trim().toLowerCase();
44
+ if (raw.startsWith("[")) {
45
+ const end = raw.indexOf("]");
46
+ return end === -1 ? raw : raw.slice(1, end);
47
+ }
48
+ const first = raw.indexOf(":");
49
+ // Exactly one colon = "host:port"; two or more = a bare IPv6 literal.
50
+ if (first !== -1 && raw.indexOf(":", first + 1) === -1)
51
+ return raw.slice(0, first);
52
+ return raw;
53
+ }
54
+ /** true only for an address that cannot be reached from another host. */
55
+ export function isLoopbackBind(bind) {
56
+ const host = hostOf(bind);
57
+ if (host === "localhost" || host === "::1" || host === "127.0.0.1")
58
+ return true;
59
+ if (host.startsWith("127."))
60
+ return true;
61
+ // IPv4-mapped IPv6 loopback (::ffff:127.0.0.1).
62
+ if (host.startsWith("::ffff:127."))
63
+ return true;
64
+ return false;
65
+ }
66
+ /** The configured token, or null when unset/blank. */
67
+ export function readAuthToken(env = process.env) {
68
+ const raw = env[ENV_AUTH_TOKEN];
69
+ if (typeof raw !== "string")
70
+ return null;
71
+ const trimmed = raw.trim();
72
+ return trimmed === "" ? null : trimmed;
73
+ }
74
+ /** The token a request presents, from either accepted header (or null). */
75
+ export function presentedToken(authHeader, tokenHeader) {
76
+ if (typeof tokenHeader === "string" && tokenHeader.trim() !== "")
77
+ return tokenHeader.trim();
78
+ if (typeof authHeader === "string") {
79
+ const match = /^Bearer\s+(.+)$/i.exec(authHeader.trim());
80
+ const value = match?.[1];
81
+ if (value !== undefined && value.trim() !== "")
82
+ return value.trim();
83
+ }
84
+ return null;
85
+ }
86
+ /** Constant-time string comparison (a length mismatch is a plain false). */
87
+ export function tokensMatch(presented, expected) {
88
+ const a = Buffer.from(presented, "utf8");
89
+ const b = Buffer.from(expected, "utf8");
90
+ if (a.length !== b.length)
91
+ return false;
92
+ return timingSafeEqual(a, b);
93
+ }
94
+ /**
95
+ * The cookie value for a token. An HMAC keyed by the token, so the cookie the
96
+ * browser stores is NOT the raw secret (a leaked cookie does not reveal the
97
+ * token, and the token never appears in a page or a cookie jar verbatim).
98
+ */
99
+ export function tokenCookieValue(token) {
100
+ return createHmac("sha256", token).update("celestea-api-token-v1").digest("hex");
101
+ }
102
+ /** true when the presented cookie value is this token's cookie value. */
103
+ export function cookieMatches(presented, token) {
104
+ if (typeof presented !== "string" || presented === "")
105
+ return false;
106
+ return tokensMatch(presented, tokenCookieValue(token));
107
+ }
108
+ /** The refusal when a non-loopback bind has no token. */
109
+ export class InsecureBindError extends Error {
110
+ name = "InsecureBindError";
111
+ constructor(bind) {
112
+ super(`refusing to bind ${bind} without authentication: that would expose ` +
113
+ `POST /api/exec (arbitrary shell as this user), GET /api/fs/list (any directory) ` +
114
+ `and every agent endpoint to anyone who can reach the port.\n` +
115
+ ` Fix it one of two ways:\n` +
116
+ ` 1. keep it local: --bind 127.0.0.1 (the default), or\n` +
117
+ ` 2. set a token: --token <secret> (or ${ENV_AUTH_TOKEN}=<secret>) and send it as ` +
118
+ `'Authorization: Bearer <secret>' (or '${AUTH_TOKEN_HEADER}: <secret>') on every /api/* request.\n` +
119
+ ` For a browser, visit /auth/token?token=<secret> once to set the session cookie.\n` +
120
+ ` For the nginx login gate, put nginx in front and bind 127.0.0.1.`);
121
+ }
122
+ }
123
+ /** Refuse a non-loopback bind that has no token (throws [InsecureBindError]). */
124
+ export function assertBindIsSafe(bind, token) {
125
+ if (isLoopbackBind(bind) || token !== null)
126
+ return;
127
+ throw new InsecureBindError(bind);
128
+ }
129
+ /**
130
+ * The token middleware: every `/api/*` request except `/api/health` must
131
+ * present the token as a header OR the bootstrap cookie, else 401. `/auth/*`
132
+ * and the static UI are not under `/api/*` and stay reachable.
133
+ */
134
+ export function apiTokenMiddleware(token) {
135
+ return async (c, next) => {
136
+ if (c.req.path === AUTH_EXEMPT_PATH)
137
+ return next();
138
+ const presented = presentedToken(c.req.header("authorization"), c.req.header(AUTH_TOKEN_HEADER));
139
+ if (presented !== null && tokensMatch(presented, token))
140
+ return next();
141
+ if (cookieMatches(cookieValue(c.req.header("cookie"), AUTH_TOKEN_COOKIE), token))
142
+ return next();
143
+ return c.json({ ok: false, error: "unauthorized" }, 401);
144
+ };
145
+ }
146
+ /** The `Set-Cookie` value: HttpOnly + SameSite=Strict + Path=/ (+Secure over TLS). */
147
+ export function apiTokenCookie(token, secure) {
148
+ const attrs = [`${AUTH_TOKEN_COOKIE}=${tokenCookieValue(token)}`, "Path=/", `Max-Age=${AUTH_TOKEN_COOKIE_MAX_AGE}`, "HttpOnly", "SameSite=Strict"];
149
+ if (secure)
150
+ attrs.push("Secure");
151
+ return attrs.join("; ");
152
+ }
153
+ /** `Secure` is set only when the request really arrived over TLS (nginx). */
154
+ function isSecureRequest(c) {
155
+ const first = (c.req.header("x-forwarded-proto") ?? "").toLowerCase().split(",")[0];
156
+ return first !== undefined && first.trim() === "https";
157
+ }
158
+ /**
159
+ * `GET /auth/token?token=<secret>`: correct -> set the cookie and 302 to `/`;
160
+ * wrong/missing -> 401 with NO cookie. Not a contract endpoint (a security-layer
161
+ * route, registered outside the frozen route table).
162
+ */
163
+ export function registerTokenBootstrap(app, token) {
164
+ app.get(AUTH_TOKEN_PATH, (c) => {
165
+ const presented = c.req.query("token");
166
+ if (presented === undefined || !tokensMatch(presented, token)) {
167
+ return c.json({ ok: false, error: "unauthorized" }, 401);
168
+ }
169
+ c.header("set-cookie", apiTokenCookie(token, isSecureRequest(c)));
170
+ return c.redirect("/", 302);
171
+ });
172
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * W767 — password verification against Studio's OWN password file.
3
+ *
4
+ * `htpasswd -vbi` is used deliberately: `-b` takes the password on **stdin**
5
+ * (never in argv, so it cannot show up in `ps`/audit logs), `-v` verifies, `-i`
6
+ * reads stdin. The file is read-only input — Studio never writes it, and it is
7
+ * the only credential source this module knows about.
8
+ *
9
+ * The username is restricted to a conservative charset: it travels in argv, and
10
+ * a leading `-` or a newline must never be able to steer the helper.
11
+ */
12
+ /** The verifier binary (net/httpd-tools `htpasswd`). */
13
+ export declare const HTPASSWD_BIN = "htpasswd";
14
+ /** Accepted username shape (argv-safe, 1..64 chars). */
15
+ export declare const HTPASSWD_USER_RE: RegExp;
16
+ /**
17
+ * `ok` = the password verified; `denied` = wrong password or unknown user (the
18
+ * two are deliberately indistinguishable); `error` = the helper or the file is
19
+ * unusable (an operator problem, never reported as a wrong password).
20
+ */
21
+ export type PasswordVerdict = "ok" | "denied" | "error";
22
+ export declare function verifyPassword(file: string, user: string, password: string): PasswordVerdict;