@westbayberry/dg 2.0.1 → 2.0.3

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.
@@ -182,6 +182,14 @@ function resolveApiBaseUrl(env) {
182
182
  function resolveToken(env) {
183
183
  return envAuthToken(env) ?? readAuthStateSafe(env)?.token;
184
184
  }
185
+ export function identityHeaders(env) {
186
+ const headers = { "X-Device-Id": getOrCreateDeviceId(env) };
187
+ const token = resolveToken(env);
188
+ if (token) {
189
+ headers.Authorization = `Bearer ${token}`;
190
+ }
191
+ return headers;
192
+ }
185
193
  function readAuthStateSafe(env) {
186
194
  try {
187
195
  return readAuthState(env);
package/dist/bin/dg.js CHANGED
@@ -66,6 +66,11 @@ else {
66
66
  // The auth flows (browser login, paid-verify gate, deep audit upload) already
67
67
  // tell the user exactly what to do; the throttled nudges would just be noise.
68
68
  if (!deviceLogin.handled && !verifyPackage.handled && !audit.handled) {
69
- const { maybeShowNudges } = await import("../runtime/nudges.js");
70
- maybeShowNudges(args);
69
+ try {
70
+ const { maybeShowNudges } = await import("../runtime/nudges.js");
71
+ maybeShowNudges(args);
72
+ }
73
+ catch {
74
+ // dg deleted its own files mid-run (uninstall of itself); nudges are cosmetic.
75
+ }
71
76
  }
@@ -9,12 +9,12 @@ function rule() {
9
9
  return "─".repeat(width);
10
10
  }
11
11
  export const LiveInstall = ({ view }) => {
12
+ if (view.phase === "scanning") {
13
+ return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(InkSpinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", view.total === 0 ? "DG starting protection…" : `DG verifying ${packageCount(view.total)}…`] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", view.current] }) : null] }));
14
+ }
12
15
  if (view.total === 0 && !view.blocked) {
13
16
  return null;
14
17
  }
