pi-forge 0.0.0 → 1.1.4

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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -4
  3. package/bin/pi-forge.mjs +37 -0
  4. package/dist/client/assets/CodeMirrorEditor-BqaaP1EE.js +34 -0
  5. package/dist/client/assets/CodeMirrorEditor-BqaaP1EE.js.map +1 -0
  6. package/dist/client/assets/index-B-529kgJ.css +32 -0
  7. package/dist/client/assets/index-BzKzxXFs.js +392 -0
  8. package/dist/client/assets/index-BzKzxXFs.js.map +1 -0
  9. package/dist/client/assets/workbox-window.prod.es5-BBnX5xw4.js +3 -0
  10. package/dist/client/assets/workbox-window.prod.es5-BBnX5xw4.js.map +1 -0
  11. package/dist/client/icons/icon-192.png +0 -0
  12. package/dist/client/icons/icon-512.png +0 -0
  13. package/dist/client/icons/icon-maskable-512.png +0 -0
  14. package/dist/client/icons/icon.svg +9 -0
  15. package/dist/client/index.html +24 -0
  16. package/dist/client/manifest.webmanifest +1 -0
  17. package/dist/client/offline.html +142 -0
  18. package/dist/client/sw.js +3 -0
  19. package/dist/client/sw.js.map +1 -0
  20. package/dist/client/workbox-6d7155ed.js +3 -0
  21. package/dist/client/workbox-6d7155ed.js.map +1 -0
  22. package/dist/server/agent-resource-loader.js +126 -0
  23. package/dist/server/agent-resource-loader.js.map +1 -0
  24. package/dist/server/attachment-converters.js +96 -0
  25. package/dist/server/attachment-converters.js.map +1 -0
  26. package/dist/server/auth.js +209 -0
  27. package/dist/server/auth.js.map +1 -0
  28. package/dist/server/compaction-history.js +106 -0
  29. package/dist/server/compaction-history.js.map +1 -0
  30. package/dist/server/concurrency.js +49 -0
  31. package/dist/server/concurrency.js.map +1 -0
  32. package/dist/server/config-export.js +220 -0
  33. package/dist/server/config-export.js.map +1 -0
  34. package/dist/server/config-manager.js +528 -0
  35. package/dist/server/config-manager.js.map +1 -0
  36. package/dist/server/config.js +326 -0
  37. package/dist/server/config.js.map +1 -0
  38. package/dist/server/conversion-worker.mjs +90 -0
  39. package/dist/server/diagnostics.js +137 -0
  40. package/dist/server/diagnostics.js.map +1 -0
  41. package/dist/server/extensions-discovery.js +147 -0
  42. package/dist/server/extensions-discovery.js.map +1 -0
  43. package/dist/server/file-manager.js +734 -0
  44. package/dist/server/file-manager.js.map +1 -0
  45. package/dist/server/file-references.js +215 -0
  46. package/dist/server/file-references.js.map +1 -0
  47. package/dist/server/file-searcher.js +385 -0
  48. package/dist/server/file-searcher.js.map +1 -0
  49. package/dist/server/git-runner.js +684 -0
  50. package/dist/server/git-runner.js.map +1 -0
  51. package/dist/server/index.js +468 -0
  52. package/dist/server/index.js.map +1 -0
  53. package/dist/server/mcp/config.js +133 -0
  54. package/dist/server/mcp/config.js.map +1 -0
  55. package/dist/server/mcp/manager.js +351 -0
  56. package/dist/server/mcp/manager.js.map +1 -0
  57. package/dist/server/mcp/tool-bridge.js +173 -0
  58. package/dist/server/mcp/tool-bridge.js.map +1 -0
  59. package/dist/server/project-manager.js +301 -0
  60. package/dist/server/project-manager.js.map +1 -0
  61. package/dist/server/pty-manager.js +354 -0
  62. package/dist/server/pty-manager.js.map +1 -0
  63. package/dist/server/routes/_schemas.js +73 -0
  64. package/dist/server/routes/_schemas.js.map +1 -0
  65. package/dist/server/routes/auth.js +164 -0
  66. package/dist/server/routes/auth.js.map +1 -0
  67. package/dist/server/routes/config.js +1163 -0
  68. package/dist/server/routes/config.js.map +1 -0
  69. package/dist/server/routes/control.js +464 -0
  70. package/dist/server/routes/control.js.map +1 -0
  71. package/dist/server/routes/exec.js +217 -0
  72. package/dist/server/routes/exec.js.map +1 -0
  73. package/dist/server/routes/files.js +847 -0
  74. package/dist/server/routes/files.js.map +1 -0
  75. package/dist/server/routes/git.js +837 -0
  76. package/dist/server/routes/git.js.map +1 -0
  77. package/dist/server/routes/health.js +97 -0
  78. package/dist/server/routes/health.js.map +1 -0
  79. package/dist/server/routes/mcp.js +300 -0
  80. package/dist/server/routes/mcp.js.map +1 -0
  81. package/dist/server/routes/projects.js +259 -0
  82. package/dist/server/routes/projects.js.map +1 -0
  83. package/dist/server/routes/prompt.js +496 -0
  84. package/dist/server/routes/prompt.js.map +1 -0
  85. package/dist/server/routes/sessions.js +783 -0
  86. package/dist/server/routes/sessions.js.map +1 -0
  87. package/dist/server/routes/stream.js +69 -0
  88. package/dist/server/routes/stream.js.map +1 -0
  89. package/dist/server/routes/terminal.js +335 -0
  90. package/dist/server/routes/terminal.js.map +1 -0
  91. package/dist/server/session-registry.js +1197 -0
  92. package/dist/server/session-registry.js.map +1 -0
  93. package/dist/server/skill-overrides.js +151 -0
  94. package/dist/server/skill-overrides.js.map +1 -0
  95. package/dist/server/skills-export.js +257 -0
  96. package/dist/server/skills-export.js.map +1 -0
  97. package/dist/server/sse-bridge.js +220 -0
  98. package/dist/server/sse-bridge.js.map +1 -0
  99. package/dist/server/tool-overrides.js +277 -0
  100. package/dist/server/tool-overrides.js.map +1 -0
  101. package/dist/server/turn-diff-builder.js +280 -0
  102. package/dist/server/turn-diff-builder.js.map +1 -0
  103. package/package.json +53 -12
