failproofai 1.0.0-beta.1 → 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 (97) 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]__1yo1gyz._.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]__1ig795d._.js → [root-of-the-server]__1psjybo._.js} +2 -2
  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_14q3-7b.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/{07ix3lb4gib0x.js → 0fi645si-uiiq.js} +1 -1
  78. package/.next/standalone/.next/static/chunks/{191njgoik03fm.js → 18rvdw5v72364.js} +1 -1
  79. package/.next/standalone/.next/static/chunks/{2ovmv4esqalpb.js → 2j94uwkb4s3qr.js} +1 -1
  80. package/.next/standalone/.next/static/chunks/{35584kn409ad1.js → 2sbelanzvxvv0.js} +1 -1
  81. package/.next/standalone/.next/static/chunks/{24gs8ip6_spcb.js → 31feojxhjyjpl.js} +1 -1
  82. package/.next/standalone/.next/static/chunks/{1b3a1scouf40o.js → 3hp8ulqknybil.js} +1 -1
  83. package/.next/standalone/.next/static/chunks/{1d71o1_tav721.js → 3s7myk_co0gbh.js} +1 -1
  84. package/.next/standalone/.next/static/chunks/{30is-6r00sbd7.js → 3vs4ay36tjds_.js} +1 -1
  85. package/.next/standalone/.next/static/chunks/{0fdx7ikab08-g.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 +113 -30
  91. package/dist/worker.mjs +1 -1
  92. package/package.json +1 -1
  93. package/src/hooks/configure-wizard.ts +87 -13
  94. package/src/hooks/daemon-service.ts +125 -24
  95. /package/.next/standalone/.next/static/{avJMC2AXvrg13Pbm4QH8F → NWAjs0tHFyzk97gRGG09r}/_buildManifest.js +0 -0
  96. /package/.next/standalone/.next/static/{avJMC2AXvrg13Pbm4QH8F → NWAjs0tHFyzk97gRGG09r}/_clientMiddlewareManifest.js +0 -0
  97. /package/.next/standalone/.next/static/{avJMC2AXvrg13Pbm4QH8F → NWAjs0tHFyzk97gRGG09r}/_ssgManifest.js +0 -0
@@ -8,7 +8,15 @@
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, rmSync } from "node:fs";
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ mkdtempSync,
15
+ readFileSync,
16
+ writeFileSync,
17
+ unlinkSync,
18
+ rmSync,
19
+ } from "node:fs";
12
20
  import { homedir, tmpdir, userInfo } from "node:os";
13
21
  import { resolve, dirname } from "node:path";
14
22
  import { execFileSync } from "node:child_process";
