dazzer-connect 0.1.0 → 0.1.1

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/index.js CHANGED
@@ -1,55 +1,910 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * @purpose Entry point for the dazzer CLI — wires Commander subcommands for
4
- * auth login, logout, and status against a configurable authorization server.
5
- */
2
+
3
+ // src/index.ts
6
4
  import { createRequire } from "node:module";
7
5
  import { Command } from "commander";
8
- import { runLogin } from "./commands/login.js";
9
- import { runLogout } from "./commands/logout.js";
10
- import { runStatus } from "./commands/status.js";
11
- import { validateEnv } from "./env.js";
12
- const require = createRequire(import.meta.url);
13
- const { version } = require("../package.json");
14
- const program = new Command();
6
+
7
+ // src/lib/config-store.ts
8
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import envPaths from "env-paths";
11
+
12
+ // src/lib/discovery.ts
13
+ function assertHttpsIssuer(issuer) {
14
+ let parsed;
15
+ try {
16
+ parsed = new URL(issuer);
17
+ } catch {
18
+ throw new Error(`Not a valid issuer URL: ${issuer}`);
19
+ }
20
+ if (parsed.protocol !== "https:") {
21
+ throw new Error(`The issuer must be an https URL (got ${parsed.protocol}//). Check --issuer.`);
22
+ }
23
+ return issuer.replace(/\/$/, "");
24
+ }
25
+ async function discoverEndpoints(issuer) {
26
+ const normalized = assertHttpsIssuer(issuer);
27
+ const url = `${normalized}/.well-known/oauth-authorization-server`;
28
+ const res = await fetch(url);
29
+ if (!res.ok) {
30
+ throw new Error(`Could not reach the authorization server (${res.status}). Check --issuer.`);
31
+ }
32
+ const body = await res.json();
33
+ const authorizationEndpoint = body.authorization_endpoint;
34
+ const deviceAuthorizationEndpoint = body.device_authorization_endpoint;
35
+ const tokenEndpoint = body.token_endpoint;
36
+ const revocationEndpoint = body.revocation_endpoint;
37
+ if (typeof tokenEndpoint !== "string" || tokenEndpoint === "") {
38
+ throw new Error("This authorization server is missing a token endpoint.");
39
+ }
40
+ const hasAuthorization = typeof authorizationEndpoint === "string" && authorizationEndpoint !== "";
41
+ const hasDevice = typeof deviceAuthorizationEndpoint === "string" && deviceAuthorizationEndpoint !== "";
42
+ const hasRevocation = typeof revocationEndpoint === "string" && revocationEndpoint !== "";
43
+ if (!hasAuthorization && !hasDevice) {
44
+ throw new Error("This authorization server does not support browser or device sign-in.");
45
+ }
46
+ return {
47
+ issuer: typeof body.issuer === "string" ? body.issuer : normalized,
48
+ authorizationEndpoint: hasAuthorization ? authorizationEndpoint : void 0,
49
+ deviceAuthorizationEndpoint: hasDevice ? deviceAuthorizationEndpoint : void 0,
50
+ tokenEndpoint,
51
+ revocationEndpoint: hasRevocation ? revocationEndpoint : void 0
52
+ };
53
+ }
54
+
55
+ // src/lib/config-store.ts
56
+ var paths = envPaths("dazzer", { suffix: "" });
57
+ var CONFIG_FILE = join(paths.config, "auth.json");
58
+ async function ensureConfigDir() {
59
+ await mkdir(paths.config, { recursive: true });
60
+ }
61
+ var MAX_CLIENT_ID_LENGTH = 512;
62
+ async function saveAuthMetadata(meta) {
63
+ const issuer = assertHttpsIssuer(meta.issuer);
64
+ const clientId = typeof meta.clientId === "string" ? meta.clientId.trim() : "";
65
+ if (clientId === "" || clientId.length > MAX_CLIENT_ID_LENGTH || /[\r\n]/.test(clientId)) {
66
+ throw new Error("The authorization server returned an unusable client id.");
67
+ }
68
+ const expiresAt = typeof meta.expiresAt === "number" && Number.isFinite(meta.expiresAt) ? meta.expiresAt : null;
69
+ await ensureConfigDir();
70
+ const checked = { issuer, clientId, expiresAt };
71
+ await writeFile(CONFIG_FILE, `${JSON.stringify(checked, null, 2)}
72
+ `, "utf8");
73
+ }
74
+ async function loadAuthMetadata() {
75
+ try {
76
+ const raw = await readFile(CONFIG_FILE, "utf8");
77
+ const parsed = JSON.parse(raw);
78
+ if (typeof parsed.issuer !== "string" || typeof parsed.clientId !== "string") {
79
+ return null;
80
+ }
81
+ return parsed;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ async function clearAuthMetadata() {
87
+ try {
88
+ await unlink(CONFIG_FILE);
89
+ } catch {
90
+ }
91
+ }
92
+
93
+ // src/lib/device-flow.ts
94
+ var DEFAULT_SCOPE = "openid offline_access";
95
+ function parseDeviceResponse(body) {
96
+ const deviceCode = body.device_code;
97
+ const userCode = body.user_code;
98
+ const verificationUri = body.verification_uri;
99
+ const expiresIn = body.expires_in;
100
+ if (typeof deviceCode !== "string" || typeof userCode !== "string" || typeof verificationUri !== "string" || typeof expiresIn !== "number") {
101
+ throw new Error("The authorization server returned an unexpected device response.");
102
+ }
103
+ return {
104
+ device_code: deviceCode,
105
+ user_code: userCode,
106
+ verification_uri: verificationUri,
107
+ verification_uri_complete: typeof body.verification_uri_complete === "string" ? body.verification_uri_complete : void 0,
108
+ expires_in: expiresIn,
109
+ interval: typeof body.interval === "number" ? body.interval : void 0
110
+ };
111
+ }
112
+ async function requestDeviceCode(options) {
113
+ const fetchFn = options.fetchFn ?? fetch;
114
+ const endpoint = options.discovery.deviceAuthorizationEndpoint;
115
+ if (!endpoint) {
116
+ throw new Error("This authorization server does not support device sign-in.");
117
+ }
118
+ const params = new URLSearchParams({
119
+ client_id: options.clientId,
120
+ scope: options.scope ?? DEFAULT_SCOPE
121
+ });
122
+ if (options.resource) {
123
+ params.set("resource", options.resource);
124
+ }
125
+ const res = await fetchFn(endpoint, {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
128
+ body: params.toString()
129
+ });
130
+ if (!res.ok) {
131
+ throw new Error(`Device sign-in request failed (${res.status}).`);
132
+ }
133
+ return parseDeviceResponse(await res.json());
134
+ }
135
+ async function pollForTokens(options) {
136
+ const fetchFn = options.fetchFn ?? fetch;
137
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
138
+ const baseIntervalMs = (options.deviceAuth.interval ?? 5) * 1e3;
139
+ let intervalMs = options.pollIntervalMs ?? baseIntervalMs;
140
+ const deadline = Date.now() + (options.maxWaitMs ?? options.deviceAuth.expires_in * 1e3);
141
+ while (Date.now() < deadline) {
142
+ await sleep(intervalMs);
143
+ const params = new URLSearchParams({
144
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
145
+ device_code: options.deviceCode,
146
+ client_id: options.clientId
147
+ });
148
+ if (options.resource) {
149
+ params.set("resource", options.resource);
150
+ }
151
+ const res = await fetchFn(options.discovery.tokenEndpoint, {
152
+ method: "POST",
153
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
154
+ body: params.toString()
155
+ });
156
+ const body = await res.json();
157
+ if (res.ok) {
158
+ const accessToken = body.access_token;
159
+ if (typeof accessToken !== "string") {
160
+ throw new Error("The authorization server returned an unexpected token response.");
161
+ }
162
+ return {
163
+ access_token: accessToken,
164
+ refresh_token: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
165
+ expires_in: typeof body.expires_in === "number" ? body.expires_in : void 0,
166
+ token_type: typeof body.token_type === "string" ? body.token_type : void 0
167
+ };
168
+ }
169
+ const error = typeof body.error === "string" ? body.error : "unknown_error";
170
+ if (error === "authorization_pending") {
171
+ continue;
172
+ }
173
+ if (error === "slow_down") {
174
+ intervalMs += 5e3;
175
+ continue;
176
+ }
177
+ if (error === "access_denied") {
178
+ throw new Error("Sign-in was denied.");
179
+ }
180
+ if (error === "expired_token") {
181
+ throw new Error("The device code expired. Run auth login again.");
182
+ }
183
+ throw new Error(`Token request failed: ${error}`);
184
+ }
185
+ throw new Error("Timed out waiting for sign-in. Run auth login again.");
186
+ }
187
+
188
+ // src/lib/loopback-flow.ts
189
+ import { spawn } from "node:child_process";
190
+ import { createHash, randomBytes } from "node:crypto";
191
+ import { createServer } from "node:http";
192
+
193
+ // ../auth-ui/dist/html.js
194
+ function escapeHtml(value) {
195
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
196
+ }
197
+
198
+ // ../auth-ui/dist/theme.js
199
+ var TOKENS = {
200
+ orange: "#ff5000",
201
+ orangeMid: "#ee7d4e",
202
+ lavender: "#dae2ff",
203
+ skyMid: "#d2e7f3",
204
+ ink: "#000000",
205
+ inkSoft: "#0d0d0d",
206
+ muted: "#595959",
207
+ line: "#e6e1da",
208
+ error: "#8a2a00",
209
+ gbtnBorder: "#DADCE0",
210
+ gbtnText: "#3C4043"
211
+ };
212
+ var baseCss = `
213
+ :root{--orange:${TOKENS.orange};--orange-mid:${TOKENS.orangeMid};--lavender:${TOKENS.lavender};--sky-mid:${TOKENS.skyMid};--ink:${TOKENS.ink};--ink-soft:${TOKENS.inkSoft};--muted:${TOKENS.muted};--line:${TOKENS.line};--error:${TOKENS.error};--gbtn-border:${TOKENS.gbtnBorder};--gbtn-text:${TOKENS.gbtnText};--serif:'Source Serif 4',Georgia,serif;--mono:'IBM Plex Mono',ui-monospace,monospace;}
214
+ *{box-sizing:border-box;margin:0;padding:0;}html,body{height:100%;}
215
+ body{font-family:var(--mono);color:var(--ink);
216
+ background:linear-gradient(180deg,${TOKENS.orange} 0%,${TOKENS.orange} 12.98%,${TOKENS.skyMid} 69.23%,${TOKENS.lavender} 100%);
217
+ -webkit-font-smoothing:antialiased;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:32px 20px;}
218
+ .card{width:100%;max-width:392px;max-height:calc(100dvh - 64px);overflow-y:auto;background:rgba(255,255,255,0.80);border:1px solid rgba(255,255,255,0.55);border-radius:16px;
219
+ -webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);
220
+ padding:40px 36px 30px;box-shadow:0 12px 40px rgba(0,0,0,.05),inset 0 1px 1px rgba(255,255,255,.35);}
221
+ .mark{width:40px;height:40px;margin-bottom:22px;border-radius:22px;display:flex;align-items:center;justify-content:center;}
222
+ .mark svg{display:block;width:100%;height:100%;}
223
+ .icon{width:46px;height:46px;margin:0 auto 18px;display:block;}
224
+ h1{font-family:var(--serif);font-weight:600;font-size:21px;letter-spacing:-.015em;margin-bottom:7px;}
225
+ h1 em{font-weight:600;font-style:italic;}
226
+ .sub{font-size:14px;line-height:1.5;color:var(--muted);margin-bottom:24px;}
227
+ .sub strong{color:var(--ink);font-weight:600;}
228
+ p{font-size:14px;line-height:1.55;color:var(--muted);}
229
+ .perms-label{font-size:13px;font-weight:600;color:var(--ink);margin-bottom:10px;}
230
+ .perms{list-style:none;margin:0 0 24px;padding:16px;background:#fff;border:1px solid var(--line);border-radius:10px;display:grid;gap:11px;}
231
+ .perms li{position:relative;padding-left:18px;font-size:13.5px;color:var(--muted);line-height:1.35;}
232
+ .perms li::before{content:"";position:absolute;left:1px;top:7px;width:6px;height:6px;border-radius:50%;background:var(--orange);}
233
+ .actions{display:flex;flex-direction:column;gap:10px;margin-bottom:4px;}
234
+ .btn{width:100%;height:44px;border-radius:6px;font-family:var(--mono);font-weight:600;font-size:14px;cursor:pointer;
235
+ display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease,box-shadow .15s ease,border-color .15s ease;}
236
+ .btn:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
237
+ .btn-primary{border:0;background:var(--orange);color:#fff;}
238
+ .btn-primary:hover{background:#e64800;box-shadow:0 1px 3px rgba(0,0,0,.12);}
239
+ .btn-secondary{border:1px solid var(--line);background:#fff;color:var(--ink);}
240
+ .btn-secondary:hover{background:#fafafa;border-color:#d4cfc8;}
241
+ .gbtn{width:100%;height:44px;border:1px solid var(--gbtn-border);border-radius:6px;background:#fff;color:var(--gbtn-text);
242
+ font-family:'Roboto',-apple-system,sans-serif;font-weight:500;font-size:14px;cursor:pointer;
243
+ display:inline-flex;align-items:center;justify-content:center;gap:12px;transition:background .15s ease,box-shadow .15s ease,border-color .15s ease;}
244
+ .gbtn:hover{background:#F8FAFB;box-shadow:0 1px 3px rgba(0,0,0,.12);border-color:#C9CDD2;}
245
+ .gbtn:disabled{opacity:.65;cursor:default;box-shadow:none;background:#fff;}
246
+ .gbtn:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
247
+ .gbtn svg{display:block;}
248
+ .err{margin-top:14px;font-size:13px;color:var(--error);min-height:16px;}
249
+ .trust,.foot{margin-top:22px;padding-top:18px;border-top:1px solid var(--line);display:flex;align-items:center;gap:7px;font-size:12px;color:var(--muted);}
250
+ .trust svg,.foot svg{flex:none;}
251
+ .code-input{width:100%;height:44px;margin:16px 0;padding:0 14px;border:1px solid var(--line);border-radius:6px;background:#fff;
252
+ font-family:var(--mono);font-size:18px;letter-spacing:.08em;text-align:center;color:var(--ink);}
253
+ .code-input:focus{outline:2px solid var(--ink);outline-offset:0;border-color:var(--ink);}
254
+ .msg{margin-bottom:16px;font-size:14px;line-height:1.5;color:var(--muted);}
255
+ .msg strong{color:var(--ink);font-weight:600;}
256
+ `;
257
+ var fontLinks = `<link rel="preconnect" href="https://fonts.googleapis.com">
258
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
259
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,600;1,400&family=Roboto:wght@500&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;1,8..60,400;1,8..60,600&display=swap" rel="stylesheet">`;
260
+
261
+ // ../auth-ui/dist/marks.js
262
+ var brandMark = `<svg viewBox="0 0 100 100" aria-hidden="true"><defs><linearGradient id="dz" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${TOKENS.orange}"/><stop offset=".52" stop-color="${TOKENS.orangeMid}"/><stop offset="1" stop-color="${TOKENS.lavender}"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#dz)"/><text x="50" y="54" text-anchor="middle" dominant-baseline="central" font-family="'Source Serif 4',Georgia,serif" font-size="68" fill="${TOKENS.inkSoft}">d</text></svg>`;
263
+ var shieldIcon = `<svg width="13" height="13" viewBox="0 0 14 14" aria-hidden="true"><path d="M7 1l4.5 1.8v3.4c0 3-1.9 5-4.5 6.3C4.4 11.2 2.5 9.2 2.5 6.2V2.8L7 1z" fill="none" stroke="${TOKENS.muted}" stroke-width="1.1" stroke-linejoin="round"/></svg>`;
264
+ var successMark = `<svg class="icon" viewBox="0 0 46 46" aria-hidden="true"><circle cx="23" cy="23" r="23" fill="#ffe3d6"/><path d="M14 23.5l6 6 12-13" fill="none" stroke="${TOKENS.orange}" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
265
+ var errorMark = `<svg class="icon" viewBox="0 0 46 46" aria-hidden="true"><circle cx="23" cy="23" r="23" fill="#f6e0d6"/><path d="M23 13v13M23 31.5v.5" fill="none" stroke="${TOKENS.error}" stroke-width="2.6" stroke-linecap="round"/></svg>`;
266
+
267
+ // ../auth-ui/dist/layout.js
268
+ var faviconHref = `data:image/svg+xml,${encodeURIComponent(brandMark)}`;
269
+ function pageShell(innerHtml) {
270
+ return `<main class="card">${innerHtml}</main>`;
271
+ }
272
+ function renderDocument(params) {
273
+ const { title, body, headExtra, centered } = params;
274
+ const centerStyle = centered ? "body{text-align:center;}" : "";
275
+ return `<!DOCTYPE html>
276
+ <html lang="en">
277
+ <head>
278
+ <meta charset="utf-8">
279
+ <meta name="viewport" content="width=device-width, initial-scale=1">
280
+ <meta name="theme-color" content="${TOKENS.orange}">
281
+ <title>${title}</title>
282
+ <link rel="icon" type="image/svg+xml" href="${faviconHref}">
283
+ ${fontLinks}
284
+ <style>${baseCss}${centerStyle}${headExtra ?? ""}</style>
285
+ </head>
286
+ <body>
287
+ ${body}
288
+ </body>
289
+ </html>`;
290
+ }
291
+
292
+ // src/lib/loopback-flow.ts
293
+ var DEFAULT_SCOPE2 = "openid offline_access brain";
294
+ var CALLBACK_PATH = "/callback";
295
+ var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
296
+ function base64url(buf) {
297
+ return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
298
+ }
299
+ function createPkce() {
300
+ const verifier = base64url(randomBytes(32));
301
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
302
+ return { verifier, challenge };
303
+ }
304
+ function launchBrowser(url) {
305
+ const platform = process.platform;
306
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
307
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
308
+ try {
309
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
310
+ child.on("error", () => {
311
+ });
312
+ child.unref();
313
+ } catch {
314
+ }
315
+ }
316
+ function buildAuthorizeUrl(endpoint, params) {
317
+ const url = new URL(endpoint);
318
+ url.searchParams.set("response_type", "code");
319
+ url.searchParams.set("client_id", params.clientId);
320
+ url.searchParams.set("redirect_uri", params.redirectUri);
321
+ url.searchParams.set("scope", params.scope);
322
+ url.searchParams.set("state", params.state);
323
+ url.searchParams.set("code_challenge", params.challenge);
324
+ url.searchParams.set("code_challenge_method", "S256");
325
+ if (params.resource) {
326
+ url.searchParams.set("resource", params.resource);
327
+ }
328
+ return url.toString();
329
+ }
330
+ function statusPage(title, heading, body, mark) {
331
+ return renderDocument({
332
+ title,
333
+ centered: true,
334
+ body: pageShell(`${mark}<h1>${heading}</h1><p>${body}</p>
335
+ <div class="foot">Dazzer secure sign-in</div>`)
336
+ });
337
+ }
338
+ function successPage() {
339
+ return statusPage(
340
+ "Signed in to Dazzer",
341
+ "You're signed in",
342
+ "You can close this tab and return to your terminal.",
343
+ successMark
344
+ );
345
+ }
346
+ function errorPage(reason) {
347
+ return statusPage(
348
+ "Sign-in failed",
349
+ "Something went wrong",
350
+ `We couldn't complete sign-in (${escapeHtml(reason)}). Close this tab and run the command again.`,
351
+ errorMark
352
+ );
353
+ }
354
+ function sendHtml(res, html) {
355
+ res.statusCode = 200;
356
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
357
+ res.end(html);
358
+ }
359
+ async function runLoopbackFlow(options) {
360
+ const fetchFn = options.fetchFn ?? fetch;
361
+ const { authorizationEndpoint } = options.discovery;
362
+ if (!authorizationEndpoint) {
363
+ throw new Error("This authorization server does not support browser sign-in.");
364
+ }
365
+ const { verifier, challenge } = createPkce();
366
+ const state = base64url(randomBytes(16));
367
+ const scope = options.scope ?? DEFAULT_SCOPE2;
368
+ const { code, redirectUri } = await new Promise(
369
+ (resolve, reject) => {
370
+ let timer;
371
+ const server = createServer((req, res2) => {
372
+ const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
373
+ if (requestUrl.pathname !== CALLBACK_PATH) {
374
+ res2.statusCode = 404;
375
+ res2.end("Not found");
376
+ return;
377
+ }
378
+ const addr = server.address();
379
+ const redirect = `http://127.0.0.1:${addr.port}${CALLBACK_PATH}`;
380
+ const returnedState = requestUrl.searchParams.get("state");
381
+ const errorParam = requestUrl.searchParams.get("error");
382
+ const returnedCode = requestUrl.searchParams.get("code");
383
+ const finish = (err) => {
384
+ if (timer) clearTimeout(timer);
385
+ server.close();
386
+ if (err) reject(err);
387
+ };
388
+ if (errorParam) {
389
+ sendHtml(res2, errorPage(errorParam));
390
+ finish(new Error(`Sign-in failed: ${errorParam}.`));
391
+ return;
392
+ }
393
+ if (returnedState !== state) {
394
+ sendHtml(res2, errorPage("state_mismatch"));
395
+ finish(new Error("Sign-in failed: state mismatch."));
396
+ return;
397
+ }
398
+ if (!returnedCode) {
399
+ sendHtml(res2, errorPage("missing_code"));
400
+ finish(new Error("Sign-in failed: no authorization code returned."));
401
+ return;
402
+ }
403
+ sendHtml(res2, successPage());
404
+ finish();
405
+ resolve({ code: returnedCode, redirectUri: redirect });
406
+ });
407
+ server.on("error", (err) => {
408
+ if (timer) clearTimeout(timer);
409
+ reject(err);
410
+ });
411
+ server.listen(0, "127.0.0.1", () => {
412
+ const addr = server.address();
413
+ const redirect = `http://127.0.0.1:${addr.port}${CALLBACK_PATH}`;
414
+ const authorizeUrl = buildAuthorizeUrl(authorizationEndpoint, {
415
+ clientId: options.clientId,
416
+ redirectUri: redirect,
417
+ scope,
418
+ state,
419
+ challenge,
420
+ resource: options.resource
421
+ });
422
+ timer = setTimeout(() => {
423
+ server.close();
424
+ reject(new Error("Timed out waiting for sign-in. Run the command again."));
425
+ }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
426
+ (options.openBrowser ?? launchBrowser)(authorizeUrl);
427
+ options.onPrompt?.(authorizeUrl);
428
+ });
429
+ }
430
+ );
431
+ const params = new URLSearchParams({
432
+ grant_type: "authorization_code",
433
+ code,
434
+ redirect_uri: redirectUri,
435
+ client_id: options.clientId,
436
+ code_verifier: verifier
437
+ });
438
+ if (options.resource) {
439
+ params.set("resource", options.resource);
440
+ }
441
+ const res = await fetchFn(options.discovery.tokenEndpoint, {
442
+ method: "POST",
443
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
444
+ body: params.toString()
445
+ });
446
+ const body = await res.json();
447
+ if (!res.ok) {
448
+ const error = typeof body.error === "string" ? body.error : "token_exchange_failed";
449
+ throw new Error(`Token request failed: ${error}`);
450
+ }
451
+ const accessToken = body.access_token;
452
+ if (typeof accessToken !== "string") {
453
+ throw new Error("The authorization server returned an unexpected token response.");
454
+ }
455
+ return {
456
+ access_token: accessToken,
457
+ refresh_token: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
458
+ expires_in: typeof body.expires_in === "number" ? body.expires_in : void 0,
459
+ token_type: typeof body.token_type === "string" ? body.token_type : void 0
460
+ };
461
+ }
462
+ function browserLikelyAvailable() {
463
+ if (process.platform === "darwin" || process.platform === "win32") {
464
+ return true;
465
+ }
466
+ return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
467
+ }
468
+
469
+ // src/lib/output.ts
470
+ function writeOut(message) {
471
+ process.stdout.write(`${message}
472
+ `);
473
+ }
474
+ function writeErr(message) {
475
+ process.stderr.write(`${message}
476
+ `);
477
+ }
478
+ function printDeviceInstructions(deviceAuth) {
479
+ writeOut("");
480
+ if (deviceAuth.verification_uri_complete) {
481
+ writeOut("To finish signing in, open this link in your browser:");
482
+ writeOut(` ${deviceAuth.verification_uri_complete}`);
483
+ } else {
484
+ writeOut("To finish signing in, open this page in your browser:");
485
+ writeOut(` ${deviceAuth.verification_uri}`);
486
+ writeOut("");
487
+ writeOut("Then enter this code:");
488
+ writeOut(` ${deviceAuth.user_code}`);
489
+ }
490
+ writeOut("");
491
+ writeOut("Waiting for you to approve in the browser...");
492
+ }
493
+ function printBrowserOpening(url) {
494
+ writeOut("");
495
+ writeOut("Opening your browser to sign in\u2026");
496
+ writeOut("If it doesn't open automatically, paste this into your browser:");
497
+ writeOut(` ${url}`);
498
+ writeOut("");
499
+ }
500
+ function printLoginSuccess(keychainOk) {
501
+ writeOut("");
502
+ writeOut("Signed in successfully.");
503
+ if (!keychainOk) {
504
+ writeErr("Warning: could not save tokens to the OS keychain. Use the config below.");
505
+ }
506
+ }
507
+ function printConnectGuidance(mcpServerUrl, accessToken) {
508
+ const url = mcpServerUrl.replace(/\/$/, "");
509
+ writeOut("");
510
+ writeOut("Connect an AI client to Dazzer.");
511
+ writeOut("");
512
+ writeOut("Recommended \u2014 add this server by URL and sign in from your client;");
513
+ writeOut("it stays connected and renews itself:");
514
+ writeOut("");
515
+ writeOut("{");
516
+ writeOut(' "mcpServers": {');
517
+ writeOut(' "dazzer": {');
518
+ writeOut(` "url": "${url}"`);
519
+ writeOut(" }");
520
+ writeOut(" }");
521
+ writeOut("}");
522
+ writeOut("");
523
+ writeOut("Fallback \u2014 only if your client cannot sign in and needs a pasted token.");
524
+ writeOut("This token expires in about an hour; re-run `dazzer auth login` to renew:");
525
+ writeOut("");
526
+ writeOut("{");
527
+ writeOut(' "mcpServers": {');
528
+ writeOut(' "dazzer": {');
529
+ writeOut(` "url": "${url}",`);
530
+ writeOut(' "headers": {');
531
+ writeOut(` "Authorization": "Bearer ${accessToken}"`);
532
+ writeOut(" }");
533
+ writeOut(" }");
534
+ writeOut(" }");
535
+ writeOut("}");
536
+ writeOut("");
537
+ }
538
+ function printAuthStatus(params) {
539
+ switch (params.state) {
540
+ case "signed-out":
541
+ writeOut("Not signed in. Run: dazzer auth login");
542
+ return;
543
+ case "store-unreadable":
544
+ writeOut("Cannot tell \u2014 this machine's credential store could not be opened.");
545
+ writeOut(" Common on servers and containers, which often have none.");
546
+ writeOut(" Run: dazzer auth login \u2014 it prints a pass you can paste in.");
547
+ return;
548
+ case "expired":
549
+ writeOut("Your sign-in has expired or been revoked. Run: dazzer auth login");
550
+ return;
551
+ case "unverified":
552
+ writeOut(
553
+ "Signed in on this device, but the sign-in server could not be reached to confirm it."
554
+ );
555
+ if (params.issuer) {
556
+ writeOut(` Signed in at: ${params.issuer}`);
557
+ }
558
+ if (params.lastKnownExpiresAt) {
559
+ writeOut(` Last confirmed expiry: ${new Date(params.lastKnownExpiresAt).toISOString()}`);
560
+ }
561
+ writeOut(" Check your connection, then run this again to confirm.");
562
+ return;
563
+ case "signed-in":
564
+ writeOut("Signed in \u2014 confirmed just now.");
565
+ if (params.issuer) {
566
+ writeOut(` Signed in at: ${params.issuer}`);
567
+ }
568
+ if (params.expiresAt) {
569
+ writeOut(
570
+ ` This token expires ${new Date(params.expiresAt).toISOString()} and refreshes automatically.`
571
+ );
572
+ }
573
+ return;
574
+ }
575
+ }
576
+ function printLogoutSuccess(serverRevoked) {
577
+ if (serverRevoked) {
578
+ writeOut("Signed out.");
579
+ return;
580
+ }
581
+ writeOut(
582
+ "Signed out on this device. We could not reach the server to revoke your tokens, so they will expire on their own."
583
+ );
584
+ }
585
+ function fail(message) {
586
+ writeErr(`Error: ${message}`);
587
+ process.exit(1);
588
+ }
589
+
590
+ // src/lib/refresh.ts
591
+ var RefreshRejectedError = class extends Error {
592
+ constructor(oauthError) {
593
+ super(`Could not refresh token: ${oauthError}`);
594
+ this.oauthError = oauthError;
595
+ this.name = "RefreshRejectedError";
596
+ }
597
+ oauthError;
598
+ };
599
+ async function refreshAccessToken(options) {
600
+ const fetchFn = options.fetchFn ?? fetch;
601
+ const params = new URLSearchParams({
602
+ grant_type: "refresh_token",
603
+ client_id: options.clientId,
604
+ refresh_token: options.refreshToken
605
+ });
606
+ if (options.resource) {
607
+ params.set("resource", options.resource);
608
+ }
609
+ const res = await fetchFn(options.discovery.tokenEndpoint, {
610
+ method: "POST",
611
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
612
+ body: params.toString()
613
+ });
614
+ const body = await res.json();
615
+ if (!res.ok) {
616
+ const error = typeof body.error === "string" ? body.error : "refresh_failed";
617
+ throw new RefreshRejectedError(error);
618
+ }
619
+ const accessToken = body.access_token;
620
+ const refreshToken = body.refresh_token;
621
+ if (typeof accessToken !== "string") {
622
+ throw new Error("The authorization server returned an unexpected refresh response.");
623
+ }
624
+ const newRefresh = typeof refreshToken === "string" ? refreshToken : options.refreshToken;
625
+ const expiresIn = typeof body.expires_in === "number" ? body.expires_in : null;
626
+ await options.tokenStore.save({ accessToken, refreshToken: newRefresh });
627
+ return { accessToken, refreshToken: newRefresh, expiresIn };
628
+ }
629
+ async function getValidAccessToken(options) {
630
+ const stored = await options.tokenStore.load();
631
+ if (!stored) {
632
+ return { status: "signed-out" };
633
+ }
634
+ try {
635
+ const refreshed = await refreshAccessToken({ ...options, refreshToken: stored.refreshToken });
636
+ return { status: "refreshed", expiresIn: refreshed.expiresIn };
637
+ } catch (err) {
638
+ if (err instanceof RefreshRejectedError) {
639
+ return { status: "rejected" };
640
+ }
641
+ return { status: "unreachable" };
642
+ }
643
+ }
644
+ async function persistLoginTokens(tokenStore, tokens) {
645
+ if (!tokens.refresh_token) {
646
+ throw new Error("The authorization server did not return a refresh token.");
647
+ }
648
+ const stored = {
649
+ accessToken: tokens.access_token,
650
+ refreshToken: tokens.refresh_token
651
+ };
652
+ await tokenStore.save(stored);
653
+ return stored;
654
+ }
655
+
656
+ // src/lib/token-store.ts
657
+ import { Entry } from "@napi-rs/keyring";
658
+ var SERVICE = "dazzer-oauth-cli";
659
+ var ACCESS_ACCOUNT = "access-token";
660
+ var REFRESH_ACCOUNT = "refresh-token";
661
+ function accessEntry() {
662
+ return new Entry(SERVICE, ACCESS_ACCOUNT);
663
+ }
664
+ function refreshEntry() {
665
+ return new Entry(SERVICE, REFRESH_ACCOUNT);
666
+ }
667
+ var KeychainTokenStore = class {
668
+ async save(tokens) {
669
+ accessEntry().setPassword(tokens.accessToken);
670
+ refreshEntry().setPassword(tokens.refreshToken);
671
+ }
672
+ async load() {
673
+ const accessToken = accessEntry().getPassword();
674
+ const refreshToken = refreshEntry().getPassword();
675
+ if (!accessToken || !refreshToken) {
676
+ return null;
677
+ }
678
+ return { accessToken, refreshToken };
679
+ }
680
+ async clear() {
681
+ try {
682
+ accessEntry().deletePassword();
683
+ } catch {
684
+ }
685
+ try {
686
+ refreshEntry().deletePassword();
687
+ } catch {
688
+ }
689
+ }
690
+ };
691
+ function createTokenStore() {
692
+ return new KeychainTokenStore();
693
+ }
694
+
695
+ // src/commands/login.ts
696
+ async function runLogin(options) {
697
+ const tokenStore = createTokenStore();
698
+ let keychainOk = true;
699
+ let discovery;
700
+ try {
701
+ discovery = await discoverEndpoints(options.issuer);
702
+ } catch (err) {
703
+ fail(err instanceof Error ? err.message : "Could not reach the authorization server.");
704
+ }
705
+ const useLoopback = !options.noBrowser && Boolean(discovery.authorizationEndpoint) && browserLikelyAvailable();
706
+ let tokens;
707
+ try {
708
+ if (useLoopback) {
709
+ tokens = await runLoopbackFlow({
710
+ discovery,
711
+ clientId: options.clientId,
712
+ resource: options.resource,
713
+ onPrompt: printBrowserOpening
714
+ });
715
+ } else {
716
+ const deviceAuth = await requestDeviceCode({
717
+ discovery,
718
+ clientId: options.clientId
719
+ });
720
+ printDeviceInstructions(deviceAuth);
721
+ tokens = await pollForTokens({
722
+ discovery,
723
+ clientId: options.clientId,
724
+ deviceCode: deviceAuth.device_code,
725
+ deviceAuth,
726
+ resource: options.resource
727
+ });
728
+ }
729
+ } catch (err) {
730
+ fail(err instanceof Error ? err.message : "Sign-in failed.");
731
+ }
732
+ if (!tokens.refresh_token) {
733
+ fail("The authorization server did not return a refresh token.");
734
+ }
735
+ try {
736
+ await persistLoginTokens(tokenStore, tokens);
737
+ } catch {
738
+ keychainOk = false;
739
+ }
740
+ const expiresAt = typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : null;
741
+ await saveAuthMetadata({
742
+ issuer: discovery.issuer,
743
+ clientId: options.clientId,
744
+ expiresAt
745
+ });
746
+ printLoginSuccess(keychainOk);
747
+ printConnectGuidance(options.resource ?? discovery.issuer, tokens.access_token);
748
+ }
749
+
750
+ // src/lib/revoke.ts
751
+ async function revokeToken(options) {
752
+ const fetchFn = options.fetchFn ?? fetch;
753
+ const params = new URLSearchParams({
754
+ token: options.token,
755
+ token_type_hint: options.tokenTypeHint,
756
+ client_id: options.clientId
757
+ });
758
+ const res = await fetchFn(options.revocationEndpoint, {
759
+ method: "POST",
760
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
761
+ body: params.toString()
762
+ });
763
+ if (!res.ok) {
764
+ throw new Error(`Token revocation failed (${res.status}).`);
765
+ }
766
+ }
767
+
768
+ // src/commands/logout.ts
769
+ async function runLogout(options = {}) {
770
+ const tokenStore = options.tokenStore ?? createTokenStore();
771
+ const meta = await loadAuthMetadata();
772
+ const tokens = await tokenStore.load().catch(() => null);
773
+ let serverRevoked = true;
774
+ if (meta && tokens) {
775
+ try {
776
+ const discovery = await discoverEndpoints(options.issuer ?? meta.issuer);
777
+ if (!discovery.revocationEndpoint) {
778
+ throw new Error("This authorization server does not support token revocation.");
779
+ }
780
+ const clientId = options.clientId ?? meta.clientId;
781
+ await revokeToken({
782
+ revocationEndpoint: discovery.revocationEndpoint,
783
+ clientId,
784
+ token: tokens.refreshToken,
785
+ tokenTypeHint: "refresh_token",
786
+ fetchFn: options.fetchFn
787
+ });
788
+ await revokeToken({
789
+ revocationEndpoint: discovery.revocationEndpoint,
790
+ clientId,
791
+ token: tokens.accessToken,
792
+ tokenTypeHint: "access_token",
793
+ fetchFn: options.fetchFn
794
+ });
795
+ } catch {
796
+ serverRevoked = false;
797
+ }
798
+ }
799
+ await tokenStore.clear();
800
+ await clearAuthMetadata();
801
+ printLogoutSuccess(serverRevoked);
802
+ }
803
+
804
+ // src/commands/status.ts
805
+ async function runStatus(options) {
806
+ const tokenStore = createTokenStore();
807
+ const meta = await loadAuthMetadata();
808
+ let tokens;
809
+ try {
810
+ tokens = await tokenStore.load();
811
+ } catch {
812
+ printAuthStatus({ state: "store-unreadable" });
813
+ return;
814
+ }
815
+ if (!meta || !tokens) {
816
+ printAuthStatus({ state: "signed-out" });
817
+ return;
818
+ }
819
+ const issuer = options.issuer ?? meta.issuer;
820
+ const clientId = options.clientId ?? meta.clientId;
821
+ let discovery;
822
+ try {
823
+ discovery = await discoverEndpoints(issuer);
824
+ } catch {
825
+ printAuthStatus({
826
+ state: "unverified",
827
+ issuer: meta.issuer,
828
+ lastKnownExpiresAt: meta.expiresAt
829
+ });
830
+ return;
831
+ }
832
+ const result = await getValidAccessToken({
833
+ discovery,
834
+ clientId,
835
+ tokenStore,
836
+ resource: options.resource
837
+ });
838
+ switch (result.status) {
839
+ case "signed-out":
840
+ printAuthStatus({ state: "signed-out" });
841
+ return;
842
+ case "rejected":
843
+ printAuthStatus({ state: "expired" });
844
+ return;
845
+ case "unreachable":
846
+ printAuthStatus({
847
+ state: "unverified",
848
+ issuer: meta.issuer,
849
+ lastKnownExpiresAt: meta.expiresAt
850
+ });
851
+ return;
852
+ case "refreshed": {
853
+ const expiresAt = result.expiresIn ? Date.now() + result.expiresIn * 1e3 : null;
854
+ await saveAuthMetadata({ issuer: discovery.issuer, clientId, expiresAt });
855
+ printAuthStatus({ state: "signed-in", issuer: discovery.issuer, expiresAt });
856
+ return;
857
+ }
858
+ }
859
+ }
860
+
861
+ // src/env.ts
862
+ import { z } from "zod/v4";
863
+ var cliEnvSchema = z.object({
864
+ /** Authorization server issuer URL. Defaults to production; override for local dev. */
865
+ DAZZER_AUTH_ISSUER: z.url().default("https://auth.dazzer.io"),
866
+ /** OAuth public client id. */
867
+ DAZZER_AUTH_CLIENT_ID: z.string().min(1).default("dazzer-cli-local"),
868
+ /** Resource indicator for JWT access tokens. Defaults to the production MCP server. */
869
+ DAZZER_AUTH_RESOURCE: z.url().default("https://graph.dazzer.io/mcp")
870
+ });
871
+ var cachedEnv = null;
872
+ function validateEnv() {
873
+ if (!cachedEnv) {
874
+ cachedEnv = cliEnvSchema.parse(process.env);
875
+ }
876
+ return cachedEnv;
877
+ }
878
+
879
+ // src/index.ts
880
+ var require2 = createRequire(import.meta.url);
881
+ var { version } = require2("../package.json");
882
+ var program = new Command();
15
883
  program.name("dazzer").description("Sign in to Dazzer from your terminal").version(version);
16
- const auth = program.command("auth").description("Manage sign-in");
17
- auth
18
- .command("login")
19
- .description("Sign in through your browser; falls back to a device code when headless")
20
- .option("--issuer <url>", "Authorization server URL")
21
- .option("--client-id <id>", "OAuth client id")
22
- .option("--resource <url>", "Resource indicator for access tokens")
23
- .option("--no-browser", "Use the device-code flow instead of opening a browser")
24
- .action(async (opts) => {
884
+ var auth = program.command("auth").description("Manage sign-in");
885
+ auth.command("login").description("Sign in through your browser; falls back to a device code when headless").option("--issuer <url>", "Authorization server URL").option("--client-id <id>", "OAuth client id").option("--resource <url>", "Resource indicator for access tokens").option("--no-browser", "Use the device-code flow instead of opening a browser").action(
886
+ async (opts) => {
25
887
  const env = validateEnv();
26
888
  await runLogin({
27
- issuer: opts.issuer ?? env.DAZZER_AUTH_ISSUER,
28
- clientId: opts.clientId ?? env.DAZZER_AUTH_CLIENT_ID,
29
- resource: opts.resource ?? env.DAZZER_AUTH_RESOURCE,
30
- noBrowser: opts.browser === false,
889
+ issuer: opts.issuer ?? env.DAZZER_AUTH_ISSUER,
890
+ clientId: opts.clientId ?? env.DAZZER_AUTH_CLIENT_ID,
891
+ resource: opts.resource ?? env.DAZZER_AUTH_RESOURCE,
892
+ noBrowser: opts.browser === false
31
893
  });
894
+ }
895
+ );
896
+ auth.command("logout").description("Sign out and remove stored tokens").action(async () => {
897
+ await runLogout();
32
898
  });
33
- auth
34
- .command("logout")
35
- .description("Sign out and remove stored tokens")
36
- .action(async () => {
37
- await runLogout();
38
- });
39
- auth
40
- .command("status")
41
- .description("Show sign-in status")
42
- .option("--issuer <url>", "Authorization server URL")
43
- .option("--client-id <id>", "OAuth client id")
44
- .option("--resource <url>", "Resource indicator for access tokens")
45
- .action(async (opts) => {
46
- await runStatus({
47
- issuer: opts.issuer,
48
- clientId: opts.clientId,
49
- resource: opts.resource,
50
- });
899
+ auth.command("status").description("Show sign-in status").option("--issuer <url>", "Authorization server URL").option("--client-id <id>", "OAuth client id").option("--resource <url>", "Resource indicator for access tokens").action(async (opts) => {
900
+ await runStatus({
901
+ issuer: opts.issuer,
902
+ clientId: opts.clientId,
903
+ resource: opts.resource
904
+ });
51
905
  });
52
906
  program.parseAsync(process.argv).catch((err) => {
53
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
54
- process.exit(1);
907
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
908
+ `);
909
+ process.exit(1);
55
910
  });