@westbayberry/dg 1.3.3 → 2.0.0
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/LICENSE +1 -201
- package/NOTICE +1 -4
- package/README.md +293 -0
- package/dist/api/analyze.js +210 -0
- package/dist/audit/deep.js +180 -0
- package/dist/audit/detectors.js +247 -0
- package/dist/audit/events.js +41 -0
- package/dist/audit/rules.js +426 -0
- package/dist/audit-ui/AuditApp.js +39 -0
- package/dist/audit-ui/components/AuditHeader.js +24 -0
- package/dist/audit-ui/components/AuditResultsView.js +307 -0
- package/dist/audit-ui/components/DeepStatusRow.js +11 -0
- package/dist/audit-ui/export.js +85 -0
- package/dist/audit-ui/format.js +34 -0
- package/dist/audit-ui/launch.js +34 -0
- package/dist/auth/device-login.js +271 -0
- package/dist/auth/env-token.js +6 -0
- package/dist/auth/login-app.js +156 -0
- package/dist/auth/store.js +147 -0
- package/dist/bin/dg.js +71 -0
- package/dist/commands/audit.js +357 -0
- package/dist/commands/completion.js +116 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/doctor.js +39 -0
- package/dist/commands/explain.js +100 -0
- package/dist/commands/guard-commit.js +158 -0
- package/dist/commands/help.js +74 -0
- package/dist/commands/licenses.js +435 -0
- package/dist/commands/login.js +81 -0
- package/dist/commands/logout.js +37 -0
- package/dist/commands/router.js +98 -0
- package/dist/commands/scan.js +18 -0
- package/dist/commands/service.js +475 -0
- package/dist/commands/setup.js +302 -0
- package/dist/commands/status.js +115 -0
- package/dist/commands/suggest.js +35 -0
- package/dist/commands/types.js +4 -0
- package/dist/commands/unavailable.js +11 -0
- package/dist/commands/uninstall.js +111 -0
- package/dist/commands/update.js +210 -0
- package/dist/commands/verify.js +151 -0
- package/dist/commands/version.js +22 -0
- package/dist/commands/wrap.js +55 -0
- package/dist/config/settings.js +302 -0
- package/dist/install-ui/LiveInstall.js +24 -0
- package/dist/install-ui/block-render.js +83 -0
- package/dist/install-ui/live-install-app.js +48 -0
- package/dist/install-ui/prompt.js +24 -0
- package/dist/launcher/classify.js +116 -0
- package/dist/launcher/env.js +53 -0
- package/dist/launcher/live-install.js +50 -0
- package/dist/launcher/output-redaction.js +77 -0
- package/dist/launcher/preflight-prompt.js +139 -0
- package/dist/launcher/resolve-real-binary.js +73 -0
- package/dist/launcher/run.js +417 -0
- package/dist/policy/evaluate.js +128 -0
- package/dist/presentation/mode.js +52 -0
- package/dist/presentation/theme.js +29 -0
- package/dist/proxy/buffer-budget.js +64 -0
- package/dist/proxy/ca.js +126 -0
- package/dist/proxy/classify-host.js +26 -0
- package/dist/proxy/enforcement.js +102 -0
- package/dist/proxy/metadata-map.js +336 -0
- package/dist/proxy/server.js +909 -0
- package/dist/proxy/upstream-proxy.js +102 -0
- package/dist/proxy/worker.js +39 -0
- package/dist/publish-set/collect.js +51 -0
- package/dist/publish-set/no-exec-shell.js +19 -0
- package/dist/publish-set/npm.js +109 -0
- package/dist/publish-set/pack.js +36 -0
- package/dist/publish-set/pypi.js +59 -0
- package/dist/runtime/cli.js +17 -0
- package/dist/runtime/first-run.js +60 -0
- package/dist/runtime/node-version.js +58 -0
- package/dist/runtime/nudges.js +105 -0
- package/dist/scan/analyze-worker.js +21 -0
- package/dist/scan/collect.js +153 -0
- package/dist/scan/command.js +159 -0
- package/dist/scan/discovery.js +209 -0
- package/dist/scan/render.js +240 -0
- package/dist/scan/scanner-report.js +82 -0
- package/dist/scan/staged.js +173 -0
- package/dist/scan/types.js +1 -0
- package/dist/scan-ui/LegacyApp.js +156 -0
- package/dist/scan-ui/alt-screen.js +84 -0
- package/dist/scan-ui/api-aliases.js +1 -0
- package/dist/scan-ui/components/ErrorView.js +23 -0
- package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
- package/dist/scan-ui/components/ProgressBar.js +89 -0
- package/dist/scan-ui/components/ProjectSelector.js +62 -0
- package/dist/scan-ui/components/ScoreHeader.js +20 -0
- package/dist/scan-ui/components/SetupBanner.js +13 -0
- package/dist/scan-ui/components/Spinner.js +4 -0
- package/dist/scan-ui/format-helpers.js +40 -0
- package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
- package/dist/scan-ui/hooks/useScan.js +113 -0
- package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
- package/dist/scan-ui/launch.js +27 -0
- package/dist/scan-ui/logo.js +91 -0
- package/dist/scan-ui/shims.js +30 -0
- package/dist/security/sanitize.js +28 -0
- package/dist/service/state.js +837 -0
- package/dist/service/trust-store.js +234 -0
- package/dist/service/worker.js +88 -0
- package/dist/setup/git-hook.js +244 -0
- package/dist/setup/optional-support.js +58 -0
- package/dist/setup/plan.js +899 -0
- package/dist/state/cleanup-registry.js +60 -0
- package/dist/state/index.js +5 -0
- package/dist/state/locks.js +161 -0
- package/dist/state/paths.js +24 -0
- package/dist/state/sessions.js +170 -0
- package/dist/state/store.js +50 -0
- package/dist/telemetry/events.js +40 -0
- package/dist/util/git.js +20 -0
- package/dist/util/tty-prompt.js +43 -0
- package/dist/verify/local.js +400 -0
- package/dist/verify/package-check.js +240 -0
- package/dist/verify/preflight.js +698 -0
- package/dist/verify/render.js +184 -0
- package/dist/verify/types.js +1 -0
- package/package.json +33 -50
- package/dist/index.mjs +0 -54116
- package/dist/postinstall.mjs +0 -731
- package/dist/python-hook/dg_pip_hook.pth +0 -1
- package/dist/python-hook/dg_pip_hook.py +0 -130
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import { displayTier, writeAuthState } from "./store.js";
|
|
4
|
+
import { loadUserConfig } from "../config/settings.js";
|
|
5
|
+
import { createTheme } from "../presentation/theme.js";
|
|
6
|
+
import { resolvePresentation } from "../presentation/mode.js";
|
|
7
|
+
const DEFAULT_WEB_BASE = "https://westbayberry.com";
|
|
8
|
+
export const POLL_INTERVAL_MS = 2000;
|
|
9
|
+
export const POLL_TIMEOUT_MS = 5 * 60_000;
|
|
10
|
+
export function resolveWebBase(env) {
|
|
11
|
+
const override = env.DG_AUTH_BASE;
|
|
12
|
+
if (override) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(override);
|
|
15
|
+
if (url.protocol === "https:" || url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
16
|
+
return override.replace(/\/$/, "");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// fall through to derived/default base
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(loadUserConfig(env).api.baseUrl);
|
|
25
|
+
if (url.hostname.startsWith("api.")) {
|
|
26
|
+
return `${url.protocol}//${url.hostname.slice(4)}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// fall through to default base
|
|
31
|
+
}
|
|
32
|
+
return DEFAULT_WEB_BASE;
|
|
33
|
+
}
|
|
34
|
+
function isSameOrSubdomain(host, base) {
|
|
35
|
+
return host === base || host.endsWith(`.${base}`);
|
|
36
|
+
}
|
|
37
|
+
export function assertTrustedVerifyUrl(verifyUrl, webBase) {
|
|
38
|
+
let verify;
|
|
39
|
+
try {
|
|
40
|
+
verify = new URL(verifyUrl);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error("login server returned an invalid verify URL");
|
|
44
|
+
}
|
|
45
|
+
if (verify.protocol !== "https:" && verify.protocol !== "http:") {
|
|
46
|
+
throw new Error("login server returned an unsupported verify URL");
|
|
47
|
+
}
|
|
48
|
+
const baseHost = new URL(webBase).hostname;
|
|
49
|
+
if (!isSameOrSubdomain(verify.hostname, baseHost)) {
|
|
50
|
+
throw new Error(`refusing to open verify URL on untrusted host '${verify.hostname}'`);
|
|
51
|
+
}
|
|
52
|
+
return verifyUrl;
|
|
53
|
+
}
|
|
54
|
+
export async function createAuthSession(webBase, fetchImpl) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetchImpl(`${webBase}/cli/auth/sessions`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
signal: controller.signal
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`could not start login (HTTP ${response.status})`);
|
|
65
|
+
}
|
|
66
|
+
const json = (await response.json());
|
|
67
|
+
const verifyUrl = assertTrustedVerifyUrl(json.verify_url, webBase);
|
|
68
|
+
return { sessionId: json.session_id, verifyUrl, expiresIn: json.expires_in };
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function pollAuthSession(webBase, sessionId, fetchImpl) {
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeout = setTimeout(() => controller.abort(), 8_000);
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetchImpl(`${webBase}/cli/auth/sessions/${sessionId}/token`, { signal: controller.signal });
|
|
79
|
+
if (response.status >= 500) {
|
|
80
|
+
return { status: "pending" };
|
|
81
|
+
}
|
|
82
|
+
if (response.status === 404 || !response.ok) {
|
|
83
|
+
return { status: "expired" };
|
|
84
|
+
}
|
|
85
|
+
const json = (await response.json());
|
|
86
|
+
return { status: json.status, apiKey: json.api_key, email: json.email };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
90
|
+
return { status: "pending" };
|
|
91
|
+
}
|
|
92
|
+
return { status: "expired" };
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function openBrowser(url) {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = new URL(url);
|
|
101
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let command;
|
|
109
|
+
let args;
|
|
110
|
+
switch (process.platform) {
|
|
111
|
+
case "darwin":
|
|
112
|
+
command = "open";
|
|
113
|
+
args = [url];
|
|
114
|
+
break;
|
|
115
|
+
case "linux":
|
|
116
|
+
command = "xdg-open";
|
|
117
|
+
args = [url];
|
|
118
|
+
break;
|
|
119
|
+
case "win32":
|
|
120
|
+
command = "cmd.exe";
|
|
121
|
+
args = ["/c", "start", "", url];
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
128
|
+
child.on("error", () => { });
|
|
129
|
+
child.unref();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// a failed browser open is non-fatal; the verify URL is printed too
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function delay(ms) {
|
|
136
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
137
|
+
}
|
|
138
|
+
function waitForEnter() {
|
|
139
|
+
let tty;
|
|
140
|
+
try {
|
|
141
|
+
tty = openSync("/dev/tty", "rs");
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const byte = Buffer.alloc(1);
|
|
148
|
+
for (;;) {
|
|
149
|
+
let read = 0;
|
|
150
|
+
try {
|
|
151
|
+
read = readSync(tty, byte, 0, 1, null);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code === "EAGAIN") {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
if (read === 0) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
const char = byte.toString("utf8");
|
|
163
|
+
if (char === "\n" || char === "\r") {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
closeSync(tty);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export async function fetchAccountTier(token, env, fetchImpl) {
|
|
173
|
+
let apiBase;
|
|
174
|
+
try {
|
|
175
|
+
apiBase = loadUserConfig(env).api.baseUrl.replace(/\/$/, "");
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const timer = setTimeout(() => controller.abort(), 5_000);
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetchImpl(`${apiBase}/v1/auth/status`, {
|
|
184
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
185
|
+
signal: controller.signal
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const body = (await response.json());
|
|
191
|
+
if (typeof body.tier !== "string" || body.tier.length === 0) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return body.tier.toLowerCase();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export async function runDeviceLogin(io = {}) {
|
|
204
|
+
const env = io.env ?? process.env;
|
|
205
|
+
const fetchImpl = io.fetchImpl ?? fetch;
|
|
206
|
+
const stderr = io.stderr ?? process.stderr;
|
|
207
|
+
const open = io.open ?? openBrowser;
|
|
208
|
+
const confirm = io.confirm ?? waitForEnter;
|
|
209
|
+
const now = io.now ?? Date.now;
|
|
210
|
+
const sleep = io.sleep ?? delay;
|
|
211
|
+
const webBase = resolveWebBase(env);
|
|
212
|
+
const theme = createTheme(resolvePresentation().color);
|
|
213
|
+
const accent = (text) => theme.paint("accent", text);
|
|
214
|
+
const muted = (text) => theme.paint("muted", text);
|
|
215
|
+
let session;
|
|
216
|
+
try {
|
|
217
|
+
session = await createAuthSession(webBase, fetchImpl);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return {
|
|
221
|
+
exitCode: 1,
|
|
222
|
+
stdout: "",
|
|
223
|
+
stderr: `dg login: ${error instanceof Error ? error.message : "could not start login"}.\n`
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
stderr.write(`\n Sign in at:\n ${accent(session.verifyUrl)}\n\n ${muted("Press Enter to open it in your browser…")}`);
|
|
227
|
+
confirm();
|
|
228
|
+
open(session.verifyUrl);
|
|
229
|
+
stderr.write(`\n ${muted("Waiting for you to approve in the browser…")}\n\n`);
|
|
230
|
+
const deadline = now() + POLL_TIMEOUT_MS;
|
|
231
|
+
for (;;) {
|
|
232
|
+
const result = await pollAuthSession(webBase, session.sessionId, fetchImpl);
|
|
233
|
+
if (result.status === "complete" && result.apiKey) {
|
|
234
|
+
const tier = await fetchAccountTier(result.apiKey, env, fetchImpl);
|
|
235
|
+
writeAuthState({ token: result.apiKey, email: result.email, tier: tier ?? undefined });
|
|
236
|
+
const who = result.email ? ` as ${result.email}` : "";
|
|
237
|
+
return {
|
|
238
|
+
exitCode: 0,
|
|
239
|
+
stdout: `✓ Logged in${who}${tier ? ` ${muted(`(${displayTier(tier)} plan)`)}` : ""}.\n`,
|
|
240
|
+
stderr: ""
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (result.status === "expired") {
|
|
244
|
+
return { exitCode: 1, stdout: "", stderr: "dg login: that login link expired. Run 'dg login' again.\n" };
|
|
245
|
+
}
|
|
246
|
+
if (now() >= deadline) {
|
|
247
|
+
return { exitCode: 1, stdout: "", stderr: "dg login: timed out waiting for browser approval. Run 'dg login' again.\n" };
|
|
248
|
+
}
|
|
249
|
+
await sleep(POLL_INTERVAL_MS);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export async function maybeDeviceLogin(args) {
|
|
253
|
+
const noop = { exitCode: 0, stdout: "", stderr: "" };
|
|
254
|
+
if (args[0] !== "login") {
|
|
255
|
+
return { handled: false, result: noop };
|
|
256
|
+
}
|
|
257
|
+
const rest = args.slice(1);
|
|
258
|
+
if (rest.some((arg) => arg === "--token" || arg.startsWith("--token=") || arg === "--help" || arg === "-h")) {
|
|
259
|
+
return { handled: false, result: noop };
|
|
260
|
+
}
|
|
261
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
262
|
+
return { handled: false, result: noop };
|
|
263
|
+
}
|
|
264
|
+
const { resolvePresentation } = await import("../presentation/mode.js");
|
|
265
|
+
if (resolvePresentation().mode === "rich") {
|
|
266
|
+
const { runDeviceLoginTui } = await import("./login-app.js");
|
|
267
|
+
const exitCode = await runDeviceLoginTui();
|
|
268
|
+
return { handled: true, result: { exitCode, stdout: "", stderr: "" } };
|
|
269
|
+
}
|
|
270
|
+
return { handled: true, result: await runDeviceLogin() };
|
|
271
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// DG_API_KEY is the documented public name for the CI auth token (every docs page,
|
|
2
|
+
// the settings UI, and the CLI reference use it); DG_API_TOKEN is the historical
|
|
3
|
+
// alias kept for back-compat. Accept either so CI that follows the docs works.
|
|
4
|
+
export function envAuthToken(env) {
|
|
5
|
+
return env.DG_API_KEY || env.DG_API_TOKEN || undefined;
|
|
6
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
4
|
+
import { Spinner } from "../scan-ui/components/Spinner.js";
|
|
5
|
+
import { displayTier, writeAuthState } from "./store.js";
|
|
6
|
+
import { POLL_INTERVAL_MS, POLL_TIMEOUT_MS, createAuthSession, fetchAccountTier, openBrowser, pollAuthSession, resolveWebBase } from "./device-login.js";
|
|
7
|
+
const SUCCESS_MIN_MS = 600;
|
|
8
|
+
const SUCCESS_MAX_MS = 2500;
|
|
9
|
+
function useLogin(webBase, env) {
|
|
10
|
+
const [state, setState] = useState({ phase: "creating", verifyUrl: "", email: "", plan: "", message: "" });
|
|
11
|
+
const sessionId = useRef("");
|
|
12
|
+
const started = useRef(false);
|
|
13
|
+
const cancelled = useRef(false);
|
|
14
|
+
const tierSettled = useRef(false);
|
|
15
|
+
const pollTimer = useRef(undefined);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let sessionCancelled = false;
|
|
18
|
+
createAuthSession(webBase, fetch)
|
|
19
|
+
.then((session) => {
|
|
20
|
+
if (sessionCancelled)
|
|
21
|
+
return;
|
|
22
|
+
sessionId.current = session.sessionId;
|
|
23
|
+
setState((prev) => ({ ...prev, phase: "ready", verifyUrl: session.verifyUrl }));
|
|
24
|
+
})
|
|
25
|
+
.catch((error) => {
|
|
26
|
+
if (sessionCancelled)
|
|
27
|
+
return;
|
|
28
|
+
const message = error instanceof Error ? error.message : "could not start login";
|
|
29
|
+
setState((prev) => ({ ...prev, phase: "error", message }));
|
|
30
|
+
});
|
|
31
|
+
return () => {
|
|
32
|
+
sessionCancelled = true;
|
|
33
|
+
};
|
|
34
|
+
}, [webBase]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
return () => {
|
|
37
|
+
cancelled.current = true;
|
|
38
|
+
if (pollTimer.current) {
|
|
39
|
+
clearTimeout(pollTimer.current);
|
|
40
|
+
pollTimer.current = undefined;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
const openAndPoll = () => {
|
|
45
|
+
if (started.current)
|
|
46
|
+
return;
|
|
47
|
+
started.current = true;
|
|
48
|
+
openBrowser(state.verifyUrl);
|
|
49
|
+
setState((prev) => ({ ...prev, phase: "waiting" }));
|
|
50
|
+
void (async () => {
|
|
51
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
52
|
+
while (!cancelled.current) {
|
|
53
|
+
const result = await pollAuthSession(webBase, sessionId.current, fetch);
|
|
54
|
+
if (cancelled.current)
|
|
55
|
+
return;
|
|
56
|
+
if (result.status === "complete" && result.apiKey) {
|
|
57
|
+
const apiKey = result.apiKey;
|
|
58
|
+
const email = result.email ?? "";
|
|
59
|
+
writeAuthState({ token: apiKey, email: result.email });
|
|
60
|
+
setState((prev) => ({ ...prev, phase: "success", email }));
|
|
61
|
+
void fetchAccountTier(apiKey, env, fetch)
|
|
62
|
+
.then((tier) => {
|
|
63
|
+
if (tier) {
|
|
64
|
+
writeAuthState({ token: apiKey, email: result.email, tier });
|
|
65
|
+
if (!cancelled.current) {
|
|
66
|
+
setState((prev) => ({ ...prev, plan: tier }));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.finally(() => {
|
|
71
|
+
tierSettled.current = true;
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (result.status === "expired") {
|
|
76
|
+
setState((prev) => ({ ...prev, phase: "expired" }));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (Date.now() >= deadline) {
|
|
80
|
+
setState((prev) => ({ ...prev, phase: "error", message: "timed out waiting for browser approval" }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await new Promise((resolve) => {
|
|
84
|
+
pollTimer.current = setTimeout(resolve, POLL_INTERVAL_MS);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
};
|
|
89
|
+
return { state, tierSettled, openAndPoll };
|
|
90
|
+
}
|
|
91
|
+
const LoginApp = ({ webBase, env }) => {
|
|
92
|
+
const { state, tierSettled, openAndPoll } = useLogin(webBase, env);
|
|
93
|
+
const { exit } = useApp();
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (state.phase === "expired" || state.phase === "error") {
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
const timer = setTimeout(() => exit(), 0);
|
|
98
|
+
return () => clearTimeout(timer);
|
|
99
|
+
}
|
|
100
|
+
if (state.phase === "success") {
|
|
101
|
+
process.exitCode = 0;
|
|
102
|
+
const start = Date.now();
|
|
103
|
+
let poll;
|
|
104
|
+
let cap;
|
|
105
|
+
const tick = () => {
|
|
106
|
+
if (tierSettled.current && Date.now() - start >= SUCCESS_MIN_MS) {
|
|
107
|
+
exit();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
poll = setTimeout(tick, 50);
|
|
111
|
+
};
|
|
112
|
+
poll = setTimeout(tick, SUCCESS_MIN_MS);
|
|
113
|
+
cap = setTimeout(() => exit(), SUCCESS_MAX_MS);
|
|
114
|
+
return () => {
|
|
115
|
+
if (poll)
|
|
116
|
+
clearTimeout(poll);
|
|
117
|
+
if (cap)
|
|
118
|
+
clearTimeout(cap);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}, [state.phase, exit, tierSettled]);
|
|
123
|
+
useInput((_input, key) => {
|
|
124
|
+
if (state.phase === "ready" && key.return) {
|
|
125
|
+
openAndPoll();
|
|
126
|
+
}
|
|
127
|
+
else if (state.phase === "success") {
|
|
128
|
+
exit();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
switch (state.phase) {
|
|
132
|
+
case "creating":
|
|
133
|
+
return (_jsx(Box, { paddingLeft: 1, paddingTop: 1, children: _jsx(Spinner, { label: "Creating login session\u2026" }) }));
|
|
134
|
+
case "ready":
|
|
135
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: [_jsx(Text, { children: "Sign in at:" }), _jsx(Text, { color: "cyan", children: state.verifyUrl }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press Enter to open it in your browser\u2026" })] }));
|
|
136
|
+
case "waiting":
|
|
137
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: [_jsx(Text, { children: "Sign in at:" }), _jsx(Text, { color: "cyan", children: state.verifyUrl }), _jsx(Text, { children: " " }), _jsx(Spinner, { label: "Waiting for you to approve in the browser\u2026" })] }));
|
|
138
|
+
case "success":
|
|
139
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Logged in", state.email ? ` as ${state.email}` : "", state.plan ? ` (${displayTier(state.plan)} plan)` : ""] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Run `dg scan` to check your dependencies." })] }));
|
|
140
|
+
case "expired":
|
|
141
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: [_jsx(Text, { color: "yellow", children: "That login link expired." }), _jsx(Text, { dimColor: true, children: "Run `dg login` to try again." })] }));
|
|
142
|
+
case "error":
|
|
143
|
+
return (_jsx(Box, { flexDirection: "column", paddingLeft: 1, paddingTop: 1, children: _jsxs(Text, { color: "red", children: ["dg login: ", state.message, "."] }) }));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
export async function runDeviceLoginTui(env = process.env) {
|
|
147
|
+
const ci = process.env.CI;
|
|
148
|
+
if (ci === "" || ci === "0" || ci === "false") {
|
|
149
|
+
delete process.env.CI;
|
|
150
|
+
}
|
|
151
|
+
const { render } = await import("ink");
|
|
152
|
+
const webBase = resolveWebBase(env);
|
|
153
|
+
const instance = render(_jsx(LoginApp, { webBase: webBase, env: env }), { exitOnCtrlC: true });
|
|
154
|
+
await instance.waitUntilExit();
|
|
155
|
+
return Number(process.exitCode ?? 0);
|
|
156
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { loadUserConfig, saveUserConfig } from "../config/settings.js";
|
|
5
|
+
import { resolveDgPaths } from "../state/index.js";
|
|
6
|
+
import { envAuthToken } from "./env-token.js";
|
|
7
|
+
export class AuthError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "AuthError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function authPath(paths) {
|
|
14
|
+
return join(paths.configDir, "auth.json");
|
|
15
|
+
}
|
|
16
|
+
export function readAuthState(env = process.env) {
|
|
17
|
+
const paths = resolveDgPaths(env);
|
|
18
|
+
const path = authPath(paths);
|
|
19
|
+
if (!existsSync(path)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
+
if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl || parsed.loggedInAt === undefined) {
|
|
25
|
+
throw new AuthError("unsupported auth state");
|
|
26
|
+
}
|
|
27
|
+
const email = typeof parsed.email === "string" && parsed.email.length > 0 ? parsed.email : undefined;
|
|
28
|
+
const tier = typeof parsed.tier === "string" && parsed.tier.length > 0 ? parsed.tier : undefined;
|
|
29
|
+
return {
|
|
30
|
+
version: 1,
|
|
31
|
+
token: parsed.token,
|
|
32
|
+
tokenPreview: parsed.tokenPreview ?? redactToken(parsed.token),
|
|
33
|
+
apiBaseUrl: parsed.apiBaseUrl,
|
|
34
|
+
orgId: parsed.orgId ?? "",
|
|
35
|
+
loggedInAt: parsed.loggedInAt,
|
|
36
|
+
...(email ? { email } : {}),
|
|
37
|
+
...(tier ? { tier } : {})
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new AuthError(`Malformed dg auth state at ${path}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function writeAuthState(options, env = process.env) {
|
|
45
|
+
const token = options.token.trim();
|
|
46
|
+
if (token.length < 8) {
|
|
47
|
+
throw new AuthError("token must be at least 8 characters");
|
|
48
|
+
}
|
|
49
|
+
const config = loadUserConfig(env);
|
|
50
|
+
const apiBaseUrl = options.apiBaseUrl ?? config.api.baseUrl;
|
|
51
|
+
const orgId = options.orgId ?? config.org.id;
|
|
52
|
+
const email = typeof options.email === "string" && options.email.length > 0 ? options.email : undefined;
|
|
53
|
+
const tier = typeof options.tier === "string" && options.tier.length > 0 ? options.tier : undefined;
|
|
54
|
+
const state = {
|
|
55
|
+
version: 1,
|
|
56
|
+
token,
|
|
57
|
+
tokenPreview: redactToken(token),
|
|
58
|
+
apiBaseUrl,
|
|
59
|
+
orgId,
|
|
60
|
+
loggedInAt: (options.now ?? new Date()).toISOString(),
|
|
61
|
+
...(email ? { email } : {}),
|
|
62
|
+
...(tier ? { tier } : {})
|
|
63
|
+
};
|
|
64
|
+
const paths = resolveDgPaths(env);
|
|
65
|
+
writeJsonAtomic(authPath(paths), state);
|
|
66
|
+
saveUserConfig({
|
|
67
|
+
...config,
|
|
68
|
+
api: {
|
|
69
|
+
baseUrl: apiBaseUrl
|
|
70
|
+
},
|
|
71
|
+
org: {
|
|
72
|
+
id: orgId
|
|
73
|
+
}
|
|
74
|
+
}, env);
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
export function clearAuthState(env = process.env) {
|
|
78
|
+
const path = authPath(resolveDgPaths(env));
|
|
79
|
+
if (!existsSync(path)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
unlinkSync(path);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
export function authStatus(env = process.env) {
|
|
86
|
+
const config = loadUserConfig(env);
|
|
87
|
+
const envToken = envAuthToken(env);
|
|
88
|
+
if (envToken) {
|
|
89
|
+
return {
|
|
90
|
+
authenticated: true,
|
|
91
|
+
source: "env",
|
|
92
|
+
tokenPreview: redactToken(envToken),
|
|
93
|
+
apiBaseUrl: config.api.baseUrl,
|
|
94
|
+
orgId: config.org.id
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const state = readAuthState(env);
|
|
98
|
+
if (state) {
|
|
99
|
+
return {
|
|
100
|
+
authenticated: true,
|
|
101
|
+
source: "file",
|
|
102
|
+
tokenPreview: state.tokenPreview,
|
|
103
|
+
apiBaseUrl: state.apiBaseUrl,
|
|
104
|
+
orgId: state.orgId,
|
|
105
|
+
...(state.email ? { email: state.email } : {}),
|
|
106
|
+
...(state.tier ? { tier: state.tier } : {})
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
authenticated: false,
|
|
111
|
+
source: "none",
|
|
112
|
+
tokenPreview: "",
|
|
113
|
+
apiBaseUrl: config.api.baseUrl,
|
|
114
|
+
orgId: config.org.id
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export function displayTier(tier) {
|
|
118
|
+
return tier.charAt(0).toUpperCase() + tier.slice(1);
|
|
119
|
+
}
|
|
120
|
+
export function redactToken(token) {
|
|
121
|
+
const trimmed = token.trim();
|
|
122
|
+
if (trimmed.length <= 8) {
|
|
123
|
+
return "<redacted>";
|
|
124
|
+
}
|
|
125
|
+
return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
|
|
126
|
+
}
|
|
127
|
+
function writeJsonAtomic(path, value) {
|
|
128
|
+
mkdirSync(dirname(path), {
|
|
129
|
+
recursive: true,
|
|
130
|
+
mode: 0o700
|
|
131
|
+
});
|
|
132
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
133
|
+
try {
|
|
134
|
+
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
|
|
135
|
+
encoding: "utf8",
|
|
136
|
+
flag: "wx",
|
|
137
|
+
mode: 0o600
|
|
138
|
+
});
|
|
139
|
+
renameSync(tempPath, path);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
rmSync(tempPath, {
|
|
143
|
+
force: true
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/bin/dg.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { assertCurrentNode } from "../runtime/node-version.js";
|
|
3
|
+
assertCurrentNode();
|
|
4
|
+
const EXIT_TOOL_ERROR = 70;
|
|
5
|
+
function exitOnFatal(error) {
|
|
6
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
+
try {
|
|
8
|
+
process.stderr.write(`dg: unexpected error — ${message}\nRun 'dg doctor' to check your installation; remove ~/.dg/config.json if it is corrupted.\n`);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// stderr is gone; nothing left to report to.
|
|
12
|
+
}
|
|
13
|
+
process.exit(EXIT_TOOL_ERROR);
|
|
14
|
+
}
|
|
15
|
+
process.on("uncaughtException", (error) => {
|
|
16
|
+
if (error.code === "EPIPE") {
|
|
17
|
+
process.exit(process.exitCode ?? 0);
|
|
18
|
+
}
|
|
19
|
+
exitOnFatal(error);
|
|
20
|
+
});
|
|
21
|
+
process.on("unhandledRejection", exitOnFatal);
|
|
22
|
+
process.stdout.on("error", (error) => {
|
|
23
|
+
if (error.code === "EPIPE") {
|
|
24
|
+
process.exit(process.exitCode ?? 0);
|
|
25
|
+
}
|
|
26
|
+
exitOnFatal(error);
|
|
27
|
+
});
|
|
28
|
+
process.stderr.on("error", () => { });
|
|
29
|
+
const { runCli, writeCliResult } = await import("../runtime/cli.js");
|
|
30
|
+
const { maybeShowFirstRun } = await import("../runtime/first-run.js");
|
|
31
|
+
const { maybePreflightInstallPrompt } = await import("../launcher/preflight-prompt.js");
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
maybeShowFirstRun(args);
|
|
34
|
+
const { maybeDeviceLogin } = await import("../auth/device-login.js");
|
|
35
|
+
const { maybeVerifyPackage } = await import("../verify/package-check.js");
|
|
36
|
+
const { maybeAudit } = await import("../commands/audit.js");
|
|
37
|
+
const notHandled = { handled: false };
|
|
38
|
+
const deviceLogin = await maybeDeviceLogin(args);
|
|
39
|
+
const verifyPackage = deviceLogin.handled ? notHandled : await maybeVerifyPackage(args);
|
|
40
|
+
const audit = deviceLogin.handled || verifyPackage.handled ? notHandled : await maybeAudit(args);
|
|
41
|
+
if (deviceLogin.handled) {
|
|
42
|
+
writeCliResult(deviceLogin.result);
|
|
43
|
+
}
|
|
44
|
+
else if (verifyPackage.handled) {
|
|
45
|
+
writeCliResult(verifyPackage.result);
|
|
46
|
+
}
|
|
47
|
+
else if (audit.handled) {
|
|
48
|
+
writeCliResult(audit.result);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const preflight = await maybePreflightInstallPrompt(args);
|
|
52
|
+
if (preflight.handled) {
|
|
53
|
+
writeCliResult(preflight.result);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const { maybeRunLiveInstall } = await import("../launcher/live-install.js");
|
|
57
|
+
const liveInstall = await maybeRunLiveInstall(args);
|
|
58
|
+
if (liveInstall.handled) {
|
|
59
|
+
writeCliResult(liveInstall.result);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
writeCliResult(await runCli(args));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// The auth flows (browser login, paid-verify gate, deep audit upload) already
|
|
67
|
+
// tell the user exactly what to do; the throttled nudges would just be noise.
|
|
68
|
+
if (!deviceLogin.handled && !verifyPackage.handled && !audit.handled) {
|
|
69
|
+
const { maybeShowNudges } = await import("../runtime/nudges.js");
|
|
70
|
+
maybeShowNudges(args);
|
|
71
|
+
}
|