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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +9 -9
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0id_xf7._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19120tr._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0114ewg._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02r5bgf._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e8sjqm._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lkzqax._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1yo1gyz._.js → [root-of-the-server]__0nb0syg._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qucxyj._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u02miy._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ath6v_._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ig795d._.js → [root-of-the-server]__1psjybo._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1zqz4v8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- 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
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +11 -11
- package/.next/standalone/.next/static/chunks/{07ix3lb4gib0x.js → 0fi645si-uiiq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{191njgoik03fm.js → 18rvdw5v72364.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2ovmv4esqalpb.js → 2j94uwkb4s3qr.js} +1 -1
- package/.next/standalone/.next/static/chunks/{35584kn409ad1.js → 2sbelanzvxvv0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{24gs8ip6_spcb.js → 31feojxhjyjpl.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1b3a1scouf40o.js → 3hp8ulqknybil.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1d71o1_tav721.js → 3s7myk_co0gbh.js} +1 -1
- package/.next/standalone/.next/static/chunks/{30is-6r00sbd7.js → 3vs4ay36tjds_.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0fdx7ikab08-g.js → 40k1_v4os4cw6.js} +1 -1
- package/.next/standalone/Cargo.lock +2 -2
- package/.next/standalone/Cargo.toml +1 -1
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/dist/cli.mjs +113 -30
- package/dist/worker.mjs +1 -1
- package/package.json +1 -1
- package/src/hooks/configure-wizard.ts +87 -13
- package/src/hooks/daemon-service.ts +125 -24
- /package/.next/standalone/.next/static/{avJMC2AXvrg13Pbm4QH8F → NWAjs0tHFyzk97gRGG09r}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{avJMC2AXvrg13Pbm4QH8F → NWAjs0tHFyzk97gRGG09r}/_clientMiddlewareManifest.js +0 -0
- /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 {
|
|
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
|
-
|
|
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/${
|
|
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
|
|
272
|
+
/** The pre-1.0.0-beta.1 LaunchAgent. */
|
|
234
273
|
function legacyLaunchAgentPlistPath(): string {
|
|
235
|
-
return resolve(homedir(), "Library", "LaunchAgents",
|
|
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/${
|
|
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
|
-
|
|
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(
|
|
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
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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>${
|
|
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
|
|
475
|
-
`
|
|
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/${
|
|
735
|
+
const args = ["print", `system/${launchdLabel()}`];
|
|
635
736
|
const out = root
|
|
636
737
|
? execFileSync("launchctl", args, {
|
|
637
738
|
stdio: ["ignore", "pipe", "ignore"],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|