15
- if (view.phase === "scanning") {
16
- return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(InkSpinner, { type: "dots" }) }), _jsxs(Text, { children: [" DG verifying ", packageCount(view.total), "\u2026"] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", view.current] }) : null] }));
17
- }
18
18
  if (view.blocked) {
19
19
  const blocked = view.blocked;
20
20
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: rule() }), blocked.kind === "blocked" ? (_jsxs(Text, { color: "red", children: ["\u2718 DG blocked install \u2014 ", blocked.headline] })) : (_jsxs(Text, { color: "yellow", children: ["? DG could not verify ", blocked.packageName, " \u2014 ", blocked.headline] })), _jsxs(Text, { children: [" ", blocked.packageName, " ", blocked.reason] }), blocked.override ? _jsxs(Text, { dimColor: true, children: [" ", "Override: ", blocked.override] }) : null, blocked.nextStep ? _jsxs(Text, { dimColor: true, children: [" ", "Next: ", blocked.nextStep] }) : null] }));
@@ -15,6 +15,7 @@ import { artifactDisplayName, artifactUrlHash, extractRegistryMetadataIdentities
15
15
  import { authorityFor, connectViaUpstreamProxy, selectUpstreamProxy } from "./upstream-proxy.js";
16
16
  import { redactSecrets } from "../launcher/output-redaction.js";
17
17
  import { envAuthToken } from "../auth/env-token.js";
18
+ import { identityHeaders } from "../api/analyze.js";
18
19
  export async function startProductionHttpProxy(options) {
19
20
  const ca = createEphemeralCertificateAuthority(options.session.files.ca);
20
21
  const state = {
@@ -629,7 +630,8 @@ async function lookupVerdict(options, target, sha256, upstream, identity) {
629
630
  const response = await fetch(`${options.apiBaseUrl}/v1/install-verdict`, {
630
631
  method: "POST",
631
632
  headers: {
632
- "Content-Type": "application/json"
633
+ "Content-Type": "application/json",
634
+ ...identityHeaders(options.env)
633
635
  },
634
636
  body: JSON.stringify({
635
637
  manager: options.classification.manager,
@@ -1,4 +1,4 @@
1
- import { accessSync, constants, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
1
+ import { accessSync, constants, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { basename, delimiter, dirname, join, resolve } from "node:path";
3
3
  import { chmodSync } from "node:fs";
4
4
  import { createTheme } from "../presentation/theme.js";
@@ -21,6 +21,9 @@ const LEGACY_RC_MARKERS = [
21
21
  { begin: "# >>> dg-managed >>>", end: "# <<< dg-managed <<<" }
22
22
  ];
23
23
  const LEGACY_RC_CANDIDATES = [".zshrc", ".bashrc", ".bash_profile", ".profile", join(".config", "fish", "config.fish")];
24
+ const LEGACY_PYTHON_HOOK_PY = "dg_pip_hook.py";
25
+ const LEGACY_PYTHON_HOOK_PTH = "dg_pip_hook.pth";
26
+ const LEGACY_PYTHON_HOOK_MARKER = "Dependency Guardian pip-install interceptor";
24
27
  export const SETUP_UNINSTALL_LOCK = "setup-uninstall";
25
28
  export const SETUP_UNINSTALL_LOCK_STALE_MS = 30 * 60 * 1000;
26
29
  export const STALE_SESSION_OLDER_THAN_MS = 24 * 60 * 60 * 1000;
@@ -38,6 +41,7 @@ const DOCTOR_GROUP_BY_NAME = {
38
41
  telemetry: "setup",
39
42
  shims: "setup",
40
43
  "shell-rc": "setup",
44
+ "python-hook-drift": "setup",
41
45
  path: "setup",
42
46
  "stale-sessions": "setup",
43
47
  service: "setup",
@@ -53,6 +57,7 @@ const DOCTOR_FIX_BY_NAME = {
53
57
  telemetry: "fix ~/.dg config",
54
58
  shims: "dg setup",
55
59
  "shell-rc": "dg setup",
60
+ "python-hook-drift": "dg uninstall, or re-run dg setup, to remove the stale pip hook",
56
61
  path: "reload your shell after setup",
57
62
  "stale-sessions": "clears on the next protected run",
58
63
  auth: "dg login"
@@ -142,6 +147,7 @@ export function applySetupPlan(plan, now = new Date()) {
142
147
  });
143
148
  writeFileSync(plan.rcPath, withRcBlock(readText(plan.rcPath), plan), "utf8");
144
149
  entries.push(cleanupEntry("rc", plan.rcPath, "mode1", now, RC_SENTINEL));
150
+ sweepLegacyPythonHooks(plan.paths.homeDir, [], []);
145
151
  const registry = withRegistryLock(plan.paths, () => {
146
152
  const merged = mergeRegistry(readRegistry(plan.paths).registry, entries);
147
153
  writeRegistry(plan.paths, merged);
@@ -196,6 +202,7 @@ export function uninstallSetup(options) {
196
202
  }
197
203
  }
198
204
  sweepLegacyRcBlocks(paths.homeDir, removed, warnings);
205
+ sweepLegacyPythonHooks(paths.homeDir, removed, warnings);
199
206
  if (!options.all && !registryRead.malformed && options.keepConfig) {
200
207
  writeRegistryWithLock(paths, {
201
208
  version: 1,
@@ -271,6 +278,14 @@ export function doctorReport(options = {}) {
271
278
  status: rcEntries.length > 0 && missingRc.length === 0 ? "pass" : "warn",
272
279
  message: rcEntries.length === 0 ? "No dg shell rc block is registered" : `Registered shell rc blocks: ${rcEntries.length}`
273
280
  });
281
+ const staleHookSites = legacyPythonHookSites(paths.homeDir);
282
+ checks.push({
283
+ name: "python-hook-drift",
284
+ status: staleHookSites.length === 0 ? "pass" : "warn",
285
+ message: staleHookSites.length === 0
286
+ ? "No legacy dg pip hooks in user site-packages"
287
+ : `Legacy dg pip hooks break pip in: ${staleHookSites.join(", ")}`
288
+ });
274
289
  checks.push(pathPrecedenceCheck(env, shimDir));
275
290
  checks.push({
276
291
  name: "stale-sessions",
@@ -453,11 +468,31 @@ function reloadInstructions(shell) {
453
468
  }
454
469
  return ["start a new shell or run: source ~/.zshrc", "clear cached command paths with: rehash"];
455
470
  }
456
- // Absolute path: a bare `exec dg` resolves through PATH, so anything that
457
- // shadows dg (npm run prepending node_modules/.bin with a vendored copy)
458
- // hijacks every shimmed package manager.
471
+ // The baked absolute dg path stops a PATH-shadowing dg from hijacking the shim;
472
+ // the fallbacks make it fail open a removed or moved dg (uninstall, mid-upgrade)
473
+ // runs the real manager instead of bricking it.
459
474
  export function shimSource(command) {
460
- return `#!/bin/sh\n# ${SHIM_SENTINEL}\nDG_SHIM_ACTIVE="\${DG_SHIM_ACTIVE:+$DG_SHIM_ACTIVE,}${command}:$$" exec "${escapeDoubleQuotedSh(dgEntrypoint())}" ${command} "$@"\n`;
475
+ const dg = escapeDoubleQuotedSh(dgEntrypoint());
476
+ const nonce = '"${DG_SHIM_ACTIVE:+$DG_SHIM_ACTIVE,}' + `${command}:$$"`;
477
+ return [
478
+ "#!/bin/sh",
479
+ `# ${SHIM_SENTINEL}`,
480
+ `if [ -x "${dg}" ]; then`,
481
+ ` DG_SHIM_ACTIVE=${nonce} exec "${dg}" ${command} "$@"`,
482
+ "fi",
483
+ `dg_path=$(printf '%s' "$PATH" | awk -v RS=':' -v ORS=':' '$0 != ENVIRON["HOME"] "/.dg/shims"' | sed 's/:$//')`,
484
+ `dg_bin=$(PATH="$dg_path" command -v dg 2>/dev/null)`,
485
+ `if [ -n "$dg_bin" ]; then`,
486
+ ` DG_SHIM_ACTIVE=${nonce} exec "$dg_bin" ${command} "$@"`,
487
+ "fi",
488
+ `real_bin=$(PATH="$dg_path" command -v ${command} 2>/dev/null)`,
489
+ `if [ -n "$real_bin" ]; then`,
490
+ ` exec "$real_bin" "$@"`,
491
+ "fi",
492
+ `echo "dg: protection unavailable and no real ${command} found on PATH" >&2`,
493
+ "exit 127",
494
+ ""
495
+ ].join("\n");
461
496
  }
462
497
  function escapeDoubleQuotedSh(value) {
463
498
  return value.replace(/[\\"$`]/g, "\\$&");
@@ -517,6 +552,55 @@ function sweepLegacyRcBlocks(homeDir, removed, warnings) {
517
552
  }
518
553
  }
519
554
  }
555
+ export function legacyPythonHookSites(homeDir) {
556
+ return candidateSitePackagesDirs(homeDir).filter((dir) => existsSync(join(dir, LEGACY_PYTHON_HOOK_PTH)) || readText(join(dir, LEGACY_PYTHON_HOOK_PY)).includes(LEGACY_PYTHON_HOOK_MARKER));
557
+ }
558
+ function sweepLegacyPythonHooks(homeDir, removed, warnings) {
559
+ for (const dir of candidateSitePackagesDirs(homeDir)) {
560
+ const pyPath = join(dir, LEGACY_PYTHON_HOOK_PY);
561
+ const pthPath = join(dir, LEGACY_PYTHON_HOOK_PTH);
562
+ const pyIsHook = readText(pyPath).includes(LEGACY_PYTHON_HOOK_MARKER);
563
+ const pthPresent = existsSync(pthPath);
564
+ if (!pyIsHook && !pthPresent) {
565
+ continue;
566
+ }
567
+ if (pthPresent) {
568
+ removePythonHookFile(pthPath, removed, warnings);
569
+ }
570
+ if (pyIsHook) {
571
+ removePythonHookFile(pyPath, removed, warnings);
572
+ }
573
+ }
574
+ }
575
+ function removePythonHookFile(path, removed, warnings) {
576
+ try {
577
+ rmSync(path, { force: true });
578
+ removed.push(`${path} (legacy dg pip hook)`);
579
+ }
580
+ catch (error) {
581
+ warnings.push(`could not remove legacy dg pip hook ${path}: ${error instanceof Error ? error.message : "remove error"}`);
582
+ }
583
+ }
584
+ function candidateSitePackagesDirs(homeDir) {
585
+ const dirs = [];
586
+ for (const version of safeReaddir(join(homeDir, "Library", "Python"))) {
587
+ dirs.push(join(homeDir, "Library", "Python", version, "lib", "python", "site-packages"));
588
+ }
589
+ for (const entry of safeReaddir(join(homeDir, ".local", "lib"))) {
590
+ if (entry.startsWith("python")) {
591
+ dirs.push(join(homeDir, ".local", "lib", entry, "site-packages"));
592
+ }
593
+ }
594
+ return dirs;
595
+ }
596
+ function safeReaddir(dir) {
597
+ try {
598
+ return readdirSync(dir);
599
+ }
600
+ catch {
601
+ return [];
602
+ }
603
+ }
520
604
  export function cleanupEntry(kind, path, mode, now, sentinel) {
521
605
  return {
522
606
  kind,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westbayberry/dg",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Dependency Guardian supply-chain firewall CLI",
5
5
  "type": "module",
6
6
  "bin": {