@kidd-cli/core 0.1.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 +21 -0
- package/README.md +214 -0
- package/dist/config-BvGapuFJ.js +282 -0
- package/dist/config-BvGapuFJ.js.map +1 -0
- package/dist/create-store-BQUX0tAn.js +197 -0
- package/dist/create-store-BQUX0tAn.js.map +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1034 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +64 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +4 -0
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +55 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/output.d.ts +62 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +276 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/project.d.ts +59 -0
- package/dist/lib/project.d.ts.map +1 -0
- package/dist/lib/project.js +3 -0
- package/dist/lib/prompts.d.ts +24 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +3 -0
- package/dist/lib/store.d.ts +56 -0
- package/dist/lib/store.d.ts.map +1 -0
- package/dist/lib/store.js +4 -0
- package/dist/logger-BkQQej8h.d.ts +76 -0
- package/dist/logger-BkQQej8h.d.ts.map +1 -0
- package/dist/middleware/auth.d.ts +22 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +759 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/http.d.ts +87 -0
- package/dist/middleware/http.d.ts.map +1 -0
- package/dist/middleware/http.js +255 -0
- package/dist/middleware/http.js.map +1 -0
- package/dist/middleware-D3psyhYo.js +54 -0
- package/dist/middleware-D3psyhYo.js.map +1 -0
- package/dist/project-NPtYX2ZX.js +181 -0
- package/dist/project-NPtYX2ZX.js.map +1 -0
- package/dist/prompts-lLfUSgd6.js +63 -0
- package/dist/prompts-lLfUSgd6.js.map +1 -0
- package/dist/types-CqKJhsYk.d.ts +135 -0
- package/dist/types-CqKJhsYk.d.ts.map +1 -0
- package/dist/types-Cz9h927W.d.ts +23 -0
- package/dist/types-Cz9h927W.d.ts.map +1 -0
- package/dist/types-DFtYg5uZ.d.ts +26 -0
- package/dist/types-DFtYg5uZ.d.ts.map +1 -0
- package/dist/types-kjpRau0U.d.ts +382 -0
- package/dist/types-kjpRau0U.d.ts.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import { n as decorateContext, t as middleware } from "../middleware-D3psyhYo.js";
|
|
2
|
+
import "../project-NPtYX2ZX.js";
|
|
3
|
+
import { t as createStore } from "../create-store-BQUX0tAn.js";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ok } from "@kidd-cli/utils/fp";
|
|
6
|
+
import { match as match$1 } from "ts-pattern";
|
|
7
|
+
import { platform } from "node:os";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { parse } from "dotenv";
|
|
10
|
+
import { attempt as attempt$1 } from "es-toolkit";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { createServer } from "node:http";
|
|
15
|
+
|
|
16
|
+
//#region src/middleware/auth/constants.ts
|
|
17
|
+
/**
|
|
18
|
+
* Default filename for file-based credential storage.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_AUTH_FILENAME = "auth.json";
|
|
21
|
+
/**
|
|
22
|
+
* Suffix appended to the derived token environment variable name.
|
|
23
|
+
*/
|
|
24
|
+
const TOKEN_VAR_SUFFIX = "_TOKEN";
|
|
25
|
+
/**
|
|
26
|
+
* Derive the default environment variable name from a CLI name.
|
|
27
|
+
*
|
|
28
|
+
* Converts kebab-case to SCREAMING_SNAKE_CASE and appends `_TOKEN`.
|
|
29
|
+
* Example: `my-app` → `MY_APP_TOKEN`
|
|
30
|
+
*
|
|
31
|
+
* @param cliName - The CLI name.
|
|
32
|
+
* @returns The derived environment variable name.
|
|
33
|
+
*/
|
|
34
|
+
function deriveTokenVar(cliName) {
|
|
35
|
+
return `${cliName.replaceAll("-", "_").toUpperCase()}${TOKEN_VAR_SUFFIX}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/middleware/auth/resolve-dotenv.ts
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a bearer credential from a `.env` file without mutating `process.env`.
|
|
42
|
+
*
|
|
43
|
+
* Reads the file and parses it with `dotenv.parse`. If the target variable
|
|
44
|
+
* is present, returns a bearer credential. Otherwise returns null.
|
|
45
|
+
*
|
|
46
|
+
* Skips a separate existence check to avoid a TOCTOU race — if the file
|
|
47
|
+
* does not exist, `readFileSync` throws and `attempt` captures the error.
|
|
48
|
+
*
|
|
49
|
+
* @param options - Options with the env variable name and file path.
|
|
50
|
+
* @returns A bearer credential if found, null otherwise.
|
|
51
|
+
*/
|
|
52
|
+
function resolveFromDotenv(options) {
|
|
53
|
+
const [readError, content] = attempt$1(() => readFileSync(options.path, "utf8"));
|
|
54
|
+
if (readError || content === null) return null;
|
|
55
|
+
const token = parse(content)[options.tokenVar];
|
|
56
|
+
if (!token) return null;
|
|
57
|
+
return {
|
|
58
|
+
token,
|
|
59
|
+
type: "bearer"
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/middleware/auth/resolve-env.ts
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a bearer credential from a process environment variable.
|
|
67
|
+
*
|
|
68
|
+
* @param options - Options containing the environment variable name.
|
|
69
|
+
* @returns A bearer credential if the variable is set, null otherwise.
|
|
70
|
+
*/
|
|
71
|
+
function resolveFromEnv(options) {
|
|
72
|
+
const token = process.env[options.tokenVar];
|
|
73
|
+
if (!token) return null;
|
|
74
|
+
return {
|
|
75
|
+
token,
|
|
76
|
+
type: "bearer"
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/middleware/auth/schema.ts
|
|
82
|
+
/**
|
|
83
|
+
* Zod schema for bearer credentials.
|
|
84
|
+
*/
|
|
85
|
+
const bearerCredentialSchema = z.object({
|
|
86
|
+
token: z.string().min(1),
|
|
87
|
+
type: z.literal("bearer")
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* Zod schema for basic auth credentials.
|
|
91
|
+
*/
|
|
92
|
+
const basicCredentialSchema = z.object({
|
|
93
|
+
password: z.string().min(1),
|
|
94
|
+
type: z.literal("basic"),
|
|
95
|
+
username: z.string().min(1)
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* Zod schema for API key credentials.
|
|
99
|
+
*/
|
|
100
|
+
const apiKeyCredentialSchema = z.object({
|
|
101
|
+
headerName: z.string().min(1),
|
|
102
|
+
key: z.string().min(1),
|
|
103
|
+
type: z.literal("api-key")
|
|
104
|
+
});
|
|
105
|
+
/**
|
|
106
|
+
* Zod schema for custom header credentials.
|
|
107
|
+
*/
|
|
108
|
+
const customCredentialSchema = z.object({
|
|
109
|
+
headers: z.record(z.string(), z.string()),
|
|
110
|
+
type: z.literal("custom")
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* Zod discriminated union schema for validating auth.json credential payloads.
|
|
114
|
+
* Validates against all four credential types using the `type` field as discriminator.
|
|
115
|
+
*/
|
|
116
|
+
const authCredentialSchema = z.discriminatedUnion("type", [
|
|
117
|
+
bearerCredentialSchema,
|
|
118
|
+
basicCredentialSchema,
|
|
119
|
+
apiKeyCredentialSchema,
|
|
120
|
+
customCredentialSchema
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/middleware/auth/resolve-file.ts
|
|
125
|
+
/**
|
|
126
|
+
* Resolve credentials from a JSON file on disk.
|
|
127
|
+
*
|
|
128
|
+
* Uses the file-backed store with local-then-global resolution to find
|
|
129
|
+
* the credentials file, then validates its contents against the auth
|
|
130
|
+
* credential schema.
|
|
131
|
+
*
|
|
132
|
+
* @param options - Options with the filename and directory name.
|
|
133
|
+
* @returns A validated auth credential, or null if not found or invalid.
|
|
134
|
+
*/
|
|
135
|
+
function resolveFromFile(options) {
|
|
136
|
+
const data = createStore({ dirName: options.dirName }).load(options.filename);
|
|
137
|
+
if (data === null) return null;
|
|
138
|
+
const result = authCredentialSchema.safeParse(data);
|
|
139
|
+
if (!result.success) return null;
|
|
140
|
+
return result.data;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/middleware/auth/resolve-oauth.ts
|
|
145
|
+
/**
|
|
146
|
+
* Maximum request body size in bytes (16 KB).
|
|
147
|
+
*
|
|
148
|
+
* Limits memory consumption from the local OAuth callback server
|
|
149
|
+
* to prevent resource exhaustion from oversized payloads.
|
|
150
|
+
*
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
const MAX_BODY_BYTES = 16384;
|
|
154
|
+
const CLOSE_PAGE_HTML = [
|
|
155
|
+
"<!DOCTYPE html>",
|
|
156
|
+
"<html>",
|
|
157
|
+
"<body><p>Authentication complete. You can close this tab.</p></body>",
|
|
158
|
+
"</html>"
|
|
159
|
+
].join("\n");
|
|
160
|
+
/**
|
|
161
|
+
* Resolve a bearer credential via an OAuth browser flow.
|
|
162
|
+
*
|
|
163
|
+
* Starts a minimal HTTP server on a local port, opens the user's browser
|
|
164
|
+
* to the auth URL with a callback parameter, and waits for the token
|
|
165
|
+
* to arrive via POST body.
|
|
166
|
+
*
|
|
167
|
+
* Only POST requests with a JSON body containing a `token` field are
|
|
168
|
+
* accepted. Query-string tokens are rejected to avoid leaking credentials
|
|
169
|
+
* in server logs, browser history, and referrer headers.
|
|
170
|
+
*
|
|
171
|
+
* @param options - OAuth flow configuration.
|
|
172
|
+
* @returns A bearer credential on success, null on timeout.
|
|
173
|
+
*/
|
|
174
|
+
async function resolveFromOAuth(options) {
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const state = randomBytes(32).toString("hex");
|
|
177
|
+
const timeout = createTimeout(options.timeout);
|
|
178
|
+
const tokenPromise = listenForToken({
|
|
179
|
+
callbackPath: options.callbackPath,
|
|
180
|
+
port: options.port,
|
|
181
|
+
signal: controller.signal,
|
|
182
|
+
state
|
|
183
|
+
});
|
|
184
|
+
const timeoutPromise = timeout.promise.then(() => {
|
|
185
|
+
controller.abort();
|
|
186
|
+
return null;
|
|
187
|
+
});
|
|
188
|
+
const serverPort = await getServerPort(tokenPromise);
|
|
189
|
+
if (serverPort === null) {
|
|
190
|
+
controller.abort();
|
|
191
|
+
timeout.clear();
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const callbackUrl = `http://127.0.0.1:${String(serverPort)}${options.callbackPath}`;
|
|
195
|
+
openBrowser(`${options.authUrl}?callback_url=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`);
|
|
196
|
+
const result = await Promise.race([tokenPromise.result, timeoutPromise]);
|
|
197
|
+
timeout.clear();
|
|
198
|
+
controller.abort();
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Start an HTTP server that listens for an OAuth callback token.
|
|
203
|
+
*
|
|
204
|
+
* The server accepts POST requests with a JSON body `{ "token": "..." }`
|
|
205
|
+
* on the configured callback path. All other requests receive a 400.
|
|
206
|
+
*
|
|
207
|
+
* @private
|
|
208
|
+
* @param options - Listener configuration.
|
|
209
|
+
* @returns A TokenListener with port and result promises.
|
|
210
|
+
*/
|
|
211
|
+
function listenForToken(options) {
|
|
212
|
+
const portResolvers = createDeferred();
|
|
213
|
+
const resultResolvers = createDeferred();
|
|
214
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
215
|
+
const server = createServer((req, res) => {
|
|
216
|
+
extractTokenFromBody(req, options.callbackPath, options.state, (token) => {
|
|
217
|
+
if (!token) {
|
|
218
|
+
res.writeHead(400);
|
|
219
|
+
res.end();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
sendSuccessPage(res);
|
|
223
|
+
destroyServer(server, sockets);
|
|
224
|
+
resultResolvers.resolve({
|
|
225
|
+
token,
|
|
226
|
+
type: "bearer"
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
trackConnections(server, sockets);
|
|
231
|
+
server.on("error", () => {
|
|
232
|
+
destroyServer(server, sockets);
|
|
233
|
+
portResolvers.resolve(null);
|
|
234
|
+
resultResolvers.resolve(null);
|
|
235
|
+
});
|
|
236
|
+
options.signal.addEventListener("abort", () => {
|
|
237
|
+
destroyServer(server, sockets);
|
|
238
|
+
resultResolvers.resolve(null);
|
|
239
|
+
});
|
|
240
|
+
server.listen(options.port, "127.0.0.1", () => {
|
|
241
|
+
const addr = server.address();
|
|
242
|
+
if (addr === null || typeof addr === "string") {
|
|
243
|
+
destroyServer(server, sockets);
|
|
244
|
+
portResolvers.resolve(null);
|
|
245
|
+
resultResolvers.resolve(null);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
portResolvers.resolve(addr.port);
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
port: portResolvers.promise,
|
|
252
|
+
result: resultResolvers.promise
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Track socket connections on a server so they can be destroyed on close.
|
|
257
|
+
*
|
|
258
|
+
* Mutates the provided socket set — this is an intentional exception to
|
|
259
|
+
* immutability rules because the HTTP server API is inherently stateful.
|
|
260
|
+
*
|
|
261
|
+
* @private
|
|
262
|
+
* @param server - The HTTP server.
|
|
263
|
+
* @param sockets - The set to track sockets in.
|
|
264
|
+
*/
|
|
265
|
+
function trackConnections(server, sockets) {
|
|
266
|
+
server.on("connection", (socket) => {
|
|
267
|
+
sockets.add(socket);
|
|
268
|
+
socket.on("close", () => {
|
|
269
|
+
sockets.delete(socket);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Close a server and destroy all active connections immediately.
|
|
275
|
+
*
|
|
276
|
+
* `server.close()` only stops accepting new connections — existing
|
|
277
|
+
* keep-alive connections hold the event loop open. This helper
|
|
278
|
+
* destroys every tracked socket so the process can exit cleanly.
|
|
279
|
+
*
|
|
280
|
+
* @private
|
|
281
|
+
* @param server - The HTTP server to close.
|
|
282
|
+
* @param sockets - The set of tracked sockets.
|
|
283
|
+
*/
|
|
284
|
+
function destroyServer(server, sockets) {
|
|
285
|
+
server.close();
|
|
286
|
+
Array.from(sockets, (socket) => socket.destroy());
|
|
287
|
+
sockets.clear();
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Create a deferred promise with externally accessible resolve.
|
|
291
|
+
*
|
|
292
|
+
* Uses a mutable state container to capture the promise resolver —
|
|
293
|
+
* this is an intentional exception to immutability rules because the
|
|
294
|
+
* Promise constructor API requires synchronous resolver capture.
|
|
295
|
+
*
|
|
296
|
+
* @private
|
|
297
|
+
* @returns A deferred object with promise and resolve.
|
|
298
|
+
*/
|
|
299
|
+
function createDeferred() {
|
|
300
|
+
const state = { resolve: null };
|
|
301
|
+
return {
|
|
302
|
+
promise: new Promise((resolve) => {
|
|
303
|
+
state.resolve = resolve;
|
|
304
|
+
}),
|
|
305
|
+
resolve: (value) => {
|
|
306
|
+
if (state.resolve) state.resolve(value);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Create a clearable timeout.
|
|
312
|
+
*
|
|
313
|
+
* Returns a promise that resolves after `ms` milliseconds and a `clear`
|
|
314
|
+
* function that cancels the timer so it does not hold the event loop open.
|
|
315
|
+
*
|
|
316
|
+
* Uses a mutable state container to capture the timer id — this is an
|
|
317
|
+
* intentional exception to immutability rules because `setTimeout`
|
|
318
|
+
* returns an opaque handle that must be stored for later cancellation.
|
|
319
|
+
*
|
|
320
|
+
* @private
|
|
321
|
+
* @param ms - Duration in milliseconds.
|
|
322
|
+
* @returns A Timeout with `promise` and `clear`.
|
|
323
|
+
*/
|
|
324
|
+
function createTimeout(ms) {
|
|
325
|
+
const state = { id: null };
|
|
326
|
+
return {
|
|
327
|
+
clear: () => {
|
|
328
|
+
if (state.id !== null) {
|
|
329
|
+
clearTimeout(state.id);
|
|
330
|
+
state.id = null;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
promise: new Promise((resolve) => {
|
|
334
|
+
state.id = setTimeout(resolve, ms);
|
|
335
|
+
})
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Get the server port from a token listener.
|
|
340
|
+
*
|
|
341
|
+
* @private
|
|
342
|
+
* @param listener - The token listener.
|
|
343
|
+
* @returns The port number, or null if the server failed to start.
|
|
344
|
+
*/
|
|
345
|
+
async function getServerPort(listener) {
|
|
346
|
+
return listener.port;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Extract a token from the POST body of an incoming HTTP request.
|
|
350
|
+
*
|
|
351
|
+
* Only POST requests to the callback path with `application/json`
|
|
352
|
+
* Content-Type and a JSON body containing `token` and matching `state`
|
|
353
|
+
* fields are accepted. Query-string tokens are intentionally rejected
|
|
354
|
+
* to prevent credential leakage through browser history, server logs,
|
|
355
|
+
* and referrer headers.
|
|
356
|
+
*
|
|
357
|
+
* The `Content-Type` check prevents CORS-safelisted simple requests
|
|
358
|
+
* (which skip preflight) from delivering forged payloads — `text/plain`
|
|
359
|
+
* is safelisted, but `application/json` is not (Fetch Standard §2.2.2).
|
|
360
|
+
*
|
|
361
|
+
* Body size is capped at {@link MAX_BODY_BYTES} to prevent resource
|
|
362
|
+
* exhaustion from oversized payloads.
|
|
363
|
+
*
|
|
364
|
+
* @private
|
|
365
|
+
* @param req - The incoming request.
|
|
366
|
+
* @param callbackPath - The expected callback path.
|
|
367
|
+
* @param expectedState - The state nonce to validate against.
|
|
368
|
+
* @param callback - Called with the extracted token or null.
|
|
369
|
+
*/
|
|
370
|
+
function extractTokenFromBody(req, callbackPath, expectedState, callback) {
|
|
371
|
+
if (new URL(req.url ?? "/", "http://localhost").pathname !== callbackPath) {
|
|
372
|
+
callback(null);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (req.method !== "POST") {
|
|
376
|
+
callback(null);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (!(req.headers["content-type"] ?? "").startsWith("application/json")) {
|
|
380
|
+
callback(null);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const chunks = [];
|
|
384
|
+
const received = { bytes: 0 };
|
|
385
|
+
req.on("data", (chunk) => {
|
|
386
|
+
received.bytes += chunk.length;
|
|
387
|
+
if (received.bytes > MAX_BODY_BYTES) {
|
|
388
|
+
req.destroy();
|
|
389
|
+
callback(null);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
chunks.push(chunk);
|
|
393
|
+
});
|
|
394
|
+
req.on("end", () => {
|
|
395
|
+
callback(parseTokenFromJson(Buffer.concat(chunks).toString("utf8"), expectedState));
|
|
396
|
+
});
|
|
397
|
+
req.on("error", () => {
|
|
398
|
+
callback(null);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Parse a token string from a JSON body and validate the state nonce.
|
|
403
|
+
*
|
|
404
|
+
* Expects `{ "token": "<value>", "state": "<value>" }`. Returns null
|
|
405
|
+
* for invalid JSON, missing/empty token fields, or mismatched state.
|
|
406
|
+
*
|
|
407
|
+
* @private
|
|
408
|
+
* @param body - The raw request body string.
|
|
409
|
+
* @param expectedState - The state nonce that must match.
|
|
410
|
+
* @returns The token string or null.
|
|
411
|
+
*/
|
|
412
|
+
function parseTokenFromJson(body, expectedState) {
|
|
413
|
+
try {
|
|
414
|
+
const parsed = JSON.parse(body);
|
|
415
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
416
|
+
const record = parsed;
|
|
417
|
+
if (typeof record.token !== "string" || record.token === "") return null;
|
|
418
|
+
if (record.state !== expectedState) return null;
|
|
419
|
+
return record.token;
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Send an HTML success page and end the response.
|
|
426
|
+
*
|
|
427
|
+
* @private
|
|
428
|
+
* @param res - The server response object.
|
|
429
|
+
*/
|
|
430
|
+
function sendSuccessPage(res) {
|
|
431
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
432
|
+
res.end(CLOSE_PAGE_HTML);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Open a URL in the user's default browser using a platform-specific command.
|
|
436
|
+
*
|
|
437
|
+
* On Windows, `start` is a `cmd.exe` built-in — not a standalone executable —
|
|
438
|
+
* so it must be invoked via `cmd /c start "" <url>`. The empty string argument
|
|
439
|
+
* prevents `cmd` from interpreting the URL as a window title.
|
|
440
|
+
*
|
|
441
|
+
* @private
|
|
442
|
+
* @param url - The URL to open.
|
|
443
|
+
*/
|
|
444
|
+
function openBrowser(url) {
|
|
445
|
+
const { command, args } = match$1(platform()).with("darwin", () => ({
|
|
446
|
+
args: [url],
|
|
447
|
+
command: "open"
|
|
448
|
+
})).with("win32", () => ({
|
|
449
|
+
args: [
|
|
450
|
+
"/c",
|
|
451
|
+
"start",
|
|
452
|
+
"",
|
|
453
|
+
url
|
|
454
|
+
],
|
|
455
|
+
command: "cmd"
|
|
456
|
+
})).otherwise(() => ({
|
|
457
|
+
args: [url],
|
|
458
|
+
command: "xdg-open"
|
|
459
|
+
}));
|
|
460
|
+
execFile(command, args);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/middleware/auth/resolve-prompt.ts
|
|
465
|
+
/**
|
|
466
|
+
* Resolve a bearer credential by interactively prompting the user.
|
|
467
|
+
*
|
|
468
|
+
* Uses `prompts.password()` to ask for an API key or token. Returns
|
|
469
|
+
* null if the user cancels the prompt or provides an empty value.
|
|
470
|
+
*
|
|
471
|
+
* Should be placed last in the resolver chain as a fallback.
|
|
472
|
+
*
|
|
473
|
+
* @param options - Options with the prompt message and prompts instance.
|
|
474
|
+
* @returns A bearer credential on input, null on cancellation.
|
|
475
|
+
*/
|
|
476
|
+
async function resolveFromPrompt(options) {
|
|
477
|
+
try {
|
|
478
|
+
const token = await options.prompts.password({ message: options.message });
|
|
479
|
+
if (!token) return null;
|
|
480
|
+
return {
|
|
481
|
+
token,
|
|
482
|
+
type: "bearer"
|
|
483
|
+
};
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
//#endregion
|
|
490
|
+
//#region src/middleware/auth/resolve-credentials.ts
|
|
491
|
+
const DEFAULT_OAUTH_PORT = 0;
|
|
492
|
+
const DEFAULT_OAUTH_CALLBACK_PATH = "/callback";
|
|
493
|
+
const DEFAULT_OAUTH_TIMEOUT = 12e4;
|
|
494
|
+
const DEFAULT_PROMPT_MESSAGE = "Enter your API key";
|
|
495
|
+
/**
|
|
496
|
+
* Chain credential resolvers, returning the first non-null result.
|
|
497
|
+
*
|
|
498
|
+
* Walks the resolver list in order, dispatching each config to the
|
|
499
|
+
* appropriate resolver function via pattern matching. Short-circuits
|
|
500
|
+
* on the first successful resolution.
|
|
501
|
+
*
|
|
502
|
+
* @param options - Options with resolvers, CLI name, and prompts instance.
|
|
503
|
+
* @returns The first resolved credential, or null if all resolvers fail.
|
|
504
|
+
*/
|
|
505
|
+
async function resolveCredentials(options) {
|
|
506
|
+
const defaultTokenVar = deriveTokenVar(options.cliName);
|
|
507
|
+
return tryResolvers(options.resolvers, 0, defaultTokenVar, options);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Recursively try resolvers until one returns a credential or the list is exhausted.
|
|
511
|
+
*
|
|
512
|
+
* @private
|
|
513
|
+
* @param configs - The resolver configs.
|
|
514
|
+
* @param index - The current index.
|
|
515
|
+
* @param defaultTokenVar - The derived default token env var name.
|
|
516
|
+
* @param context - The resolve options for prompts access.
|
|
517
|
+
* @returns The first resolved credential, or null.
|
|
518
|
+
*/
|
|
519
|
+
async function tryResolvers(configs, index, defaultTokenVar, context) {
|
|
520
|
+
if (index >= configs.length) return null;
|
|
521
|
+
const config = configs[index];
|
|
522
|
+
if (config === void 0) return null;
|
|
523
|
+
const credential = await dispatchResolver(config, defaultTokenVar, context);
|
|
524
|
+
if (credential) return credential;
|
|
525
|
+
return tryResolvers(configs, index + 1, defaultTokenVar, context);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Dispatch a single resolver config to its implementation.
|
|
529
|
+
*
|
|
530
|
+
* @private
|
|
531
|
+
* @param config - The resolver config to dispatch.
|
|
532
|
+
* @param defaultTokenVar - The derived default token env var name.
|
|
533
|
+
* @param context - The resolve options for prompts access.
|
|
534
|
+
* @returns The resolved credential, or null.
|
|
535
|
+
*/
|
|
536
|
+
async function dispatchResolver(config, defaultTokenVar, context) {
|
|
537
|
+
return match$1(config).with({ source: "env" }, (c) => resolveFromEnv({ tokenVar: resolveOptionalString(c.tokenVar, defaultTokenVar) })).with({ source: "dotenv" }, (c) => resolveFromDotenv({
|
|
538
|
+
path: resolveOptionalString(c.path, join(process.cwd(), ".env")),
|
|
539
|
+
tokenVar: resolveOptionalString(c.tokenVar, defaultTokenVar)
|
|
540
|
+
})).with({ source: "file" }, (c) => resolveFromFile({
|
|
541
|
+
dirName: resolveOptionalString(c.dirName, `.${context.cliName}`),
|
|
542
|
+
filename: resolveOptionalString(c.filename, DEFAULT_AUTH_FILENAME)
|
|
543
|
+
})).with({ source: "oauth" }, (c) => resolveFromOAuth({
|
|
544
|
+
authUrl: c.authUrl,
|
|
545
|
+
callbackPath: resolveOptionalString(c.callbackPath, DEFAULT_OAUTH_CALLBACK_PATH),
|
|
546
|
+
port: resolveOptionalNumber(c.port, DEFAULT_OAUTH_PORT),
|
|
547
|
+
timeout: resolveOptionalNumber(c.timeout, DEFAULT_OAUTH_TIMEOUT)
|
|
548
|
+
})).with({ source: "prompt" }, (c) => resolveFromPrompt({
|
|
549
|
+
message: resolveOptionalString(c.message, DEFAULT_PROMPT_MESSAGE),
|
|
550
|
+
prompts: context.prompts
|
|
551
|
+
})).with({ source: "custom" }, (c) => c.resolver()).exhaustive();
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Resolve an optional string value, falling back to a default.
|
|
555
|
+
*
|
|
556
|
+
* @private
|
|
557
|
+
* @param value - The optional value.
|
|
558
|
+
* @param fallback - The default value.
|
|
559
|
+
* @returns The resolved string.
|
|
560
|
+
*/
|
|
561
|
+
function resolveOptionalString(value, fallback) {
|
|
562
|
+
if (value !== void 0) return value;
|
|
563
|
+
return fallback;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Resolve an optional number value, falling back to a default.
|
|
567
|
+
*
|
|
568
|
+
* @private
|
|
569
|
+
* @param value - The optional value.
|
|
570
|
+
* @param fallback - The default value.
|
|
571
|
+
* @returns The resolved number.
|
|
572
|
+
*/
|
|
573
|
+
function resolveOptionalNumber(value, fallback) {
|
|
574
|
+
if (value !== void 0) return value;
|
|
575
|
+
return fallback;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
//#endregion
|
|
579
|
+
//#region src/middleware/auth/create-auth-context.ts
|
|
580
|
+
/**
|
|
581
|
+
* Create an {@link AuthContext} value for `ctx.auth`.
|
|
582
|
+
*
|
|
583
|
+
* No credential data is stored on the returned object. `credential()`
|
|
584
|
+
* resolves passively on every call, `authenticated()` checks existence,
|
|
585
|
+
* and `authenticate()` runs the configured interactive resolvers, saves
|
|
586
|
+
* the credential to the global file store, and returns a Result.
|
|
587
|
+
*
|
|
588
|
+
* @param options - Factory options.
|
|
589
|
+
* @returns An AuthContext instance.
|
|
590
|
+
*/
|
|
591
|
+
function createAuthContext(options) {
|
|
592
|
+
const { resolvers, cliName, prompts, resolveCredential } = options;
|
|
593
|
+
/**
|
|
594
|
+
* Resolve the current credential from passive sources (file, env).
|
|
595
|
+
*
|
|
596
|
+
* @private
|
|
597
|
+
* @returns The credential, or null when none exists.
|
|
598
|
+
*/
|
|
599
|
+
function credential() {
|
|
600
|
+
return resolveCredential();
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Check whether a credential is available from passive sources.
|
|
604
|
+
*
|
|
605
|
+
* @private
|
|
606
|
+
* @returns True when a credential exists.
|
|
607
|
+
*/
|
|
608
|
+
function authenticated() {
|
|
609
|
+
return resolveCredential() !== null;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Run configured resolvers interactively and persist the credential.
|
|
613
|
+
*
|
|
614
|
+
* @private
|
|
615
|
+
* @returns A Result with the credential on success or a LoginError on failure.
|
|
616
|
+
*/
|
|
617
|
+
async function authenticate() {
|
|
618
|
+
const resolved = await resolveCredentials({
|
|
619
|
+
cliName,
|
|
620
|
+
prompts,
|
|
621
|
+
resolvers
|
|
622
|
+
});
|
|
623
|
+
if (resolved === null) return loginError({
|
|
624
|
+
message: "No credential resolved from any source",
|
|
625
|
+
type: "no_credential"
|
|
626
|
+
});
|
|
627
|
+
const [saveError] = createStore({ dirName: `.${cliName}` }).save(DEFAULT_AUTH_FILENAME, resolved);
|
|
628
|
+
if (saveError) return loginError({
|
|
629
|
+
message: `Failed to save credential: ${saveError.message}`,
|
|
630
|
+
type: "save_failed"
|
|
631
|
+
});
|
|
632
|
+
return ok(resolved);
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
authenticate,
|
|
636
|
+
authenticated,
|
|
637
|
+
credential
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Construct a failure Result tuple with a {@link LoginError}.
|
|
642
|
+
*
|
|
643
|
+
* @private
|
|
644
|
+
* @param error - The login error.
|
|
645
|
+
* @returns A Result tuple `[LoginError, null]`.
|
|
646
|
+
*/
|
|
647
|
+
function loginError(error) {
|
|
648
|
+
return [error, null];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/middleware/auth/auth.ts
|
|
653
|
+
/**
|
|
654
|
+
* Auth middleware factory.
|
|
655
|
+
*
|
|
656
|
+
* Decorates `ctx.auth` with functions to resolve credentials on demand
|
|
657
|
+
* and run interactive authentication.
|
|
658
|
+
*
|
|
659
|
+
* @module
|
|
660
|
+
*/
|
|
661
|
+
/**
|
|
662
|
+
* Create an auth middleware that decorates `ctx.auth`.
|
|
663
|
+
*
|
|
664
|
+
* No credential data is stored on the context. `ctx.auth.credential()`
|
|
665
|
+
* resolves passively from two sources on every call:
|
|
666
|
+
* 1. File — `~/.cli-name/auth.json`
|
|
667
|
+
* 2. Env — `CLI_NAME_TOKEN`
|
|
668
|
+
*
|
|
669
|
+
* Interactive resolvers (OAuth, prompt, custom) only run when the
|
|
670
|
+
* command handler explicitly calls `ctx.auth.authenticate()`.
|
|
671
|
+
*
|
|
672
|
+
* @param options - Auth middleware configuration.
|
|
673
|
+
* @returns A Middleware that decorates ctx.auth.
|
|
674
|
+
*/
|
|
675
|
+
function auth(options) {
|
|
676
|
+
const { resolvers } = options;
|
|
677
|
+
return middleware((ctx, next) => {
|
|
678
|
+
const cliName = ctx.meta.name;
|
|
679
|
+
decorateContext(ctx, "auth", createAuthContext({
|
|
680
|
+
cliName,
|
|
681
|
+
prompts: ctx.prompts,
|
|
682
|
+
resolveCredential: () => resolvePassive(cliName, resolvers),
|
|
683
|
+
resolvers
|
|
684
|
+
}));
|
|
685
|
+
return next();
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Attempt to resolve a credential from passive (non-interactive) sources.
|
|
690
|
+
*
|
|
691
|
+
* Checks the file store first, then falls back to the environment variable.
|
|
692
|
+
* Scans the resolver list for `env` and `file` source configs to respect
|
|
693
|
+
* user-configured overrides (e.g. a custom `tokenVar` or `dirName`).
|
|
694
|
+
*
|
|
695
|
+
* @private
|
|
696
|
+
* @param cliName - The CLI name, used to derive paths and env var names.
|
|
697
|
+
* @param resolvers - The configured resolver list for extracting overrides.
|
|
698
|
+
* @returns The resolved credential, or null.
|
|
699
|
+
*/
|
|
700
|
+
function resolvePassive(cliName, resolvers) {
|
|
701
|
+
const fileConfig = findResolverBySource(resolvers, "file");
|
|
702
|
+
const envConfig = findResolverBySource(resolvers, "env");
|
|
703
|
+
const fromFile = resolveFromFile({
|
|
704
|
+
dirName: resolveFileDir(fileConfig, cliName),
|
|
705
|
+
filename: resolveFileFilename(fileConfig)
|
|
706
|
+
});
|
|
707
|
+
if (fromFile) return fromFile;
|
|
708
|
+
return resolveFromEnv({ tokenVar: resolveEnvTokenVar(envConfig, cliName) });
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Find the first resolver config matching a given source type.
|
|
712
|
+
*
|
|
713
|
+
* @private
|
|
714
|
+
* @param resolvers - The resolver config list.
|
|
715
|
+
* @param source - The source type to find.
|
|
716
|
+
* @returns The matching config, or undefined.
|
|
717
|
+
*/
|
|
718
|
+
function findResolverBySource(resolvers, source) {
|
|
719
|
+
return resolvers.find((r) => r.source === source);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Resolve the file store directory name from a file resolver config.
|
|
723
|
+
*
|
|
724
|
+
* @private
|
|
725
|
+
* @param config - The file resolver config, or undefined.
|
|
726
|
+
* @param cliName - The CLI name for deriving the default.
|
|
727
|
+
* @returns The directory name.
|
|
728
|
+
*/
|
|
729
|
+
function resolveFileDir(config, cliName) {
|
|
730
|
+
if (config !== void 0 && config.dirName !== void 0) return config.dirName;
|
|
731
|
+
return `.${cliName}`;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Resolve the file store filename from a file resolver config.
|
|
735
|
+
*
|
|
736
|
+
* @private
|
|
737
|
+
* @param config - The file resolver config, or undefined.
|
|
738
|
+
* @returns The filename.
|
|
739
|
+
*/
|
|
740
|
+
function resolveFileFilename(config) {
|
|
741
|
+
if (config !== void 0 && config.filename !== void 0) return config.filename;
|
|
742
|
+
return DEFAULT_AUTH_FILENAME;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Resolve the environment variable name from an env resolver config.
|
|
746
|
+
*
|
|
747
|
+
* @private
|
|
748
|
+
* @param config - The env resolver config, or undefined.
|
|
749
|
+
* @param cliName - The CLI name for deriving the default.
|
|
750
|
+
* @returns The token variable name.
|
|
751
|
+
*/
|
|
752
|
+
function resolveEnvTokenVar(config, cliName) {
|
|
753
|
+
if (config !== void 0 && config.tokenVar !== void 0) return config.tokenVar;
|
|
754
|
+
return deriveTokenVar(cliName);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
//#endregion
|
|
758
|
+
export { auth };
|
|
759
|
+
//# sourceMappingURL=auth.js.map
|