@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.
- package/LICENSE +21 -0
- package/dist/app.d.ts +111 -0
- package/dist/app.js +219 -0
- package/dist/auth/api-token.d.ts +76 -0
- package/dist/auth/api-token.js +172 -0
- package/dist/auth/htpasswd.d.ts +22 -0
- package/dist/auth/htpasswd.js +61 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +14 -0
- package/dist/auth/page.d.ts +19 -0
- package/dist/auth/page.js +73 -0
- package/dist/auth/rate-limit.d.ts +27 -0
- package/dist/auth/rate-limit.js +47 -0
- package/dist/auth/token.d.ts +49 -0
- package/dist/auth/token.js +112 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +72 -0
- package/dist/deployment.d.ts +92 -0
- package/dist/deployment.js +154 -0
- package/dist/fake-runtime-adapter.d.ts +37 -0
- package/dist/fake-runtime-adapter.js +276 -0
- package/dist/handlers/auth.d.ts +24 -0
- package/dist/handlers/auth.js +155 -0
- package/dist/handlers/common.d.ts +76 -0
- package/dist/handlers/common.js +127 -0
- package/dist/handlers/config-shape.d.ts +76 -0
- package/dist/handlers/config-shape.js +227 -0
- package/dist/handlers/config.d.ts +26 -0
- package/dist/handlers/config.js +161 -0
- package/dist/handlers/context-shape.d.ts +33 -0
- package/dist/handlers/context-shape.js +59 -0
- package/dist/handlers/dialog.d.ts +22 -0
- package/dist/handlers/dialog.js +292 -0
- package/dist/handlers/exec.d.ts +22 -0
- package/dist/handlers/exec.js +131 -0
- package/dist/handlers/fs-read.d.ts +63 -0
- package/dist/handlers/fs-read.js +159 -0
- package/dist/handlers/fs.d.ts +57 -0
- package/dist/handlers/fs.js +153 -0
- package/dist/handlers/grants-shape.d.ts +45 -0
- package/dist/handlers/grants-shape.js +130 -0
- package/dist/handlers/grants.d.ts +21 -0
- package/dist/handlers/grants.js +267 -0
- package/dist/handlers/health.d.ts +37 -0
- package/dist/handlers/health.js +139 -0
- package/dist/handlers/index.d.ts +40 -0
- package/dist/handlers/index.js +85 -0
- package/dist/handlers/permissions.d.ts +9 -0
- package/dist/handlers/permissions.js +183 -0
- package/dist/handlers/plugins.d.ts +18 -0
- package/dist/handlers/plugins.js +23 -0
- package/dist/handlers/prompts.d.ts +12 -0
- package/dist/handlers/prompts.js +120 -0
- package/dist/handlers/providers.d.ts +12 -0
- package/dist/handlers/providers.js +178 -0
- package/dist/handlers/questions.d.ts +23 -0
- package/dist/handlers/questions.js +115 -0
- package/dist/handlers/session-model.d.ts +38 -0
- package/dist/handlers/session-model.js +105 -0
- package/dist/handlers/session-move.d.ts +22 -0
- package/dist/handlers/session-move.js +188 -0
- package/dist/handlers/session-tools.d.ts +19 -0
- package/dist/handlers/session-tools.js +75 -0
- package/dist/handlers/sessions.d.ts +32 -0
- package/dist/handlers/sessions.js +242 -0
- package/dist/handlers/usage.d.ts +26 -0
- package/dist/handlers/usage.js +77 -0
- package/dist/handlers/worker.d.ts +12 -0
- package/dist/handlers/worker.js +82 -0
- package/dist/handlers/workspaces.d.ts +12 -0
- package/dist/handlers/workspaces.js +88 -0
- package/dist/harness.test-util.d.ts +101 -0
- package/dist/harness.test-util.js +176 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +35 -0
- package/dist/plugins.d.ts +105 -0
- package/dist/plugins.js +97 -0
- package/dist/question-registry.d.ts +124 -0
- package/dist/question-registry.js +212 -0
- package/dist/question-rows.d.ts +32 -0
- package/dist/question-rows.js +43 -0
- package/dist/replay/compare.d.ts +47 -0
- package/dist/replay/compare.js +89 -0
- package/dist/replay/e2e-replay.d.ts +46 -0
- package/dist/replay/e2e-replay.js +91 -0
- package/dist/replay/expect-compact.d.ts +31 -0
- package/dist/replay/expect-compact.js +92 -0
- package/dist/replay/fixtures.d.ts +52 -0
- package/dist/replay/fixtures.js +53 -0
- package/dist/replay/host.d.ts +32 -0
- package/dist/replay/host.js +62 -0
- package/dist/replay/index.d.ts +16 -0
- package/dist/replay/index.js +16 -0
- package/dist/replay/probes.d.ts +55 -0
- package/dist/replay/probes.js +225 -0
- package/dist/replay/report.d.ts +13 -0
- package/dist/replay/report.js +103 -0
- package/dist/replay/session-e2e.d.ts +44 -0
- package/dist/replay/session-e2e.js +91 -0
- package/dist/routes.d.ts +47 -0
- package/dist/routes.js +59 -0
- package/dist/runtime/attachments-llm.d.ts +13 -0
- package/dist/runtime/attachments-llm.js +37 -0
- package/dist/runtime/boot-recovery.d.ts +32 -0
- package/dist/runtime/boot-recovery.js +71 -0
- package/dist/runtime/context-snapshot.d.ts +59 -0
- package/dist/runtime/context-snapshot.js +113 -0
- package/dist/runtime/engine-grants.d.ts +121 -0
- package/dist/runtime/engine-grants.js +338 -0
- package/dist/runtime/engine-permissions.d.ts +29 -0
- package/dist/runtime/engine-permissions.js +92 -0
- package/dist/runtime/engine-plugins.d.ts +189 -0
- package/dist/runtime/engine-plugins.js +292 -0
- package/dist/runtime/engine-profile.d.ts +28 -0
- package/dist/runtime/engine-profile.js +88 -0
- package/dist/runtime/engine-session.d.ts +9 -0
- package/dist/runtime/engine-session.js +9 -0
- package/dist/runtime/fallback-contract.d.ts +32 -0
- package/dist/runtime/fallback-contract.js +10 -0
- package/dist/runtime/fallback-host.d.ts +134 -0
- package/dist/runtime/fallback-host.js +299 -0
- package/dist/runtime/host-autowake.d.ts +79 -0
- package/dist/runtime/host-autowake.js +86 -0
- package/dist/runtime/image-downgrade.d.ts +49 -0
- package/dist/runtime/image-downgrade.js +90 -0
- package/dist/runtime/inbox-message.d.ts +11 -0
- package/dist/runtime/inbox-message.js +20 -0
- package/dist/runtime/index.d.ts +17 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/ledger-view.d.ts +40 -0
- package/dist/runtime/ledger-view.js +56 -0
- package/dist/runtime/llm-assembly.d.ts +66 -0
- package/dist/runtime/llm-assembly.js +114 -0
- package/dist/runtime/offline-llm.d.ts +71 -0
- package/dist/runtime/offline-llm.js +132 -0
- package/dist/runtime/provider-target.d.ts +10 -0
- package/dist/runtime/provider-target.js +10 -0
- package/dist/runtime/question-host.d.ts +33 -0
- package/dist/runtime/question-host.js +41 -0
- package/dist/runtime/question-view.d.ts +48 -0
- package/dist/runtime/question-view.js +76 -0
- package/dist/runtime/real-runtime-adapter.d.ts +95 -0
- package/dist/runtime/real-runtime-adapter.js +553 -0
- package/dist/runtime/recovery-audit.d.ts +81 -0
- package/dist/runtime/recovery-audit.js +95 -0
- package/dist/runtime/recovery-view.d.ts +36 -0
- package/dist/runtime/recovery-view.js +63 -0
- package/dist/runtime/session-compose.d.ts +235 -0
- package/dist/runtime/session-compose.js +350 -0
- package/dist/runtime/session-grants.d.ts +36 -0
- package/dist/runtime/session-grants.js +69 -0
- package/dist/runtime/session-lifecycle.d.ts +40 -0
- package/dist/runtime/session-lifecycle.js +86 -0
- package/dist/runtime/session-publisher.d.ts +40 -0
- package/dist/runtime/session-publisher.js +48 -0
- package/dist/runtime/session-release.d.ts +53 -0
- package/dist/runtime/session-release.js +80 -0
- package/dist/runtime/test-util.d.ts +78 -0
- package/dist/runtime/test-util.js +162 -0
- package/dist/runtime/watchdog-view.d.ts +33 -0
- package/dist/runtime/watchdog-view.js +45 -0
- package/dist/runtime/worker-bridge.d.ts +76 -0
- package/dist/runtime/worker-bridge.js +156 -0
- package/dist/runtime/worker-live.d.ts +22 -0
- package/dist/runtime/worker-live.js +25 -0
- package/dist/runtime/worker-recovery.d.ts +35 -0
- package/dist/runtime/worker-recovery.js +64 -0
- package/dist/runtime/worker-table.d.ts +58 -0
- package/dist/runtime/worker-table.js +70 -0
- package/dist/runtime-adapter.d.ts +421 -0
- package/dist/runtime-adapter.js +76 -0
- package/dist/serial-queue.d.ts +16 -0
- package/dist/serial-queue.js +22 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.js +119 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +31 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.js +186 -0
- package/dist/static.d.ts +20 -0
- package/dist/static.js +120 -0
- package/dist/store/builtin-sections.d.ts +52 -0
- package/dist/store/builtin-sections.js +118 -0
- package/dist/store/celestea-home.d.ts +12 -0
- package/dist/store/celestea-home.js +11 -0
- package/dist/store/fs-json.d.ts +51 -0
- package/dist/store/fs-json.js +119 -0
- package/dist/store/grants-audit.d.ts +84 -0
- package/dist/store/grants-audit.js +109 -0
- package/dist/store/grants-service.d.ts +44 -0
- package/dist/store/grants-service.js +41 -0
- package/dist/store/grants-tokens.d.ts +62 -0
- package/dist/store/grants-tokens.js +105 -0
- package/dist/store/grants.d.ts +113 -0
- package/dist/store/grants.js +238 -0
- package/dist/store/index.d.ts +36 -0
- package/dist/store/index.js +36 -0
- package/dist/store/mode.d.ts +35 -0
- package/dist/store/mode.js +41 -0
- package/dist/store/permissions.d.ts +61 -0
- package/dist/store/permissions.js +139 -0
- package/dist/store/prompts-compose.d.ts +56 -0
- package/dist/store/prompts-compose.js +62 -0
- package/dist/store/prompts-template.d.ts +26 -0
- package/dist/store/prompts-template.js +99 -0
- package/dist/store/prompts.d.ts +142 -0
- package/dist/store/prompts.js +273 -0
- package/dist/store/provider-probe.d.ts +81 -0
- package/dist/store/provider-probe.js +154 -0
- package/dist/store/providers.d.ts +97 -0
- package/dist/store/providers.js +219 -0
- package/dist/store/result.d.ts +29 -0
- package/dist/store/result.js +30 -0
- package/dist/store/session-id.d.ts +119 -0
- package/dist/store/session-id.js +220 -0
- package/dist/store/session-meta.d.ts +41 -0
- package/dist/store/session-meta.js +66 -0
- package/dist/store/session-ops.d.ts +109 -0
- package/dist/store/session-ops.js +264 -0
- package/dist/store/session-tools.d.ts +42 -0
- package/dist/store/session-tools.js +69 -0
- package/dist/store/sessions.d.ts +162 -0
- package/dist/store/sessions.js +307 -0
- package/dist/store/validate.d.ts +13 -0
- package/dist/store/validate.js +38 -0
- package/dist/store/workspaces.d.ts +67 -0
- package/dist/store/workspaces.js +228 -0
- package/dist/user-questions.d.ts +58 -0
- package/dist/user-questions.js +157 -0
- package/dist/version.d.ts +14 -0
- package/dist/version.js +52 -0
- package/package.json +38 -0
- package/webdist/assets/index-Bngs7gUm.js +98 -0
- package/webdist/assets/index-Bu6ci_rN.css +1 -0
- package/webdist/assets/katex-CCbS2qAY.js +257 -0
- package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
- package/webdist/build-meta.json +7 -0
- package/webdist/index.html +261 -0
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { accessSync, constants } from "node:fs";
|
|
14
|
+
/** The verifier binary (net/httpd-tools `htpasswd`). */
|
|
15
|
+
export const HTPASSWD_BIN = "htpasswd";
|
|
16
|
+
/** Accepted username shape (argv-safe, 1..64 chars). */
|
|
17
|
+
export const HTPASSWD_USER_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
18
|
+
/** Bound the helper: a hung/absent binary must not hang a login. */
|
|
19
|
+
const HTPASSWD_TIMEOUT_MS = 5_000;
|
|
20
|
+
export function verifyPassword(file, user, password) {
|
|
21
|
+
if (password === "" || !HTPASSWD_USER_RE.test(user))
|
|
22
|
+
return "denied";
|
|
23
|
+
if (!isReadable(file))
|
|
24
|
+
return "error";
|
|
25
|
+
let run = runHelper(file, user, password);
|
|
26
|
+
// A transient SPAWN failure (EAGAIN/EMFILE while the host is loaded — observed
|
|
27
|
+
// as an intermittent `error` verdict under a full parallel test run) is not
|
|
28
|
+
// "the credential store is unusable": retry ONCE before calling it an operator
|
|
29
|
+
// problem. A genuine timeout is NOT retried — the 5 s bound stays the bound.
|
|
30
|
+
if (run.error !== undefined && !isTimeout(run.error)) {
|
|
31
|
+
run = runHelper(file, user, password);
|
|
32
|
+
}
|
|
33
|
+
if (run.error !== undefined)
|
|
34
|
+
return "error";
|
|
35
|
+
if (run.status === 0)
|
|
36
|
+
return "ok";
|
|
37
|
+
// 3 = password mismatch, 6 = user not found (both are "denied"); anything
|
|
38
|
+
// else (4 = file error, a signal, a timeout) is an operator problem.
|
|
39
|
+
return run.status === 3 || run.status === 6 ? "denied" : "error";
|
|
40
|
+
}
|
|
41
|
+
/** One `htpasswd -vbi` attempt (the password travels on stdin, never argv). */
|
|
42
|
+
function runHelper(file, user, password) {
|
|
43
|
+
return spawnSync(HTPASSWD_BIN, ["-vbi", file, user], {
|
|
44
|
+
input: `${password}\n`,
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
timeout: HTPASSWD_TIMEOUT_MS,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/** `spawnSync` hit its own `timeout` — a bound we must not silently double. */
|
|
50
|
+
function isTimeout(error) {
|
|
51
|
+
return error.code === "ETIMEDOUT";
|
|
52
|
+
}
|
|
53
|
+
function isReadable(file) {
|
|
54
|
+
try {
|
|
55
|
+
accessSync(file, constants.R_OK);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W767 — Studio's own login-cookie gate (see `docs/feature-studio-auth.md`).
|
|
3
|
+
*
|
|
4
|
+
* token.ts cookie token format + secret file (0600, Studio's data dir)
|
|
5
|
+
* htpasswd.ts password verification against Studio's read-only password file
|
|
6
|
+
* rate-limit.ts per-username / per-IP failure limiter (60 s window)
|
|
7
|
+
* page.ts the self-contained login page the backend renders
|
|
8
|
+
* api-token.ts H: the minimal self-cert token for a non-loopback bind
|
|
9
|
+
*/
|
|
10
|
+
export * from "./token.js";
|
|
11
|
+
export * from "./api-token.js";
|
|
12
|
+
export * from "./htpasswd.js";
|
|
13
|
+
export * from "./rate-limit.js";
|
|
14
|
+
export * from "./page.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W767 — Studio's own login-cookie gate (see `docs/feature-studio-auth.md`).
|
|
3
|
+
*
|
|
4
|
+
* token.ts cookie token format + secret file (0600, Studio's data dir)
|
|
5
|
+
* htpasswd.ts password verification against Studio's read-only password file
|
|
6
|
+
* rate-limit.ts per-username / per-IP failure limiter (60 s window)
|
|
7
|
+
* page.ts the self-contained login page the backend renders
|
|
8
|
+
* api-token.ts H: the minimal self-cert token for a non-loopback bind
|
|
9
|
+
*/
|
|
10
|
+
export * from "./token.js";
|
|
11
|
+
export * from "./api-token.js";
|
|
12
|
+
export * from "./htpasswd.js";
|
|
13
|
+
export * from "./rate-limit.js";
|
|
14
|
+
export * from "./page.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W767 — Studio's OWN login page, rendered by the Studio backend.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained on purpose: inline CSS, no external asset, no dependency on
|
|
5
|
+
* the Vite build (so the login page keeps working while the SPA is being
|
|
6
|
+
* rebuilt, and nginx can serve it as the one unauthenticated page).
|
|
7
|
+
*
|
|
8
|
+
* The form posts `application/x-www-form-urlencoded`, which every browser and
|
|
9
|
+
* password manager understands; the JSON body form is accepted too (curl /
|
|
10
|
+
* API clients). ALL pages returned here are `Cache-Control: no-store`.
|
|
11
|
+
*/
|
|
12
|
+
/** `error === null` = a clean form; otherwise the message is shown above it. */
|
|
13
|
+
export declare function loginPage(error?: string | null): string;
|
|
14
|
+
/**
|
|
15
|
+
* The 200 answer to a SUCCESSFUL form login. It is a page (never a 302): a
|
|
16
|
+
* redirect response is allowed to drop `Set-Cookie` on some mobile clients, so
|
|
17
|
+
* the cookie and the navigation travel in the SAME 200 response.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LOGIN_OK_PAGE = "<!doctype html>\n<html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><meta name=\"robots\" content=\"noindex\">\n<title>\u5DF2\u767B\u5F55 \u00B7 Celestea Studio</title></head>\n<body><p>\u767B\u5F55\u6210\u529F\uFF0C\u6B63\u5728\u8FDB\u5165\u2026</p>\n<script>location.replace(\"/\")</script>\n<noscript><a href=\"/\">\u8FDB\u5165 Celestea Studio</a></noscript>\n</body></html>\n";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W767 — Studio's OWN login page, rendered by the Studio backend.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained on purpose: inline CSS, no external asset, no dependency on
|
|
5
|
+
* the Vite build (so the login page keeps working while the SPA is being
|
|
6
|
+
* rebuilt, and nginx can serve it as the one unauthenticated page).
|
|
7
|
+
*
|
|
8
|
+
* The form posts `application/x-www-form-urlencoded`, which every browser and
|
|
9
|
+
* password manager understands; the JSON body form is accepted too (curl /
|
|
10
|
+
* API clients). ALL pages returned here are `Cache-Control: no-store`.
|
|
11
|
+
*/
|
|
12
|
+
/** The page itself (the error slot is the only dynamic part). */
|
|
13
|
+
const HEAD = `<!doctype html>
|
|
14
|
+
<html lang="zh-CN">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
18
|
+
<meta name="robots" content="noindex">
|
|
19
|
+
<title>Celestea Studio 登录</title>
|
|
20
|
+
<style>
|
|
21
|
+
:root{color-scheme:dark light}
|
|
22
|
+
*{box-sizing:border-box}
|
|
23
|
+
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;
|
|
24
|
+
background:#0f1115;color:#e8eaed;font:16px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans SC",sans-serif}
|
|
25
|
+
main{width:100%;max-width:22rem;padding:2rem 1.5rem}
|
|
26
|
+
h1{margin:0 0 .25rem;font-size:1.35rem;font-weight:600}
|
|
27
|
+
p.sub{margin:0 0 1.5rem;color:#9aa0a6;font-size:.9rem}
|
|
28
|
+
label{display:block;margin:0 0 .35rem;font-size:.85rem;color:#bdc1c6}
|
|
29
|
+
input{width:100%;padding:.65rem .75rem;margin:0 0 1rem;border-radius:.5rem;border:1px solid #3c4043;
|
|
30
|
+
background:#1a1d21;color:inherit;font-size:1rem}
|
|
31
|
+
input:focus{outline:2px solid #8ab4f8;outline-offset:1px;border-color:#8ab4f8}
|
|
32
|
+
button{width:100%;padding:.7rem;border:0;border-radius:.5rem;background:#8ab4f8;color:#202124;
|
|
33
|
+
font-size:1rem;font-weight:600;cursor:pointer}
|
|
34
|
+
button:hover{background:#aecbfa}
|
|
35
|
+
.err{margin:0 0 1rem;padding:.6rem .75rem;border-radius:.5rem;background:#3b1d1d;border:1px solid #8c2f2f;
|
|
36
|
+
color:#f6aea9;font-size:.9rem}
|
|
37
|
+
footer{margin:1.5rem 0 0;color:#5f6368;font-size:.75rem;text-align:center}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body><main>
|
|
41
|
+
<h1>Celestea Studio</h1>
|
|
42
|
+
<p class="sub">请登录后继续</p>
|
|
43
|
+
`;
|
|
44
|
+
/** `error === null` = a clean form; otherwise the message is shown above it. */
|
|
45
|
+
export function loginPage(error = null) {
|
|
46
|
+
const slot = error === null ? "" : `<p class="err">${escapeHtml(error)}</p>`;
|
|
47
|
+
return `${HEAD}${slot}<form method="POST" action="/auth/login">
|
|
48
|
+
<label for="u">用户名</label>
|
|
49
|
+
<input id="u" name="username" autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus>
|
|
50
|
+
<label for="p">密码</label>
|
|
51
|
+
<input id="p" name="password" type="password" autocomplete="current-password" required>
|
|
52
|
+
<button type="submit">登录</button>
|
|
53
|
+
</form>
|
|
54
|
+
<footer>celestea-studio · 登录状态保持 30 天</footer>
|
|
55
|
+
</main></body></html>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The 200 answer to a SUCCESSFUL form login. It is a page (never a 302): a
|
|
60
|
+
* redirect response is allowed to drop `Set-Cookie` on some mobile clients, so
|
|
61
|
+
* the cookie and the navigation travel in the SAME 200 response.
|
|
62
|
+
*/
|
|
63
|
+
export const LOGIN_OK_PAGE = `<!doctype html>
|
|
64
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="robots" content="noindex">
|
|
65
|
+
<title>已登录 · Celestea Studio</title></head>
|
|
66
|
+
<body><p>登录成功,正在进入…</p>
|
|
67
|
+
<script>location.replace("/")</script>
|
|
68
|
+
<noscript><a href="/">进入 Celestea Studio</a></noscript>
|
|
69
|
+
</body></html>
|
|
70
|
+
`;
|
|
71
|
+
function escapeHtml(text) {
|
|
72
|
+
return text.replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W767 — login failure limiter (fixed 60 s window).
|
|
3
|
+
*
|
|
4
|
+
* Counted per USERNAME and per CLIENT IP separately, so neither "spray one
|
|
5
|
+
* password over many accounts" nor "spray many passwords at one account" gets a
|
|
6
|
+
* free run, while a legitimate user behind a shared IP is only slowed after the
|
|
7
|
+
* same key fails repeatedly. Only FAILURES count; a success clears both keys of
|
|
8
|
+
* that attempt. In-memory on purpose: Studio is a single process, the window is
|
|
9
|
+
* one minute, and the counter must not survive a restart as a lockout.
|
|
10
|
+
*/
|
|
11
|
+
/** Window length in ms (the spec's 60 s). */
|
|
12
|
+
export declare const AUTH_WINDOW_MS = 60000;
|
|
13
|
+
/** Failures allowed inside one window before 429. */
|
|
14
|
+
export declare const AUTH_MAX_FAILURES = 5;
|
|
15
|
+
export interface FailureLimiter {
|
|
16
|
+
/** true = the key already exhausted its window (the caller answers 429). */
|
|
17
|
+
blocked(key: string): boolean;
|
|
18
|
+
/** Record one failure for the key (opens/extends its window). */
|
|
19
|
+
fail(key: string): void;
|
|
20
|
+
/** Forget the key (a successful login). */
|
|
21
|
+
clear(key: string): void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createFailureLimiter(opts: {
|
|
24
|
+
now: () => number;
|
|
25
|
+
windowMs?: number;
|
|
26
|
+
maxFailures?: number;
|
|
27
|
+
}): FailureLimiter;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W767 — login failure limiter (fixed 60 s window).
|
|
3
|
+
*
|
|
4
|
+
* Counted per USERNAME and per CLIENT IP separately, so neither "spray one
|
|
5
|
+
* password over many accounts" nor "spray many passwords at one account" gets a
|
|
6
|
+
* free run, while a legitimate user behind a shared IP is only slowed after the
|
|
7
|
+
* same key fails repeatedly. Only FAILURES count; a success clears both keys of
|
|
8
|
+
* that attempt. In-memory on purpose: Studio is a single process, the window is
|
|
9
|
+
* one minute, and the counter must not survive a restart as a lockout.
|
|
10
|
+
*/
|
|
11
|
+
/** Window length in ms (the spec's 60 s). */
|
|
12
|
+
export const AUTH_WINDOW_MS = 60_000;
|
|
13
|
+
/** Failures allowed inside one window before 429. */
|
|
14
|
+
export const AUTH_MAX_FAILURES = 5;
|
|
15
|
+
/** Ceiling on tracked keys: a spray can never grow the map without bound. */
|
|
16
|
+
const MAX_KEYS = 4_096;
|
|
17
|
+
export function createFailureLimiter(opts) {
|
|
18
|
+
const windowMs = opts.windowMs ?? AUTH_WINDOW_MS;
|
|
19
|
+
const maxFailures = opts.maxFailures ?? AUTH_MAX_FAILURES;
|
|
20
|
+
const hits = new Map();
|
|
21
|
+
return {
|
|
22
|
+
blocked(key) {
|
|
23
|
+
const entry = hits.get(key);
|
|
24
|
+
return entry !== undefined && entry.resetAt > opts.now() && entry.count >= maxFailures;
|
|
25
|
+
},
|
|
26
|
+
fail(key) {
|
|
27
|
+
const now = opts.now();
|
|
28
|
+
prune(hits, now);
|
|
29
|
+
if (hits.size >= MAX_KEYS)
|
|
30
|
+
hits.clear();
|
|
31
|
+
const entry = hits.get(key);
|
|
32
|
+
if (entry === undefined || entry.resetAt <= now)
|
|
33
|
+
hits.set(key, { count: 1, resetAt: now + windowMs });
|
|
34
|
+
else
|
|
35
|
+
entry.count += 1;
|
|
36
|
+
},
|
|
37
|
+
clear(key) {
|
|
38
|
+
hits.delete(key);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function prune(hits, now) {
|
|
43
|
+
for (const [key, entry] of hits) {
|
|
44
|
+
if (entry.resetAt <= now)
|
|
45
|
+
hits.delete(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W767 — Studio's OWN login cookie: token minting, verification, secret file.
|
|
3
|
+
*
|
|
4
|
+
* The cookie gate belongs to Studio alone: this module knows nothing about any
|
|
5
|
+
* other service, portal or credential store. The only external input is Studio's
|
|
6
|
+
* own password file, checked in `htpasswd.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Token wire format (four dot-separated parts, all but the expiry base64url):
|
|
9
|
+
*
|
|
10
|
+
* <b64url(user)> . <b64url(rand16)> . <expUnix> . <b64url(HMAC-SHA256)>
|
|
11
|
+
*
|
|
12
|
+
* The signature covers the FIRST THREE parts verbatim, so neither the user, the
|
|
13
|
+
* nonce nor the expiry can be edited without invalidating the cookie. The nonce
|
|
14
|
+
* makes every issued token unique (no replay of a captured value into a second
|
|
15
|
+
* browser). The secret is 32 random bytes kept in Studio's own data directory
|
|
16
|
+
* (mode 0600); it is created on first use and reused afterwards, so a restart
|
|
17
|
+
* does NOT log every user out.
|
|
18
|
+
*/
|
|
19
|
+
/** Cookie name (also the nginx `auth_request` marker name). */
|
|
20
|
+
export declare const AUTH_COOKIE = "studio_auth";
|
|
21
|
+
/** `<data dir>/studio-auth.secret` — this project's own secret, nothing else's. */
|
|
22
|
+
export declare const AUTH_SECRET_FILE = "studio-auth.secret";
|
|
23
|
+
/** 30 days, in seconds (the cookie's `Max-Age`). */
|
|
24
|
+
export declare const AUTH_TTL_SECONDS = 2592000;
|
|
25
|
+
/** Secret length in bytes (base64url-encoded on disk). */
|
|
26
|
+
export declare const AUTH_SECRET_BYTES = 32;
|
|
27
|
+
export type TokenCheck = {
|
|
28
|
+
ok: true;
|
|
29
|
+
user: string;
|
|
30
|
+
} | {
|
|
31
|
+
ok: false;
|
|
32
|
+
reason: "malformed" | "bad_signature" | "expired";
|
|
33
|
+
};
|
|
34
|
+
/** base64url, no padding — the only encoding this cookie uses. */
|
|
35
|
+
export declare function b64url(input: Buffer | string): string;
|
|
36
|
+
/** Mint one token for `user`; `nowSeconds` is unix seconds (injectable for tests). */
|
|
37
|
+
export declare function mintToken(user: string, secret: Buffer, nowSeconds: number, ttlSeconds?: number): string;
|
|
38
|
+
/** Verify a token: shape, signature (constant-time) and expiry, in that order. */
|
|
39
|
+
export declare function verifyToken(token: string, secret: Buffer, nowSeconds: number): TokenCheck;
|
|
40
|
+
/** The `Set-Cookie` value: HttpOnly + Secure + SameSite=Lax, 30 days. */
|
|
41
|
+
export declare function authCookie(token: string): string;
|
|
42
|
+
/** One cookie's raw value out of a `Cookie:` header (null when absent). */
|
|
43
|
+
export declare function cookieValue(header: string | undefined, name: string): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Read the secret, creating it (32 random bytes, base64url, mode 0600) when the
|
|
46
|
+
* file is missing or unusable. A short/garbled file is regenerated rather than
|
|
47
|
+
* silently widening the key space.
|
|
48
|
+
*/
|
|
49
|
+
export declare function loadAuthSecret(path: string): Buffer;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W767 — Studio's OWN login cookie: token minting, verification, secret file.
|
|
3
|
+
*
|
|
4
|
+
* The cookie gate belongs to Studio alone: this module knows nothing about any
|
|
5
|
+
* other service, portal or credential store. The only external input is Studio's
|
|
6
|
+
* own password file, checked in `htpasswd.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Token wire format (four dot-separated parts, all but the expiry base64url):
|
|
9
|
+
*
|
|
10
|
+
* <b64url(user)> . <b64url(rand16)> . <expUnix> . <b64url(HMAC-SHA256)>
|
|
11
|
+
*
|
|
12
|
+
* The signature covers the FIRST THREE parts verbatim, so neither the user, the
|
|
13
|
+
* nonce nor the expiry can be edited without invalidating the cookie. The nonce
|
|
14
|
+
* makes every issued token unique (no replay of a captured value into a second
|
|
15
|
+
* browser). The secret is 32 random bytes kept in Studio's own data directory
|
|
16
|
+
* (mode 0600); it is created on first use and reused afterwards, so a restart
|
|
17
|
+
* does NOT log every user out.
|
|
18
|
+
*/
|
|
19
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
20
|
+
import { chmodSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
/** Cookie name (also the nginx `auth_request` marker name). */
|
|
22
|
+
export const AUTH_COOKIE = "studio_auth";
|
|
23
|
+
/** `<data dir>/studio-auth.secret` — this project's own secret, nothing else's. */
|
|
24
|
+
export const AUTH_SECRET_FILE = "studio-auth.secret";
|
|
25
|
+
/** 30 days, in seconds (the cookie's `Max-Age`). */
|
|
26
|
+
export const AUTH_TTL_SECONDS = 2_592_000;
|
|
27
|
+
/** Secret length in bytes (base64url-encoded on disk). */
|
|
28
|
+
export const AUTH_SECRET_BYTES = 32;
|
|
29
|
+
/** Upper bound on a cookie we are willing to decode (a 4-part token is ~120). */
|
|
30
|
+
const MAX_TOKEN_CHARS = 512;
|
|
31
|
+
/** base64url, no padding — the only encoding this cookie uses. */
|
|
32
|
+
export function b64url(input) {
|
|
33
|
+
return Buffer.from(input).toString("base64url");
|
|
34
|
+
}
|
|
35
|
+
function signature(secret, payload) {
|
|
36
|
+
return createHmac("sha256", secret).update(payload).digest();
|
|
37
|
+
}
|
|
38
|
+
/** Mint one token for `user`; `nowSeconds` is unix seconds (injectable for tests). */
|
|
39
|
+
export function mintToken(user, secret, nowSeconds, ttlSeconds = AUTH_TTL_SECONDS) {
|
|
40
|
+
const exp = Math.floor(nowSeconds + ttlSeconds);
|
|
41
|
+
const head = `${b64url(user)}.${b64url(randomBytes(16))}.${exp}`;
|
|
42
|
+
return `${head}.${b64url(signature(secret, head))}`;
|
|
43
|
+
}
|
|
44
|
+
/** Verify a token: shape, signature (constant-time) and expiry, in that order. */
|
|
45
|
+
export function verifyToken(token, secret, nowSeconds) {
|
|
46
|
+
if (token.length === 0 || token.length > MAX_TOKEN_CHARS)
|
|
47
|
+
return { ok: false, reason: "malformed" };
|
|
48
|
+
const parts = token.split(".");
|
|
49
|
+
if (parts.length !== 4)
|
|
50
|
+
return { ok: false, reason: "malformed" };
|
|
51
|
+
const [userPart, noncePart, expPart, sigPart] = parts;
|
|
52
|
+
const expected = signature(secret, `${userPart}.${noncePart}.${expPart}`);
|
|
53
|
+
const given = Buffer.from(sigPart, "base64url");
|
|
54
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected))
|
|
55
|
+
return { ok: false, reason: "bad_signature" };
|
|
56
|
+
const exp = Number.parseInt(expPart, 10);
|
|
57
|
+
if (!Number.isSafeInteger(exp))
|
|
58
|
+
return { ok: false, reason: "malformed" };
|
|
59
|
+
if (exp <= Math.floor(nowSeconds))
|
|
60
|
+
return { ok: false, reason: "expired" };
|
|
61
|
+
const user = Buffer.from(userPart, "base64url").toString("utf8");
|
|
62
|
+
return user === "" ? { ok: false, reason: "malformed" } : { ok: true, user };
|
|
63
|
+
}
|
|
64
|
+
/** The `Set-Cookie` value: HttpOnly + Secure + SameSite=Lax, 30 days. */
|
|
65
|
+
export function authCookie(token) {
|
|
66
|
+
return `${AUTH_COOKIE}=${token}; Path=/; Max-Age=${AUTH_TTL_SECONDS}; HttpOnly; Secure; SameSite=Lax`;
|
|
67
|
+
}
|
|
68
|
+
/** One cookie's raw value out of a `Cookie:` header (null when absent). */
|
|
69
|
+
export function cookieValue(header, name) {
|
|
70
|
+
if (header === undefined)
|
|
71
|
+
return null;
|
|
72
|
+
for (const part of header.split(";")) {
|
|
73
|
+
const eq = part.indexOf("=");
|
|
74
|
+
if (eq <= 0)
|
|
75
|
+
continue;
|
|
76
|
+
if (part.slice(0, eq).trim() === name)
|
|
77
|
+
return part.slice(eq + 1).trim();
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Read the secret, creating it (32 random bytes, base64url, mode 0600) when the
|
|
83
|
+
* file is missing or unusable. A short/garbled file is regenerated rather than
|
|
84
|
+
* silently widening the key space.
|
|
85
|
+
*/
|
|
86
|
+
export function loadAuthSecret(path) {
|
|
87
|
+
const existing = readSecretText(path);
|
|
88
|
+
if (existing !== null) {
|
|
89
|
+
const decoded = Buffer.from(existing, "base64url");
|
|
90
|
+
if (decoded.length >= AUTH_SECRET_BYTES)
|
|
91
|
+
return decoded;
|
|
92
|
+
}
|
|
93
|
+
const secret = randomBytes(AUTH_SECRET_BYTES);
|
|
94
|
+
writeFileSync(path, `${secret.toString("base64url")}\n`, { mode: 0o600 });
|
|
95
|
+
// `mode` is masked by the umask at creation; make 0600 unconditional.
|
|
96
|
+
try {
|
|
97
|
+
chmodSync(path, 0o600);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// A filesystem without chmod support still has the creation mode.
|
|
101
|
+
}
|
|
102
|
+
return secret;
|
|
103
|
+
}
|
|
104
|
+
function readSecretText(path) {
|
|
105
|
+
try {
|
|
106
|
+
const text = readFileSync(path, "utf8").trim();
|
|
107
|
+
return text === "" ? null : text;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio host configuration (paths + frozen constants).
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a *host* decision: where the data files live, which
|
|
5
|
+
* directory the frontend build is served from, and the compile-time constants
|
|
6
|
+
* the legacy backend hardcoded (`DEFAULT_BIND`, `STATIC_ROOT`, `FS_ROOTS`).
|
|
7
|
+
* No engine semantics live here — those belong to the injected RuntimeAdapter.
|
|
8
|
+
*/
|
|
9
|
+
/** `src/main.rs:867` — the DEFAULT bind; the live one is written back at listen time. */
|
|
10
|
+
export declare const DEFAULT_BIND = "127.0.0.1:3777";
|
|
11
|
+
/**
|
|
12
|
+
* `src/main.rs` STATIC_ROOT: the Vite build, served read-only.
|
|
13
|
+
*
|
|
14
|
+
* W782: DERIVED, never written down. `apps/web/dist` lives inside this very
|
|
15
|
+
* checkout, so the repo root comes from `studioRepoRoot()` (this file's own
|
|
16
|
+
* location) — the previous literal broke silently every time the checkout moved
|
|
17
|
+
* or the frontend was relocated (it was `celestea_studio/frontend/dist` before
|
|
18
|
+
* W781). `STUDIO_STATIC_ROOT` still overrides it for an operator.
|
|
19
|
+
*
|
|
20
|
+
* H: an INSTALLED `@celestea/studio` has no checkout, so the frontend build is
|
|
21
|
+
* staged at `<package>/webdist` by `scripts/build-webdist.mjs` and preferred
|
|
22
|
+
* when present. A source checkout keeps `apps/web/dist` exactly as before.
|
|
23
|
+
*/
|
|
24
|
+
export declare function defaultStaticRoot(): string;
|
|
25
|
+
/** `src/main.rs:1311` broadcast capacity; slow clients degrade to `lagged`. */
|
|
26
|
+
export declare const BUS_CAPACITY = 512;
|
|
27
|
+
/** `src/workspaces.rs:113` informational roots shown by GET /api/fs/browse. */
|
|
28
|
+
export declare const FS_ROOTS: readonly string[];
|
|
29
|
+
/** `src/workspaces.rs:115` fs browse entry cap. */
|
|
30
|
+
export declare const MAX_DIR_ENTRIES = 200;
|
|
31
|
+
/** `src/api.rs` MIN_STEPS: POST /api/config can only raise max_steps. */
|
|
32
|
+
export declare const MIN_STEPS = 4096;
|
|
33
|
+
/** Contract default context window (statusline fallback). */
|
|
34
|
+
export declare const CONTEXT_WINDOW = 1000000;
|
|
35
|
+
/**
|
|
36
|
+
* W767: Studio's OWN password file (read-only input to `htpasswd -vbi`). This
|
|
37
|
+
* is the file this project owns; no other service's credential store is ever
|
|
38
|
+
* consulted.
|
|
39
|
+
*/
|
|
40
|
+
export declare const DEFAULT_AUTH_HTPASSWD_FILE = "/etc/nginx/.htpasswd-studio";
|
|
41
|
+
export interface StudioPaths {
|
|
42
|
+
/** CELESTEA_WORKSPACES_FILE ?? <cwd>/workspaces.json (mode 0644). */
|
|
43
|
+
workspacesFile: string;
|
|
44
|
+
/** CELESTEA_PROVIDERS_FILE ?? <cwd>/providers.json (mode 0600, secret). */
|
|
45
|
+
providersFile: string;
|
|
46
|
+
/** CELESTEA_PROMPTS_FILE ?? <cwd>/prompts.json (mode 0644). */
|
|
47
|
+
promptsFile: string;
|
|
48
|
+
/** STUDIO_STATIC_ROOT ?? the Vite build directory. */
|
|
49
|
+
staticRoot: string;
|
|
50
|
+
/**
|
|
51
|
+
* W767: `<data dir>/studio-auth.secret` (0600, created on first use) — the
|
|
52
|
+
* HMAC key of Studio's own login cookie. `CELESTEA_AUTH_SECRET_FILE` overrides.
|
|
53
|
+
*/
|
|
54
|
+
authSecretFile: string;
|
|
55
|
+
/** W767: the read-only password file `CELESTEA_AUTH_HTPASSWD_FILE` overrides. */
|
|
56
|
+
authHtpasswdFile: string;
|
|
57
|
+
}
|
|
58
|
+
export interface StudioConfig {
|
|
59
|
+
/** Constant "celestea-studio" in /api/health. */
|
|
60
|
+
name: string;
|
|
61
|
+
bind: string;
|
|
62
|
+
apiKeyEnv: string;
|
|
63
|
+
/**
|
|
64
|
+
* H-security: the self-cert bearer token required on every `/api/*` request
|
|
65
|
+
* except `/api/health`. `null` = no token (loopback / nginx-delegated).
|
|
66
|
+
*/
|
|
67
|
+
authToken: string | null;
|
|
68
|
+
paths: StudioPaths;
|
|
69
|
+
}
|
|
70
|
+
export interface StudioConfigInput {
|
|
71
|
+
cwd?: string;
|
|
72
|
+
env?: NodeJS.ProcessEnv;
|
|
73
|
+
paths?: Partial<StudioPaths>;
|
|
74
|
+
/** H-security: explicit token (the `--token` flag); else `CELESTEA_AUTH_TOKEN`. */
|
|
75
|
+
authToken?: string | null;
|
|
76
|
+
}
|
|
77
|
+
/** Build the host config from the environment; every value is overridable. */
|
|
78
|
+
export declare function loadStudioConfig(input?: StudioConfigInput): StudioConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio host configuration (paths + frozen constants).
|
|
3
|
+
*
|
|
4
|
+
* Everything here is a *host* decision: where the data files live, which
|
|
5
|
+
* directory the frontend build is served from, and the compile-time constants
|
|
6
|
+
* the legacy backend hardcoded (`DEFAULT_BIND`, `STATIC_ROOT`, `FS_ROOTS`).
|
|
7
|
+
* No engine semantics live here — those belong to the injected RuntimeAdapter.
|
|
8
|
+
*/
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { AUTH_SECRET_FILE } from "./auth/token.js";
|
|
11
|
+
import { readAuthToken } from "./auth/api-token.js";
|
|
12
|
+
import { packagedWebDist, studioRepoRoot } from "./deployment.js";
|
|
13
|
+
/** `src/main.rs:867` — the DEFAULT bind; the live one is written back at listen time. */
|
|
14
|
+
export const DEFAULT_BIND = "127.0.0.1:3777";
|
|
15
|
+
/**
|
|
16
|
+
* `src/main.rs` STATIC_ROOT: the Vite build, served read-only.
|
|
17
|
+
*
|
|
18
|
+
* W782: DERIVED, never written down. `apps/web/dist` lives inside this very
|
|
19
|
+
* checkout, so the repo root comes from `studioRepoRoot()` (this file's own
|
|
20
|
+
* location) — the previous literal broke silently every time the checkout moved
|
|
21
|
+
* or the frontend was relocated (it was `celestea_studio/frontend/dist` before
|
|
22
|
+
* W781). `STUDIO_STATIC_ROOT` still overrides it for an operator.
|
|
23
|
+
*
|
|
24
|
+
* H: an INSTALLED `@celestea/studio` has no checkout, so the frontend build is
|
|
25
|
+
* staged at `<package>/webdist` by `scripts/build-webdist.mjs` and preferred
|
|
26
|
+
* when present. A source checkout keeps `apps/web/dist` exactly as before.
|
|
27
|
+
*/
|
|
28
|
+
export function defaultStaticRoot() {
|
|
29
|
+
return packagedWebDist() ?? join(studioRepoRoot(), "apps", "web", "dist");
|
|
30
|
+
}
|
|
31
|
+
/** `src/main.rs:1311` broadcast capacity; slow clients degrade to `lagged`. */
|
|
32
|
+
export const BUS_CAPACITY = 512;
|
|
33
|
+
/** `src/workspaces.rs:113` informational roots shown by GET /api/fs/browse. */
|
|
34
|
+
export const FS_ROOTS = ["/src", "/tmp", "/srv", "/home"];
|
|
35
|
+
/** `src/workspaces.rs:115` fs browse entry cap. */
|
|
36
|
+
export const MAX_DIR_ENTRIES = 200;
|
|
37
|
+
/** `src/api.rs` MIN_STEPS: POST /api/config can only raise max_steps. */
|
|
38
|
+
export const MIN_STEPS = 4096;
|
|
39
|
+
/** Contract default context window (statusline fallback). */
|
|
40
|
+
export const CONTEXT_WINDOW = 1_000_000;
|
|
41
|
+
/**
|
|
42
|
+
* W767: Studio's OWN password file (read-only input to `htpasswd -vbi`). This
|
|
43
|
+
* is the file this project owns; no other service's credential store is ever
|
|
44
|
+
* consulted.
|
|
45
|
+
*/
|
|
46
|
+
export const DEFAULT_AUTH_HTPASSWD_FILE = "/etc/nginx/.htpasswd-studio";
|
|
47
|
+
/** Build the host config from the environment; every value is overridable. */
|
|
48
|
+
export function loadStudioConfig(input = {}) {
|
|
49
|
+
const env = input.env ?? process.env;
|
|
50
|
+
const cwd = input.cwd ?? process.cwd();
|
|
51
|
+
const explicit = input.paths ?? {};
|
|
52
|
+
const workspacesFile = explicit.workspacesFile ?? env["CELESTEA_WORKSPACES_FILE"] ?? resolve(cwd, "workspaces.json");
|
|
53
|
+
const paths = {
|
|
54
|
+
workspacesFile,
|
|
55
|
+
providersFile: explicit.providersFile ?? env["CELESTEA_PROVIDERS_FILE"] ?? resolve(cwd, "providers.json"),
|
|
56
|
+
promptsFile: explicit.promptsFile ?? env["CELESTEA_PROMPTS_FILE"] ?? resolve(cwd, "prompts.json"),
|
|
57
|
+
staticRoot: explicit.staticRoot ?? env["STUDIO_STATIC_ROOT"] ?? defaultStaticRoot(),
|
|
58
|
+
// W767: the login secret lives in the SAME data dir as workspaces.json —
|
|
59
|
+
// `createStudioEngine` derives `<data dir>` the same way, so there is one
|
|
60
|
+
// notion of "Studio's data directory", not two.
|
|
61
|
+
authSecretFile: explicit.authSecretFile ?? env["CELESTEA_AUTH_SECRET_FILE"] ?? join(dirname(workspacesFile), AUTH_SECRET_FILE),
|
|
62
|
+
authHtpasswdFile: explicit.authHtpasswdFile ?? env["CELESTEA_AUTH_HTPASSWD_FILE"] ?? DEFAULT_AUTH_HTPASSWD_FILE,
|
|
63
|
+
};
|
|
64
|
+
const token = input.authToken ?? readAuthToken(env);
|
|
65
|
+
return {
|
|
66
|
+
name: "celestea-studio",
|
|
67
|
+
bind: DEFAULT_BIND,
|
|
68
|
+
apiKeyEnv: env["CELESTEA_API_KEY_ENV"] ?? "CELESTEA_API_KEY",
|
|
69
|
+
authToken: token === null || token.trim() === "" ? null : token.trim(),
|
|
70
|
+
paths,
|
|
71
|
+
};
|
|
72
|
+
}
|