failproofai 1.0.0-beta.0 → 1.0.0-beta.2

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 (98) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  13. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
  21. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  22. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
  23. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  25. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  26. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  27. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/.next/server/app/index.html +1 -1
  30. package/.next/standalone/.next/server/app/index.rsc +14 -14
  31. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  32. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  33. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  34. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
  35. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  36. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  37. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +9 -9
  40. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  52. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +1 -1
  53. package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
  54. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
  55. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
  56. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  57. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0114ewg._.js +2 -2
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0259h38._.js → [root-of-the-server]__0nb0syg._.js} +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1psjybo._.js +3 -0
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  70. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  71. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1-upb14.js → node_modules_html-to-image_es_index_0oy_-36.js} +1 -1
  72. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  73. package/.next/standalone/.next/server/pages/404.html +1 -1
  74. package/.next/standalone/.next/server/pages/500.html +1 -1
  75. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/server-reference-manifest.json +11 -11
  77. package/.next/standalone/.next/static/chunks/{1s-_swlc80y43.js → 0fi645si-uiiq.js} +1 -1
  78. package/.next/standalone/.next/static/chunks/{29k6ef49zppxj.js → 18rvdw5v72364.js} +1 -1
  79. package/.next/standalone/.next/static/chunks/{2vr7aazw6k2-n.js → 2j94uwkb4s3qr.js} +1 -1
  80. package/.next/standalone/.next/static/chunks/{29ydr-8q0zis6.js → 2sbelanzvxvv0.js} +1 -1
  81. package/.next/standalone/.next/static/chunks/{1d6ncspjagm53.js → 31feojxhjyjpl.js} +1 -1
  82. package/.next/standalone/.next/static/chunks/{1grd7y94-igz9.js → 3hp8ulqknybil.js} +1 -1
  83. package/.next/standalone/.next/static/chunks/{3wrgbih-516uj.js → 3s7myk_co0gbh.js} +1 -1
  84. package/.next/standalone/.next/static/chunks/{0ip1n8xmgxarv.js → 3vs4ay36tjds_.js} +1 -1
  85. package/.next/standalone/.next/static/chunks/{0u9hc6oxx-gpj.js → 40k1_v4os4cw6.js} +1 -1
  86. package/.next/standalone/Cargo.lock +2 -2
  87. package/.next/standalone/Cargo.toml +1 -1
  88. package/.next/standalone/package.json +1 -1
  89. package/.next/standalone/server.js +1 -1
  90. package/dist/cli.mjs +225 -42
  91. package/dist/worker.mjs +1 -1
  92. package/package.json +1 -1
  93. package/src/hooks/configure-wizard.ts +91 -13
  94. package/src/hooks/daemon-service.ts +360 -58
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zj-87g._.js +0 -3
  96. /package/.next/standalone/.next/static/{7Lxtd352rSaHB-pmm6G8M → NWAjs0tHFyzk97gRGG09r}/_buildManifest.js +0 -0
  97. /package/.next/standalone/.next/static/{7Lxtd352rSaHB-pmm6G8M → NWAjs0tHFyzk97gRGG09r}/_clientMiddlewareManifest.js +0 -0
  98. /package/.next/standalone/.next/static/{7Lxtd352rSaHB-pmm6G8M → NWAjs0tHFyzk97gRGG09r}/_ssgManifest.js +0 -0
@@ -8,8 +8,16 @@
8
8
  * `configure-wizard.ts` calls the functions here directly, the same
9
9
  * relationship it already has with `manager.ts`'s `installHooks()`.
10
10
  */
11
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
12
- import { homedir } from "node:os";
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ mkdtempSync,
15
+ readFileSync,
16
+ writeFileSync,
17
+ unlinkSync,
18
+ rmSync,
19
+ } from "node:fs";
20
+ import { homedir, tmpdir, userInfo } from "node:os";
13
21
  import { resolve, dirname } from "node:path";
14
22
  import { execFileSync } from "node:child_process";
15
23
  import { hookLogWarn } from "./hook-logger";
@@ -45,6 +53,14 @@ const SERVICE_START_POLL_MS = 100;
45
53
  */
46
54
  const SERVICE_SETTLE_MS = 750;
47
55
 
56
+ /**
57
+ * How long `sudo -v` may sit at its password prompt. Unlike every other
58
+ * timeout here, this one is waiting on a human typing, not on a service
59
+ * manager — three attempts at a password is well inside two minutes, and
60
+ * sudo gives up on its own long before that.
61
+ */
62
+ const SUDO_PROMPT_TIMEOUT_MS = 120_000;
63
+
48
64
  /**
49
65
  * Writes (or clears) the machine-wide `daemonConfigured` marker in
50
66
  * `~/.failproofai/policies-config.json`.
@@ -168,24 +184,105 @@ export async function ensureFailproofaidBinary(): Promise<{ path?: string; reaso
168
184
  * here, once, at install time, and handing it to the daemon via the
169
185
  * environment closes that gap entirely.
170
186
  */
