@settlemint/dalp-cli 3.1.32-main.42194559254 → 3.1.32-main.42209626311
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/dist/dalp.js +27 -20
- package/dist/dalp.js.map +11 -11
- package/dist/{openapi-surface-3d7zmjwm.json → openapi-surface-n7qbvwjz.json} +405 -250
- package/package.json +13 -6
- package/src/bin.ts +6 -0
- package/src/commands/config.ts +73 -0
- package/src/commands/login.ts +453 -0
- package/src/commands/logout.ts +46 -0
- package/src/commands/org.ts +103 -0
- package/src/commands/whoami.ts +29 -0
- package/src/generated/openapi-surface.json +79145 -0
- package/src/hosted-mcp.ts +127 -0
- package/src/lib/api-client.ts +50 -0
- package/src/lib/auth-request.ts +83 -0
- package/src/lib/cli-config.ts +104 -0
- package/src/lib/cli-cta.ts +231 -0
- package/src/lib/cli-status.ts +102 -0
- package/src/lib/coverage.ts +41 -0
- package/src/lib/credential-store.ts +222 -0
- package/src/lib/errors.ts +188 -0
- package/src/lib/openapi-surface-types.ts +41 -0
- package/src/lib/openapi-surface.ts +17 -0
- package/src/lib/operation-inventory.ts +69 -0
- package/src/lib/register-operations.ts +631 -0
- package/src/lib/run-command.ts +38 -0
- package/src/lib/sdk-cli-schema.ts +363 -0
- package/src/lib/secret-prompt.ts +23 -0
- package/src/lib/vars.ts +19 -0
- package/src/lib/webhook-fixtures.ts +1048 -0
- package/src/main.ts +99 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@settlemint/dalp-cli",
|
|
3
|
-
"version": "3.1.32-main.
|
|
3
|
+
"version": "3.1.32-main.42209626311",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "CLI for the Digital Asset Lifecycle Platform — manage tokens, identities, compliance, and more from the command line.",
|
|
6
6
|
"homepage": "https://settlemint.com",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|
|
21
|
+
"src",
|
|
21
22
|
"README.md",
|
|
22
23
|
"LICENSE"
|
|
23
24
|
],
|
|
@@ -28,28 +29,34 @@
|
|
|
28
29
|
"types": "./dist/dalp.d.ts",
|
|
29
30
|
"import": "./dist/dalp.js"
|
|
30
31
|
},
|
|
31
|
-
"./package.json": "./package.json"
|
|
32
|
+
"./package.json": "./package.json",
|
|
33
|
+
"./hosted-mcp": {
|
|
34
|
+
"types": "./src/hosted-mcp.ts",
|
|
35
|
+
"dalp-source": "./src/hosted-mcp.ts",
|
|
36
|
+
"default": "./src/hosted-mcp.ts"
|
|
37
|
+
}
|
|
32
38
|
},
|
|
33
39
|
"scripts": {
|
|
34
40
|
"lint": "taskguard oxlint --deny-warnings .",
|
|
35
41
|
"codegen": "taskguard bun tools/extract-openapi-surface.ts",
|
|
36
42
|
"build": "taskguard sh -c 'rm -rf dist && bun build src/bin.ts --target bun --format esm --no-deprecated-namespace-object-setters --sourcemap=external --outdir dist --entry-naming dalp.js && printf '\\''export {};\n'\\'' > dist/dalp.d.ts && bun dist/dalp.js --help > /dev/null'",
|
|
37
43
|
"publint": "publint run --strict --pack bun",
|
|
38
|
-
"attw": "archive=$(bun pm pack --destination \"$PWD\" --quiet | tail -n 1) && attw \"$archive\" --
|
|
44
|
+
"attw": "archive=$(bun pm pack --destination \"$PWD\" --quiet | tail -n 1) && attw \"$archive\" --profile esm-only; code=$?; rm -f \"$archive\"; exit $code",
|
|
39
45
|
"publish": "cp ../LICENSE . && bun run --cwd .. publish-if-absent \"$PWD\"",
|
|
40
46
|
"prepack": "cp ../LICENSE .",
|
|
41
47
|
"test": "taskguard bun test --parallel=4 --randomize --preload @testkit/effect-bun-test/preload --conditions dalp-source ./tests/unit",
|
|
42
48
|
"typecheck": "taskguard effect-tsgo tsrs --customConditions dalp-types"
|
|
43
49
|
},
|
|
44
50
|
"dependencies": {
|
|
45
|
-
"@
|
|
51
|
+
"@modelcontextprotocol/server": "2.0.0-alpha.4",
|
|
52
|
+
"@settlemint/dalp-sdk": "3.1.32-main.42209626311",
|
|
46
53
|
"incur": "0.7.1",
|
|
47
54
|
"zod": "4.6.7"
|
|
48
55
|
},
|
|
49
56
|
"devDependencies": {
|
|
50
57
|
"@arethetypeswrong/cli": "0.18.5",
|
|
51
|
-
"@testkit/effect-bun-test": "3.1.32-main.
|
|
52
|
-
"@tools/typescript-config": "3.1.32-main.
|
|
58
|
+
"@testkit/effect-bun-test": "3.1.32-main.42209626311",
|
|
59
|
+
"@tools/typescript-config": "3.1.32-main.42209626311",
|
|
53
60
|
"@types/bun": "1.4.4",
|
|
54
61
|
"publint": "0.3.25"
|
|
55
62
|
},
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Cli, z } from "incur";
|
|
2
|
+
import { type DalpConfig, loadGlobalConfig, resolveConfig, saveGlobalConfig } from "../lib/cli-config.ts";
|
|
3
|
+
import { runCommand } from "../lib/run-command.ts";
|
|
4
|
+
import { cliVars } from "../lib/vars.ts";
|
|
5
|
+
|
|
6
|
+
const CONFIG_KEYS = ["apiUrl", "defaultOrg", "format"] as const;
|
|
7
|
+
export type ConfigKey = (typeof CONFIG_KEYS)[number];
|
|
8
|
+
|
|
9
|
+
const isConfigKey = (value: string): value is ConfigKey => (CONFIG_KEYS as ReadonlyArray<string>).includes(value);
|
|
10
|
+
|
|
11
|
+
export const configCommand = Cli.create("config", {
|
|
12
|
+
description: "Manage CLI configuration",
|
|
13
|
+
vars: cliVars,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
configCommand.command("get", {
|
|
17
|
+
description: "Get a configuration value (resolved)",
|
|
18
|
+
args: z.object({
|
|
19
|
+
key: z.string().describe("Config key: apiUrl | defaultOrg | format"),
|
|
20
|
+
}),
|
|
21
|
+
run(c) {
|
|
22
|
+
return runCommand(
|
|
23
|
+
c,
|
|
24
|
+
(async () => {
|
|
25
|
+
if (!isConfigKey(c.args.key)) {
|
|
26
|
+
return c.error({
|
|
27
|
+
code: "INVALID_CONFIG_KEY",
|
|
28
|
+
message: `Unknown config key '${c.args.key}'. Valid keys: ${CONFIG_KEYS.join(", ")}`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const config = await resolveConfig();
|
|
32
|
+
return { key: c.args.key, value: config[c.args.key] ?? null };
|
|
33
|
+
})()
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
configCommand.command("set", {
|
|
39
|
+
description: "Set a global configuration value",
|
|
40
|
+
args: z.object({
|
|
41
|
+
key: z.string().describe("Config key: apiUrl | defaultOrg | format"),
|
|
42
|
+
value: z.string().describe("Value to store"),
|
|
43
|
+
}),
|
|
44
|
+
run(c) {
|
|
45
|
+
return runCommand(
|
|
46
|
+
c,
|
|
47
|
+
(async () => {
|
|
48
|
+
if (!isConfigKey(c.args.key)) {
|
|
49
|
+
return c.error({
|
|
50
|
+
code: "INVALID_CONFIG_KEY",
|
|
51
|
+
message: `Unknown config key '${c.args.key}'. Valid keys: ${CONFIG_KEYS.join(", ")}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const updates: Partial<DalpConfig> = { [c.args.key]: c.args.value };
|
|
55
|
+
await saveGlobalConfig(updates);
|
|
56
|
+
return { key: c.args.key, value: c.args.value };
|
|
57
|
+
})()
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
configCommand.command("list", {
|
|
63
|
+
description: "List resolved configuration",
|
|
64
|
+
run(c) {
|
|
65
|
+
return runCommand(
|
|
66
|
+
c,
|
|
67
|
+
(async () => {
|
|
68
|
+
const [resolved, global] = await Promise.all([resolveConfig(), loadGlobalConfig()]);
|
|
69
|
+
return { resolved, global };
|
|
70
|
+
})()
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { Cli, z } from "incur";
|
|
3
|
+
import { resolveConfig, saveGlobalConfig } from "../lib/cli-config.ts";
|
|
4
|
+
import { revokeOwnApiKey } from "../lib/auth-request.ts";
|
|
5
|
+
import { loadCredentials, saveCredentials, type StoredCredentials } from "../lib/credential-store.ts";
|
|
6
|
+
import { makeCliError } from "../lib/errors.ts";
|
|
7
|
+
import { runCommand } from "../lib/run-command.ts";
|
|
8
|
+
import { promptSecret } from "../lib/secret-prompt.ts";
|
|
9
|
+
import { CLI_USER_AGENT, cliVars } from "../lib/vars.ts";
|
|
10
|
+
|
|
11
|
+
const deviceCodeSchema = z.object({
|
|
12
|
+
device_code: z.string(),
|
|
13
|
+
user_code: z.string(),
|
|
14
|
+
verification_uri: z.string(),
|
|
15
|
+
verification_uri_complete: z.string().optional(),
|
|
16
|
+
expires_in: z.number(),
|
|
17
|
+
interval: z.number(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const tokenResponseSchema = z.object({
|
|
21
|
+
access_token: z.string(),
|
|
22
|
+
token_type: z.string(),
|
|
23
|
+
expires_in: z.number(),
|
|
24
|
+
scope: z.string(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const tokenErrorSchema = z.object({
|
|
28
|
+
error: z.string(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const sessionResponseSchema = z.object({
|
|
32
|
+
user: z.object({ id: z.string(), email: z.string(), name: z.string() }),
|
|
33
|
+
session: z.object({ activeOrganizationId: z.string().nullable() }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const walletFactorTypes = {
|
|
37
|
+
pincode: "PINCODE",
|
|
38
|
+
otp: "OTP",
|
|
39
|
+
"secret-codes": "SECRET_CODES",
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
const walletFactorPrompts = {
|
|
43
|
+
pincode: "Wallet PIN: ",
|
|
44
|
+
otp: "Wallet authenticator code: ",
|
|
45
|
+
"secret-codes": "Wallet recovery code: ",
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
type WalletFactor = keyof typeof walletFactorTypes;
|
|
49
|
+
|
|
50
|
+
const refusalSchema = z.object({ code: z.string().optional(), message: z.string().optional() });
|
|
51
|
+
|
|
52
|
+
const readWalletCode = async (factor: WalletFactor, fromEnv: string | undefined, agent: boolean): Promise<string> => {
|
|
53
|
+
if (fromEnv !== undefined && fromEnv !== "") {
|
|
54
|
+
return fromEnv;
|
|
55
|
+
}
|
|
56
|
+
if (agent || process.stdin.isTTY !== true) {
|
|
57
|
+
throw makeCliError({
|
|
58
|
+
code: "kit.cli.WALLET_CODE_REQUIRED",
|
|
59
|
+
message: "Creating the CLI API key needs your wallet verification code, and there is no terminal to ask for it.",
|
|
60
|
+
why: "DALP asks for a wallet factor before it creates an API key, as it does before signing.",
|
|
61
|
+
fix: "Set DALP_WALLET_CODE to your wallet PIN (or pass --wallet-factor otp or secret-codes with that code), then run 'dalp login' again.",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return promptSecret(walletFactorPrompts[factor]);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const apiKeyResponseSchema = z.object({
|
|
68
|
+
id: z.string(),
|
|
69
|
+
key: z.string(),
|
|
70
|
+
name: z.string(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const MAX_LOGIN_RESPONSE_BYTES = 1024 * 1024;
|
|
74
|
+
|
|
75
|
+
const CLI_API_KEY_EXPIRES_IN_SECONDS = 90 * 24 * 60 * 60;
|
|
76
|
+
|
|
77
|
+
const DEVICE_CLIENT_ID = "dalp-cli";
|
|
78
|
+
|
|
79
|
+
const isLoopbackHost = (host: string) =>
|
|
80
|
+
host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
81
|
+
|
|
82
|
+
export function validateLoginBaseUrl(apiUrl: string): string {
|
|
83
|
+
let parsed: URL;
|
|
84
|
+
try {
|
|
85
|
+
parsed = new URL(apiUrl);
|
|
86
|
+
} catch {
|
|
87
|
+
throw makeCliError({
|
|
88
|
+
code: "kit.cli.INVALID_URL",
|
|
89
|
+
message: "DALP login URL is not a valid absolute URL.",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const isLocalhost = isLoopbackHost(parsed.hostname);
|
|
93
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
94
|
+
throw makeCliError({
|
|
95
|
+
code: "kit.cli.INVALID_URL",
|
|
96
|
+
message: "DALP login URLs must not contain embedded credentials.",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLocalhost)) {
|
|
100
|
+
throw makeCliError({
|
|
101
|
+
code: "kit.cli.INVALID_URL",
|
|
102
|
+
message: "DALP login requires HTTPS. HTTP is allowed only for localhost development.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (parsed.search !== "" || parsed.hash !== "") {
|
|
106
|
+
throw makeCliError({
|
|
107
|
+
code: "kit.cli.INVALID_URL",
|
|
108
|
+
message: "DALP login URLs must not contain a query string or fragment.",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return parsed.href.replace(/\/+$/u, "");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readBoundedJson<T>(response: Response, schema: z.ZodType<T>): Promise<T> {
|
|
115
|
+
const buffer = await response.arrayBuffer();
|
|
116
|
+
if (buffer.byteLength > MAX_LOGIN_RESPONSE_BYTES) {
|
|
117
|
+
throw makeCliError({
|
|
118
|
+
code: "kit.cli.LOGIN_RESPONSE_TOO_LARGE",
|
|
119
|
+
message: `Login response exceeded the ${MAX_LOGIN_RESPONSE_BYTES}-byte limit.`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const body: unknown = JSON.parse(new TextDecoder().decode(buffer));
|
|
123
|
+
return schema.parse(body);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function postJson(url: string, body: unknown, headers: Record<string, string> = {}): Promise<Response> {
|
|
127
|
+
return fetch(url, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
"User-Agent": CLI_USER_AGENT,
|
|
132
|
+
...headers,
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify(body),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function requestDeviceCode(apiUrl: string): Promise<z.infer<typeof deviceCodeSchema>> {
|
|
139
|
+
const baseUrl = validateLoginBaseUrl(apiUrl);
|
|
140
|
+
const response = await postJson(`${baseUrl}/api/auth/device/code`, { client_id: DEVICE_CLIENT_ID });
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
return readBoundedJson(response, deviceCodeSchema);
|
|
143
|
+
}
|
|
144
|
+
const refusal = await readBoundedJson(response, tokenErrorSchema).catch(() => null);
|
|
145
|
+
if (refusal?.error === "invalid_client") {
|
|
146
|
+
throw makeCliError({
|
|
147
|
+
code: "kit.cli.DEVICE_CLIENT_REJECTED",
|
|
148
|
+
message: `The DALP server at ${baseUrl} does not accept this CLI for device login.`,
|
|
149
|
+
why: `The server refused the device client id '${DEVICE_CLIENT_ID}'.`,
|
|
150
|
+
fix: "Check that the URL points at a DALP instance, and use a CLI version that instance supports.",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
throw makeCliError({
|
|
154
|
+
code: "DEVICE_CODE_FAILED",
|
|
155
|
+
message: `Failed to request device code from ${apiUrl}. Check that the URL is correct and the server is running.`,
|
|
156
|
+
retryable: true,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function pollForToken(
|
|
161
|
+
apiUrl: string,
|
|
162
|
+
deviceCode: string,
|
|
163
|
+
intervalSeconds: number,
|
|
164
|
+
expiresInSeconds: number,
|
|
165
|
+
sleep: (ms: number) => Promise<void> = (ms) =>
|
|
166
|
+
new Promise((resolve) => {
|
|
167
|
+
setTimeout(resolve, ms);
|
|
168
|
+
}),
|
|
169
|
+
now: () => number = () => Date.now()
|
|
170
|
+
): Promise<z.infer<typeof tokenResponseSchema>> {
|
|
171
|
+
const baseUrl = validateLoginBaseUrl(apiUrl);
|
|
172
|
+
const deadline = now() + expiresInSeconds * 1000;
|
|
173
|
+
let pollInterval = intervalSeconds * 1000;
|
|
174
|
+
|
|
175
|
+
while (now() < deadline) {
|
|
176
|
+
await sleep(pollInterval);
|
|
177
|
+
const response = await postJson(`${baseUrl}/api/auth/device/token`, {
|
|
178
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
179
|
+
device_code: deviceCode,
|
|
180
|
+
client_id: DEVICE_CLIENT_ID,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (response.ok) {
|
|
184
|
+
return readBoundedJson(response, tokenResponseSchema);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const errorBody = await readBoundedJson(response, tokenErrorSchema);
|
|
188
|
+
if (errorBody.error === "slow_down") {
|
|
189
|
+
pollInterval = Math.min(pollInterval + 5000, 60_000);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (errorBody.error === "authorization_pending") {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (errorBody.error === "expired_token") {
|
|
196
|
+
throw makeCliError({
|
|
197
|
+
code: "kit.cli.DEVICE_AUTHORIZATION_TIMEOUT",
|
|
198
|
+
message: "Device code expired. Run 'dalp login' again.",
|
|
199
|
+
retryable: true,
|
|
200
|
+
why: "The device authorization grant was not completed before its server-issued deadline.",
|
|
201
|
+
fix: "Run 'dalp login' again and approve the displayed device code before it expires.",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (errorBody.error === "access_denied") {
|
|
205
|
+
throw makeCliError({
|
|
206
|
+
code: "kit.cli.AUTHORIZATION_DENIED",
|
|
207
|
+
message: "Authorization denied by user.",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
throw makeCliError({
|
|
211
|
+
code: "kit.cli.DEVICE_TOKEN_FAILED",
|
|
212
|
+
message: `Unexpected error: ${errorBody.error}`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
throw makeCliError({
|
|
217
|
+
code: "kit.cli.DEVICE_AUTHORIZATION_TIMEOUT",
|
|
218
|
+
message: "Device code expired. Run 'dalp login' again.",
|
|
219
|
+
retryable: true,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function makeVerificationOpenCommand(
|
|
224
|
+
platform: NodeJS.Platform,
|
|
225
|
+
completeUri: string
|
|
226
|
+
): { readonly command: string; readonly args: ReadonlyArray<string> } {
|
|
227
|
+
const parsedUrl = new URL(completeUri);
|
|
228
|
+
const isLocalhost = isLoopbackHost(parsedUrl.hostname);
|
|
229
|
+
if (parsedUrl.username !== "" || parsedUrl.password !== "") {
|
|
230
|
+
throw new Error("Verification URLs must not contain embedded credentials.");
|
|
231
|
+
}
|
|
232
|
+
if (parsedUrl.protocol !== "https:" && !(parsedUrl.protocol === "http:" && isLocalhost)) {
|
|
233
|
+
throw new Error("HTTPS required. Use http://localhost for local development only.");
|
|
234
|
+
}
|
|
235
|
+
const safeUri = parsedUrl.toString();
|
|
236
|
+
if (platform === "darwin") {
|
|
237
|
+
return { command: "open", args: [safeUri] };
|
|
238
|
+
}
|
|
239
|
+
if (platform === "win32") {
|
|
240
|
+
return { command: "explorer.exe", args: [safeUri] };
|
|
241
|
+
}
|
|
242
|
+
return { command: "xdg-open", args: [safeUri] };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function openVerificationUrl(completeUri: string): Promise<void> {
|
|
246
|
+
try {
|
|
247
|
+
const { command, args } = makeVerificationOpenCommand(process.platform, completeUri);
|
|
248
|
+
const proc = Bun.spawn([command, ...args], { stdout: "ignore", stderr: "ignore" });
|
|
249
|
+
await proc.exited;
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const loginCommand = Cli.create("login", {
|
|
254
|
+
description: "Authenticate with your DALP instance via browser",
|
|
255
|
+
vars: cliVars,
|
|
256
|
+
options: z.object({
|
|
257
|
+
url: z.string().optional().describe("DALP instance URL"),
|
|
258
|
+
walletFactor: z
|
|
259
|
+
.enum(["pincode", "otp", "secret-codes"])
|
|
260
|
+
.default("pincode")
|
|
261
|
+
.describe("Wallet factor that confirms the new API key"),
|
|
262
|
+
}),
|
|
263
|
+
env: z.object({
|
|
264
|
+
DALP_URL: z.string().optional().describe("DALP instance URL"),
|
|
265
|
+
DALP_WALLET_CODE: z
|
|
266
|
+
.string()
|
|
267
|
+
.optional()
|
|
268
|
+
.describe("Wallet PIN, authenticator code or recovery code that confirms the new API key"),
|
|
269
|
+
}),
|
|
270
|
+
examples: [
|
|
271
|
+
{ description: "Login to configured instance" },
|
|
272
|
+
{ options: { url: "https://dalp.example.com" }, description: "Login to specific instance" },
|
|
273
|
+
],
|
|
274
|
+
run(c) {
|
|
275
|
+
return runCommand(
|
|
276
|
+
c,
|
|
277
|
+
(async () => {
|
|
278
|
+
const previous = await loadCredentials();
|
|
279
|
+
const config = await resolveConfig();
|
|
280
|
+
const apiUrl = c.options.url ?? c.env.DALP_URL ?? c.var.apiUrl ?? config.apiUrl;
|
|
281
|
+
|
|
282
|
+
if (apiUrl === undefined || apiUrl === "") {
|
|
283
|
+
return c.error({
|
|
284
|
+
code: "NO_URL",
|
|
285
|
+
message:
|
|
286
|
+
"No DALP instance URL configured. Provide --url, set DALP_URL, or run 'dalp config set apiUrl <url>'.",
|
|
287
|
+
retryable: true,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const baseUrl = validateLoginBaseUrl(apiUrl);
|
|
292
|
+
const deviceCode = await requestDeviceCode(apiUrl);
|
|
293
|
+
|
|
294
|
+
if (c.agent !== true) {
|
|
295
|
+
const verificationBase = deviceCode.verification_uri.startsWith("http")
|
|
296
|
+
? deviceCode.verification_uri
|
|
297
|
+
: `${baseUrl}${deviceCode.verification_uri}`;
|
|
298
|
+
const completeUri =
|
|
299
|
+
deviceCode.verification_uri_complete ?? `${verificationBase}?code=${deviceCode.user_code}`;
|
|
300
|
+
process.stdout.write(`\nOpen this URL in your browser:\n ${completeUri}\n\n`);
|
|
301
|
+
process.stdout.write(`Or go to ${verificationBase} and enter code:\n ${deviceCode.user_code}\n\n`);
|
|
302
|
+
process.stdout.write("Waiting for authorization...\n");
|
|
303
|
+
await openVerificationUrl(completeUri);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const sessionToken = await pollForToken(
|
|
307
|
+
baseUrl,
|
|
308
|
+
deviceCode.device_code,
|
|
309
|
+
deviceCode.interval,
|
|
310
|
+
deviceCode.expires_in
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const sessionBearer = `Bearer ${sessionToken.access_token}`;
|
|
314
|
+
const sessionResponse = await fetch(`${baseUrl}/api/auth/get-session`, {
|
|
315
|
+
headers: { Authorization: sessionBearer, "User-Agent": CLI_USER_AGENT },
|
|
316
|
+
});
|
|
317
|
+
if (!sessionResponse.ok) {
|
|
318
|
+
return c.error({
|
|
319
|
+
code: "SESSION_FAILED",
|
|
320
|
+
message: "Authorized but failed to fetch session. Try 'dalp login' again.",
|
|
321
|
+
retryable: true,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const session = await readBoundedJson(sessionResponse, sessionResponseSchema);
|
|
326
|
+
const activeOrgId = session.session.activeOrganizationId;
|
|
327
|
+
if (activeOrgId === null || activeOrgId === "") {
|
|
328
|
+
throw makeCliError({
|
|
329
|
+
code: "kit.cli.NO_ACTIVE_ORGANIZATION",
|
|
330
|
+
message: "You signed in, but your session has no active organization, so no API key was created.",
|
|
331
|
+
why: "A CLI key acts in one organization, and the session that approved the sign-in had none selected.",
|
|
332
|
+
fix: "Open the Console, select an organization, then run 'dalp login' again.",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const { walletFactor } = c.options;
|
|
337
|
+
const walletCode = await readWalletCode(walletFactor, c.env.DALP_WALLET_CODE, c.agent === true);
|
|
338
|
+
const apiKeyName = `dalp-cli (${hostname()})`;
|
|
339
|
+
const apiKeyResponse = await postJson(
|
|
340
|
+
`${baseUrl}/api/auth/api-key/create`,
|
|
341
|
+
{
|
|
342
|
+
name: apiKeyName,
|
|
343
|
+
expiresIn: CLI_API_KEY_EXPIRES_IN_SECONDS,
|
|
344
|
+
metadata: { organizationId: activeOrgId, scope: "read-write" },
|
|
345
|
+
walletVerification: {
|
|
346
|
+
secretVerificationCode: walletCode,
|
|
347
|
+
verificationType: walletFactorTypes[walletFactor],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
{ Authorization: sessionBearer }
|
|
351
|
+
);
|
|
352
|
+
if (!apiKeyResponse.ok) {
|
|
353
|
+
const refusal = await readBoundedJson(apiKeyResponse, refusalSchema).catch(() => ({
|
|
354
|
+
code: undefined,
|
|
355
|
+
message: undefined,
|
|
356
|
+
}));
|
|
357
|
+
const refusalCode = refusal.code ?? "";
|
|
358
|
+
if (refusalCode.startsWith("WALLET_")) {
|
|
359
|
+
return c.error({
|
|
360
|
+
code: refusalCode,
|
|
361
|
+
message: `Authorized, but the API key was not created: ${refusal.message ?? refusalCode}`,
|
|
362
|
+
retryable: refusalCode !== "WALLET_STEP_UP_UNAVAILABLE",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return c.error({
|
|
366
|
+
code: "API_KEY_FAILED",
|
|
367
|
+
message: "Authorized but failed to create API key. Check your permissions or try 'dalp login' again.",
|
|
368
|
+
retryable: true,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const apiKey = await readBoundedJson(apiKeyResponse, apiKeyResponseSchema);
|
|
373
|
+
const credentials: StoredCredentials = {
|
|
374
|
+
apiKey: apiKey.key,
|
|
375
|
+
apiKeyId: apiKey.id,
|
|
376
|
+
userEmail: session.user.email,
|
|
377
|
+
userId: session.user.id,
|
|
378
|
+
apiUrl: baseUrl,
|
|
379
|
+
activeOrgId,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
if (previous !== undefined) {
|
|
383
|
+
const revocation = await revokeOwnApiKey(previous);
|
|
384
|
+
if (revocation._tag === "Unconfirmed") {
|
|
385
|
+
const rollback = await revokeOwnApiKey(credentials);
|
|
386
|
+
const retry = `Run 'dalp login' again. If it keeps failing, revoke the key in the Console on ${previous.apiUrl} under API keys, then run 'dalp logout' and 'dalp login'.`;
|
|
387
|
+
const newKey = `'${apiKeyName}' (${credentials.apiKeyId})`;
|
|
388
|
+
throw makeCliError({
|
|
389
|
+
code: "kit.cli.API_KEY_REVOKE_FAILED",
|
|
390
|
+
retryable: true,
|
|
391
|
+
...(rollback._tag === "Revoked"
|
|
392
|
+
? {
|
|
393
|
+
message: `Could not revoke the previous CLI key on ${previous.apiUrl}, so the login was undone and the previous credentials were kept.`,
|
|
394
|
+
why: "The server that issued the previous key did not confirm its revocation.",
|
|
395
|
+
fix: retry,
|
|
396
|
+
cause: revocation.cause,
|
|
397
|
+
}
|
|
398
|
+
: {
|
|
399
|
+
message: `Could not revoke the previous CLI key on ${previous.apiUrl}, and could not revoke the new key ${newKey} on ${baseUrl}. The previous credentials were kept.`,
|
|
400
|
+
why: "Neither server confirmed the revocation of its key.",
|
|
401
|
+
fix: `Remove the new key ${newKey} in the Console on ${baseUrl} under API keys. ${retry}`,
|
|
402
|
+
cause: rollback.cause,
|
|
403
|
+
}),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await saveCredentials(credentials);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
const rollback = await revokeOwnApiKey(credentials);
|
|
412
|
+
const newKey = `'${apiKeyName}' (${credentials.apiKeyId})`;
|
|
413
|
+
const previousRevoked =
|
|
414
|
+
previous === undefined ? "" : ` The previous key on ${previous.apiUrl} was already revoked.`;
|
|
415
|
+
throw makeCliError({
|
|
416
|
+
code: "kit.cli.CREDENTIAL_STORE_FAILED",
|
|
417
|
+
retryable: true,
|
|
418
|
+
message:
|
|
419
|
+
rollback._tag === "Revoked"
|
|
420
|
+
? `Could not save the new CLI credentials, so the new key was revoked.${previousRevoked}`
|
|
421
|
+
: `Could not save the new CLI credentials, and could not revoke the new key ${newKey} on ${baseUrl}.${previousRevoked}`,
|
|
422
|
+
why: "The local credential store refused the write.",
|
|
423
|
+
fix:
|
|
424
|
+
rollback._tag === "Revoked"
|
|
425
|
+
? "Make the DALP config directory writable (or unlock the keychain), then run 'dalp login' again."
|
|
426
|
+
: `Remove the new key ${newKey} in the Console on ${baseUrl} under API keys, make the credential store writable, then run 'dalp login' again.`,
|
|
427
|
+
cause: error,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
await saveGlobalConfig({ apiUrl: baseUrl, defaultOrg: activeOrgId });
|
|
431
|
+
|
|
432
|
+
return c.ok(
|
|
433
|
+
{
|
|
434
|
+
authenticated: true,
|
|
435
|
+
user: session.user.email,
|
|
436
|
+
instance: baseUrl,
|
|
437
|
+
organizationId: activeOrgId,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
cta: {
|
|
441
|
+
description: "You're logged in. Try:",
|
|
442
|
+
commands: [
|
|
443
|
+
{ command: "whoami", description: "View current user" },
|
|
444
|
+
{ command: "org switch", description: "Save the key's organization as active" },
|
|
445
|
+
{ command: "tokens list", description: "List tokens" },
|
|
446
|
+
],
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
})()
|
|
451
|
+
);
|
|
452
|
+
},
|
|
453
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Cli } from "incur";
|
|
2
|
+
import { revokeOwnApiKey } from "../lib/auth-request.ts";
|
|
3
|
+
import { deleteCredentials, loadCredentials, type StoredCredentials } from "../lib/credential-store.ts";
|
|
4
|
+
import { makeCliError } from "../lib/errors.ts";
|
|
5
|
+
import { runCommand } from "../lib/run-command.ts";
|
|
6
|
+
import { cliVars } from "../lib/vars.ts";
|
|
7
|
+
|
|
8
|
+
const revokeUnconfirmed = (detail: string, cause: unknown) =>
|
|
9
|
+
makeCliError({
|
|
10
|
+
code: "kit.cli.API_KEY_REVOKE_UNCONFIRMED",
|
|
11
|
+
message: `Removed the local credentials, but ${detail}`,
|
|
12
|
+
why: "The CLI revokes its key on the server at logout, and the server did not confirm the revocation.",
|
|
13
|
+
fix: "Revoke the dalp-cli key for this machine under API keys in the Console.",
|
|
14
|
+
cause,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const logoutCommand = Cli.create("logout", {
|
|
18
|
+
description: "Revoke this machine's DALP CLI key and remove its stored credentials",
|
|
19
|
+
vars: cliVars,
|
|
20
|
+
run(c) {
|
|
21
|
+
return runCommand(
|
|
22
|
+
c,
|
|
23
|
+
(async () => {
|
|
24
|
+
let stored: StoredCredentials | undefined;
|
|
25
|
+
try {
|
|
26
|
+
stored = await loadCredentials();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
await deleteCredentials();
|
|
29
|
+
throw revokeUnconfirmed("they could not be read, so the key was not revoked.", error);
|
|
30
|
+
}
|
|
31
|
+
if (stored === undefined) {
|
|
32
|
+
return { loggedOut: true, revokedKeyId: null };
|
|
33
|
+
}
|
|
34
|
+
const revocation = await revokeOwnApiKey(stored);
|
|
35
|
+
await deleteCredentials();
|
|
36
|
+
if (revocation._tag === "Unconfirmed") {
|
|
37
|
+
throw revokeUnconfirmed(
|
|
38
|
+
`${stored.apiUrl} did not confirm that key ${stored.apiKeyId} was revoked.`,
|
|
39
|
+
revocation.cause
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return { loggedOut: true, revokedKeyId: stored.apiKeyId };
|
|
43
|
+
})()
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
});
|