@get-dx/cli 0.3.1 → 0.3.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/README.md CHANGED
@@ -18,6 +18,13 @@ Interactively login and install the AI agent skill:
18
18
  dx init
19
19
  ```
20
20
 
21
+ For CI, Docker/dev containers, or other headless environments, pass the token
22
+ through the environment instead of storing it in the OS credential store:
23
+
24
+ ```shell
25
+ DX_API_TOKEN="$DX_TOKEN" dx auth status
26
+ ```
27
+
21
28
  Show help text:
22
29
 
23
30
  ```shell
@@ -42,8 +49,8 @@ Set the env vars once when initializing:
42
49
  # Interactive login
43
50
  DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" dx init
44
51
 
45
- # Non-interactive login for CI or remote agents
46
- DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" dx auth login --token $DX_TOKEN
52
+ # Non-interactive use for CI, containers, or remote agents
53
+ DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" DX_API_TOKEN="$DX_TOKEN" dx auth status
47
54
  ```
48
55
 
49
56
  #### For managed deployments
@@ -54,8 +61,8 @@ Set the env vars once when initializing:
54
61
  # Interactive login
55
62
  DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" dx init
56
63
 
57
- # Non-interactive login for CI or remote agents
58
- DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" dx auth login --token $DX_TOKEN
64
+ # Non-interactive use for CI, containers, or remote agents
65
+ DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" DX_API_TOKEN="$DX_TOKEN" dx auth status
59
66
  ```
60
67
 
61
68
  ## Logging
