@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/dist/config-BvGapuFJ.js +282 -0
  4. package/dist/config-BvGapuFJ.js.map +1 -0
  5. package/dist/create-store-BQUX0tAn.js +197 -0
  6. package/dist/create-store-BQUX0tAn.js.map +1 -0
  7. package/dist/index.d.ts +73 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +1034 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/lib/config.d.ts +64 -0
  12. package/dist/lib/config.d.ts.map +1 -0
  13. package/dist/lib/config.js +4 -0
  14. package/dist/lib/logger.d.ts +2 -0
  15. package/dist/lib/logger.js +55 -0
  16. package/dist/lib/logger.js.map +1 -0
  17. package/dist/lib/output.d.ts +62 -0
  18. package/dist/lib/output.d.ts.map +1 -0
  19. package/dist/lib/output.js +276 -0
  20. package/dist/lib/output.js.map +1 -0
  21. package/dist/lib/project.d.ts +59 -0
  22. package/dist/lib/project.d.ts.map +1 -0
  23. package/dist/lib/project.js +3 -0
  24. package/dist/lib/prompts.d.ts +24 -0
  25. package/dist/lib/prompts.d.ts.map +1 -0
  26. package/dist/lib/prompts.js +3 -0
  27. package/dist/lib/store.d.ts +56 -0
  28. package/dist/lib/store.d.ts.map +1 -0
  29. package/dist/lib/store.js +4 -0
  30. package/dist/logger-BkQQej8h.d.ts +76 -0
  31. package/dist/logger-BkQQej8h.d.ts.map +1 -0
  32. package/dist/middleware/auth.d.ts +22 -0
  33. package/dist/middleware/auth.d.ts.map +1 -0
  34. package/dist/middleware/auth.js +759 -0
  35. package/dist/middleware/auth.js.map +1 -0
  36. package/dist/middleware/http.d.ts +87 -0
  37. package/dist/middleware/http.d.ts.map +1 -0
  38. package/dist/middleware/http.js +255 -0
  39. package/dist/middleware/http.js.map +1 -0
  40. package/dist/middleware-D3psyhYo.js +54 -0
  41. package/dist/middleware-D3psyhYo.js.map +1 -0
  42. package/dist/project-NPtYX2ZX.js +181 -0
  43. package/dist/project-NPtYX2ZX.js.map +1 -0
  44. package/dist/prompts-lLfUSgd6.js +63 -0
  45. package/dist/prompts-lLfUSgd6.js.map +1 -0
  46. package/dist/types-CqKJhsYk.d.ts +135 -0
  47. package/dist/types-CqKJhsYk.d.ts.map +1 -0
  48. package/dist/types-Cz9h927W.d.ts +23 -0
  49. package/dist/types-Cz9h927W.d.ts.map +1 -0
  50. package/dist/types-DFtYg5uZ.d.ts +26 -0
  51. package/dist/types-DFtYg5uZ.d.ts.map +1 -0
  52. package/dist/types-kjpRau0U.d.ts +382 -0
  53. package/dist/types-kjpRau0U.d.ts.map +1 -0
  54. 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