@@ -0,0 +1,354 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as nodePty from "node-pty";
3
+ import { config } from "./config.js";
4
+ /** Rolling output buffer cap per PTY (in bytes). 256 KB ≈ ~3000 lines of typical shell output. */
5
+ const OUTPUT_BUFFER_BYTES = 256 * 1024;
6
+ /**
7
+ * Time a detached PTY (no WS attached) is held alive before being reaped.
8
+ * Sourced from `config.terminalIdleReapMs` so operators in resource-
9
+ * constrained deployments can shorten the window, AND so the integration
10
+ * test can pin it down to ~200 ms to actually exercise the reap path
11
+ * within a test budget.
12
+ */
13
+ const IDLE_REAP_MS = config.terminalIdleReapMs;
14
+ const ptys = new Map();
15
+ function defaultShell() {
16
+ return process.env.SHELL ?? "/bin/sh";
17
+ }
18
+ /**
19
+ * Find an existing PTY for `tabId` within `projectId`. Returns
20
+ * undefined if none exists (caller should spawn) or if a PTY with
21
+ * that tabId belongs to a DIFFERENT project (caller should treat as
22
+ * "no match" — never reattach across projects, that would expose
23
+ * one project's shell to another).
24
+ */
25
+ export function findPtyByTabId(tabId, projectId) {
26
+ for (const entry of ptys.values()) {
27
+ if (entry.managed.tabId !== tabId)
28
+ continue;
29
+ if (entry.managed.projectId !== projectId)
30
+ continue;
31
+ return entry.managed;
32
+ }
33
+ return undefined;
34
+ }
35
+ export function spawnPty(opts) {
36
+ const shell = opts.shell ?? defaultShell();
37
+ const cols = opts.cols ?? 80;
38
+ const rows = opts.rows ?? 24;
39
+ const env = opts.env ?? process.env;
40
+ const proc = nodePty.spawn(shell, [], {
41
+ name: "xterm-color",
42
+ cols,
43
+ rows,
44
+ cwd: opts.cwd,
45
+ env: filterEnv(env),
46
+ });
47
+ const ptyId = randomUUID();
48
+ const managed = {
49
+ ptyId,
50
+ tabId: opts.tabId,
51
+ projectId: opts.projectId,
52
+ process: proc,
53
+ cwd: opts.cwd,
54
+ };
55
+ const entry = {
56
+ managed,
57
+ dataDisposable: undefined,
58
+ closeActiveSocket: undefined,
59
+ idleTimer: undefined,
60
+ buffer: [],
61
+ bufferBytes: 0,
62
+ };
63
+ ptys.set(ptyId, entry);
64
+ // Always-on output capture, independent of any attached socket —
65
+ // that way disconnected periods still accumulate the rolling
66
+ // buffer for the next reattach to replay.
67
+ const captureDisposable = proc.onData((chunk) => {
68
+ appendToBuffer(entry, chunk);
69
+ });
70
+ proc.onExit(() => {
71
+ captureDisposable.dispose();
72
+ ptys.delete(ptyId);
73
+ if (entry.idleTimer !== undefined)
74
+ clearTimeout(entry.idleTimer);
75
+ });
76
+ return managed;
77
+ }
78
+ /**
79
+ * Attach a socket-style sink to a managed PTY. Replays the rolling
80
+ * output buffer immediately, then forwards every subsequent
81
+ * `onData` chunk to `onData(chunk)`. Returns a detach function the
82
+ * caller MUST invoke on socket close — without this, the prior
83
+ * sink keeps receiving bytes and a reattach can't replace it.
84
+ *
85
+ * Cancels any pending idle reaper — the PTY is back in active use.
86
+ *
87
+ * `replayBytes` lets the caller request only the tail of the
88
+ * buffer (e.g. xterm already has prior scrollback locally and only
89
+ * wants the last ~16 KB). Pass `Infinity` (default) to replay all.
90
+ */
91
+ export function attachSink(ptyId, onData, replayBytes = OUTPUT_BUFFER_BYTES, closeActiveSocket) {
92
+ const entry = ptys.get(ptyId);
93
+ if (entry === undefined)
94
+ return undefined;
95
+ if (entry.idleTimer !== undefined) {
96
+ clearTimeout(entry.idleTimer);
97
+ entry.idleTimer = undefined;
98
+ }
99
+ // Replace any existing data sink so a stale reconnect never
100
+ // double-delivers chunks. The previous detach() the caller
101
+ // captured still works (it disposes whatever disposable it
102
+ // captured), but the route should always call the latest detach.
103
+ if (entry.dataDisposable !== undefined) {
104
+ entry.dataDisposable.dispose();
105
+ entry.dataDisposable = undefined;
106
+ }
107
+ // Close the previously-attached WS, if any. Without this, two
108
+ // browsers with the same tabId both keep their input handlers wired
109
+ // up to write to the PTY; only the most recently attached one sees
110
+ // output, but BOTH can send input — interleaved keystrokes corrupt
111
+ // line-edit state. Closing the predecessor's WS lets the route's
112
+ // close handler unwire the input listener cleanly.
113
+ if (entry.closeActiveSocket !== undefined) {
114
+ try {
115
+ entry.closeActiveSocket();
116
+ }
117
+ catch {
118
+ // socket already gone — fine
119
+ }
120
+ entry.closeActiveSocket = undefined;
121
+ }
122
+ entry.closeActiveSocket = closeActiveSocket;
123
+ // Replay only the tail the caller asked for. The buffer is a
124
+ // chunk array; flatten just enough from the right edge to hit
125
+ // `replayBytes`. Edge case: replayBytes <= 0 → skip replay.
126
+ if (replayBytes > 0 && entry.bufferBytes > 0) {
127
+ let remaining = Math.min(replayBytes, entry.bufferBytes);
128
+ const tail = [];
129
+ for (let i = entry.buffer.length - 1; i >= 0 && remaining > 0; i--) {
130
+ const chunk = entry.buffer[i];
131
+ if (chunk.byteLength <= remaining) {
132
+ tail.unshift(chunk);
133
+ remaining -= chunk.byteLength;
134
+ }
135
+ else {
136
+ tail.unshift(chunk.subarray(chunk.byteLength - remaining));
137
+ remaining = 0;
138
+ }
139
+ }
140
+ for (const chunk of tail)
141
+ onData(chunk.toString("utf8"));
142
+ }
143
+ const live = entry.managed.process.onData((chunk) => {
144
+ onData(chunk);
145
+ });
146
+ entry.dataDisposable = live;
147
+ return () => {
148
+ if (entry.dataDisposable === live) {
149
+ live.dispose();
150
+ entry.dataDisposable = undefined;
151
+ // Only clear closeActiveSocket if WE are still the active
152
+ // attachment — a newer attachSink may have already swapped a
153
+ // different socket in.
154
+ if (entry.closeActiveSocket === closeActiveSocket) {
155
+ entry.closeActiveSocket = undefined;
156
+ }
157
+ }
158
+ else {
159
+ // A newer attach replaced ours; nothing to do.
160
+ }
161
+ // Start the idle reaper. If a fresh attach arrives within
162
+ // IDLE_REAP_MS the timer is cancelled in the next attachSink.
163
+ if (entry.idleTimer === undefined && ptys.has(ptyId)) {
164
+ entry.idleTimer = setTimeout(() => {
165
+ entry.idleTimer = undefined;
166
+ killPty(ptyId);
167
+ }, IDLE_REAP_MS);
168
+ }
169
+ };
170
+ }
171
+ function appendToBuffer(entry, chunk) {
172
+ const buf = Buffer.from(chunk, "utf8");
173
+ entry.buffer.push(buf);
174
+ entry.bufferBytes += buf.byteLength;
175
+ // Evict from the front until we're under cap. Keeps memory
176
+ // bounded across multi-hour shells running noisy output (npm
177
+ // install, pip install, etc.).
178
+ while (entry.bufferBytes > OUTPUT_BUFFER_BYTES && entry.buffer.length > 0) {
179
+ const head = entry.buffer.shift();
180
+ entry.bufferBytes -= head.byteLength;
181
+ }
182
+ }
183
+ export function getPty(ptyId) {
184
+ return ptys.get(ptyId)?.managed;
185
+ }
186
+ /**
187
+ * Grace window between SIGTERM and SIGKILL. Long enough for a
188
+ * well-behaved shell to clean up (zsh trap, bash exit handler), short
189
+ * enough that an unkillable shell doesn't linger past a deploy /
190
+ * shutdown for noticeable time.
191
+ */
192
+ const SIGKILL_GRACE_MS = 2_000;
193
+ export function killPty(ptyId) {
194
+ const entry = ptys.get(ptyId);
195
+ if (entry === undefined)
196
+ return false;
197
+ ptys.delete(ptyId);
198
+ if (entry.idleTimer !== undefined)
199
+ clearTimeout(entry.idleTimer);
200
+ if (entry.dataDisposable !== undefined)
201
+ entry.dataDisposable.dispose();
202
+ // Try SIGTERM first (gives the shell a chance to flush, run trap
203
+ // handlers, release tty). Schedule a SIGKILL fallback for trapped
204
+ // / unresponsive shells. Without the fallback, a `trap '' TERM`
205
+ // bash leaves an orphan process holding the PTY fd.
206
+ let killed = false;
207
+ const exitDisposable = entry.managed.process.onExit(() => {
208
+ killed = true;
209
+ });
210
+ try {
211
+ entry.managed.process.kill("SIGTERM");
212
+ }
213
+ catch {
214
+ // already exited between get + kill; nothing to do
215
+ return true;
216
+ }
217
+ setTimeout(() => {
218
+ exitDisposable.dispose();
219
+ if (killed)
220
+ return;
221
+ try {
222
+ entry.managed.process.kill("SIGKILL");
223
+ }
224
+ catch {
225
+ // already exited
226
+ }
227
+ }, SIGKILL_GRACE_MS).unref();
228
+ return true;
229
+ }
230
+ export function ptyCount() {
231
+ return ptys.size;
232
+ }
233
+ export function disposeAllPtys() {
234
+ for (const ptyId of Array.from(ptys.keys())) {
235
+ killPty(ptyId);
236
+ }
237
+ }
238
+ let exitHandlerInstalled = false;
239
+ /**
240
+ * Install a `process.on("exit")` last-resort SIGTERM-all handler.
241
+ * Idempotent.
242
+ *
243
+ * The previous version of this module called `installExitHandler()`
244
+ * at module load, which meant any unit test that imported the module
245
+ * also installed the handler — and the handler couldn't be undone.
246
+ * Tests that fork child processes ended up with the wrong handler
247
+ * count and unpredictable shutdown behavior. The fix: require an
248
+ * explicit `installPtyExitHandler()` call from `index.ts` (the
249
+ * production entry point); tests that import `pty-manager` for unit
250
+ * coverage skip the install.
251
+ */
252
+ export function installPtyExitHandler() {
253
+ if (exitHandlerInstalled)
254
+ return;
255
+ exitHandlerInstalled = true;
256
+ process.on("exit", () => {
257
+ for (const entry of ptys.values()) {
258
+ try {
259
+ entry.managed.process.kill("SIGTERM");
260
+ }
261
+ catch {
262
+ // Process already gone — fine. We're in the exit handler so
263
+ // there's no point trying anything more aggressive.
264
+ }
265
+ }
266
+ ptys.clear();
267
+ });
268
+ }
269
+ /**
270
+ * Allowlist of env-var names the PTY shell (and the `!` exec route)
271
+ * may inherit from the pi-forge process. Everything else is dropped.
272
+ *
273
+ * Allowlist instead of denylist because the threat model is "any
274
+ * secret an operator has in their host env leaks into the shell." A
275
+ * denylist of named secrets is fail-open — every newly-named provider
276
+ * key (`SOMEPROVIDER_API_KEY`), kube credential, cloud token, etc.
277
+ * leaks until we add it by name. An allowlist is fail-safe: anything
278
+ * not on this list is hidden by default; operators with a legitimate
279
+ * need pass specific vars through via `TERMINAL_PASSTHROUGH_ENV`.
280
+ *
281
+ * What's on the list and why:
282
+ * PATH, HOME, USER, LOGNAME, SHELL — required for a usable shell
283
+ * PWD — the shell sets it but inheriting
284
+ * avoids a transient empty value
285
+ * TERM, COLORTERM — xterm rendering / 256-color
286
+ * TERMINFO — fallback terminfo lookup
287
+ * LANG, LC_* — locale (UTF-8, sorting, dates)
288
+ * TZ — timezone-aware tools (`date`)
289
+ * HOSTNAME — prompt customization (`\h` in PS1)
290
+ * EDITOR, PAGER, VISUAL — interactive defaults (git
291
+ * commit, less, etc.)
292
+ *
293
+ * Conspicuously NOT on the list (must be opt-in via
294
+ * `TERMINAL_PASSTHROUGH_ENV` if the operator wants them in-shell):
295
+ * - pi-forge secrets: JWT_SECRET, API_KEY, UI_PASSWORD
296
+ * - Provider keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.
297
+ * - Cloud credentials: AWS_*, GOOGLE_APPLICATION_CREDENTIALS,
298
+ * GH_TOKEN, GITHUB_TOKEN, KUBECONFIG, ...
299
+ * - Anything else — assume it's sensitive until proven harmless.
300
+ */
301
+ const TERMINAL_ENV_ALLOWLIST = new Set([
302
+ "PATH",
303
+ "HOME",
304
+ "USER",
305
+ "LOGNAME",
306
+ "SHELL",
307
+ "PWD",
308
+ "TERM",
309
+ "COLORTERM",
310
+ "TERMINFO",
311
+ "LANG",
312
+ "TZ",
313
+ "HOSTNAME",
314
+ "EDITOR",
315
+ "PAGER",
316
+ "VISUAL",
317
+ ]);
318
+ /**
319
+ * Variable names matching this prefix-style pattern are also allowed
320
+ * — covers the locale family (`LC_ALL`, `LC_CTYPE`, `LC_MESSAGES`, …)
321
+ * without enumerating every variant.
322
+ */
323
+ const TERMINAL_ENV_ALLOWLIST_PREFIXES = ["LC_"];
324
+ function isAllowedByDefault(name) {
325
+ if (TERMINAL_ENV_ALLOWLIST.has(name))
326
+ return true;
327
+ for (const prefix of TERMINAL_ENV_ALLOWLIST_PREFIXES) {
328
+ if (name.startsWith(prefix))
329
+ return true;
330
+ }
331
+ return false;
332
+ }
333
+ function filterEnv(env) {
334
+ const passthrough = new Set(config.terminalPassthroughEnv);
335
+ const out = {};
336
+ for (const [k, v] of Object.entries(env)) {
337
+ if (typeof v !== "string")
338
+ continue;
339
+ if (!isAllowedByDefault(k) && !passthrough.has(k))
340
+ continue;
341
+ out[k] = v;
342
+ }
343
+ return out;
344
+ }
345
+ /**
346
+ * Re-export so non-PTY callers (the user-bash `!` exec route) get the
347
+ * same env allowlist without having to re-implement it. The PTY and
348
+ * the one-shot user-bash share an identical threat model: an
349
+ * authenticated browser user must not be able to `echo
350
+ * $JWT_SECRET` (or any other host-env secret) from a shell the
351
+ * pi-forge spawned on their behalf.
352
+ */
353
+ export const scrubbedEnv = () => filterEnv(process.env);
354
+ //# sourceMappingURL=pty-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pty-manager.js","sourceRoot":"","sources":["../src/pty-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared response schemas used across multiple route files. Extracting these
3
+ * keeps the wire shape consistent — e.g. /sessions, /sessions/:id, and /fork
4
+ * all return the same liveSummary fields rather than each route declaring its
5
+ * own subset.
6
+ */
7
+ /**
8
+ * Standard error envelope for 4xx/5xx responses. `error` is required so
9
+ * generated SDK clients can rely on its presence (no extra null-check).
10
+ * `message` is optional context for callers that want a human-readable hint.
11
+ */
12
+ export const errorSchema = {
13
+ type: "object",
14
+ required: ["error"],
15
+ properties: {
16
+ error: { type: "string" },
17
+ message: { type: "string" },
18
+ },
19
+ };
20
+ /**
21
+ * Live session summary — the shape returned by routes that produce a single
22
+ * session metadata object (POST /sessions, GET /sessions/:id, POST /fork).
23
+ * `name` is optional because not every session has a user-defined display
24
+ * name; consumers should treat its absence as "no name set."
25
+ */
26
+ export const liveSummarySchema = {
27
+ type: "object",
28
+ required: [
29
+ "sessionId",
30
+ "projectId",
31
+ "workspacePath",
32
+ "createdAt",
33
+ "lastActivityAt",
34
+ "isLive",
35
+ "messageCount",
36
+ "isStreaming",
37
+ ],
38
+ properties: {
39
+ sessionId: { type: "string" },
40
+ projectId: { type: "string" },
41
+ workspacePath: { type: "string" },
42
+ createdAt: { type: "string", format: "date-time" },
43
+ lastActivityAt: { type: "string", format: "date-time" },
44
+ isLive: { type: "boolean" },
45
+ name: { type: "string" },
46
+ messageCount: { type: "integer", minimum: 0 },
47
+ isStreaming: { type: "boolean" },
48
+ },
49
+ };
50
+ /**
51
+ * Build a wire-shaped LiveSession summary, omitting `name` when unset so the
52
+ * serializer doesn't emit an explicit undefined.
53
+ *
54
+ * `isLive` defaults to `true` because most callers (POST /sessions, /fork,
55
+ * /sessions/:id when in-memory) are returning a live session. Disk-only
56
+ * callers should pass `isLive: false` explicitly.
57
+ */
58
+ export function liveSummaryBody(args) {
59
+ const out = {
60
+ sessionId: args.sessionId,
61
+ projectId: args.projectId,
62
+ workspacePath: args.workspacePath,
63
+ createdAt: args.createdAt.toISOString(),
64
+ lastActivityAt: args.lastActivityAt.toISOString(),
65
+ isLive: args.isLive ?? true,
66
+ messageCount: args.messageCount,
67
+ isStreaming: args.isStreaming,
68
+ };
69
+ if (args.name !== undefined)
70
+ out.name = args.name;
71
+ return out;
72
+ }
73
+ //# sourceMappingURL=_schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_schemas.js","sourceRoot":"","sources":["../../src/routes/_schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,OAAO,CAAC;IACnB,UAAU,EAAE;QACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACzB,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;KAC5B;CACO,CAAC;AAEX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE;QACR,WAAW;QACX,WAAW;QACX,eAAe;QACf,WAAW;QACX,gBAAgB;QAChB,QAAQ;QACR,cAAc;QACd,aAAa;KACd;IACD,UAAU,EAAE;QACV,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC7B,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QAC7B,aAAa,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACjC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE;QAClD,cAAc,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE;QACvD,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;QAC3B,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACxB,YAAY,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE;QAC7C,WAAW,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;KACjC;CACO,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,IAU/B;IACC,MAAM,GAAG,GAA4B;QACnC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;QACvC,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,WAAW,EAAE;QACjD,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI;QAC3B,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,WAAW,EAAE,IAAI,CAAC,WAAW;KAC9B,CAAC;IACF,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IAClD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,164 @@
1
+ import { config, authEnabled } from "../config.js";
2
+ import { extractBearer, generateToken, passwordConfigured, persistPassword, verifyPasswordWithSource, verifyToken, } from "../auth.js";
3
+ import { errorSchema } from "./_schemas.js";
4
+ const MIN_PASSWORD_LENGTH = 8;
5
+ const MAX_PASSWORD_LENGTH = 1024;
6
+ export const authRoutes = async (fastify) => {
7
+ fastify.get("/auth/status", {
8
+ config: { public: true },
9
+ schema: {
10
+ description: "Returns whether auth is required to call protected routes.",
11
+ tags: ["auth"],
12
+ security: [],
13
+ response: {
14
+ 200: {
15
+ type: "object",
16
+ required: ["authEnabled"],
17
+ properties: {
18
+ authEnabled: { type: "boolean" },
19
+ },
20
+ },
21
+ },
22
+ },
23
+ }, async () => ({ authEnabled: authEnabled() }));
24
+ fastify.post("/auth/login", {
25
+ config: {
26
+ public: true,
27
+ rateLimit: {
28
+ max: config.auth.loginRateLimitMax,
29
+ timeWindow: config.auth.loginRateLimitWindowMs,
30
+ },
31
+ },
32
+ schema: {
33
+ description: "Exchange a password for a short-lived JWT. Returns 401 if the password is wrong, " +
34
+ "or 503 if browser password auth is not configured (no UI_PASSWORD set and no " +
35
+ "stored password hash). When the password matched the env-supplied " +
36
+ "UI_PASSWORD AND no stored hash exists yet, `mustChangePassword` is true on " +
37
+ "the response and the issued token may only call POST /auth/change-password.",
38
+ tags: ["auth"],
39
+ security: [],
40
+ body: {
41
+ type: "object",
42
+ required: ["password"],
43
+ additionalProperties: false,
44
+ properties: {
45
+ password: { type: "string", minLength: 1, maxLength: MAX_PASSWORD_LENGTH },
46
+ },
47
+ },
48
+ response: {
49
+ 200: {
50
+ type: "object",
51
+ required: ["token", "expiresAt", "mustChangePassword"],
52
+ properties: {
53
+ token: { type: "string" },
54
+ expiresAt: { type: "string", format: "date-time" },
55
+ mustChangePassword: { type: "boolean" },
56
+ },
57
+ },
58
+ 401: errorSchema,
59
+ 503: errorSchema,
60
+ },
61
+ },
62
+ }, async (req, reply) => {
63
+ if (!passwordConfigured()) {
64
+ return reply.code(503).send({
65
+ error: "ui_password_not_configured",
66
+ message: "browser login is disabled (no UI_PASSWORD set and no stored password hash)",
67
+ });
68
+ }
69
+ const { password } = req.body;
70
+ const result = await verifyPasswordWithSource(password);
71
+ if (!result.ok) {
72
+ return reply.code(401).send({
73
+ error: "invalid_password",
74
+ message: "the password did not match",
75
+ });
76
+ }
77
+ const mustChangePassword = result.source === "env" && config.auth.requirePasswordChange;
78
+ const issued = generateToken({ mustChangePassword });
79
+ return { ...issued, mustChangePassword };
80
+ });
81
+ // Change-password is `public: true` at the route-config level so the
82
+ // global `must_change_password` gate (in index.ts) doesn't refuse a
83
+ // token that was issued specifically to call THIS endpoint. We
84
+ // enforce auth manually inside the handler.
85
+ fastify.post("/auth/change-password", {
86
+ config: {
87
+ public: true,
88
+ rateLimit: {
89
+ max: config.auth.loginRateLimitMax,
90
+ timeWindow: config.auth.loginRateLimitWindowMs,
91
+ },
92
+ },
93
+ schema: {
94
+ description: "Verify the current password, persist a new scrypt hash to " +
95
+ "${FORGE_DATA_DIR}/password-hash, and issue a fresh JWT " +
96
+ "(mustChangePassword=false). Once a stored hash exists the env " +
97
+ "UI_PASSWORD is ignored on subsequent logins. Requires a valid " +
98
+ "JWT (initial-login `mustChangePassword:true` tokens are accepted).",
99
+ tags: ["auth"],
100
+ body: {
101
+ type: "object",
102
+ required: ["currentPassword", "newPassword"],
103
+ additionalProperties: false,
104
+ properties: {
105
+ currentPassword: {
106
+ type: "string",
107
+ minLength: 1,
108
+ maxLength: MAX_PASSWORD_LENGTH,
109
+ },
110
+ newPassword: {
111
+ type: "string",
112
+ minLength: MIN_PASSWORD_LENGTH,
113
+ maxLength: MAX_PASSWORD_LENGTH,
114
+ },
115
+ },
116
+ },
117
+ response: {
118
+ 200: {
119
+ type: "object",
120
+ required: ["token", "expiresAt", "mustChangePassword"],
121
+ properties: {
122
+ token: { type: "string" },
123
+ expiresAt: { type: "string", format: "date-time" },
124
+ mustChangePassword: { type: "boolean" },
125
+ },
126
+ },
127
+ 400: errorSchema,
128
+ 401: errorSchema,
129
+ 503: errorSchema,
130
+ },
131
+ },
132
+ }, async (req, reply) => {
133
+ // Manual auth check — the route is `public: true` so the global
134
+ // hook doesn't run, but we still require a valid JWT here.
135
+ const presented = extractBearer(req.headers.authorization);
136
+ if (presented === undefined || verifyToken(presented) === undefined) {
137
+ return reply.code(401).send({ error: "auth_required" });
138
+ }
139
+ if (!passwordConfigured()) {
140
+ return reply.code(503).send({
141
+ error: "ui_password_not_configured",
142
+ message: "password auth is not configured on this server",
143
+ });
144
+ }
145
+ const { currentPassword, newPassword } = req.body;
146
+ const verify = await verifyPasswordWithSource(currentPassword);
147
+ if (!verify.ok) {
148
+ return reply.code(401).send({
149
+ error: "invalid_password",
150
+ message: "the current password did not match",
151
+ });
152
+ }
153
+ if (currentPassword === newPassword) {
154
+ return reply.code(400).send({
155
+ error: "password_unchanged",
156
+ message: "new password must differ from the current one",
157
+ });
158
+ }
159
+ await persistPassword(newPassword);
160
+ const issued = generateToken({ mustChangePassword: false });
161
+ return { ...issued, mustChangePassword: false };
162
+ });
163
+ };
164
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"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"}