@delega-dev/cli 1.0.4 → 1.0.6
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 +33 -4
- package/SECURITY.md +20 -0
- package/dist/api.d.ts +6 -0
- package/dist/api.js +27 -10
- package/dist/commands/agents.js +3 -3
- package/dist/commands/login.js +72 -17
- package/dist/commands/stats.js +1 -1
- package/dist/commands/tasks.js +6 -6
- package/dist/commands/whoami.js +27 -20
- package/dist/config.d.ts +2 -0
- package/dist/config.js +43 -4
- package/dist/secret-store.d.ts +3 -0
- package/dist/secret-store.js +128 -0
- package/package.json +7 -1
- package/.github/workflows/ci.yml +0 -26
- package/.github/workflows/publish.yml +0 -66
- package/src/api.ts +0 -61
- package/src/commands/agents.ts +0 -115
- package/src/commands/login.ts +0 -79
- package/src/commands/stats.ts +0 -48
- package/src/commands/tasks.ts +0 -189
- package/src/commands/whoami.ts +0 -47
- package/src/config.ts +0 -53
- package/src/index.ts +0 -41
- package/src/ui.ts +0 -92
- package/tsconfig.json +0 -14
package/README.md
CHANGED
|
@@ -79,31 +79,60 @@ delega stats # Show usage statistics
|
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
81
|
--json # Output raw JSON for any command
|
|
82
|
-
--api-url <url> # Override API URL
|
|
82
|
+
--api-url <url> # Override API URL
|
|
83
83
|
--version # Show version
|
|
84
84
|
--help # Show help
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
## Configuration
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
Non-secret CLI settings are stored in `~/.delega/config.json`:
|
|
90
90
|
|
|
91
91
|
```json
|
|
92
92
|
{
|
|
93
|
-
"api_key": "dlg_...",
|
|
94
93
|
"api_url": "https://api.delega.dev"
|
|
95
94
|
}
|
|
96
95
|
```
|
|
97
96
|
|
|
97
|
+
`delega login` stores API keys in the OS credential store when one is available:
|
|
98
|
+
|
|
99
|
+
- macOS: Keychain
|
|
100
|
+
- Linux: libsecret keyring via `secret-tool`
|
|
101
|
+
- Windows: DPAPI-protected user storage
|
|
102
|
+
|
|
103
|
+
Existing `api_key` entries in `~/.delega/config.json` are still read for backward compatibility until the next successful `delega login`.
|
|
104
|
+
|
|
98
105
|
## Environment Variables
|
|
99
106
|
|
|
100
107
|
| Variable | Description |
|
|
101
108
|
|---|---|
|
|
102
|
-
| `DELEGA_API_KEY` | API key (overrides config
|
|
109
|
+
| `DELEGA_API_KEY` | API key (overrides secure storage and config) |
|
|
103
110
|
| `DELEGA_API_URL` | API base URL (overrides config file) |
|
|
104
111
|
|
|
105
112
|
Environment variables take precedence over the config file.
|
|
106
113
|
|
|
114
|
+
## Hosted vs Self-Hosted
|
|
115
|
+
|
|
116
|
+
The CLI defaults to the hosted API at `https://api.delega.dev/v1`.
|
|
117
|
+
|
|
118
|
+
For self-hosted deployments:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export DELEGA_API_URL="http://localhost:18890"
|
|
122
|
+
# or for a remote reverse-proxied instance:
|
|
123
|
+
export DELEGA_API_URL="https://delega.yourcompany.com/api"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Bare localhost URLs automatically use the self-hosted `/api` namespace. For remote self-hosted instances, include `/api` explicitly.
|
|
127
|
+
|
|
128
|
+
## Security Notes
|
|
129
|
+
|
|
130
|
+
- `delega login` now hides API key input instead of echoing it back to the terminal.
|
|
131
|
+
- `delega login` stores API keys in the OS credential store instead of plaintext config when secure storage is available.
|
|
132
|
+
- `~/.delega/config.json` is written with owner-only permissions (`0600`), and the config directory is locked to `0700`.
|
|
133
|
+
- Remote API URLs must use `https://`; plain `http://` is only accepted for `localhost` / `127.0.0.1`.
|
|
134
|
+
- On servers that do not expose `/agent/me`, `delega login` and `delega whoami` fall back to generic authentication checks instead of printing hosted account metadata.
|
|
135
|
+
|
|
107
136
|
## License
|
|
108
137
|
|
|
109
138
|
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a Vulnerability
|
|
4
|
+
|
|
5
|
+
Do not open a public GitHub issue for security-sensitive reports.
|
|
6
|
+
|
|
7
|
+
Email `hello@delega.dev` with:
|
|
8
|
+
|
|
9
|
+
- A clear description of the issue
|
|
10
|
+
- Steps to reproduce it
|
|
11
|
+
- The affected version or commit, if known
|
|
12
|
+
- Any suggested mitigation or fix
|
|
13
|
+
|
|
14
|
+
Use a subject line like `Security report: delega-cli`.
|
|
15
|
+
|
|
16
|
+
We will acknowledge receipt and work on triage privately.
|
|
17
|
+
|
|
18
|
+
## Supported Versions
|
|
19
|
+
|
|
20
|
+
Security fixes are applied to the latest published release and the current `main` branch.
|
package/dist/api.d.ts
CHANGED
|
@@ -2,4 +2,10 @@ export interface ApiError {
|
|
|
2
2
|
error?: string;
|
|
3
3
|
message?: string;
|
|
4
4
|
}
|
|
5
|
+
export interface ApiResponse<T = unknown> {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
status: number;
|
|
8
|
+
data: T | ApiError;
|
|
9
|
+
}
|
|
10
|
+
export declare function apiRequest<T = unknown>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>>;
|
|
5
11
|
export declare function apiCall<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
|
package/dist/api.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { getApiKey, getApiUrl } from "./config.js";
|
|
2
|
-
export async function
|
|
2
|
+
export async function apiRequest(method, path, body) {
|
|
3
3
|
const apiKey = getApiKey();
|
|
4
4
|
if (!apiKey) {
|
|
5
5
|
console.error("Not authenticated. Run: delega login");
|
|
6
6
|
process.exit(1);
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
let apiBase;
|
|
9
|
+
try {
|
|
10
|
+
apiBase = getApiUrl();
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14
|
+
console.error(`Configuration error: ${msg}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const url = apiBase + path;
|
|
9
18
|
const headers = {
|
|
10
19
|
"X-Agent-Key": apiKey,
|
|
11
20
|
"Content-Type": "application/json",
|
|
@@ -23,10 +32,6 @@ export async function apiCall(method, path, body) {
|
|
|
23
32
|
console.error(`Connection error: ${msg}`);
|
|
24
33
|
process.exit(1);
|
|
25
34
|
}
|
|
26
|
-
if (res.status === 401) {
|
|
27
|
-
console.error("Authentication failed. Run: delega login");
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
35
|
let data;
|
|
31
36
|
const text = await res.text();
|
|
32
37
|
try {
|
|
@@ -35,11 +40,23 @@ export async function apiCall(method, path, body) {
|
|
|
35
40
|
catch {
|
|
36
41
|
data = { message: text };
|
|
37
42
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
return {
|
|
44
|
+
ok: res.ok,
|
|
45
|
+
status: res.status,
|
|
46
|
+
data: data,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function apiCall(method, path, body) {
|
|
50
|
+
const result = await apiRequest(method, path, body);
|
|
51
|
+
if (result.status === 401) {
|
|
52
|
+
console.error("Authentication failed. Run: delega login");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
const errData = result.data;
|
|
57
|
+
const msg = errData.error || errData.message || `Request failed (${result.status})`;
|
|
41
58
|
console.error(`Error: ${msg}`);
|
|
42
59
|
process.exit(1);
|
|
43
60
|
}
|
|
44
|
-
return data;
|
|
61
|
+
return result.data;
|
|
45
62
|
}
|
package/dist/commands/agents.js
CHANGED
|
@@ -19,7 +19,7 @@ const agentsList = new Command("list")
|
|
|
19
19
|
.description("List agents")
|
|
20
20
|
.option("--json", "Output raw JSON")
|
|
21
21
|
.action(async (opts) => {
|
|
22
|
-
const data = await apiCall("GET", "/
|
|
22
|
+
const data = await apiCall("GET", "/agents");
|
|
23
23
|
if (opts.json) {
|
|
24
24
|
console.log(JSON.stringify(data, null, 2));
|
|
25
25
|
return;
|
|
@@ -47,7 +47,7 @@ const agentsCreate = new Command("create")
|
|
|
47
47
|
const body = { name };
|
|
48
48
|
if (opts.displayName)
|
|
49
49
|
body.display_name = opts.displayName;
|
|
50
|
-
const agent = await apiCall("POST", "/
|
|
50
|
+
const agent = await apiCall("POST", "/agents", body);
|
|
51
51
|
if (opts.json) {
|
|
52
52
|
console.log(JSON.stringify(agent, null, 2));
|
|
53
53
|
return;
|
|
@@ -73,7 +73,7 @@ const agentsRotate = new Command("rotate")
|
|
|
73
73
|
console.log("Cancelled.");
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
-
const result = await apiCall("POST", `/
|
|
76
|
+
const result = await apiCall("POST", `/agents/${id}/rotate-key`);
|
|
77
77
|
console.log();
|
|
78
78
|
console.log(` New API Key: ${chalk.cyan.bold(result.api_key)}`);
|
|
79
79
|
console.log(chalk.yellow(" Save this key — it will not be shown again."));
|
package/dist/commands/login.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import node_readline from "node:readline";
|
|
3
|
-
import { saveConfig, loadConfig } from "../config.js";
|
|
3
|
+
import { saveConfig, loadConfig, normalizeApiUrl, persistApiKey } from "../config.js";
|
|
4
4
|
import { printBanner } from "../ui.js";
|
|
5
|
-
async function
|
|
5
|
+
async function promptSecret(question) {
|
|
6
|
+
const mutedOutput = {
|
|
7
|
+
muted: false,
|
|
8
|
+
write(chunk) {
|
|
9
|
+
if (!this.muted || chunk.includes(question)) {
|
|
10
|
+
process.stdout.write(chunk);
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
};
|
|
6
14
|
const rl = node_readline.createInterface({
|
|
7
15
|
input: process.stdin,
|
|
8
|
-
output:
|
|
16
|
+
output: mutedOutput,
|
|
17
|
+
terminal: true,
|
|
9
18
|
});
|
|
19
|
+
mutedOutput.muted = true;
|
|
10
20
|
return new Promise((resolve) => {
|
|
11
21
|
rl.question(question, (answer) => {
|
|
12
22
|
rl.close();
|
|
23
|
+
process.stdout.write("\n");
|
|
13
24
|
resolve(answer.trim());
|
|
14
25
|
});
|
|
15
26
|
});
|
|
@@ -18,7 +29,7 @@ export const loginCommand = new Command("login")
|
|
|
18
29
|
.description("Authenticate with the Delega API")
|
|
19
30
|
.action(async () => {
|
|
20
31
|
printBanner();
|
|
21
|
-
const key = await
|
|
32
|
+
const key = await promptSecret("Enter your API key (starts with dlg_): ");
|
|
22
33
|
if (!key) {
|
|
23
34
|
console.error("No key provided.");
|
|
24
35
|
process.exit(1);
|
|
@@ -29,10 +40,18 @@ export const loginCommand = new Command("login")
|
|
|
29
40
|
}
|
|
30
41
|
// Validate by calling the API
|
|
31
42
|
const config = loadConfig();
|
|
32
|
-
|
|
43
|
+
let apiUrl;
|
|
44
|
+
try {
|
|
45
|
+
apiUrl = normalizeApiUrl(config.api_url || process.env.DELEGA_API_URL || "https://api.delega.dev");
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
49
|
+
console.error(`Configuration error: ${msg}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
33
52
|
let res;
|
|
34
53
|
try {
|
|
35
|
-
res = await fetch(`${apiUrl}/
|
|
54
|
+
res = await fetch(`${apiUrl}/agent/me`, {
|
|
36
55
|
headers: {
|
|
37
56
|
"X-Agent-Key": key,
|
|
38
57
|
"Content-Type": "application/json",
|
|
@@ -44,23 +63,59 @@ export const loginCommand = new Command("login")
|
|
|
44
63
|
console.error(`Connection error: ${msg}`);
|
|
45
64
|
process.exit(1);
|
|
46
65
|
}
|
|
47
|
-
if (!res.ok) {
|
|
66
|
+
if (!res.ok && res.status !== 404) {
|
|
48
67
|
console.error("Invalid API key. Authentication failed.");
|
|
49
68
|
process.exit(1);
|
|
50
69
|
}
|
|
51
70
|
let agentName = "agent";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
let validatedWithoutMetadata = false;
|
|
72
|
+
if (res.ok) {
|
|
73
|
+
try {
|
|
74
|
+
const data = (await res.json());
|
|
75
|
+
if (data.agent?.name) {
|
|
76
|
+
agentName = data.agent.display_name || data.agent.name;
|
|
77
|
+
}
|
|
56
78
|
}
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
catch {
|
|
80
|
+
// Proceed with default name
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
try {
|
|
85
|
+
res = await fetch(`${apiUrl}/tasks?completed=true`, {
|
|
86
|
+
headers: {
|
|
87
|
+
"X-Agent-Key": key,
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
59
91
|
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
+
console.error(`Connection error: ${msg}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
console.error("Invalid API key. Authentication failed.");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
validatedWithoutMetadata = true;
|
|
102
|
+
}
|
|
103
|
+
let storageLocation;
|
|
104
|
+
try {
|
|
105
|
+
storageLocation = persistApiKey(key);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
109
|
+
console.error(`Unable to store API key securely: ${msg}`);
|
|
110
|
+
process.exit(1);
|
|
60
111
|
}
|
|
61
|
-
|
|
62
|
-
|
|
112
|
+
const nextConfig = { ...config };
|
|
113
|
+
delete nextConfig.api_key;
|
|
114
|
+
saveConfig(nextConfig);
|
|
115
|
+
if (validatedWithoutMetadata) {
|
|
116
|
+
console.log(`\nLogged in. Key saved to ${storageLocation}`);
|
|
117
|
+
console.log("Current server validated the key but does not expose /agent/me metadata.");
|
|
118
|
+
return;
|
|
63
119
|
}
|
|
64
|
-
|
|
65
|
-
console.log(`\nLogged in as ${agentName}. Key saved to ~/.delega/config.json`);
|
|
120
|
+
console.log(`\nLogged in as ${agentName}. Key saved to ${storageLocation}`);
|
|
66
121
|
});
|
package/dist/commands/stats.js
CHANGED
|
@@ -5,7 +5,7 @@ export const statsCommand = new Command("stats")
|
|
|
5
5
|
.description("Show usage statistics")
|
|
6
6
|
.option("--json", "Output raw JSON")
|
|
7
7
|
.action(async (opts) => {
|
|
8
|
-
const data = await apiCall("GET", "/
|
|
8
|
+
const data = await apiCall("GET", "/stats");
|
|
9
9
|
if (opts.json) {
|
|
10
10
|
console.log(JSON.stringify(data, null, 2));
|
|
11
11
|
return;
|
package/dist/commands/tasks.js
CHANGED
|
@@ -20,7 +20,7 @@ const tasksList = new Command("list")
|
|
|
20
20
|
.option("--limit <n>", "Limit results", parseInt)
|
|
21
21
|
.option("--json", "Output raw JSON")
|
|
22
22
|
.action(async (opts) => {
|
|
23
|
-
let path = "/
|
|
23
|
+
let path = "/tasks";
|
|
24
24
|
const params = [];
|
|
25
25
|
if (opts.completed)
|
|
26
26
|
params.push("completed=true");
|
|
@@ -62,7 +62,7 @@ const tasksCreate = new Command("create")
|
|
|
62
62
|
body.labels = opts.labels.split(",").map((l) => l.trim());
|
|
63
63
|
if (opts.due)
|
|
64
64
|
body.due_date = opts.due;
|
|
65
|
-
const task = await apiCall("POST", "/
|
|
65
|
+
const task = await apiCall("POST", "/tasks", body);
|
|
66
66
|
if (opts.json) {
|
|
67
67
|
console.log(JSON.stringify(task, null, 2));
|
|
68
68
|
return;
|
|
@@ -80,7 +80,7 @@ const tasksShow = new Command("show")
|
|
|
80
80
|
.argument("<id>", "Task ID")
|
|
81
81
|
.option("--json", "Output raw JSON")
|
|
82
82
|
.action(async (id, opts) => {
|
|
83
|
-
const task = await apiCall("GET", `/
|
|
83
|
+
const task = await apiCall("GET", `/tasks/${id}`);
|
|
84
84
|
if (opts.json) {
|
|
85
85
|
console.log(JSON.stringify(task, null, 2));
|
|
86
86
|
return;
|
|
@@ -116,7 +116,7 @@ const tasksComplete = new Command("complete")
|
|
|
116
116
|
.description("Mark a task as completed")
|
|
117
117
|
.argument("<id>", "Task ID")
|
|
118
118
|
.action(async (id) => {
|
|
119
|
-
await apiCall("POST", `/
|
|
119
|
+
await apiCall("POST", `/tasks/${id}/complete`);
|
|
120
120
|
console.log(`Task ${id} completed.`);
|
|
121
121
|
});
|
|
122
122
|
const tasksDelete = new Command("delete")
|
|
@@ -128,7 +128,7 @@ const tasksDelete = new Command("delete")
|
|
|
128
128
|
console.log("Cancelled.");
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
|
-
await apiCall("DELETE", `/
|
|
131
|
+
await apiCall("DELETE", `/tasks/${id}`);
|
|
132
132
|
console.log(`Task ${id} deleted.`);
|
|
133
133
|
});
|
|
134
134
|
const tasksDelegate = new Command("delegate")
|
|
@@ -140,7 +140,7 @@ const tasksDelegate = new Command("delegate")
|
|
|
140
140
|
const body = { assigned_to_agent_id: agentId };
|
|
141
141
|
if (opts.content)
|
|
142
142
|
body.content = opts.content;
|
|
143
|
-
await apiCall("POST", `/
|
|
143
|
+
await apiCall("POST", `/tasks/${taskId}/delegate`, body);
|
|
144
144
|
console.log(`Task delegated to ${agentId}.`);
|
|
145
145
|
});
|
|
146
146
|
export const tasksCommand = new Command("tasks")
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { apiCall } from "../api.js";
|
|
2
|
+
import { apiCall, apiRequest } from "../api.js";
|
|
3
3
|
import { label } from "../ui.js";
|
|
4
4
|
export const whoamiCommand = new Command("whoami")
|
|
5
5
|
.description("Show current authenticated agent")
|
|
6
6
|
.action(async () => {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const me = await apiRequest("GET", "/agent/me");
|
|
8
|
+
if (me.ok) {
|
|
9
|
+
const payload = me.data;
|
|
10
|
+
const agent = payload.agent;
|
|
11
|
+
if (!agent) {
|
|
12
|
+
console.error("Current server did not return agent details.");
|
|
12
13
|
process.exit(1);
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
console.log();
|
|
16
|
+
label("Agent", agent.name);
|
|
17
|
+
if (agent.display_name) {
|
|
18
|
+
label("Display Name", agent.display_name);
|
|
19
|
+
}
|
|
20
|
+
if (payload.account?.email || agent.user?.email || agent.email) {
|
|
21
|
+
label("Email", payload.account?.email || agent.user?.email || agent.email || "");
|
|
22
|
+
}
|
|
23
|
+
if (payload.account?.plan || agent.user?.plan || agent.plan) {
|
|
24
|
+
label("Plan", payload.account?.plan || agent.user?.plan || agent.plan || "");
|
|
25
|
+
}
|
|
26
|
+
label("Active", agent.active !== false ? "yes" : "no");
|
|
27
|
+
console.log();
|
|
28
|
+
return;
|
|
15
29
|
}
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
if (me.status !== 404) {
|
|
31
|
+
await apiCall("GET", "/agent/me");
|
|
32
|
+
return;
|
|
18
33
|
}
|
|
34
|
+
await apiCall("GET", "/tasks?completed=true");
|
|
19
35
|
console.log();
|
|
20
|
-
label("
|
|
21
|
-
|
|
22
|
-
label("Display Name", agent.display_name);
|
|
23
|
-
}
|
|
24
|
-
if (agent.user?.email || agent.email) {
|
|
25
|
-
label("Email", agent.user?.email || agent.email || "");
|
|
26
|
-
}
|
|
27
|
-
if (agent.user?.plan || agent.plan) {
|
|
28
|
-
label("Plan", agent.user?.plan || agent.plan || "");
|
|
29
|
-
}
|
|
30
|
-
label("Active", agent.active !== false ? "yes" : "no");
|
|
36
|
+
label("Authenticated", "yes");
|
|
37
|
+
label("Server", "Current API does not expose /agent/me");
|
|
31
38
|
console.log();
|
|
32
39
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -4,5 +4,7 @@ export interface DelegaConfig {
|
|
|
4
4
|
}
|
|
5
5
|
export declare function loadConfig(): DelegaConfig;
|
|
6
6
|
export declare function saveConfig(config: DelegaConfig): void;
|
|
7
|
+
export declare function persistApiKey(apiKey: string): string;
|
|
8
|
+
export declare function normalizeApiUrl(rawUrl: string): string;
|
|
7
9
|
export declare function getApiKey(): string | undefined;
|
|
8
10
|
export declare function getApiUrl(): string;
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import node_fs from "node:fs";
|
|
2
2
|
import node_path from "node:path";
|
|
3
3
|
import node_os from "node:os";
|
|
4
|
+
import { loadStoredApiKey, storeApiKey } from "./secret-store.js";
|
|
5
|
+
const LOCAL_API_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
6
|
+
function normalizeHost(hostname) {
|
|
7
|
+
return hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
function isLocalApiHost(hostname) {
|
|
10
|
+
return LOCAL_API_HOSTS.has(normalizeHost(hostname));
|
|
11
|
+
}
|
|
12
|
+
function defaultApiBasePath(hostname) {
|
|
13
|
+
return isLocalApiHost(hostname) ? "/api" : "/v1";
|
|
14
|
+
}
|
|
4
15
|
function getConfigDir() {
|
|
5
16
|
return node_path.join(node_os.homedir(), ".delega");
|
|
6
17
|
}
|
|
@@ -23,15 +34,43 @@ export function loadConfig() {
|
|
|
23
34
|
export function saveConfig(config) {
|
|
24
35
|
const configDir = getConfigDir();
|
|
25
36
|
if (!node_fs.existsSync(configDir)) {
|
|
26
|
-
node_fs.mkdirSync(configDir, { recursive: true });
|
|
37
|
+
node_fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
38
|
+
}
|
|
39
|
+
node_fs.chmodSync(configDir, 0o700);
|
|
40
|
+
node_fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
41
|
+
node_fs.chmodSync(getConfigPath(), 0o600);
|
|
42
|
+
}
|
|
43
|
+
export function persistApiKey(apiKey) {
|
|
44
|
+
const storeLabel = storeApiKey(apiKey);
|
|
45
|
+
if (storeLabel) {
|
|
46
|
+
return storeLabel;
|
|
47
|
+
}
|
|
48
|
+
throw new Error("Secure credential storage is unavailable on this system. Set DELEGA_API_KEY manually instead.");
|
|
49
|
+
}
|
|
50
|
+
export function normalizeApiUrl(rawUrl) {
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(rawUrl);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error("Invalid Delega API URL");
|
|
57
|
+
}
|
|
58
|
+
if (parsed.protocol !== "https:" && !isLocalApiHost(parsed.hostname)) {
|
|
59
|
+
throw new Error("Delega API URL must use HTTPS unless it points to localhost");
|
|
27
60
|
}
|
|
28
|
-
|
|
61
|
+
parsed.search = "";
|
|
62
|
+
parsed.hash = "";
|
|
63
|
+
const normalizedPath = parsed.pathname.replace(/\/+$/, "");
|
|
64
|
+
parsed.pathname = normalizedPath && normalizedPath !== "/"
|
|
65
|
+
? normalizedPath
|
|
66
|
+
: defaultApiBasePath(parsed.hostname);
|
|
67
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
29
68
|
}
|
|
30
69
|
export function getApiKey() {
|
|
31
|
-
return process.env.DELEGA_API_KEY || loadConfig().api_key;
|
|
70
|
+
return process.env.DELEGA_API_KEY || loadStoredApiKey() || loadConfig().api_key;
|
|
32
71
|
}
|
|
33
72
|
export function getApiUrl() {
|
|
34
|
-
return (process.env.DELEGA_API_URL ||
|
|
73
|
+
return normalizeApiUrl(process.env.DELEGA_API_URL ||
|
|
35
74
|
loadConfig().api_url ||
|
|
36
75
|
"https://api.delega.dev");
|
|
37
76
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import node_child_process from "node:child_process";
|
|
2
|
+
import node_fs from "node:fs";
|
|
3
|
+
import node_os from "node:os";
|
|
4
|
+
import node_path from "node:path";
|
|
5
|
+
const SERVICE_NAME = "@delega-dev/cli";
|
|
6
|
+
const ACCOUNT_NAME = "default";
|
|
7
|
+
const WINDOWS_SECRET_PATH = node_path.join(node_os.homedir(), ".delega", "api-key.dpapi");
|
|
8
|
+
function ensureConfigDir() {
|
|
9
|
+
const configDir = node_path.dirname(WINDOWS_SECRET_PATH);
|
|
10
|
+
if (!node_fs.existsSync(configDir)) {
|
|
11
|
+
node_fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isLinuxSecretToolAvailable() {
|
|
15
|
+
if (process.platform !== "linux") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
node_child_process.execFileSync("sh", ["-lc", "command -v secret-tool >/dev/null 2>&1"], {
|
|
20
|
+
stdio: "ignore",
|
|
21
|
+
});
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function readMacosKeychain() {
|
|
29
|
+
try {
|
|
30
|
+
return node_child_process.execFileSync("security", ["find-generic-password", "-a", ACCOUNT_NAME, "-s", SERVICE_NAME, "-w"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function writeMacosKeychain(apiKey) {
|
|
37
|
+
node_child_process.execFileSync("security", ["add-generic-password", "-U", "-a", ACCOUNT_NAME, "-s", SERVICE_NAME, "-w", apiKey], { stdio: "ignore" });
|
|
38
|
+
}
|
|
39
|
+
function readLinuxSecretTool() {
|
|
40
|
+
if (!isLinuxSecretToolAvailable()) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return node_child_process.execFileSync("secret-tool", ["lookup", "service", SERVICE_NAME, "account", ACCOUNT_NAME], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writeLinuxSecretTool(apiKey) {
|
|
51
|
+
node_child_process.execFileSync("secret-tool", ["store", "--label", "Delega CLI API key", "service", SERVICE_NAME, "account", ACCOUNT_NAME], { input: apiKey, encoding: "utf-8", stdio: ["pipe", "ignore", "ignore"] });
|
|
52
|
+
}
|
|
53
|
+
function readWindowsProtectedFile() {
|
|
54
|
+
if (!node_fs.existsSync(WINDOWS_SECRET_PATH)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return node_child_process.execFileSync("powershell.exe", [
|
|
59
|
+
"-NoProfile",
|
|
60
|
+
"-NonInteractive",
|
|
61
|
+
"-Command",
|
|
62
|
+
"$secure = Get-Content -Raw $env:DELEGA_SECRET_PATH | ConvertTo-SecureString; $cred = [System.Management.Automation.PSCredential]::new('delega', $secure); $cred.GetNetworkCredential().Password",
|
|
63
|
+
], {
|
|
64
|
+
encoding: "utf-8",
|
|
65
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
66
|
+
env: { ...process.env, DELEGA_SECRET_PATH: WINDOWS_SECRET_PATH },
|
|
67
|
+
}).trim();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function writeWindowsProtectedFile(apiKey) {
|
|
74
|
+
ensureConfigDir();
|
|
75
|
+
const encrypted = node_child_process.execFileSync("powershell.exe", [
|
|
76
|
+
"-NoProfile",
|
|
77
|
+
"-NonInteractive",
|
|
78
|
+
"-Command",
|
|
79
|
+
"$secure = ConvertTo-SecureString $env:DELEGA_API_KEY -AsPlainText -Force; $secure | ConvertFrom-SecureString",
|
|
80
|
+
], {
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
83
|
+
env: { ...process.env, DELEGA_API_KEY: apiKey },
|
|
84
|
+
}).trim();
|
|
85
|
+
node_fs.writeFileSync(WINDOWS_SECRET_PATH, encrypted + "\n", {
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
mode: 0o600,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function secureStoreLabel() {
|
|
91
|
+
if (process.platform === "darwin") {
|
|
92
|
+
return "macOS Keychain";
|
|
93
|
+
}
|
|
94
|
+
if (process.platform === "linux" && isLinuxSecretToolAvailable()) {
|
|
95
|
+
return "libsecret keyring";
|
|
96
|
+
}
|
|
97
|
+
if (process.platform === "win32") {
|
|
98
|
+
return "Windows user-protected storage";
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
export function loadStoredApiKey() {
|
|
103
|
+
if (process.platform === "darwin") {
|
|
104
|
+
return readMacosKeychain();
|
|
105
|
+
}
|
|
106
|
+
if (process.platform === "linux") {
|
|
107
|
+
return readLinuxSecretTool();
|
|
108
|
+
}
|
|
109
|
+
if (process.platform === "win32") {
|
|
110
|
+
return readWindowsProtectedFile();
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
export function storeApiKey(apiKey) {
|
|
115
|
+
if (process.platform === "darwin") {
|
|
116
|
+
writeMacosKeychain(apiKey);
|
|
117
|
+
return "macOS Keychain";
|
|
118
|
+
}
|
|
119
|
+
if (process.platform === "linux" && isLinuxSecretToolAvailable()) {
|
|
120
|
+
writeLinuxSecretTool(apiKey);
|
|
121
|
+
return "libsecret keyring";
|
|
122
|
+
}
|
|
123
|
+
if (process.platform === "win32") {
|
|
124
|
+
writeWindowsProtectedFile(apiKey);
|
|
125
|
+
return "Windows user-protected storage";
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delega-dev/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "CLI for Delega task API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
"type": "git",
|
|
25
25
|
"url": "https://github.com/delega-dev/delega-cli"
|
|
26
26
|
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"README.md",
|
|
31
|
+
"SECURITY.md"
|
|
32
|
+
],
|
|
27
33
|
"keywords": [
|
|
28
34
|
"delega",
|
|
29
35
|
"task-management",
|