@@ -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`.
@@ -175,6 +191,14 @@ export function resolveWorkerCommand(): string | null {
175
191
  if (!packageRoot) return null;
176
192
  const workerScript = resolve(packageRoot, "dist", "worker.mjs");
177
193
  if (!existsSync(workerScript)) return null;
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
+ //
178
202
  // `process.execPath`, not a bare `node`. A system-scope service does not
179
203
  // inherit a login environment, so its PATH is the system default — and the
180
204
  // single most common way to install Node is nvm, which puts it under
@@ -183,7 +207,13 @@ export function resolveWorkerCommand(): string | null {
183
207
  // silently, on exactly the machines least likely to notice. execPath is
184
208
  // whatever runtime is executing this CLI right now (node for the published
185
209
  // bin, bun in a source checkout), absolute either way.
186
- return `${process.execPath} ${workerScript}`;
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, `'\\''`)}'`;
187
217
  }
188
218
 
189
219
  /**
@@ -224,15 +254,33 @@ function legacySystemdUserUnitPath(): string {
224
254
  return resolve(homedir(), ".config", "systemd", "user", "failproofaid.service");
225
255
  }
226
256
 
227
- const LAUNCHD_LABEL = "ai.failproof.failproofaid";
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
+ }
228
267
 
229
- function launchdPlistPath(): string {
230
- return `/Library/LaunchDaemons/${LAUNCHD_LABEL}.plist`;
268
+ function launchdPlistPath(user: string = serviceUser()): string {
269
+ return `/Library/LaunchDaemons/${launchdLabel(user)}.plist`;
231
270
  }
232
271
 
233
- /** The pre-1.0.0-beta.1 LaunchAgent — same migration problem as above. */
272
+ /** The pre-1.0.0-beta.1 LaunchAgent. */
234
273
  function legacyLaunchAgentPlistPath(): string {
235
- return resolve(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
274
+ return resolve(homedir(), "Library", "LaunchAgents", "ai.failproof.failproofaid.plist");
275
+ }
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";
236
284
  }
237
285
 
238
286
  /**
@@ -255,7 +303,38 @@ export function daemonStatusCommand(): string | null {
255
303
  if (!isDaemonSupportedPlatform()) return null;
256
304
  return process.platform === "linux"
257
305
  ? `systemctl status ${systemdUnitName()}`
258
- : `sudo launchctl print system/${LAUNCHD_LABEL}`;
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
+ }
259
338
  }
260
339
 
261
340
  /** True when privileged commands can run without prompting for a password. */
@@ -289,12 +368,19 @@ function runPrivileged(command: string, args: string[]): void {
289
368
  * mode in the same step, so the file is never briefly world-writable.
290
369
  */
291
370
  function writePrivilegedFile(destination: string, contents: string, mode = "0644"): void {
292
- const staging = resolve(tmpdir(), `failproofaid-${process.pid}-${Date.now()}.tmp`);
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");
293
379
  try {
294
380
  writeFileSync(staging, contents, "utf8");
295
381
  runPrivileged("install", ["-m", mode, staging, destination]);
296
382
  } finally {
297
- rmSync(staging, { force: true });
383
+ rmSync(stagingDir, { recursive: true, force: true });
298
384
  }
299
385
  }
300
386
 
@@ -326,17 +412,30 @@ function removeLegacyUserService(): void {
326
412
  /* best-effort */
327
413
  }
328
414
  } else {
329
- const legacy = legacyLaunchAgentPlistPath();
330
- if (!existsSync(legacy)) return;
331
- try {
332
- execFileSync("launchctl", ["unload", "-w", legacy], {
333
- stdio: "ignore",
334
- timeout: SERVICE_CMD_TIMEOUT_MS,
335
- });
336
- } catch {
337
- /* not loaded */
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]);
338
438
  }
339
- unlinkSync(legacy);
340
439
  }
341
440
  } catch (err) {
342
441
  hookLogWarn(`could not remove the legacy user-scope daemon: ${err instanceof Error ? err.message : String(err)}`);
@@ -390,7 +489,7 @@ function launchdPlistContents(binaryPath: string, logDir: string, workerCmd: str
390
489
  <plist version="1.0">
391
490
  <dict>
392
491
  <key>Label</key>
393
- <string>${LAUNCHD_LABEL}</string>
492
+ <string>${escapeXml(launchdLabel())}</string>
394
493
  <key>ProgramArguments</key>
395
494
  <array>
396
495
  <string>${escapeXml(binaryPath)}</string>
@@ -471,8 +570,10 @@ export async function installDaemonService(): Promise<DaemonInstallResult> {
471
570
  return {
472
571
  installed: false,
473
572
  reason:
474
- "root privileges are required to install the failproofaid system service, and sudo is not available without a password. " +
475
- `Run: sudo failproofai config (or install manually: ${daemonInstallCommands(binaryPath, workerCmd).join(" && ")})`,
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(" && "),
476
577
  };
477
578
  }
478
579
 
@@ -631,7 +732,7 @@ export function daemonServiceStatus(): DaemonServiceStatus {
631
732
  // `launchctl list` cannot see — unlike systemd, reading the state needs
632
733
  // the same elevation installing it did.
633
734
  const root = typeof process.getuid === "function" && process.getuid() === 0;
634
- const args = ["print", `system/${LAUNCHD_LABEL}`];
735
+ const args = ["print", `system/${launchdLabel()}`];
635
736
  const out = root
636
737
  ? execFileSync("launchctl", args, {
637
738
  stdio: ["ignore", "pipe", "ignore"],