@@ -3,6 +3,7 @@ import { CliError } from "./errors.js";
3
3
  export class AuthCodeCallbackServer {
4
4
  server = null;
5
5
  address = null;
6
+ requestReceived = false;
6
7
  async start() {
7
8
  await new Promise((resolve, reject) => {
8
9
  const srv = createServer();
@@ -23,19 +24,34 @@ export class AuthCodeCallbackServer {
23
24
  }
24
25
  async listenForCode(onCodeReceipt) {
25
26
  return new Promise((resolve, reject) => {
26
- if (!this.server) {
27
+ const srv = this.server;
28
+ if (!srv) {
27
29
  reject(new Error("Unreachable: server not started"));
28
30
  return;
29
31
  }
30
- this.server.on("request", async (req, res) => {
31
- this.stop();
32
+ srv.on("request", async (req, res) => {
33
+ if (this.requestReceived) {
34
+ res.writeHead(409, { Connection: "close" }).end();
35
+ return;
36
+ }
37
+ this.requestReceived = true;
32
38
  const url = new URL(req.url ?? "/", this.address);
33
39
  const code = url.searchParams.get("code");
34
40
  const state = url.searchParams.get("state");
41
+ const stopServerAndResolve = (result) => {
42
+ res.once("close", () => {
43
+ this.stop();
44
+ resolve(result);
45
+ });
46
+ };
35
47
  if (!code) {
36
48
  const message = "Authentication failed: no code received";
37
- res.writeHead(500, { "Content-Type": "text/plain" }).end(message);
38
- resolve({ type: "ERROR", error: new CliError(message) });
49
+ res.writeHead(500, {
50
+ Connection: "close",
51
+ "Content-Type": "text/plain",
52
+ });
53
+ stopServerAndResolve({ type: "ERROR", error: new CliError(message) });
54
+ res.end(message);
39
55
  return;
40
56
  }
41
57
  let result;
@@ -46,34 +62,52 @@ export class AuthCodeCallbackServer {
46
62
  const message = err instanceof Error
47
63
  ? err.message
48
64
  : "Authentication failed: unknown error";
49
- res.writeHead(500, { "Content-Type": "text/plain" }).end(message);
50
- resolve({
65
+ res.writeHead(500, {
66
+ Connection: "close",
67
+ "Content-Type": "text/plain",
68
+ });
69
+ stopServerAndResolve({
51
70
  type: "ERROR",
52
71
  error: err instanceof Error ? err : new CliError(message),
53
72
  });
73
+ res.end(message);
54
74
  return;
55
75
  }
56
76
  if (result.type === "SUCCESS") {
57
77
  if (result.redirectUri) {
58
- res.writeHead(302, { Location: result.redirectUri }).end();
78
+ res.writeHead(302, {
79
+ Connection: "close",
80
+ Location: result.redirectUri,
81
+ });
82
+ stopServerAndResolve(result);
83
+ res.end();
59
84
  }
60
85
  else {
61
- res
62
- .writeHead(200, { "Content-Type": "text/plain" })
63
- .end("Authentication successful");
86
+ res.writeHead(200, {
87
+ Connection: "close",
88
+ "Content-Type": "text/plain",
89
+ });
90
+ stopServerAndResolve(result);
91
+ res.end("Authentication successful");
64
92
  }
65
93
  }
66
94
  else {
67
- res
68
- .writeHead(500, { "Content-Type": "text/plain" })
69
- .end(result.error.message);
95
+ res.writeHead(500, {
96
+ Connection: "close",
97
+ "Content-Type": "text/plain",
98
+ });
99
+ stopServerAndResolve(result);
100
+ res.end(result.error.message);
70
101
  }
71
- resolve(result);
72
102
  });
73
103
  });
74
104
  }
75
105
  stop() {
76
- this.server?.close();
106
+ const srv = this.server;
107
+ if (!srv)
108
+ return;
109
+ srv.closeAllConnections();
110
+ srv.close();
77
111
  this.server = null;
78
112
  }
79
113
  }
package/dist/secrets.js CHANGED
@@ -40,16 +40,28 @@ function macosSecretStore() {
40
40
  }
41
41
  },
42
42
  set(baseUrl, token) {
43
- execaSync("security", [
44
- "add-generic-password",
45
- "-U",
46
- "-s",
47
- SERVICE,
48
- "-a",
49
- baseUrl,
50
- "-w",
51
- token,
52
- ]);
43
+ try {
44
+ execaSync("security", [
45
+ "add-generic-password",
46
+ "-U",
47
+ "-s",
48
+ SERVICE,
49
+ "-a",
50
+ baseUrl,
51
+ "-w",
52
+ token,
53
+ ]);
54
+ }
55
+ catch (error) {
56
+ throw new CliError([
57
+ "Unable to save the DX API token securely using macOS Keychain.",
58
+ "",
59
+ "Set DX_API_TOKEN in the environment, or try again from a macOS session with Keychain access.",
60
+ formatErrorDetails(error),
61
+ ]
62
+ .filter(Boolean)
63
+ .join("\n"));
64
+ }
53
65
  },
54
66
  delete(baseUrl) {
55
67
  try {
@@ -84,10 +96,78 @@ function linuxSecretStore() {
84
96
  }
85
97
  },
86
98
  set(baseUrl, token) {
87
- execaSync("secret-tool", ["store", "--label=dx-cli", "service", SERVICE, "account", baseUrl], { input: token });
99
+ try {
100
+ execaSync("secret-tool", ["store", "--label=dx-cli", "service", SERVICE, "account", baseUrl], { input: token });
101
+ }
102
+ catch (error) {
103
+ throw buildLinuxTokenStorageError(error);
104
+ }
88
105
  },
89
106
  delete(_baseUrl) {
90
107
  throw new CliError("Linux token deletion is not supported by secret-tool in this implementation");
91
108
  },
92
109
  };
93
110
  }
111
+ function buildLinuxTokenStorageError(error) {
112
+ if (hasErrorCode(error, "ENOENT")) {
113
+ return new CliError([
114
+ "Unable to save the DX API token securely because `secret-tool` is not installed.",
115
+ "",
116
+ "Install `libsecret-tools`, or set DX_API_TOKEN in the environment instead of using `dx auth login` or `dx init` to store credentials.",
117
+ ].join("\n"));
118
+ }
119
+ if (isLinuxSecretServiceUnavailable(error)) {
120
+ return new CliError([
121
+ "Unable to save the DX API token securely because the Linux Secret Service/keyring is not available.",
122
+ "",
123
+ "This often happens in Docker or dev containers, even when `secret-tool` is installed. Set DX_API_TOKEN in the container environment instead of using `dx auth login` or `dx init` to store credentials, or run the CLI in an environment with a working Secret Service provider such as GNOME Keyring.",
124
+ formatErrorDetails(error),
125
+ ]
126
+ .filter(Boolean)
127
+ .join("\n"));
128
+ }
129
+ return new CliError([
130
+ "Unable to save the DX API token securely using `secret-tool`.",
131
+ "",
132
+ "Set DX_API_TOKEN in the environment, or configure a working Linux Secret Service provider such as GNOME Keyring.",
133
+ formatErrorDetails(error),
134
+ ]
135
+ .filter(Boolean)
136
+ .join("\n"));
137
+ }
138
+ function hasErrorCode(error, code) {
139
+ if (!isRecord(error)) {
140
+ return false;
141
+ }
142
+ return error.code === code;
143
+ }
144
+ function isLinuxSecretServiceUnavailable(error) {
145
+ const text = errorText(error);
146
+ return [
147
+ "org.freedesktop.secrets",
148
+ "Cannot autolaunch D-Bus",
149
+ "No such secret service",
150
+ ].some((message) => text.includes(message));
151
+ }
152
+ function formatErrorDetails(error) {
153
+ if (!isRecord(error) || typeof error.stderr !== "string") {
154
+ return null;
155
+ }
156
+ const stderr = error.stderr.trim();
157
+ return stderr ? `Details: ${stderr}` : null;
158
+ }
159
+ function errorText(error) {
160
+ if (!isRecord(error)) {
161
+ return String(error);
162
+ }
163
+ return [
164
+ typeof error.stderr === "string" ? error.stderr : null,
165
+ typeof error.shortMessage === "string" ? error.shortMessage : null,
166
+ typeof error.message === "string" ? error.message : null,
167
+ ]
168
+ .filter(Boolean)
169
+ .join("\n");
170
+ }
171
+ function isRecord(value) {
172
+ return typeof value === "object" && value !== null;
173
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@get-dx/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "DX CLI",
5
5
  "author": "DX <developers@getdx.com>",
6
6
  "bugs": {