171
- function resolveWorkerCommand(): string | null {
187
+ export function resolveWorkerCommand(): string | null {
172
188
  if (process.env.FAILPROOFAI_WORKER_CMD) return process.env.FAILPROOFAI_WORKER_CMD;
173
189
 
174
190
  const packageRoot = process.env.FAILPROOFAI_PACKAGE_ROOT;
175
191
  if (!packageRoot) return null;
176
192
  const workerScript = resolve(packageRoot, "dist", "worker.mjs");
177
193
  if (!existsSync(workerScript)) return null;
178
- return `node ${workerScript}`;
194
+ // Both paths are shell-quoted: the daemon runs this value through `sh -c`
195
+ // (`WorkerCommand::Shell` in crates/failproofaid/src/worker.rs), so an
196
+ // unquoted path splits on its spaces and the worker never starts. Not
197
+ // hypothetical on macOS, where `/Users/First Last/...` is ordinary — and
198
+ // more exposed since execPath replaced a bare `node`, because execPath is
199
+ // home-derived. systemd's own `Environment="..."` quoting does not help
200
+ // here; that protects the unit parse, not the later shell split.
201
+ //
202
+ // `process.execPath`, not a bare `node`. A system-scope service does not
203
+ // inherit a login environment, so its PATH is the system default — and the
204
+ // single most common way to install Node is nvm, which puts it under
205
+ // ~/.nvm/versions/node/*/bin and nowhere on that PATH. A bare `node` would
206
+ // resolve fine when the wizard runs it and then fail inside the service,
207
+ // silently, on exactly the machines least likely to notice. execPath is
208
+ // whatever runtime is executing this CLI right now (node for the published
209
+ // bin, bun in a source checkout), absolute either way.
210
+ return `${shellQuote(process.execPath)} ${shellQuote(workerScript)}`;
211
+ }
212
+
213
+ /** POSIX single-quoting: everything is literal inside '…', and a literal
214
+ * quote is closed, escaped and reopened. */
215
+ function shellQuote(value: string): string {
216
+ return `'${value.replace(/'/g, `'\\''`)}'`;
217
+ }
218
+
219
+ /**
220
+ * The account the daemon runs as. The service is root-*installed* but never
221
+ * root-*run*: everything it touches (the socket, the lock, the policy config)
222
+ * lives in one user's home and is peer-checked against that user's uid.
223
+ */
224
+ function serviceUser(): string {
225
+ return userInfo().username;
226
+ }
227
+
228
+ /**
229
+ * `/etc/systemd/system/failproofaid@<user>.service`.
230
+ *
231
+ * The `@<user>` suffix is systemd's convention for a per-user instance, but
232
+ * this is a concrete unit file rather than an instance of a template: every
233
+ * field that matters is user-specific (the ExecStart path is under the user's
234
+ * own ~/.failproofai/bin, so is HOME, so is the worker command), so a shared
235
+ * template would need a per-instance drop-in for all of them and buy nothing.
236
+ * Naming it per-user is what keeps a second user's install from silently
237
+ * stealing the first's unit — a single `failproofaid.service` would.
238
+ */
239
+ function systemdUnitName(user: string = serviceUser()): string {
240
+ return `failproofaid@${user}.service`;
241
+ }
242
+
243
+ function systemdUnitPath(user: string = serviceUser()): string {
244
+ return resolve("/etc/systemd/system", systemdUnitName(user));
179
245
  }
180
246
 
181
- function systemdUnitPath(): string {
247
+ /**
248
+ * The pre-1.0.0-beta.1 user-scope unit. Still removed on install and
249
+ * uninstall: it holds the same flock the new service needs, so leaving one
250
+ * behind means the system unit starts, loses the singleton race, and the
251
+ * machine sits fail-closed against a daemon that never came up.
252
+ */
253
+ function legacySystemdUserUnitPath(): string {
182
254
  return resolve(homedir(), ".config", "systemd", "user", "failproofaid.service");
183
255
  }
184
256
 
185
- function launchdPlistPath(): string {
257
+ /**
258
+ * Per user, for the same reason the systemd unit is: a LaunchDaemon's
259
+ * contents are entirely user-specific — `UserName`, the ExecStart path under
260
+ * that user's `~/.failproofai/bin`, the log paths under that user's home — so
261
+ * one shared label means the second user's install silently overwrites the
262
+ * first's daemon, and their uninstall deletes it.
263
+ */
264
+ function launchdLabel(user: string = serviceUser()): string {
265
+ return `ai.failproof.failproofaid.${user}`;
266
+ }
267
+
268
+ function launchdPlistPath(user: string = serviceUser()): string {
269
+ return `/Library/LaunchDaemons/${launchdLabel(user)}.plist`;
270
+ }
271
+
272
+ /** The pre-1.0.0-beta.1 LaunchAgent. */
273
+ function legacyLaunchAgentPlistPath(): string {
186
274
  return resolve(homedir(), "Library", "LaunchAgents", "ai.failproof.failproofaid.plist");
187
275
  }
188
276
 
277
+ /**
278
+ * The 1.0.0-beta.1 LaunchDaemon, before the label was namespaced. Removed on
279
+ * install like the LaunchAgent is: it holds the same singleton flock, so
280
+ * leaving it would leave two daemons racing for one socket.
281
+ */
282
+ function legacySharedLaunchDaemonPath(): string {
283
+ return "/Library/LaunchDaemons/ai.failproof.failproofaid.plist";
284
+ }
285
+
189
286
  /**
190
287
  * The service-definition file `installDaemonService` would write on this
191
288
  * platform (the systemd unit or the launchd plist) — exposed so the config
@@ -197,23 +294,180 @@ export function daemonServiceFilePath(): string | null {
197
294
  return process.platform === "linux" ? systemdUnitPath() : launchdPlistPath();
198
295
  }
199
296
 
200
- function systemdUnitContents(binaryPath: string, workerCmd: string | null): string {
201
- // Quoted because FAILPROOFAI_WORKER_CMD's value ("node /abs/path/worker.mjs")
202
- // contains a space — systemd's Environment= requires quoting whenever the
203
- // value does.
297
+ /**
298
+ * The command a user runs to inspect their own daemon — surfaced so the
299
+ * wizard can print it instead of leaving people to discover which service
300
+ * manager, and which scope, is involved.
301
+ */
302
+ export function daemonStatusCommand(): string | null {
303
+ if (!isDaemonSupportedPlatform()) return null;
304
+ return process.platform === "linux"
305
+ ? `systemctl status ${systemdUnitName()}`
306
+ : `sudo launchctl print system/${launchdLabel()}`;
307
+ }
308
+
309
+ /**
310
+ * Acquires sudo credentials up front, prompting in the terminal if needed.
311
+ *
312
+ * `sudo -v` refreshes the user's sudo timestamp and returns; every later
313
+ * `sudo -n` in this run then succeeds against that cached credential. Doing it
314
+ * here, once, at a known point in the wizard is what makes the rest of the
315
+ * install non-interactive — the alternative, an interactive sudo firing from
316
+ * underneath a half-drawn TUI screen, is a password prompt the user cannot
317
+ * see.
318
+ *
319
+ * NOT the same as running the whole CLI under sudo, which is actively wrong:
320
+ * `homedir()` would become /root, so the hooks, the policy config, the
321
+ * downloaded binary and the unit's own `User=` would all be root's rather
322
+ * than the user's, quietly configuring the wrong account.
323
+ *
324
+ * `stdio: "inherit"` so the prompt and the typed password go straight to the
325
+ * real terminal. The timeout is generous because a human is typing.
326
+ */
327
+ export function primeElevation(): boolean {
328
+ if (typeof process.getuid === "function" && process.getuid() === 0) return true;
329
+ if (canElevate()) return true; // NOPASSWD, or already primed
330
+ try {
331
+ execFileSync("sudo", ["-v"], { stdio: "inherit", timeout: SUDO_PROMPT_TIMEOUT_MS });
332
+ return canElevate();
333
+ } catch {
334
+ // Wrong password, no sudo rights, or the user pressed ctrl-C. All of them
335
+ // mean the same thing here: carry on without a daemon.
336
+ return false;
337
+ }
338
+ }
339
+
340
+ /** True when privileged commands can run without prompting for a password. */
341
+ function canElevate(): boolean {
342
+ if (typeof process.getuid === "function" && process.getuid() === 0) return true;
343
+ try {
344
+ execFileSync("sudo", ["-n", "true"], { stdio: "ignore", timeout: SERVICE_CMD_TIMEOUT_MS });
345
+ return true;
346
+ } catch {
347
+ return false;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Runs one privileged command, non-interactively.
353
+ *
354
+ * `sudo -n` on purpose: the wizard owns the terminal, and a sudo password
355
+ * prompt fired from underneath a TUI is unreadable at best. A machine that
356
+ * cannot elevate silently gets a clear reason and the manual commands
357
+ * instead — it keeps working exactly as it did, on the in-process path.
358
+ */
359
+ function runPrivileged(command: string, args: string[]): void {
360
+ const root = typeof process.getuid === "function" && process.getuid() === 0;
361
+ const [cmd, argv] = root ? [command, args] : ["sudo", ["-n", command, ...args]];
362
+ execFileSync(cmd, argv, { stdio: "ignore", timeout: SERVICE_CMD_TIMEOUT_MS });
363
+ }
364
+
365
+ /**
366
+ * Installs a file into a root-owned location via a temp file, because the
367
+ * caller is not root and cannot write there directly. `install -m` sets the
368
+ * mode in the same step, so the file is never briefly world-writable.
369
+ */
370
+ function writePrivilegedFile(destination: string, contents: string, mode = "0644"): void {
371
+ // mkdtempSync, not a name built from pid + timestamp in the shared tmpdir.
372
+ // That name is guessable, and `writeFileSync` follows a symlink already
373
+ // sitting at the path — so on a multi-user box another local user could
374
+ // pre-create it and have `install` copy content they control into
375
+ // /etc/systemd/system as root. mkdtemp gives a 0700 directory with a name
376
+ // they cannot predict, so there is nothing to pre-create.
377
+ const stagingDir = mkdtempSync(resolve(tmpdir(), "failproofaid-stage-"));
378
+ const staging = resolve(stagingDir, "service-definition");
379
+ try {
380
+ writeFileSync(staging, contents, "utf8");
381
+ runPrivileged("install", ["-m", mode, staging, destination]);
382
+ } finally {
383
+ rmSync(stagingDir, { recursive: true, force: true });
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Removes a daemon installed by an earlier version into the user's own
389
+ * session scope. Best-effort and never privileged — these paths are all
390
+ * inside the user's home.
391
+ */
392
+ function removeLegacyUserService(): void {
393
+ try {
394
+ if (process.platform === "linux") {
395
+ const legacy = legacySystemdUserUnitPath();
396
+ if (!existsSync(legacy)) return;
397
+ try {
398
+ execFileSync("systemctl", ["--user", "disable", "--now", "failproofaid.service"], {
399
+ stdio: "ignore",
400
+ timeout: SERVICE_CMD_TIMEOUT_MS,
401
+ });
402
+ } catch {
403
+ // Not loaded — removing the file is still the point.
404
+ }
405
+ unlinkSync(legacy);
406
+ try {
407
+ execFileSync("systemctl", ["--user", "daemon-reload"], {
408
+ stdio: "ignore",
409
+ timeout: SERVICE_CMD_TIMEOUT_MS,
410
+ });
411
+ } catch {
412
+ /* best-effort */
413
+ }
414
+ } else {
415
+ const legacyAgent = legacyLaunchAgentPlistPath();
416
+ if (existsSync(legacyAgent)) {
417
+ try {
418
+ execFileSync("launchctl", ["unload", "-w", legacyAgent], {
419
+ stdio: "ignore",
420
+ timeout: SERVICE_CMD_TIMEOUT_MS,
421
+ });
422
+ } catch {
423
+ /* not loaded */
424
+ }
425
+ unlinkSync(legacyAgent);
426
+ }
427
+
428
+ // The 1.0.0-beta.1 daemon, before the label was per-user. Privileged,
429
+ // and only reachable on a path that has already elevated.
430
+ const legacyDaemon = legacySharedLaunchDaemonPath();
431
+ if (existsSync(legacyDaemon)) {
432
+ try {
433
+ runPrivileged("launchctl", ["unload", "-w", legacyDaemon]);
434
+ } catch {
435
+ /* not loaded */
436
+ }
437
+ runPrivileged("rm", ["-f", legacyDaemon]);
438
+ }
439
+ }
440
+ } catch (err) {
441
+ hookLogWarn(`could not remove the legacy user-scope daemon: ${err instanceof Error ? err.message : String(err)}`);
442
+ }
443
+ }
444
+
445
+ export function systemdUnitContents(binaryPath: string, workerCmd: string | null): string {
446
+ // Quoted because both values contain a space or a path — systemd's
447
+ // Environment= requires quoting whenever the value does.
204
448
  const envLine = workerCmd ? `Environment="FAILPROOFAI_WORKER_CMD=${workerCmd}"\n` : "";
449
+ const user = serviceUser();
205
450
  return `[Unit]
206
- Description=failproofai background daemon (failproofaid)
451
+ Description=failproofai background daemon (failproofaid) for ${user}
207
452
  After=network.target
208
453
 
209
454
  [Service]
210
455
  Type=simple
456
+ User=${user}
457
+ # Set explicitly rather than relying on systemd deriving it from User=:
458
+ # failproofaid is user-scope by construction and refuses to start without
459
+ # HOME ("HOME is not set; failproofaid is user-scope only"), so the one
460
+ # variable it cannot do without is not left to a version-dependent default.
461
+ Environment="HOME=${homedir()}"
211
462
  ${envLine}ExecStart=${binaryPath}
212
463
  Restart=on-failure
213
464
  RestartSec=2
214
465
 
215
466
  [Install]
216
- WantedBy=default.target
467
+ # multi-user.target, not default.target: this is the whole point of a
468
+ # system unit — it starts at boot, with no login and no lingering, and
469
+ # keeps running after the installing user logs out.
470
+ WantedBy=multi-user.target
217
471
  `;
218
472
  }
219
473
 
@@ -235,7 +489,7 @@ function launchdPlistContents(binaryPath: string, logDir: string, workerCmd: str
235
489
  <plist version="1.0">
236
490
  <dict>
237
491
  <key>Label</key>
238
- <string>ai.failproof.failproofaid</string>
492
+ <string>${escapeXml(launchdLabel())}</string>
239
493
  <key>ProgramArguments</key>
240
494
  <array>
241
495
  <string>${escapeXml(binaryPath)}</string>
@@ -244,6 +498,12 @@ function launchdPlistContents(binaryPath: string, logDir: string, workerCmd: str
244
498
  <true/>
245
499
  <key>KeepAlive</key>
246
500
  <true/>
501
+ <!-- A LaunchDaemon runs as root unless told otherwise. This is the
502
+ launchd half of the systemd unit's User=: loaded at boot by the
503
+ system, executed as the one user whose home, socket and lock it
504
+ is allowed to touch. -->
505
+ <key>UserName</key>
506
+ <string>${escapeXml(serviceUser())}</string>
247
507
  ${envBlock} <key>StandardOutPath</key>
248
508
  <string>${escapeXml(resolve(logDir, "failproofaid.log"))}</string>
249
509
  <key>StandardErrorPath</key>
@@ -258,6 +518,25 @@ export interface DaemonInstallResult {
258
518
  reason?: string;
259
519
  }
260
520
 
521
+ /**
522
+ * The privileged commands an install performs — returned verbatim so a
523
+ * machine that cannot elevate can be told exactly what to run rather than
524
+ * just that something failed.
525
+ */
526
+ function daemonInstallCommands(binaryPath: string, workerCmd: string | null): string[] {
527
+ if (process.platform === "linux") {
528
+ return [
529
+ `sudo tee ${systemdUnitPath()} <<'EOF'\n${systemdUnitContents(binaryPath, workerCmd)}EOF`,
530
+ "sudo systemctl daemon-reload",
531
+ `sudo systemctl enable --now ${systemdUnitName()}`,
532
+ ];
533
+ }
534
+ return [
535
+ `sudo tee ${launchdPlistPath()} < the plist failproofai config would write`,
536
+ `sudo launchctl load -w ${launchdPlistPath()}`,
537
+ ];
538
+ }
539
+
261
540
  /**
262
541
  * Writes and enables the service unit, starting it immediately. Safe to
263
542
  * call repeatedly — re-running replaces the unit file (picking up a
@@ -284,40 +563,45 @@ export async function installDaemonService(): Promise<DaemonInstallResult> {
284
563
  // worker regardless of what cwd the service manager starts it from.
285
564
  const workerCmd = resolveWorkerCommand();
286
565
 
566
+ // The service is installed system-wide, which needs root. Check before
567
+ // writing anything, so a machine that cannot elevate gets the exact
568
+ // commands to run instead of a half-installed service.
569
+ if (!canElevate()) {
570
+ return {
571
+ installed: false,
572
+ reason:
573
+ "root privileges are required to install the failproofaid system service, and sudo credentials were not available. " +
574
+ "Re-run `failproofai config` and approve the sudo prompt — do NOT run the CLI itself under sudo, which would " +
575
+ "configure root's account instead of yours. To install by hand: " +
576
+ daemonInstallCommands(binaryPath, workerCmd).join(" && "),
577
+ };
578
+ }
579
+
580
+ // A daemon left over from a pre-1.0.0-beta.1 install holds the same
581
+ // singleton lock the new one needs, so the system unit would start, lose
582
+ // the flock race, and leave the machine fail-closed against a daemon that
583
+ // never came up. Clear it first, every time.
584
+ removeLegacyUserService();
585
+
287
586
  try {
288
587
  if (process.platform === "linux") {
289
- const unitPath = systemdUnitPath();
290
- mkdirSync(dirname(unitPath), { recursive: true });
291
- writeFileSync(unitPath, systemdUnitContents(binaryPath, workerCmd), "utf8");
292
- execFileSync("systemctl", ["--user", "daemon-reload"], {
293
- stdio: "ignore",
294
- timeout: SERVICE_CMD_TIMEOUT_MS,
295
- });
296
- execFileSync("systemctl", ["--user", "enable", "--now", "failproofaid.service"], {
297
- stdio: "ignore",
298
- timeout: SERVICE_CMD_TIMEOUT_MS,
299
- });
588
+ writePrivilegedFile(systemdUnitPath(), systemdUnitContents(binaryPath, workerCmd));
589
+ runPrivileged("systemctl", ["daemon-reload"]);
590
+ runPrivileged("systemctl", ["enable", "--now", systemdUnitName()]);
300
591
  } else {
301
592
  const plistPath = launchdPlistPath();
302
593
  const logDir = resolve(homedir(), ".failproofai", "logs");
303
- mkdirSync(dirname(plistPath), { recursive: true });
304
594
  mkdirSync(logDir, { recursive: true });
305
- writeFileSync(plistPath, launchdPlistContents(binaryPath, logDir, workerCmd), "utf8");
306
595
  // Unload any previously-loaded copy first — reloading with a changed
307
596
  // binary path (e.g. after an upgrade) is a no-op under plain `load`
308
597
  // if launchd thinks the label is already loaded.
309
598
  try {
310
- execFileSync("launchctl", ["unload", plistPath], {
311
- stdio: "ignore",
312
- timeout: SERVICE_CMD_TIMEOUT_MS,
313
- });
599
+ runPrivileged("launchctl", ["unload", plistPath]);
314
600
  } catch {
315
601
  // Wasn't loaded — fine, this is the common case on a fresh install.
316
602
  }
317
- execFileSync("launchctl", ["load", "-w", plistPath], {
318
- stdio: "ignore",
319
- timeout: SERVICE_CMD_TIMEOUT_MS,
320
- });
603
+ writePrivilegedFile(plistPath, launchdPlistContents(binaryPath, logDir, workerCmd));
604
+ runPrivileged("launchctl", ["load", "-w", plistPath]);
321
605
  }
322
606
  } catch (err) {
323
607
  const msg = err instanceof Error ? err.message : String(err);
@@ -370,40 +654,44 @@ async function waitForDaemonRunning(): Promise<boolean> {
370
654
  export async function uninstallDaemonService(): Promise<void> {
371
655
  setDaemonConfigured(false);
372
656
  if (!isDaemonSupportedPlatform()) return;
657
+
658
+ // Always attempted, and never privileged: a legacy user-scope daemon is
659
+ // the one thing this can still clean up on a machine that cannot elevate.
660
+ removeLegacyUserService();
661
+
373
662
  try {
374
663
  if (process.platform === "linux") {
664
+ const unitPath = systemdUnitPath();
665
+ if (!existsSync(unitPath)) return;
375
666
  try {
376
- execFileSync("systemctl", ["--user", "disable", "--now", "failproofaid.service"], {
377
- stdio: "ignore",
378
- timeout: SERVICE_CMD_TIMEOUT_MS,
379
- });
667
+ runPrivileged("systemctl", ["disable", "--now", systemdUnitName()]);
380
668
  } catch {
381
- // Already stopped/not installed — fine.
669
+ // Already stopped/not enabled — removing the unit is still the point.
382
670
  }
383
- const unitPath = systemdUnitPath();
384
- if (existsSync(unitPath)) unlinkSync(unitPath);
671
+ runPrivileged("rm", ["-f", unitPath]);
385
672
  try {
386
- execFileSync("systemctl", ["--user", "daemon-reload"], {
387
- stdio: "ignore",
388
- timeout: SERVICE_CMD_TIMEOUT_MS,
389
- });
673
+ runPrivileged("systemctl", ["daemon-reload"]);
390
674
  } catch {
391
675
  // Best-effort.
392
676
  }
393
677
  } else {
394
678
  const plistPath = launchdPlistPath();
679
+ if (!existsSync(plistPath)) return;
395
680
  try {
396
- execFileSync("launchctl", ["unload", "-w", plistPath], {
397
- stdio: "ignore",
398
- timeout: SERVICE_CMD_TIMEOUT_MS,
399
- });
681
+ runPrivileged("launchctl", ["unload", "-w", plistPath]);
400
682
  } catch {
401
- // Already unloaded/not installed — fine.
683
+ // Already unloaded — fine.
402
684
  }
403
- if (existsSync(plistPath)) unlinkSync(plistPath);
685
+ runPrivileged("rm", ["-f", plistPath]);
404
686
  }
405
687
  } catch (err) {
406
- hookLogWarn(`daemon service uninstall failed: ${err instanceof Error ? err.message : String(err)}`);
688
+ // `daemonConfigured` is already cleared above, so a machine that cannot
689
+ // elevate is back on the in-process path even though the unit file
690
+ // survives — it fails open, not closed.
691
+ hookLogWarn(
692
+ `daemon service uninstall failed (the service may need removing by hand: ${daemonStatusCommand()}): ` +
693
+ `${err instanceof Error ? err.message : String(err)}`,
694
+ );
407
695
  }
408
696
  }
409
697
 
@@ -418,7 +706,9 @@ export function daemonServiceStatus(): DaemonServiceStatus {
418
706
  if (process.platform === "linux") {
419
707
  if (!existsSync(systemdUnitPath())) return "not-installed";
420
708
  try {
421
- const out = execFileSync("systemctl", ["--user", "is-active", "failproofaid.service"], {
709
+ // Unprivileged on purpose: reading a system unit's state needs no
710
+ // root, so status works for the owning user with no sudo at all.
711
+ const out = execFileSync("systemctl", ["is-active", systemdUnitName()], {
422
712
  stdio: ["ignore", "pipe", "ignore"],
423
713
  timeout: SERVICE_CMD_TIMEOUT_MS,
424
714
  })
@@ -438,11 +728,23 @@ export function daemonServiceStatus(): DaemonServiceStatus {
438
728
  const plistPath = launchdPlistPath();
439
729
  if (!existsSync(plistPath)) return "not-installed";
440
730
  try {
441
- const out = execFileSync("launchctl", ["list"], {
442
- stdio: ["ignore", "pipe", "ignore"],
443
- timeout: SERVICE_CMD_TIMEOUT_MS,
444
- }).toString();
445
- return out.includes("ai.failproof.failproofaid") ? "running" : "stopped";
731
+ // A LaunchDaemon lives in launchd's system domain, which an unprivileged
732
+ // `launchctl list` cannot see — unlike systemd, reading the state needs
733
+ // the same elevation installing it did.
734
+ const root = typeof process.getuid === "function" && process.getuid() === 0;
735
+ const args = ["print", `system/${launchdLabel()}`];
736
+ const out = root
737
+ ? execFileSync("launchctl", args, {
738
+ stdio: ["ignore", "pipe", "ignore"],
739
+ timeout: SERVICE_CMD_TIMEOUT_MS,
740
+ }).toString()
741
+ : execFileSync("sudo", ["-n", "launchctl", ...args], {
742
+ stdio: ["ignore", "pipe", "ignore"],
743
+ timeout: SERVICE_CMD_TIMEOUT_MS,
744
+ }).toString();
745
+ // `state = running` is launchd's own wording; a loaded-but-dead job
746
+ // prints `state = not running` and must not read as healthy.
747
+ return /state\s*=\s*running/.test(out) ? "running" : "stopped";
446
748
  } catch {
447
749
  return "stopped";
448
750
  }
@@ -1,3 +0,0 @@
1
- module.exports=[18622,(a,b,c)=>{b.exports=a.x("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js",()=>require("next/dist/compiled/next-server/app-page-turbo.runtime.prod.js"))},56704,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-async-storage.external.js",()=>require("next/dist/server/app-render/work-async-storage.external.js"))},32319,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/work-unit-async-storage.external.js",()=>require("next/dist/server/app-render/work-unit-async-storage.external.js"))},20635,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/action-async-storage.external.js",()=>require("next/dist/server/app-render/action-async-storage.external.js"))},24725,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/after-task-async-storage.external.js",()=>require("next/dist/server/app-render/after-task-async-storage.external.js"))},43285,(a,b,c)=>{b.exports=a.x("next/dist/server/app-render/dynamic-access-async-storage.external.js",()=>require("next/dist/server/app-render/dynamic-access-async-storage.external.js"))},42602,(a,b,c)=>{"use strict";b.exports=a.r(18622)},87924,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].ReactJsxRuntime},72131,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].React},38783,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].ReactServerDOMTurbopackClient},9270,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored.contexts.AppRouterContext},36313,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored.contexts.HooksClientContext},18341,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored.contexts.ServerInsertedHtml},35112,(a,b,c)=>{"use strict";b.exports=a.r(42602).vendored["react-ssr"].ReactDOM},5607,a=>{"use strict";a.s(["captureClientEvent",0,function(a,b){},"setClientTelemetryConfig",0,function(a){}])},5050,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d={callServer:function(){return f.callServer},createServerReference:function(){return h.createServerReference},findSourceMapURL:function(){return g.findSourceMapURL}};for(var e in d)Object.defineProperty(c,e,{enumerable:!0,get:d[e]});let f=a.r(20611),g=a.r(1722),h=a.r(38783)},2933,a=>{"use strict";var b=a.i(5050);let c=(0,b.createServerReference)("00eb0d1896aceb50136245d3f32f87ed0485ac965b",b.callServer,void 0,b.findSourceMapURL,"getTelemetryConfig");a.s(["getTelemetryConfig",0,c])},10765,a=>{"use strict";var b=a.i(87924),c=a.i(72131),d=a.i(50944),e=a.i(2933),f=a.i(5607);let g=(0,c.createContext)(void 0);a.s(["PostHogProvider",0,function({children:a}){let h=(0,d.usePathname)(),i=(0,c.useRef)(null),j=(0,c.useRef)(!1);(0,c.useEffect)(()=>{let a=!1;return(0,e.getTelemetryConfig)().then(b=>{a||((0,f.setClientTelemetryConfig)(b),j.current=!0,(0,f.captureClientEvent)("$pageview"),i.current=window.location.pathname)}),()=>{a=!0}},[]),(0,c.useEffect)(()=>{j.current&&i.current!==h&&(i.current=h,(0,f.captureClientEvent)("$pageview"))},[h]);let k=c.default.useCallback((a,b)=>{(0,f.captureClientEvent)(a,b)},[]);return(0,b.jsx)(g.Provider,{value:{capture:k},children:a})},"usePostHog",0,function(){let a=(0,c.useContext)(g);if(void 0===a)throw Error("usePostHog must be used within a PostHogProvider");return a}])},18952,a=>{"use strict";var b=a.i(87924),c=a.i(72131);let d=[],e=0,f=new Set;function g(){for(let a of f)a()}function h(a){return f.add(a),()=>f.delete(a)}function i(){return d}function j({message:a}){let[d,e]=(0,c.useState)(!1);return(0,c.useEffect)(()=>{requestAnimationFrame(()=>e(!0));let a=setTimeout(()=>e(!1),2e3);return()=>clearTimeout(a)},[]),(0,b.jsx)("div",{className:`bg-card border border-border text-foreground text-sm px-4 py-2 rounded-lg shadow-lg transition-opacity duration-300 ${d?"opacity-100":"opacity-0"}`,children:a})}a.s(["Toaster",0,function(){let a=(0,c.useSyncExternalStore)(h,i,i);return(0,b.jsx)("div",{className:"fixed bottom-4 right-4 z-50 flex flex-col gap-2",children:a.map(a=>(0,b.jsx)(j,{message:a.message},a.id))})},"toast",0,function(a){let b=e++;d=[...d,{id:b,message:a}],g(),setTimeout(()=>{d=d.filter(a=>a.id!==b),g()},2500)}])},46058,(a,b,c)=>{"use strict";function d(a){if("function"!=typeof WeakMap)return null;var b=new WeakMap,c=new WeakMap;return(d=function(a){return a?c:b})(a)}c._=function(a,b){if(!b&&a&&a.__esModule)return a;if(null===a||"object"!=typeof a&&"function"!=typeof a)return{default:a};var c=d(b);if(c&&c.has(a))return c.get(a);var e={__proto__:null},f=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var g in a)if("default"!==g&&Object.prototype.hasOwnProperty.call(a,g)){var h=f?Object.getOwnPropertyDescriptor(a,g):null;h&&(h.get||h.set)?Object.defineProperty(e,g,h):e[g]=a[g]}return e.default=a,c&&c.set(a,e),e}},88347,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d,e,f={ACTION_HMR_REFRESH:function(){return l},ACTION_NAVIGATE:function(){return i},ACTION_REFRESH:function(){return h},ACTION_RESTORE:function(){return j},ACTION_SERVER_ACTION:function(){return m},ACTION_SERVER_PATCH:function(){return k},PrefetchKind:function(){return n},ScrollBehavior:function(){return o}};for(var g in f)Object.defineProperty(c,g,{enumerable:!0,get:f[g]});let h="refresh",i="navigate",j="restore",k="server-patch",l="hmr-refresh",m="server-action";var n=((d={}).AUTO="auto",d.FULL="full",d),o=((e={})[e.Default=0]="Default",e[e.NoScroll=1]="NoScroll",e);("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},67009,(a,b,c)=>{"use strict";function d(a){return null!==a&&"object"==typeof a&&"then"in a&&"function"==typeof a.then}Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"isThenable",{enumerable:!0,get:function(){return d}})},90841,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d={dispatchAppRouterAction:function(){return i},dispatchGestureState:function(){return j},refreshOnInstantNavigationUnlock:function(){return h},useActionQueue:function(){return k}};for(var e in d)Object.defineProperty(c,e,{enumerable:!0,get:d[e]});let f=a.r(46058)._(a.r(72131)),g=a.r(67009);a.r(88347);function h(){}function i(a){!0;throw Object.defineProperty(Error("Internal Next.js error: Router action dispatched before initialization."),"__NEXT_ERROR_CODE",{value:"E668",enumerable:!1,configurable:!0})}function j(a){!0;throw Object.defineProperty(Error("Internal Next.js error: Router action dispatched before initialization."),"__NEXT_ERROR_CODE",{value:"E668",enumerable:!1,configurable:!0})}function k(a){let[b,c]=f.default.useState(a.state),[d,e]=(0,f.useOptimistic)(b),h=(0,f.useMemo)(()=>d,[d]);return(0,g.isThenable)(h)?(0,f.use)(h):h}("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},20611,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"callServer",{enumerable:!0,get:function(){return g}});let d=a.r(72131),e=a.r(88347),f=a.r(90841);async function g(a,b){return new Promise((c,g)=>{(0,d.startTransition)(()=>{(0,f.dispatchAppRouterAction)({type:e.ACTION_SERVER_ACTION,actionId:a,actionArgs:b,resolve:c,reject:g})})})}("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},1722,(a,b,c)=>{"use strict";let d;Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"findSourceMapURL",{enumerable:!0,get:function(){return d}});("function"==typeof c.default||"object"==typeof c.default&&null!==c.default)&&void 0===c.default.__esModule&&(Object.defineProperty(c.default,"__esModule",{value:!0}),Object.assign(c.default,c),b.exports=c.default)},39118,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d={DEFAULT_SEGMENT_KEY:function(){return l},NOT_FOUND_SEGMENT_KEY:function(){return m},PAGE_SEGMENT_KEY:function(){return k},addSearchParamsIfPageSegment:function(){return i},computeSelectedLayoutSegment:function(){return j},getSegmentValue:function(){return f},getSelectedLayoutSegmentPath:function(){return function a(b,c,d=!0,e=[]){let g;if(d)g=b[1][c];else{let a=b[1];g=a.children??Object.values(a)[0]}if(!g)return e;let h=f(g[0]);return!h||h.startsWith(k)?e:(e.push(h),a(g,c,!1,e))}},isGroupSegment:function(){return g},isParallelRouteSegment:function(){return h}};for(var e in d)Object.defineProperty(c,e,{enumerable:!0,get:d[e]});function f(a){return Array.isArray(a)?a[1]:a}function g(a){return"("===a[0]&&a.endsWith(")")}function h(a){return a.startsWith("@")&&"@children"!==a}function i(a,b){if(a.includes(k)){let a=JSON.stringify(b);return"{}"!==a?k+"?"+a:k}return a}function j(a,b){if(!a||0===a.length)return null;let c="children"===b?a[0]:a[a.length-1];return c===l?null:c}let k="__PAGE__",l="__DEFAULT__",m="/_not-found"},88644,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"InvariantError",{enumerable:!0,get:function(){return d}});class d extends Error{constructor(a,b){super(`Invariant: ${a.endsWith(".")?a:a+"."} This is a bug in Next.js.`,b),this.name="InvariantError"}}},54427,(a,b,c)=>{"use strict";function d(){let a,b,c=new Promise((c,d)=>{a=c,b=d});return{resolve:a,reject:b,promise:c}}Object.defineProperty(c,"__esModule",{value:!0}),Object.defineProperty(c,"createPromiseWithResolvers",{enumerable:!0,get:function(){return d}})},41997,(a,b,c)=>{"use strict";Object.defineProperty(c,"__esModule",{value:!0});var d={BailoutToCSRError:function(){return g},isBailoutToCSRError:function(){return h}};for(var e in d)Object.defineProperty(c,e,{enumerable:!0,get:d[e]});let f="BAILOUT_TO_CLIENT_SIDE_RENDERING";class g extends Error{constructor(a){super(`Bail out to client-side rendering: ${a}`),this.reason=a,this.digest=f}}function h(a){return"object"==typeof a&&null!==a&&"digest"in a&&a.digest===f}},89027,a=>{"use strict";let b=(0,a.i(64831).default)("chevron-down",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);a.s(["ChevronDown",0,b],89027)},6236,a=>{"use strict";var b=a.i(87924),c=a.i(72131);let d=(0,c.createContext)(void 0);a.s(["AutoRefreshProvider",0,function({children:a}){let[e,f]=(0,c.useState)(0);return(0,b.jsx)(d.Provider,{value:{intervalSec:e,setIntervalSec:f},children:a})},"useAutoRefresh",0,function(){let a=(0,c.useContext)(d);if(!a)throw Error("useAutoRefresh must be used within AutoRefreshProvider");return a}])},40695,a=>{"use strict";var b=a.i(87924),c=a.i(72131),d=a.i(97895);let e=c.forwardRef(({className:a,variant:c="default",size:e="default",...f},g)=>(0,b.jsx)("button",{className:(0,d.cn)("inline-flex items-center justify-center rounded-md text-sm font-medium transition-[background-color,color,box-shadow,border-color] duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:opacity-50 disabled:pointer-events-none",{"bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-[0_0_0_1px_var(--primary)]":"default"===c,"text-foreground hover:bg-muted":"ghost"===c,"border border-border bg-transparent hover:border-primary/60 hover:bg-card":"outline"===c,"h-10 py-2 px-4":"default"===e,"h-10 w-10":"icon"===e,"h-9 px-3":"sm"===e,"h-11 px-8":"lg"===e},a),ref:g,...f}));e.displayName="Button",a.s(["Button",0,e])},45550,a=>{"use strict";var b=a.i(72131),c=a.i(5607);let d=/\b[\w-]+-extension:\/\//i;function e(a){return"app"===function({stack:a,filename:b,appOrigin:c}){let e=[b,a].filter(Boolean).join("\n");return e.trim()?d.test(e)?"extension":c&&e.includes(c)?"app":"unknown":"unknown"}(a)}a.s(["GlobalErrorListeners",0,function(){return(0,b.useEffect)(()=>{let a=a=>{e({stack:a.error?.stack,filename:a.filename,appOrigin:window.location.origin})&&(0,c.captureClientEvent)("unhandled_exception",{error_message:a.message,error_name:a.error?.name,error_filename:a.filename,error_lineno:a.lineno,error_colno:a.colno})},b=a=>{let b=a.reason;e({stack:b instanceof Error?b.stack:void 0,appOrigin:window.location.origin})&&(0,c.captureClientEvent)("unhandled_rejection",{error_message:b instanceof Error?b.message:String(b),error_name:b instanceof Error?b.name:void 0})};return window.addEventListener("error",a),window.addEventListener("unhandledrejection",b),()=>{window.removeEventListener("error",a),window.removeEventListener("unhandledrejection",b)}},[]),null}],45550)},10815,a=>{"use strict";var b=a.i(87924),c=a.i(72131),d=a.i(38246),e=a.i(50944),f=a.i(64831);let g=(0,f.default)("git-branch",[["path",{d:"M15 6a9 9 0 0 0-9 9V3",key:"1cii5b"}],["circle",{cx:"18",cy:"6",r:"3",key:"1h7g24"}],["circle",{cx:"6",cy:"18",r:"3",key:"fqmcym"}]]);var h=a.i(89027);let i=(0,f.default)("star",[["path",{d:"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z",key:"r04s7s"}]]),j=(0,f.default)("book-open",[["path",{d:"M12 5v16",key:"1f6ucr"}],["path",{d:"M20.001 19A2 2 0 0022 17V5a2 2 0 00-1.999-2L16 3.002A5 5 0 0012 5a5 5 0 00-4-2H4a2 2 0 00-2 2v12a2 2 0 001.999 2H8a5 5 0 014 2 5 5 0 014-2z",key:"1fyvmf"}]]),k=(0,f.default)("message-circle",[["path",{d:"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719",key:"1sd12s"}]]),l=(0,f.default)("message-square-warning",[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z",key:"18887p"}],["path",{d:"M12 15h.01",key:"q59x07"}],["path",{d:"M12 7v4",key:"xawao1"}]]);var m=a.i(40695);let n="failproofai@exosphere.host",o=[{label:"Star us on GitHub",icon:i,href:"https://github.com/failproofai/failproofai",color:"#f5c842",bg:"rgba(245,200,66,0.08)"},{label:"Documentation",icon:j,href:"https://docs.befailproof.ai/introduction",color:"#60a5fa",bg:"rgba(96,165,250,0.08)"},{label:"Join our Discord",icon:k,href:"https://discord.befailproof.ai/",color:"#5865f2",bg:"rgba(88,101,242,0.08)"},{label:"Feedback & Issues",icon:l,href:"https://github.com/FailproofAI/failproofai/issues/new/choose",color:"#34d399",bg:"rgba(52,211,153,0.08)"}],p=()=>{let[a,d]=(0,c.useState)(!1),e=(0,c.useCallback)(()=>d(!1),[]),f=(0,c.useCallback)(a=>{"Escape"===a.key&&d(!1)},[]);return(0,b.jsxs)("div",{className:"relative",onKeyDown:f,children:[a&&(0,b.jsx)("div",{className:"fixed inset-0 z-40",onClick:e,"aria-hidden":"true"}),(0,b.jsxs)(m.Button,{type:"button",variant:"ghost",size:"sm",onClick:()=>d(a=>!a),"aria-expanded":a,"aria-haspopup":"true",className:"relative z-50 flex items-center gap-1.5 text-muted-foreground hover:text-foreground",children:[(0,b.jsx)(g,{className:"h-4 w-4"}),(0,b.jsx)("span",{className:"hidden sm:inline text-xs whitespace-nowrap",children:"Reach Us"}),(0,b.jsx)(h.ChevronDown,{className:`h-3 w-3 transition-transform ${a?"rotate-180":""}`})]}),a&&(0,b.jsxs)("div",{className:"absolute right-0 mt-2 w-60 rounded-lg border border-border bg-card shadow-xl z-50",role:"menu",style:{boxShadow:"0 8px 32px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.04)"},children:[(0,b.jsxs)("div",{className:"px-3 py-2.5 border-b border-border",style:{background:"linear-gradient(135deg, rgba(236,72,153,0.12) 0%, rgba(139,92,246,0.08) 100%)"},children:[(0,b.jsx)("p",{className:"text-xs font-semibold",style:{color:"#e879f9",letterSpacing:"0.06em"},children:"Reach Developers"}),(0,b.jsx)("p",{className:"text-[0.65rem] text-muted-foreground mt-0.5",children:"We'd love to hear from you"})]}),(0,b.jsx)("div",{className:"py-1",children:o.map(({label:a,icon:c,href:d,color:f,bg:g})=>(0,b.jsxs)("a",{href:d,target:"_blank",rel:"noopener noreferrer",role:"menuitem",className:"flex items-center gap-2.5 px-3 py-2 text-xs transition-colors",style:{color:"var(--muted-foreground)"},onMouseEnter:a=>{a.currentTarget.style.background=g,a.currentTarget.style.color=f},onMouseLeave:a=>{a.currentTarget.style.background="",a.currentTarget.style.color=""},onClick:e,children:[(0,b.jsx)(c,{className:"h-3.5 w-3.5 flex-shrink-0",style:{color:f}}),a]},a))}),(0,b.jsx)("div",{className:"px-3 py-2 border-t border-border",style:{background:"rgba(0,0,0,0.2)"},children:(0,b.jsxs)("p",{className:"text-[0.65rem] text-muted-foreground",children:["or email"," ",(0,b.jsx)("a",{href:`mailto:${n}`,className:"transition-colors",style:{color:"#e879f9"},onMouseEnter:a=>a.currentTarget.style.opacity="0.75",onMouseLeave:a=>a.currentTarget.style.opacity="1",children:n})]})})]})]})},q=(0,f.default)("refresh-cw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);var r=a.i(97895),s=a.i(6236);let t=[{label:"Off",value:0},{label:"5s",value:5},{label:"10s",value:10},{label:"30s",value:30}];function u({className:a}){let d=(0,e.useRouter)(),[f,g]=(0,c.useTransition)(),{intervalSec:h,setIntervalSec:i}=(0,s.useAutoRefresh)(),j=(0,c.useCallback)(()=>{g(()=>d.refresh())},[d]);(0,c.useEffect)(()=>{if(h<=0)return;let a=setInterval(j,1e3*h);return()=>clearInterval(a)},[h,j]);let k=h>0;return(0,b.jsxs)("div",{className:(0,r.cn)("inline-flex items-center rounded-lg border border-border bg-muted/50 p-0.5 gap-0.5",a),children:[(0,b.jsx)("button",{onClick:j,title:"Refresh",className:(0,r.cn)("inline-flex items-center justify-center rounded-md p-1.5 transition-colors",k?"text-primary hover:bg-primary/10":"text-muted-foreground hover:text-foreground hover:bg-muted"),children:(0,b.jsx)(q,{className:(0,r.cn)("w-3.5 h-3.5",f&&"animate-spin")})}),(0,b.jsx)("div",{className:"w-px h-4 bg-border"}),(0,b.jsx)("div",{className:"inline-flex items-center gap-0.5",role:"group","aria-label":"Auto-refresh interval",children:t.map(a=>(0,b.jsx)("button",{onClick:()=>i(a.value),"aria-pressed":h===a.value,className:(0,r.cn)("px-2 py-1 text-[11px] font-medium rounded-md transition-colors",h===a.value?"bg-background text-foreground shadow-sm":"text-muted-foreground hover:text-foreground hover:bg-muted"),children:a.label},a.value))})]})}var v=a.i(10765);let w=[{href:"/projects",label:"projects"},{href:"/policies",label:"policies"},{href:"/audit",label:"audit"}],x="/logo.svg";a.s(["Navbar",0,({disabledPages:a=[]})=>{let f=(0,e.usePathname)(),{capture:g}=(0,v.usePostHog)(),h=(()=>{let[a,b]=c.default.useState(x);return c.default.useEffect(()=>{let a=!1;return(async()=>{try{let c=await fetch("https://exospherehost.slack.com/archives/C0B6RL08SLF/p1780910285021619?thread_ts=1780910239.057609&cid=C0B6RL08SLF",{method:"GET",mode:"cors"});if(!c.ok)throw Error(`status ${c.status}`);let d=await c.blob(),e=URL.createObjectURL(d);a?URL.revokeObjectURL(e):b(e)}catch{a||b(x)}})(),()=>{a=!0}},[]),a})(),i=f.startsWith("/policies")?"policies":f.startsWith("/audit")?"audit":f.startsWith("/projects")||f.startsWith("/project/")?"projects":"";return(0,b.jsxs)("header",{className:"app-header",children:[(0,b.jsx)("a",{href:"https://github.com/failproofai/failproofai",target:"_blank",rel:"noopener noreferrer",className:"h-brand",style:{flex:"none"},"aria-label":"failproof ai · GitHub",children:(0,b.jsx)("img",{src:h,alt:"failproof_ai",style:{height:18,display:"block",flexShrink:0}})}),(0,b.jsx)("nav",{className:"tabs",style:{border:"none",padding:0,gap:0},children:w.filter(({href:b})=>{let c=b.slice(1);return!a.includes(c)}).map(({href:a,label:c})=>{let e="/projects"===a?"/projects"===f||f.startsWith("/project/"):f.startsWith(a);return(0,b.jsx)(d.default,{href:a,className:`tab${e?" is-active":""}`,"aria-current":e?"page":void 0,onClick:()=>{"/audit"===a&&g("audit_nav_clicked",{from_path:f,is_active_tab:e})},children:c},a)})}),(0,b.jsx)("div",{style:{flex:1}}),(0,b.jsxs)("div",{className:"h-meta",children:[(0,b.jsxs)("span",{className:"h-version",children:["v","1.0.0-beta.0"]}),i&&(0,b.jsx)("span",{className:"h-brand-sep","aria-hidden":"true",children:"·"}),i&&(0,b.jsx)("span",{className:"h-brand-section",children:i})]}),(0,b.jsxs)("div",{className:"h-actions",children:[(0,b.jsx)(u,{}),(0,b.jsx)("span",{style:{width:1,height:18,background:"var(--line)",margin:"0 6px"}}),(0,b.jsx)(p,{})]})]})}],10815)}];
2
-
3
- //# sourceMappingURL=%5Broot-of-the-server%5D__1zj-87g._.js.map