dazzer-connect 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,55 +1,2031 @@
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/commands/add.ts
8
+ import { open, stat as statPath } from "node:fs/promises";
9
+ import { basename, extname } from "node:path";
10
+
11
+ // src/lib/output.ts
12
+ function writeOut(message) {
13
+ process.stdout.write(`${message}
14
+ `);
15
+ }
16
+ function writeErr(message) {
17
+ process.stderr.write(`${message}
18
+ `);
19
+ }
20
+ var CANNOT_REACH_DAZZER = "I can't reach Dazzer right now \u2014 nothing was sent. Try again shortly.";
21
+ var SIGN_IN_NO_LONGER_VALID = "Your Dazzer sign-in is no longer valid. Reconnect your client (or run `dazzer auth login`).";
22
+ function printDeviceInstructions(deviceAuth) {
23
+ writeOut("");
24
+ if (deviceAuth.verification_uri_complete) {
25
+ writeOut("To finish signing in, open this link in your browser:");
26
+ writeOut(` ${deviceAuth.verification_uri_complete}`);
27
+ } else {
28
+ writeOut("To finish signing in, open this page in your browser:");
29
+ writeOut(` ${deviceAuth.verification_uri}`);
30
+ writeOut("");
31
+ writeOut("Then enter this code:");
32
+ writeOut(` ${deviceAuth.user_code}`);
33
+ }
34
+ writeOut("");
35
+ writeOut("Waiting for you to approve in the browser...");
36
+ }
37
+ function printBrowserOpening(url) {
38
+ writeOut("");
39
+ writeOut("Opening your browser to sign in\u2026");
40
+ writeOut("If it doesn't open automatically, paste this into your browser:");
41
+ writeOut(` ${url}`);
42
+ writeOut("");
43
+ }
44
+ function printLoginSuccess(keychainOk) {
45
+ writeOut("");
46
+ writeOut("Signed in successfully.");
47
+ if (!keychainOk) {
48
+ writeErr("Warning: could not save tokens to the OS keychain. Use the config below.");
49
+ }
50
+ }
51
+ function printConnectGuidance(mcpServerUrl, accessToken) {
52
+ const url = mcpServerUrl.replace(/\/$/, "");
53
+ writeOut("");
54
+ writeOut("Connect an AI client to Dazzer.");
55
+ writeOut("");
56
+ writeOut("Recommended \u2014 add this server by URL and sign in from your client;");
57
+ writeOut("it stays connected and renews itself:");
58
+ writeOut("");
59
+ writeOut("{");
60
+ writeOut(' "mcpServers": {');
61
+ writeOut(' "dazzer": {');
62
+ writeOut(` "url": "${url}"`);
63
+ writeOut(" }");
64
+ writeOut(" }");
65
+ writeOut("}");
66
+ writeOut("");
67
+ writeOut("Fallback \u2014 only if your client cannot sign in and needs a pasted token.");
68
+ writeOut("This token expires in about an hour; re-run `dazzer auth login` to renew:");
69
+ writeOut("");
70
+ writeOut("{");
71
+ writeOut(' "mcpServers": {');
72
+ writeOut(' "dazzer": {');
73
+ writeOut(` "url": "${url}",`);
74
+ writeOut(' "headers": {');
75
+ writeOut(` "Authorization": "Bearer ${accessToken}"`);
76
+ writeOut(" }");
77
+ writeOut(" }");
78
+ writeOut(" }");
79
+ writeOut("}");
80
+ writeOut("");
81
+ }
82
+ function printAuthStatus(params) {
83
+ switch (params.state) {
84
+ case "signed-out":
85
+ writeOut("Not signed in. Run: dazzer auth login");
86
+ return;
87
+ case "store-unreadable":
88
+ writeOut("Cannot tell \u2014 this machine's credential store could not be opened.");
89
+ writeOut(" Common on servers and containers, which often have none.");
90
+ writeOut(" Run: dazzer auth login \u2014 it prints a pass you can paste in.");
91
+ return;
92
+ case "expired":
93
+ writeOut("Your sign-in has expired or been revoked. Run: dazzer auth login");
94
+ return;
95
+ case "unverified":
96
+ writeOut(
97
+ "Signed in on this device, but the sign-in server could not be reached to confirm it."
98
+ );
99
+ if (params.issuer) {
100
+ writeOut(` Signed in at: ${params.issuer}`);
101
+ }
102
+ if (params.lastKnownExpiresAt) {
103
+ writeOut(` Last confirmed expiry: ${new Date(params.lastKnownExpiresAt).toISOString()}`);
104
+ }
105
+ writeOut(" Check your connection, then run this again to confirm.");
106
+ return;
107
+ case "signed-in":
108
+ writeOut("Signed in \u2014 confirmed just now.");
109
+ if (params.issuer) {
110
+ writeOut(` Signed in at: ${params.issuer}`);
111
+ }
112
+ if (params.expiresAt) {
113
+ writeOut(
114
+ ` This token expires ${new Date(params.expiresAt).toISOString()} and refreshes automatically.`
115
+ );
116
+ }
117
+ return;
118
+ }
119
+ }
120
+ function printLogoutSuccess() {
121
+ writeOut("Signed out.");
122
+ }
123
+ function printLogoutStoreLocked(offerForget) {
124
+ writeErr(
125
+ "Not signed out. This machine's credential store could not be opened, so a sign-in saved here may still work."
126
+ );
127
+ writeErr(
128
+ offerForget ? " Unlock it and try again. If this machine has no credential store, run: dazzer auth logout --forget-this-machine" : " Unlock it and try again."
129
+ );
130
+ }
131
+ function printLogoutNotCancelledYet() {
132
+ writeErr(
133
+ "Not signed out. Your sign-in could not be cancelled right now, so it is still saved here."
134
+ );
135
+ writeErr(" Try again: dazzer auth logout");
136
+ }
137
+ function printLogoutCannotCancel() {
138
+ writeOut(
139
+ "Signed out on this device, but your sign-in could not be cancelled at the server. It stops working once it goes 30 days unused."
140
+ );
141
+ }
142
+ function printLogoutNothingFound() {
143
+ writeOut("No saved sign-in was found here to cancel.");
144
+ }
145
+ function printLogoutIncomplete(cancelledAtServer) {
146
+ if (cancelledAtServer) {
147
+ writeErr(
148
+ "Not fully signed out. Your sign-in is cancelled, but this machine's credential store would not let it be removed, so what is saved here can work for up to an hour."
149
+ );
150
+ } else {
151
+ writeErr(
152
+ "Not signed out. This machine's credential store would not let your saved sign-in be removed, so it may still work from here."
153
+ );
154
+ }
155
+ writeErr(" Try again: dazzer auth logout");
156
+ }
157
+ function printLogoutForgotten() {
158
+ writeOut(
159
+ "Forgot this machine's sign-in record. Nothing was cancelled, and any sign-in still saved in this machine's credential store stays there."
160
+ );
161
+ }
162
+ function printForgetFailed(configDir) {
163
+ writeErr("Not forgotten: this machine's sign-in record could not be removed.");
164
+ writeErr(` Remove it by hand: ${configDir}`);
165
+ }
166
+ function printLogoutPassLeft() {
167
+ writeOut(
168
+ "No sign-in that can be renewed is saved here. A short-lived pass this machine's credential store would not remove stops working within the hour."
169
+ );
170
+ }
171
+ function printLogoutBadSettings() {
172
+ writeErr(
173
+ "Not signed out. DAZZER_AUTH_ISSUER or DAZZER_AUTH_CLIENT_ID is not a valid setting, so there is nowhere to cancel your sign-in."
174
+ );
175
+ writeErr(" Fix it, then run: dazzer auth logout");
176
+ }
177
+ function printForgetRefused() {
178
+ writeErr(
179
+ "Not forgotten: a sign-in may be saved in this machine's credential store. Unlock it if needed, then run: dazzer auth logout"
180
+ );
181
+ }
182
+ function fail(message) {
183
+ writeErr(`Error: ${message}`);
184
+ process.exit(1);
185
+ }
186
+
187
+ // src/lib/refresh.ts
188
+ var RefreshRejectedError = class extends Error {
189
+ constructor(oauthError) {
190
+ super(`Could not refresh token: ${oauthError}`);
191
+ this.oauthError = oauthError;
192
+ this.name = "RefreshRejectedError";
193
+ }
194
+ oauthError;
195
+ };
196
+ async function refreshAccessToken(options) {
197
+ const fetchFn = options.fetchFn ?? fetch;
198
+ const params = new URLSearchParams({
199
+ grant_type: "refresh_token",
200
+ client_id: options.clientId,
201
+ refresh_token: options.refreshToken
202
+ });
203
+ if (options.resource) {
204
+ params.set("resource", options.resource);
205
+ }
206
+ const res = await fetchFn(options.discovery.tokenEndpoint, {
207
+ method: "POST",
208
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
209
+ body: params.toString()
210
+ });
211
+ const body = await res.json();
212
+ if (!res.ok) {
213
+ const error = typeof body.error === "string" ? body.error : "refresh_failed";
214
+ throw new RefreshRejectedError(error);
215
+ }
216
+ const accessToken = body.access_token;
217
+ const refreshToken = body.refresh_token;
218
+ if (typeof accessToken !== "string") {
219
+ throw new Error("The authorization server returned an unexpected refresh response.");
220
+ }
221
+ const newRefresh = typeof refreshToken === "string" ? refreshToken : options.refreshToken;
222
+ const expiresIn = typeof body.expires_in === "number" ? body.expires_in : null;
223
+ await options.tokenStore.save({ accessToken, refreshToken: newRefresh });
224
+ return { accessToken, refreshToken: newRefresh, expiresIn };
225
+ }
226
+ async function getValidAccessToken(options) {
227
+ const stored = await options.tokenStore.load();
228
+ if (!stored) {
229
+ return { status: "signed-out" };
230
+ }
231
+ try {
232
+ const refreshed = await refreshAccessToken({ ...options, refreshToken: stored.refreshToken });
233
+ return { status: "refreshed", expiresIn: refreshed.expiresIn };
234
+ } catch (err) {
235
+ if (err instanceof RefreshRejectedError) {
236
+ return { status: "rejected" };
237
+ }
238
+ return { status: "unreachable" };
239
+ }
240
+ }
241
+ async function persistLoginTokens(tokenStore, tokens) {
242
+ if (!tokens.refresh_token) {
243
+ throw new Error("The authorization server did not return a refresh token.");
244
+ }
245
+ const stored = {
246
+ accessToken: tokens.access_token,
247
+ refreshToken: tokens.refresh_token
248
+ };
249
+ await tokenStore.save(stored);
250
+ return stored;
251
+ }
252
+
253
+ // src/lib/api-client.ts
254
+ function assertHttpsServiceAddress(address) {
255
+ let parsed;
256
+ try {
257
+ parsed = new URL(address);
258
+ } catch {
259
+ throw new Error(`Not a valid Dazzer address: ${address}. Check DAZZER_API_BASE_URL.`);
260
+ }
261
+ if (parsed.protocol !== "https:") {
262
+ throw new Error(
263
+ `The Dazzer address must be an https URL (got ${parsed.protocol}//). Check DAZZER_API_BASE_URL.`
264
+ );
265
+ }
266
+ return address.replace(/\/$/, "");
267
+ }
268
+ function readOfferedPlaces(offered) {
269
+ if (!Array.isArray(offered)) return [];
270
+ return offered.flatMap((place) => {
271
+ if (typeof place !== "object" || place === null) return [];
272
+ const { scope_id: scopeId, space } = place;
273
+ if (typeof scopeId !== "number" || !Number.isFinite(scopeId)) return [];
274
+ if (typeof space !== "string" || space.trim() === "") return [];
275
+ return [{ scopeId, space }];
276
+ });
277
+ }
278
+ async function readJson(res) {
279
+ try {
280
+ return await res.json();
281
+ } catch {
282
+ return void 0;
283
+ }
284
+ }
285
+ function refusalDetail(body) {
286
+ if (typeof body !== "object" || body === null) return void 0;
287
+ const flat = body.detail;
288
+ if (typeof flat === "string" && flat !== "") return flat;
289
+ const enveloped = body.error;
290
+ if (typeof enveloped === "object" && enveloped !== null) {
291
+ const message = enveloped.message;
292
+ if (typeof message === "string" && message !== "") return message;
293
+ }
294
+ return void 0;
295
+ }
296
+ function refusalCode(body) {
297
+ if (typeof body !== "object" || body === null) return void 0;
298
+ const flat = body.code;
299
+ if (typeof flat === "string" && flat !== "") return flat;
300
+ const enveloped = body.error;
301
+ if (typeof enveloped === "object" && enveloped !== null) {
302
+ const code = enveloped.code;
303
+ if (typeof code === "string" && code !== "") return code;
304
+ }
305
+ return void 0;
306
+ }
307
+ function refusalPlaces(body) {
308
+ if (typeof body !== "object" || body === null) return {};
309
+ const offered = body.choices;
310
+ const places = readOfferedPlaces(offered);
311
+ const keep = body.keep_to_yourself;
312
+ return {
313
+ ...places.length > 0 ? { choices: places } : {},
314
+ ...typeof keep === "boolean" ? { keepToYourself: keep } : {}
315
+ };
316
+ }
317
+ async function resolveCredential(auth2) {
318
+ let held;
319
+ try {
320
+ held = await auth2.tokenStore.load();
321
+ } catch {
322
+ return { status: "store-unreadable" };
323
+ }
324
+ if (!held) return { status: "signed-out" };
325
+ let state;
326
+ try {
327
+ state = await getValidAccessToken(auth2);
328
+ } catch {
329
+ return { status: "store-unreadable" };
330
+ }
331
+ switch (state.status) {
332
+ case "signed-out":
333
+ return { status: "signed-out" };
334
+ case "rejected":
335
+ return { status: "sign-in-invalid" };
336
+ case "unreachable":
337
+ return { held: held.accessToken };
338
+ case "refreshed": {
339
+ try {
340
+ const rotated = await auth2.tokenStore.load();
341
+ return rotated ? { held: rotated.accessToken } : { status: "signed-out" };
342
+ } catch {
343
+ return { status: "store-unreadable" };
344
+ }
345
+ }
346
+ }
347
+ }
348
+ async function callDazzer(request) {
349
+ const base = assertHttpsServiceAddress(request.baseUrl);
350
+ const credential = await resolveCredential(request.auth);
351
+ if (!("held" in credential)) {
352
+ if (credential.status === "sign-in-invalid") {
353
+ return { status: "sign-in-invalid", message: SIGN_IN_NO_LONGER_VALID };
354
+ }
355
+ return { status: credential.status };
356
+ }
357
+ const fetchFn = request.fetchFn ?? fetch;
358
+ const headers = {
359
+ accept: "application/json",
360
+ authorization: `Bearer ${credential.held}`
361
+ };
362
+ if (request.body !== void 0) headers["content-type"] = "application/json";
363
+ let res;
364
+ try {
365
+ res = await fetchFn(`${base}${request.path}`, {
366
+ method: request.method ?? "POST",
367
+ headers,
368
+ body: request.body === void 0 ? void 0 : JSON.stringify(request.body)
369
+ });
370
+ } catch {
371
+ return { status: "unreachable", message: CANNOT_REACH_DAZZER };
372
+ }
373
+ if (res.status === 401) {
374
+ return { status: "sign-in-invalid", message: SIGN_IN_NO_LONGER_VALID };
375
+ }
376
+ const body = await readJson(res);
377
+ if (!res.ok) {
378
+ const detail = refusalDetail(body);
379
+ const code = refusalCode(body);
380
+ return {
381
+ status: "refused",
382
+ httpStatus: res.status,
383
+ ...detail === void 0 ? {} : { detail },
384
+ ...code === void 0 ? {} : { code },
385
+ ...refusalPlaces(body)
386
+ };
387
+ }
388
+ return { status: "ok", httpStatus: res.status, body };
389
+ }
390
+
391
+ // src/lib/config-store.ts
392
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
393
+ import { join } from "node:path";
394
+ import envPaths from "env-paths";
395
+
396
+ // src/lib/discovery.ts
397
+ function assertHttpsIssuer(issuer) {
398
+ let parsed;
399
+ try {
400
+ parsed = new URL(issuer);
401
+ } catch {
402
+ throw new Error(`Not a valid issuer URL: ${issuer}`);
403
+ }
404
+ if (parsed.protocol !== "https:") {
405
+ throw new Error(`The issuer must be an https URL (got ${parsed.protocol}//). Check --issuer.`);
406
+ }
407
+ return issuer.replace(/\/$/, "");
408
+ }
409
+ var DiscoveryAnswered = class extends Error {
410
+ constructor(status) {
411
+ super(`Could not reach the authorization server (${status}). Check --issuer.`);
412
+ this.status = status;
413
+ this.name = "DiscoveryAnswered";
414
+ }
415
+ status;
416
+ };
417
+ var DiscoveryUnusable = class extends Error {
418
+ constructor(message) {
419
+ super(message);
420
+ this.name = "DiscoveryUnusable";
421
+ }
422
+ };
423
+ async function discoverEndpoints(issuer, fetchFn = fetch) {
424
+ const normalized = assertHttpsIssuer(issuer);
425
+ const url = `${normalized}/.well-known/oauth-authorization-server`;
426
+ const res = await fetchFn(url);
427
+ if (!res.ok) {
428
+ throw new DiscoveryAnswered(res.status);
429
+ }
430
+ const body = await res.json();
431
+ const authorizationEndpoint = body.authorization_endpoint;
432
+ const deviceAuthorizationEndpoint = body.device_authorization_endpoint;
433
+ const tokenEndpoint = body.token_endpoint;
434
+ const revocationEndpoint = body.revocation_endpoint;
435
+ if (typeof tokenEndpoint !== "string" || tokenEndpoint === "") {
436
+ throw new DiscoveryUnusable("This authorization server is missing a token endpoint.");
437
+ }
438
+ const hasAuthorization = typeof authorizationEndpoint === "string" && authorizationEndpoint !== "";
439
+ const hasDevice = typeof deviceAuthorizationEndpoint === "string" && deviceAuthorizationEndpoint !== "";
440
+ const hasRevocation = typeof revocationEndpoint === "string" && revocationEndpoint !== "";
441
+ if (!hasAuthorization && !hasDevice) {
442
+ throw new DiscoveryUnusable(
443
+ "This authorization server does not support browser or device sign-in."
444
+ );
445
+ }
446
+ return {
447
+ issuer: typeof body.issuer === "string" ? body.issuer : normalized,
448
+ authorizationEndpoint: hasAuthorization ? authorizationEndpoint : void 0,
449
+ deviceAuthorizationEndpoint: hasDevice ? deviceAuthorizationEndpoint : void 0,
450
+ tokenEndpoint,
451
+ revocationEndpoint: hasRevocation ? revocationEndpoint : void 0
452
+ };
453
+ }
454
+
455
+ // src/lib/config-store.ts
456
+ var paths = envPaths("dazzer", { suffix: "" });
457
+ var CONFIG_FILE = join(paths.config, "auth.json");
458
+ async function ensureConfigDir() {
459
+ await mkdir(paths.config, { recursive: true });
460
+ }
461
+ var MAX_CLIENT_ID_LENGTH = 512;
462
+ async function saveAuthMetadata(meta) {
463
+ const issuer = assertHttpsIssuer(meta.issuer);
464
+ const clientId = typeof meta.clientId === "string" ? meta.clientId.trim() : "";
465
+ if (clientId === "" || clientId.length > MAX_CLIENT_ID_LENGTH || /[\r\n]/.test(clientId)) {
466
+ throw new Error("The authorization server returned an unusable client id.");
467
+ }
468
+ const expiresAt = typeof meta.expiresAt === "number" && Number.isFinite(meta.expiresAt) ? meta.expiresAt : null;
469
+ const savedToStore = typeof meta.savedToStore === "boolean" ? meta.savedToStore : void 0;
470
+ await ensureConfigDir();
471
+ const checked = { issuer, clientId, expiresAt, savedToStore };
472
+ await writeFile(CONFIG_FILE, `${JSON.stringify(checked, null, 2)}
473
+ `, "utf8");
474
+ }
475
+ async function loadAuthMetadata() {
476
+ try {
477
+ const raw = await readFile(CONFIG_FILE, "utf8");
478
+ const parsed = JSON.parse(raw);
479
+ if (typeof parsed.issuer !== "string" || typeof parsed.clientId !== "string") {
480
+ return null;
481
+ }
482
+ const { savedToStore, ...rest } = parsed;
483
+ return typeof savedToStore === "boolean" ? { ...rest, savedToStore } : rest;
484
+ } catch {
485
+ return null;
486
+ }
487
+ }
488
+ async function clearAuthMetadata() {
489
+ try {
490
+ await unlink(CONFIG_FILE);
491
+ } catch (error) {
492
+ if (error.code !== "ENOENT") throw error;
493
+ }
494
+ }
495
+ function getConfigDir() {
496
+ return paths.config;
497
+ }
498
+
499
+ // src/lib/send.ts
500
+ var DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;
501
+ var DEFAULT_ATTEMPTS = 5;
502
+ var BLOCK_BYTES = 256 * 1024;
503
+ var RESUMABLE_START = "x-goog-resumable";
504
+ function connectionDroppedLine(arrived, total) {
505
+ const percent = total > 0 ? Math.floor(arrived / total * 100) : 0;
506
+ return `Connection dropped at ${percent}% \u2014 picking up where it stopped.`;
507
+ }
508
+ function pieceEnd(start, total, chunkBytes) {
509
+ const proposed = Math.min(start + chunkBytes, total);
510
+ if (proposed >= total) return total;
511
+ const aligned = Math.floor(proposed / BLOCK_BYTES) * BLOCK_BYTES;
512
+ return aligned > start ? aligned : proposed;
513
+ }
514
+ function rangeReached(res) {
515
+ const range = res.headers.get("range");
516
+ if (range === null || range === "") return 0;
517
+ const match = /bytes=0-(\d+)/i.exec(range);
518
+ if (match?.[1] === void 0) return null;
519
+ return Number(match[1]) + 1;
520
+ }
521
+ async function openSession(uploadUrl, contentType, fetchFn) {
522
+ let res;
523
+ try {
524
+ res = await fetchFn(uploadUrl, {
525
+ method: "POST",
526
+ headers: { [RESUMABLE_START]: "start", "content-type": contentType },
527
+ // Never followed. The store answers this exchange with a 3xx-shaped status of its own that
528
+ // is not a redirect, and following it would send the session somewhere nobody signed.
529
+ redirect: "manual"
530
+ });
531
+ } catch {
532
+ return { started: null, reached: false };
533
+ }
534
+ if (res.status !== 200 && res.status !== 201) return { started: null, reached: true };
535
+ const location = res.headers.get("location");
536
+ return location === null || location === "" ? { started: null, reached: true } : { started: location };
537
+ }
538
+ async function askWhatArrived(session, total, fetchFn) {
539
+ let res;
540
+ try {
541
+ res = await fetchFn(session, {
542
+ method: "PUT",
543
+ headers: { "content-range": `bytes */${total}` },
544
+ redirect: "manual"
545
+ });
546
+ } catch {
547
+ return null;
548
+ }
549
+ if (res.status === 200 || res.status === 201) return total;
550
+ if (res.status !== 308) return null;
551
+ return rangeReached(res);
552
+ }
553
+ async function sendFile(input) {
554
+ const fetchFn = input.fetchFn ?? fetch;
555
+ const chunkBytes = input.chunkBytes ?? DEFAULT_CHUNK_BYTES;
556
+ let attemptsLeft = input.attempts ?? DEFAULT_ATTEMPTS;
557
+ const session = await openSession(input.uploadUrl, input.contentType, fetchFn);
558
+ if (session.started === null) {
559
+ return { status: "not-started", reached: session.reached };
560
+ }
561
+ let arrived = 0;
562
+ while (arrived < input.byteSize) {
563
+ const end = pieceEnd(arrived, input.byteSize, chunkBytes);
564
+ const piece = await input.read(arrived, end - arrived);
565
+ let res;
566
+ try {
567
+ res = await fetchFn(session.started, {
568
+ method: "PUT",
569
+ headers: {
570
+ "content-type": input.contentType,
571
+ "content-range": `bytes ${arrived}-${end - 1}/${input.byteSize}`
572
+ },
573
+ body: piece,
574
+ redirect: "manual"
575
+ });
576
+ } catch {
577
+ attemptsLeft -= 1;
578
+ if (attemptsLeft <= 0) return { status: "incomplete", arrived };
579
+ const confirmed = await askWhatArrived(session.started, input.byteSize, fetchFn);
580
+ if (confirmed === null) return { status: "incomplete", arrived };
581
+ if (confirmed >= input.byteSize) return { status: "sent", arrived: input.byteSize };
582
+ input.onNote?.(connectionDroppedLine(confirmed, input.byteSize));
583
+ arrived = confirmed;
584
+ continue;
585
+ }
586
+ if (res.status === 200 || res.status === 201)
587
+ return { status: "sent", arrived: input.byteSize };
588
+ if (res.status !== 308) return { status: "incomplete", arrived };
589
+ const reached = rangeReached(res);
590
+ if (reached === null || reached <= arrived) return { status: "incomplete", arrived };
591
+ arrived = reached;
592
+ }
593
+ return { status: "sent", arrived };
594
+ }
595
+
596
+ // src/lib/token-store.ts
597
+ import { Entry } from "@napi-rs/keyring";
598
+ var SERVICE = "dazzer-oauth-cli";
599
+ var ACCESS_ACCOUNT = "access-token";
600
+ var REFRESH_ACCOUNT = "refresh-token";
601
+ var KeychainTokenStore = class {
602
+ constructor(openEntry = (account) => new Entry(SERVICE, account)) {
603
+ this.openEntry = openEntry;
604
+ }
605
+ openEntry;
606
+ async save(tokens) {
607
+ this.openEntry(ACCESS_ACCOUNT).setPassword(tokens.accessToken);
608
+ this.openEntry(REFRESH_ACCOUNT).setPassword(tokens.refreshToken);
609
+ }
610
+ async load() {
611
+ const accessToken = this.openEntry(ACCESS_ACCOUNT).getPassword();
612
+ const refreshToken = this.openEntry(REFRESH_ACCOUNT).getPassword();
613
+ if (!accessToken || !refreshToken) {
614
+ return null;
615
+ }
616
+ return { accessToken, refreshToken };
617
+ }
618
+ async loadRefreshToken() {
619
+ return this.openEntry(REFRESH_ACCOUNT).getPassword() || null;
620
+ }
621
+ /**
622
+ * NOTHING SAVED IS NOT AN ERROR, AND A FAILED REMOVAL IS NEVER HIDDEN. A delete of nothing answers
623
+ * `false` rather than throwing, so there is no missing-entry error to swallow. What the keychain
624
+ * library does throw is a removal the store refused. Swallowing that told a person they were signed
625
+ * out while their sign-in was still saved on the machine.
626
+ *
627
+ * THE LONG-LIVED HALF GOES FIRST, AND IF IT WILL NOT, NOTHING ELSE IS TOUCHED. Both then stay, so a
628
+ * second sign-out still finds a whole sign-in to cancel. Removing the access token anyway would leave
629
+ * only the refresh token — which can still renew a sign-in for weeks. Once the refresh token is gone,
630
+ * an access token left behind by a failed second removal expires within the hour.
631
+ */
632
+ async clear() {
633
+ this.openEntry(REFRESH_ACCOUNT).deletePassword();
634
+ this.openEntry(ACCESS_ACCOUNT).deletePassword();
635
+ }
636
+ };
637
+ function createTokenStore() {
638
+ return new KeychainTokenStore();
639
+ }
640
+
641
+ // src/lib/walk.ts
642
+ import { readdir, realpath, stat } from "node:fs/promises";
643
+ import { join as join2, sep } from "node:path";
644
+ function inside(base, candidate) {
645
+ return candidate === base || candidate.startsWith(base.endsWith(sep) ? base : `${base}${sep}`);
646
+ }
647
+ async function walkFolder(root) {
648
+ const base = await realpath(root);
649
+ if (!(await stat(base)).isDirectory()) {
650
+ throw new Error("walkFolder was given something that is not a folder");
651
+ }
652
+ const files = [];
653
+ const skipped = [];
654
+ const handedOver = /* @__PURE__ */ new Set();
655
+ const walked = /* @__PURE__ */ new Set([base]);
656
+ async function descend(dir, prefix, isRoot = false) {
657
+ let entries;
658
+ try {
659
+ entries = await readdir(dir, { withFileTypes: true });
660
+ } catch (err) {
661
+ if (isRoot) throw err;
662
+ skipped.push({ path: dir, relative: prefix.replace(/\/$/, ""), reason: "unreadable" });
663
+ return;
664
+ }
665
+ entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
666
+ for (const entry of entries) {
667
+ if (entry.name.startsWith(".")) continue;
668
+ const full = join2(dir, entry.name);
669
+ const relative = `${prefix}${entry.name}`;
670
+ if (entry.isSymbolicLink()) {
671
+ await followShortcut(full, relative);
672
+ continue;
673
+ }
674
+ if (entry.isDirectory()) {
675
+ await enter(full, relative);
676
+ continue;
677
+ }
678
+ if (entry.isFile()) take(full, relative);
679
+ }
680
+ }
681
+ async function enter(full, relative) {
682
+ let real;
683
+ try {
684
+ real = await realpath(full);
685
+ } catch {
686
+ skipped.push({ path: full, relative, reason: "unreadable" });
687
+ return;
688
+ }
689
+ if (walked.has(real)) return;
690
+ walked.add(real);
691
+ await descend(real, `${relative}/`);
692
+ }
693
+ function take(full, relative, real) {
694
+ const identity = real ?? full;
695
+ if (handedOver.has(identity)) return;
696
+ handedOver.add(identity);
697
+ files.push({ path: full, relative });
698
+ }
699
+ async function followShortcut(full, relative) {
700
+ let real;
701
+ try {
702
+ real = await realpath(full);
703
+ } catch {
704
+ skipped.push({ path: full, relative, reason: "unreadable" });
705
+ return;
706
+ }
707
+ if (!inside(base, real)) {
708
+ skipped.push({ path: full, relative, reason: "leads-out" });
709
+ return;
710
+ }
711
+ let kind;
712
+ try {
713
+ kind = await stat(real);
714
+ } catch {
715
+ skipped.push({ path: full, relative, reason: "unreadable" });
716
+ return;
717
+ }
718
+ if (kind.isDirectory()) {
719
+ if (walked.has(real)) return;
720
+ walked.add(real);
721
+ await descend(real, `${relative}/`);
722
+ return;
723
+ }
724
+ if (kind.isFile()) take(real, relative, real);
725
+ }
726
+ await descend(base, "", true);
727
+ files.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
728
+ skipped.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
729
+ return { files, skipped };
730
+ }
731
+
732
+ // src/commands/add.ts
733
+ var SPACE_PATH = "/documents/v1/space";
734
+ var LANDING_PATH = "/documents/v1/landing";
735
+ var LANDED_PATH = "/documents/v1/landed";
736
+ var cannotOpen = (name) => `Couldn't open ${name} \u2014 check its permissions.`;
737
+ var SLOWING_DOWN = "Slowing down to stay within limits \u2014 still going.";
738
+ var ALREADY_IN = "That one's already in \u2014 nothing to do.";
739
+ var waitingOnYou = (many) => `${many} waiting on your answer.`;
740
+ var collectedQuestion = (names, space) => `${names.length} of those look like newer versions of documents already in ${space} \u2014 ${listed(names)}. Treat all ${inWords(names.length)} as newer versions, keep all ${inWords(names.length)} as separate documents, or go through them one at a time?`;
741
+ function listed(names) {
742
+ if (names.length <= 1) return names[0] ?? "";
743
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
744
+ }
745
+ var NUMBER_WORDS = [
746
+ "",
747
+ "one",
748
+ "two",
749
+ "three",
750
+ "four",
751
+ "five",
752
+ "six",
753
+ "seven",
754
+ "eight",
755
+ "nine",
756
+ "ten"
757
+ ];
758
+ var inWords = (many) => NUMBER_WORDS[many] ?? String(many);
759
+ var cannotFind = (name) => `Couldn't find ${name}.`;
760
+ var didNotFinish = (name) => `The send didn't finish \u2014 ${name} is not in Dazzer. Try again shortly.`;
761
+ var wentWrong = (name) => `Something went wrong and ${name} is not in Dazzer. Try again shortly.`;
762
+ var choiceLine = (space, scopeId) => ` ${asPrinted(space)} \u2014 ${scopeId}`;
763
+ var answerWith = (keepToYourself, numbered) => {
764
+ const ways = [
765
+ '--space "<name>"',
766
+ ...numbered ? ["--space <number>"] : [],
767
+ ...keepToYourself ? ["--keep-to-myself"] : []
768
+ ];
769
+ const last = ways[ways.length - 1] ?? "";
770
+ const said = ways.length === 1 ? last : `${ways.slice(0, -1).join(", ")}, or ${last}`;
771
+ return `Run this again with ${said}.`;
772
+ };
773
+ var DECLARED_BY_EXTENSION = {
774
+ ".csv": "text/csv",
775
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
776
+ ".gif": "image/gif",
777
+ ".htm": "text/html",
778
+ ".html": "text/html",
779
+ ".jpeg": "image/jpeg",
780
+ ".jpg": "image/jpeg",
781
+ ".markdown": "text/markdown",
782
+ ".md": "text/markdown",
783
+ ".pdf": "application/pdf",
784
+ ".png": "image/png",
785
+ ".rtf": "application/rtf",
786
+ ".text": "text/plain",
787
+ ".txt": "text/plain",
788
+ ".webp": "image/webp"
789
+ };
790
+ var UNDECLARED = "application/octet-stream";
791
+ function asPrinted(name) {
792
+ return name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").trim();
793
+ }
794
+ function readEnvelope(body) {
795
+ if (typeof body !== "object" || body === null) return null;
796
+ const data = body.data;
797
+ return typeof data === "object" && data !== null ? data : null;
798
+ }
799
+ function readChoices(data) {
800
+ return readOfferedPlaces(data.choices);
801
+ }
802
+ function readLanding(body) {
803
+ const data = readEnvelope(body);
804
+ if (data === null) return null;
805
+ const { ticket, upload_url: uploadUrl, content_type: contentType, space_name: space } = data;
806
+ if (typeof ticket !== "string" || ticket === "") return null;
807
+ if (typeof uploadUrl !== "string" || uploadUrl === "") return null;
808
+ if (typeof contentType !== "string" || contentType === "") return null;
809
+ return {
810
+ ticket,
811
+ upload_url: uploadUrl,
812
+ content_type: contentType,
813
+ space_name: typeof space === "string" && space.trim() !== "" ? space : null
814
+ };
815
+ }
816
+ function named(space) {
817
+ return space === null ? "that space" : space;
818
+ }
819
+ async function resolveAuth(options) {
820
+ const tokenStore = createTokenStore();
821
+ const meta = await loadAuthMetadata();
822
+ const issuer = meta?.issuer ?? options.issuer;
823
+ try {
824
+ const discovery = await discoverEndpoints(issuer);
825
+ return {
826
+ discovery,
827
+ clientId: meta?.clientId ?? options.clientId,
828
+ tokenStore,
829
+ resource: options.resource
830
+ };
831
+ } catch {
832
+ return null;
833
+ }
834
+ }
835
+ var FIRST_WAIT_MS = 1e3;
836
+ var LONGEST_WAIT_MS = 3e4;
837
+ var PACED_ATTEMPTS = 6;
838
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
839
+ function voiceFor(write) {
840
+ let said = false;
841
+ return {
842
+ write,
843
+ slowingDown: () => {
844
+ if (said) return;
845
+ said = true;
846
+ write(SLOWING_DOWN);
847
+ }
848
+ };
849
+ }
850
+ async function callPaced(request, deps, voice) {
851
+ const wait = deps.delay ?? sleep;
852
+ let waiting = FIRST_WAIT_MS;
853
+ for (let attempt = 1; ; attempt += 1) {
854
+ const answer = await callDazzer(request);
855
+ if (answer.status !== "refused" || answer.httpStatus !== 429) return answer;
856
+ if (attempt >= PACED_ATTEMPTS) return answer;
857
+ voice.slowingDown();
858
+ await wait(waiting);
859
+ waiting = Math.min(waiting * 2, LONGEST_WAIT_MS);
860
+ }
861
+ }
862
+ function askedWith(options) {
863
+ if (options.keepToMyself === true) return { keep_to_myself: true };
864
+ const answer = options.space?.trim();
865
+ if (answer === void 0 || answer === "") return {};
866
+ return { space: /^\d+$/.test(answer) ? Number(answer) : answer };
867
+ }
868
+ async function settleSpace(options, auth2, deps, voice, name) {
869
+ const answer = await callPaced(
870
+ {
871
+ baseUrl: options.baseUrl,
872
+ path: SPACE_PATH,
873
+ method: "POST",
874
+ body: askedWith(options),
875
+ auth: auth2,
876
+ fetchFn: deps.fetchFn
877
+ },
878
+ deps,
879
+ voice
880
+ );
881
+ if (answer.status !== "ok") {
882
+ say(voice.write, answer, name);
883
+ return null;
884
+ }
885
+ const data = readEnvelope(answer.body);
886
+ if (data === null) {
887
+ voice.write(wentWrong(name));
888
+ return null;
889
+ }
890
+ const space = typeof data.space === "string" && data.space.trim() !== "" ? data.space : null;
891
+ if (data.settled === true) {
892
+ return { scopeId: typeof data.scope_id === "number" ? data.scope_id : null, space };
893
+ }
894
+ const question = typeof data.question === "string" && data.question !== "" ? data.question : null;
895
+ if (question === null) {
896
+ voice.write(wentWrong(name));
897
+ return null;
898
+ }
899
+ voice.write(question);
900
+ const choices = readChoices(data);
901
+ for (const choice of choices) voice.write(choiceLine(choice.space, choice.scopeId));
902
+ voice.write(answerWith(data.keep_to_yourself === true, choices.length > 0));
903
+ return null;
904
+ }
905
+ var HELD_FOR_AN_ANSWER = "same_name_different_contents";
906
+ function fromAnswer(answer) {
907
+ if (answer.status === "refused") {
908
+ if (answer.code === HELD_FOR_AN_ANSWER && answer.detail !== void 0) {
909
+ return { kind: "held", question: answer.detail };
910
+ }
911
+ return {
912
+ kind: "refused",
913
+ ...answer.detail === void 0 ? {} : { said: answer.detail },
914
+ ...answer.code === void 0 ? {} : { code: answer.code },
915
+ // Carried, not filled in: absent stays absent, so nothing downstream can print an offer the
916
+ // service did not make.
917
+ ...answer.choices === void 0 ? {} : { choices: answer.choices },
918
+ ...answer.keepToYourself === void 0 ? {} : { keepToYourself: answer.keepToYourself }
919
+ };
920
+ }
921
+ return { kind: "stop", answer };
922
+ }
923
+ async function sendOne(input) {
924
+ const { deps, handle, options, voice } = input;
925
+ const described = await callPaced(
926
+ {
927
+ baseUrl: options.baseUrl,
928
+ path: LANDING_PATH,
929
+ method: "POST",
930
+ body: {
931
+ file_name: basename(input.path),
932
+ byte_size: input.byteSize,
933
+ mime_type: DECLARED_BY_EXTENSION[extname(input.path).toLowerCase()] ?? UNDECLARED,
934
+ ...input.scopeId === null ? {} : { scope_id: input.scopeId }
935
+ },
936
+ auth: input.auth,
937
+ fetchFn: deps.fetchFn
938
+ },
939
+ deps,
940
+ voice
941
+ );
942
+ if (described.status !== "ok") return fromAnswer(described);
943
+ const landing = readLanding(described.body);
944
+ if (landing === null) return { kind: "went-wrong" };
945
+ let couldNotRead = false;
946
+ let sent;
947
+ try {
948
+ sent = await sendFile({
949
+ uploadUrl: landing.upload_url,
950
+ contentType: landing.content_type,
951
+ byteSize: input.byteSize,
952
+ read: async (start, length) => {
953
+ try {
954
+ const buffer = Buffer.allocUnsafe(length);
955
+ let filled = 0;
956
+ while (filled < length) {
957
+ const { bytesRead } = await handle.read(
958
+ buffer,
959
+ filled,
960
+ length - filled,
961
+ start + filled
962
+ );
963
+ if (bytesRead === 0) break;
964
+ filled += bytesRead;
965
+ }
966
+ return filled === length ? buffer : buffer.subarray(0, filled);
967
+ } catch (err) {
968
+ couldNotRead = true;
969
+ throw err;
970
+ }
971
+ },
972
+ fetchFn: deps.uploadFetchFn,
973
+ chunkBytes: deps.chunkBytes,
974
+ attempts: deps.attempts,
975
+ onNote: voice.write
976
+ });
977
+ } catch (err) {
978
+ if (couldNotRead) return { kind: "cannot-read" };
979
+ throw err;
980
+ }
981
+ if (sent.status !== "sent") {
982
+ return sent.status === "not-started" && !sent.reached ? { kind: "not-reached" } : { kind: "did-not-finish" };
983
+ }
984
+ const landed = await callPaced(
985
+ {
986
+ baseUrl: options.baseUrl,
987
+ path: LANDED_PATH,
988
+ method: "POST",
989
+ body: { ticket: landing.ticket },
990
+ auth: input.auth,
991
+ fetchFn: deps.fetchFn
992
+ },
993
+ deps,
994
+ voice
995
+ );
996
+ if (landed.status !== "ok") return fromAnswer(landed);
997
+ const stored = readEnvelope(landed.body);
998
+ if (stored === null) return { kind: "went-wrong" };
999
+ if (stored.already_here === true) return { kind: "already-here" };
1000
+ return {
1001
+ kind: "added",
1002
+ fileName: typeof stored.file_name === "string" ? asPrinted(stored.file_name) : asPrinted(basename(input.path)),
1003
+ space: typeof stored.space_name === "string" ? stored.space_name : null
1004
+ };
1005
+ }
1006
+ async function sendOneSafely(input) {
1007
+ try {
1008
+ return await sendOne(input);
1009
+ } catch {
1010
+ return { kind: "went-wrong" };
1011
+ }
1012
+ }
1013
+ async function openOne(path) {
1014
+ try {
1015
+ return await open(path, "r");
1016
+ } catch (err) {
1017
+ return err?.code === "ENOENT" ? "missing" : "cannot-open";
1018
+ }
1019
+ }
1020
+ var REASON_ORDER = [
1021
+ "type-not-readable",
1022
+ "too-large",
1023
+ "already-here",
1024
+ "refused",
1025
+ "cannot-open",
1026
+ "did-not-finish",
1027
+ "leads-out"
1028
+ ];
1029
+ var FROM_THE_SERVICE = {
1030
+ type_not_readable: "type-not-readable",
1031
+ too_large: "too-large"
1032
+ };
1033
+ var COUNTED = {
1034
+ "type-not-readable": (many) => `${many} file type${many === 1 ? "" : "s"} I can't read`,
1035
+ "too-large": (many) => `${many} too large`,
1036
+ "already-here": (many) => `${many} already there`,
1037
+ refused: (many) => `${many} refused`,
1038
+ "cannot-open": (many) => `${many} I couldn't open`,
1039
+ "did-not-finish": (many) => `${many} that didn't finish`,
1040
+ "leads-out": (many) => `${many} shortcut${many === 1 ? "" : "s"} out of the folder`
1041
+ };
1042
+ var IN_DETAIL = {
1043
+ "type-not-readable": "a file type I can't read",
1044
+ "too-large": "too large",
1045
+ "already-here": "already there",
1046
+ refused: "refused",
1047
+ "cannot-open": "I couldn't open it",
1048
+ "did-not-finish": "the send didn't finish",
1049
+ "leads-out": "a shortcut out of the folder"
1050
+ };
1051
+ function countLine(added, space, skipped, why) {
1052
+ const documents = `${added} document${added === 1 ? "" : "s"} in ${named(space)}.`;
1053
+ if (skipped.length === 0) return documents;
1054
+ const reasons = REASON_ORDER.flatMap((reason) => {
1055
+ const many = skipped.filter((skip) => skip.reason === reason).length;
1056
+ return many === 0 ? [] : [COUNTED[reason](many)];
1057
+ }).join(", ");
1058
+ return `${documents} ${skipped.length} skipped \u2014 ${reasons}.${why ? "" : " Run with --why to see each."}`;
1059
+ }
1060
+ function whyLine(skip) {
1061
+ const because = skip.said === void 0 ? IN_DETAIL[skip.reason] : skip.reason === "refused" ? `refused: ${skip.said}` : skip.said;
1062
+ return `${asPrinted(skip.relative)} \u2014 ${because}`;
1063
+ }
1064
+ var FROM_THE_WALK = {
1065
+ "leads-out": "leads-out",
1066
+ unreadable: "cannot-open"
1067
+ };
1068
+ async function runAdd(options, deps = {}) {
1069
+ const voice = voiceFor(deps.write ?? ((line) => process.stdout.write(`${line}
1070
+ `)));
1071
+ const name = asPrinted(basename(options.file));
1072
+ let isFolder;
1073
+ try {
1074
+ isFolder = (await statPath(options.file)).isDirectory();
1075
+ } catch (err) {
1076
+ voice.write(
1077
+ err?.code === "ENOENT" ? cannotFind(name) : cannotOpen(name)
1078
+ );
1079
+ return { ok: false };
1080
+ }
1081
+ return isFolder ? await addFolder(options, deps, voice) : await addOneFile(options, deps, voice);
1082
+ }
1083
+ async function addOneFile(options, deps, voice) {
1084
+ const name = asPrinted(basename(options.file));
1085
+ const handle = await openOne(options.file);
1086
+ if (typeof handle === "string") {
1087
+ voice.write(handle === "missing" ? cannotFind(name) : cannotOpen(name));
1088
+ return { ok: false };
1089
+ }
1090
+ try {
1091
+ const byteSize = (await handle.stat()).size;
1092
+ const auth2 = deps.auth ?? await resolveAuth(options);
1093
+ if (auth2 === null) {
1094
+ voice.write(CANNOT_REACH_DAZZER);
1095
+ return { ok: false };
1096
+ }
1097
+ const settled = await settleSpace(options, auth2, deps, voice, name);
1098
+ if (settled === null) return { ok: false };
1099
+ const outcome = await sendOneSafely({
1100
+ path: options.file,
1101
+ handle,
1102
+ byteSize,
1103
+ scopeId: settled.scopeId,
1104
+ options,
1105
+ deps,
1106
+ auth: auth2,
1107
+ voice
1108
+ });
1109
+ switch (outcome.kind) {
1110
+ case "added":
1111
+ voice.write(`${outcome.fileName} is in Dazzer, in ${named(outcome.space)}.`);
1112
+ return { ok: true };
1113
+ case "already-here":
1114
+ voice.write(ALREADY_IN);
1115
+ return { ok: true };
1116
+ // ONE FILE, ONE QUESTION, IN THE SERVICE'S OWN WORDS. It is the only thing that knows what it
1117
+ // already holds and what the space is called, and rewriting its question here would be a
1118
+ // second wording of a question somebody approved. Nothing was added and nobody has answered,
1119
+ // so this run did not do what it was asked to — the same answer a run that asked where things
1120
+ // go gives.
1121
+ case "held":
1122
+ voice.write(outcome.question);
1123
+ return { ok: false };
1124
+ // ONE FILE, ONE REFUSAL, AND THE PLACES IT OFFERED. The sentence is the service's and is
1125
+ // printed as sent; the places under it are the service's too, and without them a refusal
1126
+ // that says to name one of them is a dead end for the person holding this one file.
1127
+ case "refused":
1128
+ voice.write(outcome.said ?? wentWrong(name));
1129
+ offerThePlaces(voice.write, outcome);
1130
+ return { ok: false };
1131
+ case "did-not-finish":
1132
+ voice.write(didNotFinish(name));
1133
+ return { ok: false };
1134
+ case "not-reached":
1135
+ voice.write(CANNOT_REACH_DAZZER);
1136
+ return { ok: false };
1137
+ case "went-wrong":
1138
+ voice.write(wentWrong(name));
1139
+ return { ok: false };
1140
+ // IT OPENED AND WOULD NOT READ — answered in the approved sentence for a file that cannot be
1141
+ // opened, because that is the same thing a person can act on, and never as a crash.
1142
+ case "cannot-read":
1143
+ voice.write(cannotOpen(name));
1144
+ return { ok: false };
1145
+ case "stop":
1146
+ say(voice.write, outcome.answer, name);
1147
+ return { ok: false };
1148
+ }
1149
+ } finally {
1150
+ await handle.close();
1151
+ }
1152
+ }
1153
+ async function addFolder(options, deps, voice) {
1154
+ const name = asPrinted(basename(options.file));
1155
+ const walked = await (async () => {
1156
+ try {
1157
+ return await walkFolder(options.file);
1158
+ } catch {
1159
+ return null;
1160
+ }
1161
+ })();
1162
+ if (walked === null) {
1163
+ voice.write(cannotOpen(name));
1164
+ return { ok: false };
1165
+ }
1166
+ const auth2 = deps.auth ?? await resolveAuth(options);
1167
+ if (auth2 === null) {
1168
+ voice.write(CANNOT_REACH_DAZZER);
1169
+ return { ok: false };
1170
+ }
1171
+ const settled = await settleSpace(options, auth2, deps, voice, name);
1172
+ if (settled === null) return { ok: false };
1173
+ const skipped = walked.skipped.map((skip) => ({
1174
+ relative: skip.relative,
1175
+ reason: FROM_THE_WALK[skip.reason]
1176
+ }));
1177
+ const held = [];
1178
+ let added = 0;
1179
+ let ran = true;
1180
+ for (const file of walked.files) {
1181
+ const handle = await openOne(file.path);
1182
+ if (typeof handle === "string") {
1183
+ skipped.push({ relative: file.relative, reason: "cannot-open" });
1184
+ continue;
1185
+ }
1186
+ let outcome;
1187
+ try {
1188
+ outcome = await sendOneSafely({
1189
+ path: file.path,
1190
+ handle,
1191
+ byteSize: (await handle.stat()).size,
1192
+ scopeId: settled.scopeId,
1193
+ options,
1194
+ deps,
1195
+ auth: auth2,
1196
+ voice
1197
+ });
1198
+ } finally {
1199
+ await handle.close();
1200
+ }
1201
+ if (outcome.kind === "added") {
1202
+ added += 1;
1203
+ continue;
1204
+ }
1205
+ if (outcome.kind === "already-here") {
1206
+ skipped.push({ relative: file.relative, reason: "already-here" });
1207
+ continue;
1208
+ }
1209
+ if (outcome.kind === "held") {
1210
+ held.push({
1211
+ relative: file.relative,
1212
+ name: basename(file.path),
1213
+ question: outcome.question
1214
+ });
1215
+ continue;
1216
+ }
1217
+ if (outcome.kind === "refused") {
1218
+ skipped.push({
1219
+ relative: file.relative,
1220
+ // KEYED ON THE SERVICE'S CODE, never on the words it sent: the words are the person's and
1221
+ // may be reworded, the code is the label the service publishes.
1222
+ reason: (outcome.code === void 0 ? void 0 : FROM_THE_SERVICE[outcome.code]) ?? "refused",
1223
+ ...outcome.said === void 0 ? {} : { said: outcome.said }
1224
+ });
1225
+ continue;
1226
+ }
1227
+ if (outcome.kind === "cannot-read") {
1228
+ skipped.push({ relative: file.relative, reason: "cannot-open" });
1229
+ continue;
1230
+ }
1231
+ if (outcome.kind === "did-not-finish" || outcome.kind === "went-wrong") {
1232
+ skipped.push({ relative: file.relative, reason: "did-not-finish" });
1233
+ continue;
1234
+ }
1235
+ if (outcome.kind === "not-reached") voice.write(CANNOT_REACH_DAZZER);
1236
+ else say(voice.write, outcome.answer, name);
1237
+ ran = false;
1238
+ break;
1239
+ }
1240
+ skipped.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
1241
+ held.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
1242
+ voice.write(countLine(added, settled.space, skipped, options.why === true));
1243
+ if (held.length > 0) voice.write(waitingOnYou(held.length));
1244
+ if (options.why === true) for (const skip of skipped) voice.write(whyLine(skip));
1245
+ const onlyOne = held.length === 1 ? held[0] : void 0;
1246
+ if (onlyOne !== void 0) {
1247
+ voice.write(onlyOne.question);
1248
+ } else if (held.length > 1) {
1249
+ voice.write(
1250
+ collectedQuestion(
1251
+ held.map((one) => asPrinted(one.name)),
1252
+ named(settled.space)
1253
+ )
1254
+ );
1255
+ }
1256
+ return { ok: ran };
1257
+ }
1258
+ function offerThePlaces(write, offer) {
1259
+ const places = offer.choices ?? [];
1260
+ const keepToYourself = offer.keepToYourself === true;
1261
+ if (places.length === 0 && !keepToYourself) return;
1262
+ for (const place of places) write(choiceLine(place.space, place.scopeId));
1263
+ write(answerWith(keepToYourself, places.length > 0));
1264
+ }
1265
+ function say(write, answer, name) {
1266
+ switch (answer.status) {
1267
+ case "refused":
1268
+ write(answer.detail ?? wentWrong(name));
1269
+ offerThePlaces(write, answer);
1270
+ return;
1271
+ case "sign-in-invalid":
1272
+ case "unreachable":
1273
+ write(answer.message);
1274
+ return;
1275
+ case "signed-out":
1276
+ printAuthStatus({ state: "signed-out" });
1277
+ return;
1278
+ case "store-unreadable":
1279
+ printAuthStatus({ state: "store-unreadable" });
1280
+ return;
1281
+ case "ok":
1282
+ return;
1283
+ }
1284
+ }
1285
+
1286
+ // src/lib/device-flow.ts
1287
+ var DEFAULT_SCOPE = "openid offline_access";
1288
+ function parseDeviceResponse(body) {
1289
+ const deviceCode = body.device_code;
1290
+ const userCode = body.user_code;
1291
+ const verificationUri = body.verification_uri;
1292
+ const expiresIn = body.expires_in;
1293
+ if (typeof deviceCode !== "string" || typeof userCode !== "string" || typeof verificationUri !== "string" || typeof expiresIn !== "number") {
1294
+ throw new Error("The authorization server returned an unexpected device response.");
1295
+ }
1296
+ return {
1297
+ device_code: deviceCode,
1298
+ user_code: userCode,
1299
+ verification_uri: verificationUri,
1300
+ verification_uri_complete: typeof body.verification_uri_complete === "string" ? body.verification_uri_complete : void 0,
1301
+ expires_in: expiresIn,
1302
+ interval: typeof body.interval === "number" ? body.interval : void 0
1303
+ };
1304
+ }
1305
+ async function requestDeviceCode(options) {
1306
+ const fetchFn = options.fetchFn ?? fetch;
1307
+ const endpoint = options.discovery.deviceAuthorizationEndpoint;
1308
+ if (!endpoint) {
1309
+ throw new Error("This authorization server does not support device sign-in.");
1310
+ }
1311
+ const params = new URLSearchParams({
1312
+ client_id: options.clientId,
1313
+ scope: options.scope ?? DEFAULT_SCOPE
1314
+ });
1315
+ if (options.resource) {
1316
+ params.set("resource", options.resource);
1317
+ }
1318
+ const res = await fetchFn(endpoint, {
1319
+ method: "POST",
1320
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1321
+ body: params.toString()
1322
+ });
1323
+ if (!res.ok) {
1324
+ throw new Error(`Device sign-in request failed (${res.status}).`);
1325
+ }
1326
+ return parseDeviceResponse(await res.json());
1327
+ }
1328
+ async function pollForTokens(options) {
1329
+ const fetchFn = options.fetchFn ?? fetch;
1330
+ const sleep2 = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1331
+ const baseIntervalMs = (options.deviceAuth.interval ?? 5) * 1e3;
1332
+ let intervalMs = options.pollIntervalMs ?? baseIntervalMs;
1333
+ const deadline = Date.now() + (options.maxWaitMs ?? options.deviceAuth.expires_in * 1e3);
1334
+ while (Date.now() < deadline) {
1335
+ await sleep2(intervalMs);
1336
+ const params = new URLSearchParams({
1337
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
1338
+ device_code: options.deviceCode,
1339
+ client_id: options.clientId
1340
+ });
1341
+ if (options.resource) {
1342
+ params.set("resource", options.resource);
1343
+ }
1344
+ const res = await fetchFn(options.discovery.tokenEndpoint, {
1345
+ method: "POST",
1346
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1347
+ body: params.toString()
1348
+ });
1349
+ const body = await res.json();
1350
+ if (res.ok) {
1351
+ const accessToken = body.access_token;
1352
+ if (typeof accessToken !== "string") {
1353
+ throw new Error("The authorization server returned an unexpected token response.");
1354
+ }
1355
+ return {
1356
+ access_token: accessToken,
1357
+ refresh_token: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
1358
+ expires_in: typeof body.expires_in === "number" ? body.expires_in : void 0,
1359
+ token_type: typeof body.token_type === "string" ? body.token_type : void 0
1360
+ };
1361
+ }
1362
+ const error = typeof body.error === "string" ? body.error : "unknown_error";
1363
+ if (error === "authorization_pending") {
1364
+ continue;
1365
+ }
1366
+ if (error === "slow_down") {
1367
+ intervalMs += 5e3;
1368
+ continue;
1369
+ }
1370
+ if (error === "access_denied") {
1371
+ throw new Error("Sign-in was denied.");
1372
+ }
1373
+ if (error === "expired_token") {
1374
+ throw new Error("The device code expired. Run auth login again.");
1375
+ }
1376
+ throw new Error(`Token request failed: ${error}`);
1377
+ }
1378
+ throw new Error("Timed out waiting for sign-in. Run auth login again.");
1379
+ }
1380
+
1381
+ // src/lib/loopback-flow.ts
1382
+ import { spawn } from "node:child_process";
1383
+ import { createHash, randomBytes } from "node:crypto";
1384
+ import { createServer } from "node:http";
1385
+
1386
+ // ../auth-ui/dist/html.js
1387
+ function escapeHtml(value) {
1388
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
1389
+ }
1390
+
1391
+ // ../auth-ui/dist/theme.js
1392
+ var TOKENS = {
1393
+ orange: "#ff5000",
1394
+ orangeMid: "#ee7d4e",
1395
+ lavender: "#dae2ff",
1396
+ skyMid: "#d2e7f3",
1397
+ ink: "#000000",
1398
+ inkSoft: "#0d0d0d",
1399
+ muted: "#595959",
1400
+ line: "#e6e1da",
1401
+ error: "#8a2a00",
1402
+ gbtnBorder: "#DADCE0",
1403
+ gbtnText: "#3C4043"
1404
+ };
1405
+ var baseCss = `
1406
+ :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;}
1407
+ *{box-sizing:border-box;margin:0;padding:0;}html,body{height:100%;}
1408
+ body{font-family:var(--mono);color:var(--ink);
1409
+ background:linear-gradient(180deg,${TOKENS.orange} 0%,${TOKENS.orange} 12.98%,${TOKENS.skyMid} 69.23%,${TOKENS.lavender} 100%);
1410
+ -webkit-font-smoothing:antialiased;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:32px 20px;}
1411
+ .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;
1412
+ -webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);
1413
+ padding:40px 36px 30px;box-shadow:0 12px 40px rgba(0,0,0,.05),inset 0 1px 1px rgba(255,255,255,.35);}
1414
+ .mark{width:40px;height:40px;margin-bottom:22px;border-radius:22px;display:flex;align-items:center;justify-content:center;}
1415
+ .mark svg{display:block;width:100%;height:100%;}
1416
+ .icon{width:46px;height:46px;margin:0 auto 18px;display:block;}
1417
+ h1{font-family:var(--serif);font-weight:600;font-size:21px;letter-spacing:-.015em;margin-bottom:7px;}
1418
+ h1 em{font-weight:600;font-style:italic;}
1419
+ .sub{font-size:14px;line-height:1.5;color:var(--muted);margin-bottom:24px;}
1420
+ .sub strong{color:var(--ink);font-weight:600;}
1421
+ p{font-size:14px;line-height:1.55;color:var(--muted);}
1422
+ .perms-label{font-size:13px;font-weight:600;color:var(--ink);margin-bottom:10px;}
1423
+ .perms{list-style:none;margin:0 0 24px;padding:16px;background:#fff;border:1px solid var(--line);border-radius:10px;display:grid;gap:11px;}
1424
+ .perms li{position:relative;padding-left:18px;font-size:13.5px;color:var(--muted);line-height:1.35;}
1425
+ .perms li::before{content:"";position:absolute;left:1px;top:7px;width:6px;height:6px;border-radius:50%;background:var(--orange);}
1426
+ .actions{display:flex;flex-direction:column;gap:10px;margin-bottom:4px;}
1427
+ .btn{width:100%;height:44px;border-radius:6px;font-family:var(--mono);font-weight:600;font-size:14px;cursor:pointer;
1428
+ display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease,box-shadow .15s ease,border-color .15s ease;}
1429
+ .btn:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
1430
+ .btn-primary{border:0;background:var(--orange);color:#fff;}
1431
+ .btn-primary:hover{background:#e64800;box-shadow:0 1px 3px rgba(0,0,0,.12);}
1432
+ .btn-secondary{border:1px solid var(--line);background:#fff;color:var(--ink);}
1433
+ .btn-secondary:hover{background:#fafafa;border-color:#d4cfc8;}
1434
+ .gbtn{width:100%;height:44px;border:1px solid var(--gbtn-border);border-radius:6px;background:#fff;color:var(--gbtn-text);
1435
+ font-family:'Roboto',-apple-system,sans-serif;font-weight:500;font-size:14px;cursor:pointer;
1436
+ display:inline-flex;align-items:center;justify-content:center;gap:12px;transition:background .15s ease,box-shadow .15s ease,border-color .15s ease;}
1437
+ .gbtn:hover{background:#F8FAFB;box-shadow:0 1px 3px rgba(0,0,0,.12);border-color:#C9CDD2;}
1438
+ .gbtn:disabled{opacity:.65;cursor:default;box-shadow:none;background:#fff;}
1439
+ .gbtn:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
1440
+ .gbtn svg{display:block;}
1441
+ .err{margin-top:14px;font-size:13px;color:var(--error);min-height:16px;}
1442
+ .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);}
1443
+ .trust svg,.foot svg{flex:none;}
1444
+ .code-input{width:100%;height:44px;margin:16px 0;padding:0 14px;border:1px solid var(--line);border-radius:6px;background:#fff;
1445
+ font-family:var(--mono);font-size:18px;letter-spacing:.08em;text-align:center;color:var(--ink);}
1446
+ .code-input:focus{outline:2px solid var(--ink);outline-offset:0;border-color:var(--ink);}
1447
+ .msg{margin-bottom:16px;font-size:14px;line-height:1.5;color:var(--muted);}
1448
+ .msg strong{color:var(--ink);font-weight:600;}
1449
+ `;
1450
+ var fontLinks = `<link rel="preconnect" href="https://fonts.googleapis.com">
1451
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1452
+ <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">`;
1453
+
1454
+ // ../auth-ui/dist/marks.js
1455
+ 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>`;
1456
+ 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>`;
1457
+ 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>`;
1458
+ 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>`;
1459
+
1460
+ // ../auth-ui/dist/layout.js
1461
+ var faviconHref = `data:image/svg+xml,${encodeURIComponent(brandMark)}`;
1462
+ function pageShell(innerHtml) {
1463
+ return `<main class="card">${innerHtml}</main>`;
1464
+ }
1465
+ function renderDocument(params) {
1466
+ const { title, body, headExtra, centered } = params;
1467
+ const centerStyle = centered ? "body{text-align:center;}" : "";
1468
+ return `<!DOCTYPE html>
1469
+ <html lang="en">
1470
+ <head>
1471
+ <meta charset="utf-8">
1472
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1473
+ <meta name="theme-color" content="${TOKENS.orange}">
1474
+ <title>${title}</title>
1475
+ <link rel="icon" type="image/svg+xml" href="${faviconHref}">
1476
+ ${fontLinks}
1477
+ <style>${baseCss}${centerStyle}${headExtra ?? ""}</style>
1478
+ </head>
1479
+ <body>
1480
+ ${body}
1481
+ </body>
1482
+ </html>`;
1483
+ }
1484
+
1485
+ // src/lib/loopback-flow.ts
1486
+ var DEFAULT_SCOPE2 = "openid offline_access brain";
1487
+ var CALLBACK_PATH = "/callback";
1488
+ var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
1489
+ function base64url(buf) {
1490
+ return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
1491
+ }
1492
+ function createPkce() {
1493
+ const verifier = base64url(randomBytes(32));
1494
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
1495
+ return { verifier, challenge };
1496
+ }
1497
+ function launchBrowser(url) {
1498
+ const platform = process.platform;
1499
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
1500
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
1501
+ try {
1502
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
1503
+ child.on("error", () => {
1504
+ });
1505
+ child.unref();
1506
+ } catch {
1507
+ }
1508
+ }
1509
+ function buildAuthorizeUrl(endpoint, params) {
1510
+ const url = new URL(endpoint);
1511
+ url.searchParams.set("response_type", "code");
1512
+ url.searchParams.set("client_id", params.clientId);
1513
+ url.searchParams.set("redirect_uri", params.redirectUri);
1514
+ url.searchParams.set("scope", params.scope);
1515
+ url.searchParams.set("state", params.state);
1516
+ url.searchParams.set("code_challenge", params.challenge);
1517
+ url.searchParams.set("code_challenge_method", "S256");
1518
+ if (params.resource) {
1519
+ url.searchParams.set("resource", params.resource);
1520
+ }
1521
+ return url.toString();
1522
+ }
1523
+ function statusPage(title, heading, body, mark) {
1524
+ return renderDocument({
1525
+ title,
1526
+ centered: true,
1527
+ body: pageShell(`${mark}<h1>${heading}</h1><p>${body}</p>
1528
+ <div class="foot">Dazzer secure sign-in</div>`)
1529
+ });
1530
+ }
1531
+ function successPage() {
1532
+ return statusPage(
1533
+ "Signed in to Dazzer",
1534
+ "You're signed in",
1535
+ "You can close this tab and return to your terminal.",
1536
+ successMark
1537
+ );
1538
+ }
1539
+ function errorPage(reason) {
1540
+ return statusPage(
1541
+ "Sign-in failed",
1542
+ "Something went wrong",
1543
+ `We couldn't complete sign-in (${escapeHtml(reason)}). Close this tab and run the command again.`,
1544
+ errorMark
1545
+ );
1546
+ }
1547
+ function sendHtml(res, html) {
1548
+ res.statusCode = 200;
1549
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1550
+ res.end(html);
1551
+ }
1552
+ async function runLoopbackFlow(options) {
1553
+ const fetchFn = options.fetchFn ?? fetch;
1554
+ const { authorizationEndpoint } = options.discovery;
1555
+ if (!authorizationEndpoint) {
1556
+ throw new Error("This authorization server does not support browser sign-in.");
1557
+ }
1558
+ const { verifier, challenge } = createPkce();
1559
+ const state = base64url(randomBytes(16));
1560
+ const scope = options.scope ?? DEFAULT_SCOPE2;
1561
+ const { code, redirectUri } = await new Promise(
1562
+ (resolve, reject) => {
1563
+ let timer;
1564
+ const server = createServer((req, res2) => {
1565
+ const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
1566
+ if (requestUrl.pathname !== CALLBACK_PATH) {
1567
+ res2.statusCode = 404;
1568
+ res2.end("Not found");
1569
+ return;
1570
+ }
1571
+ const addr = server.address();
1572
+ const redirect = `http://127.0.0.1:${addr.port}${CALLBACK_PATH}`;
1573
+ const returnedState = requestUrl.searchParams.get("state");
1574
+ const errorParam = requestUrl.searchParams.get("error");
1575
+ const returnedCode = requestUrl.searchParams.get("code");
1576
+ const finish = (err) => {
1577
+ if (timer) clearTimeout(timer);
1578
+ server.close();
1579
+ if (err) reject(err);
1580
+ };
1581
+ if (errorParam) {
1582
+ sendHtml(res2, errorPage(errorParam));
1583
+ finish(new Error(`Sign-in failed: ${errorParam}.`));
1584
+ return;
1585
+ }
1586
+ if (returnedState !== state) {
1587
+ sendHtml(res2, errorPage("state_mismatch"));
1588
+ finish(new Error("Sign-in failed: state mismatch."));
1589
+ return;
1590
+ }
1591
+ if (!returnedCode) {
1592
+ sendHtml(res2, errorPage("missing_code"));
1593
+ finish(new Error("Sign-in failed: no authorization code returned."));
1594
+ return;
1595
+ }
1596
+ sendHtml(res2, successPage());
1597
+ finish();
1598
+ resolve({ code: returnedCode, redirectUri: redirect });
1599
+ });
1600
+ server.on("error", (err) => {
1601
+ if (timer) clearTimeout(timer);
1602
+ reject(err);
1603
+ });
1604
+ server.listen(0, "127.0.0.1", () => {
1605
+ const addr = server.address();
1606
+ const redirect = `http://127.0.0.1:${addr.port}${CALLBACK_PATH}`;
1607
+ const authorizeUrl = buildAuthorizeUrl(authorizationEndpoint, {
1608
+ clientId: options.clientId,
1609
+ redirectUri: redirect,
1610
+ scope,
1611
+ state,
1612
+ challenge,
1613
+ resource: options.resource
1614
+ });
1615
+ timer = setTimeout(() => {
1616
+ server.close();
1617
+ reject(new Error("Timed out waiting for sign-in. Run the command again."));
1618
+ }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
1619
+ (options.openBrowser ?? launchBrowser)(authorizeUrl);
1620
+ options.onPrompt?.(authorizeUrl);
1621
+ });
1622
+ }
1623
+ );
1624
+ const params = new URLSearchParams({
1625
+ grant_type: "authorization_code",
1626
+ code,
1627
+ redirect_uri: redirectUri,
1628
+ client_id: options.clientId,
1629
+ code_verifier: verifier
1630
+ });
1631
+ if (options.resource) {
1632
+ params.set("resource", options.resource);
1633
+ }
1634
+ const res = await fetchFn(options.discovery.tokenEndpoint, {
1635
+ method: "POST",
1636
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1637
+ body: params.toString()
1638
+ });
1639
+ const body = await res.json();
1640
+ if (!res.ok) {
1641
+ const error = typeof body.error === "string" ? body.error : "token_exchange_failed";
1642
+ throw new Error(`Token request failed: ${error}`);
1643
+ }
1644
+ const accessToken = body.access_token;
1645
+ if (typeof accessToken !== "string") {
1646
+ throw new Error("The authorization server returned an unexpected token response.");
1647
+ }
1648
+ return {
1649
+ access_token: accessToken,
1650
+ refresh_token: typeof body.refresh_token === "string" ? body.refresh_token : void 0,
1651
+ expires_in: typeof body.expires_in === "number" ? body.expires_in : void 0,
1652
+ token_type: typeof body.token_type === "string" ? body.token_type : void 0
1653
+ };
1654
+ }
1655
+ function browserLikelyAvailable() {
1656
+ if (process.platform === "darwin" || process.platform === "win32") {
1657
+ return true;
1658
+ }
1659
+ return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
1660
+ }
1661
+
1662
+ // src/commands/login.ts
1663
+ async function runLogin(options) {
1664
+ const tokenStore = createTokenStore();
1665
+ let keychainOk = true;
1666
+ let discovery;
1667
+ try {
1668
+ discovery = await discoverEndpoints(options.issuer);
1669
+ } catch (err) {
1670
+ fail(err instanceof Error ? err.message : "Could not reach the authorization server.");
1671
+ }
1672
+ const useLoopback = !options.noBrowser && Boolean(discovery.authorizationEndpoint) && browserLikelyAvailable();
1673
+ let tokens;
1674
+ try {
1675
+ if (useLoopback) {
1676
+ tokens = await runLoopbackFlow({
1677
+ discovery,
1678
+ clientId: options.clientId,
1679
+ resource: options.resource,
1680
+ onPrompt: printBrowserOpening
1681
+ });
1682
+ } else {
1683
+ const deviceAuth = await requestDeviceCode({
1684
+ discovery,
1685
+ clientId: options.clientId
1686
+ });
1687
+ printDeviceInstructions(deviceAuth);
1688
+ tokens = await pollForTokens({
1689
+ discovery,
1690
+ clientId: options.clientId,
1691
+ deviceCode: deviceAuth.device_code,
1692
+ deviceAuth,
1693
+ resource: options.resource
1694
+ });
1695
+ }
1696
+ } catch (err) {
1697
+ fail(err instanceof Error ? err.message : "Sign-in failed.");
1698
+ }
1699
+ if (!tokens.refresh_token) {
1700
+ fail("The authorization server did not return a refresh token.");
1701
+ }
1702
+ try {
1703
+ await persistLoginTokens(tokenStore, tokens);
1704
+ } catch {
1705
+ keychainOk = false;
1706
+ }
1707
+ const expiresAt = typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : null;
1708
+ const previous = await loadAuthMetadata();
1709
+ const keepPrevious = !keychainOk && previous?.savedToStore === true;
1710
+ await saveAuthMetadata(
1711
+ keepPrevious ? previous : {
1712
+ issuer: discovery.issuer,
1713
+ clientId: options.clientId,
1714
+ expiresAt,
1715
+ savedToStore: keychainOk
1716
+ }
1717
+ );
1718
+ printLoginSuccess(keychainOk);
1719
+ printConnectGuidance(options.resource ?? discovery.issuer, tokens.access_token);
1720
+ }
1721
+
1722
+ // src/lib/revoke.ts
1723
+ var RevocationRefused = class extends Error {
1724
+ constructor(status) {
1725
+ super(`Token revocation failed (${status}).`);
1726
+ this.status = status;
1727
+ this.name = "RevocationRefused";
1728
+ }
1729
+ status;
1730
+ };
1731
+ var RevocationRedirected = class extends Error {
1732
+ constructor(status) {
1733
+ super(`Token revocation was redirected (${status}).`);
1734
+ this.status = status;
1735
+ this.name = "RevocationRedirected";
1736
+ }
1737
+ status;
1738
+ };
1739
+ async function revokeToken(options) {
1740
+ const fetchFn = options.fetchFn ?? fetch;
1741
+ const params = new URLSearchParams({
1742
+ token: options.token,
1743
+ token_type_hint: options.tokenTypeHint,
1744
+ client_id: options.clientId
1745
+ });
1746
+ const res = await fetchFn(options.revocationEndpoint, {
1747
+ method: "POST",
1748
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1749
+ body: params.toString(),
1750
+ redirect: "manual"
1751
+ });
1752
+ if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
1753
+ throw new RevocationRedirected(res.status);
1754
+ }
1755
+ if (!res.ok) {
1756
+ throw new RevocationRefused(res.status);
1757
+ }
1758
+ }
1759
+
1760
+ // src/commands/logout.ts
1761
+ function mayWorkLater(status) {
1762
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1763
+ }
1764
+ function afterFailure(error) {
1765
+ if (error instanceof DiscoveryUnusable) return "refused";
1766
+ if (error instanceof DiscoveryAnswered || error instanceof RevocationRefused) {
1767
+ return mayWorkLater(error.status) ? "unanswered" : "refused";
1768
+ }
1769
+ return "unanswered";
1770
+ }
1771
+ function sameOrigin(address, issuer) {
1772
+ try {
1773
+ return new URL(address).origin === new URL(issuer).origin;
1774
+ } catch {
1775
+ return false;
1776
+ }
1777
+ }
1778
+ async function cancelAtServer(refreshToken, server, fetchFn) {
1779
+ try {
1780
+ assertHttpsIssuer(server.issuer);
1781
+ } catch {
1782
+ return "refused";
1783
+ }
1784
+ try {
1785
+ const { revocationEndpoint } = await discoverEndpoints(server.issuer, fetchFn);
1786
+ if (!revocationEndpoint || !sameOrigin(revocationEndpoint, server.issuer)) return "refused";
1787
+ await revokeToken({
1788
+ revocationEndpoint,
1789
+ clientId: server.clientId,
1790
+ token: refreshToken,
1791
+ tokenTypeHint: "refresh_token",
1792
+ fetchFn
1793
+ });
1794
+ return "cancelled";
1795
+ } catch (error) {
1796
+ return afterFailure(error);
1797
+ }
1798
+ }
1799
+ async function removeRecordAfterSignOut() {
1800
+ try {
1801
+ await clearAuthMetadata();
1802
+ } catch {
1803
+ }
1804
+ }
1805
+ function defaultServer(defaults) {
1806
+ try {
1807
+ return defaults();
1808
+ } catch {
1809
+ return null;
1810
+ }
1811
+ }
1812
+ async function endWithStoreUnopened(meta, options) {
1813
+ const platform = options.platform ?? process.platform;
1814
+ const mayForget = (platform === "linux" || platform === "freebsd") && meta?.savedToStore !== true;
1815
+ if (!options.forgetThisMachine) {
1816
+ printLogoutStoreLocked(mayForget);
1817
+ return { ok: false };
1818
+ }
1819
+ if (!mayForget) {
1820
+ printForgetRefused();
1821
+ return { ok: false };
1822
+ }
1823
+ try {
1824
+ await clearAuthMetadata();
1825
+ } catch {
1826
+ printForgetFailed(getConfigDir());
1827
+ return { ok: false };
1828
+ }
1829
+ printLogoutForgotten();
1830
+ return { ok: true };
1831
+ }
1832
+ async function runLogout(options) {
1833
+ const tokenStore = options.tokenStore ?? createTokenStore();
1834
+ const meta = await loadAuthMetadata();
1835
+ let refreshToken;
1836
+ try {
1837
+ refreshToken = await tokenStore.loadRefreshToken();
1838
+ } catch {
1839
+ return endWithStoreUnopened(meta, options);
1840
+ }
1841
+ if (refreshToken === null) {
1842
+ let passRemoved = true;
1843
+ try {
1844
+ await tokenStore.clear();
1845
+ } catch {
1846
+ passRemoved = false;
1847
+ }
1848
+ await removeRecordAfterSignOut();
1849
+ if (!passRemoved) {
1850
+ printLogoutPassLeft();
1851
+ } else if (meta?.savedToStore === false) {
1852
+ printLogoutSuccess();
1853
+ } else {
1854
+ printLogoutNothingFound();
1855
+ }
1856
+ return { ok: true };
1857
+ }
1858
+ if (options.forgetThisMachine) {
1859
+ printForgetRefused();
1860
+ return { ok: false };
1861
+ }
1862
+ const server = meta ?? defaultServer(options.defaults);
1863
+ if (!server) {
1864
+ printLogoutBadSettings();
1865
+ return { ok: false };
1866
+ }
1867
+ const atServer = await cancelAtServer(refreshToken, server, options.fetchFn ?? fetch);
1868
+ if (atServer === "unanswered") {
1869
+ printLogoutNotCancelledYet();
1870
+ return { ok: false };
1871
+ }
1872
+ try {
1873
+ await tokenStore.clear();
1874
+ } catch {
1875
+ printLogoutIncomplete(atServer === "cancelled");
1876
+ return { ok: false };
1877
+ }
1878
+ await removeRecordAfterSignOut();
1879
+ if (atServer === "refused") {
1880
+ printLogoutCannotCancel();
1881
+ } else {
1882
+ printLogoutSuccess();
1883
+ }
1884
+ return { ok: true };
1885
+ }
1886
+
1887
+ // src/commands/status.ts
1888
+ async function runStatus(options) {
1889
+ const tokenStore = createTokenStore();
1890
+ const meta = await loadAuthMetadata();
1891
+ let tokens;
1892
+ try {
1893
+ tokens = await tokenStore.load();
1894
+ } catch {
1895
+ printAuthStatus({ state: "store-unreadable" });
1896
+ return;
1897
+ }
1898
+ if (!meta || !tokens) {
1899
+ printAuthStatus({ state: "signed-out" });
1900
+ return;
1901
+ }
1902
+ const issuer = options.issuer ?? meta.issuer;
1903
+ const clientId = options.clientId ?? meta.clientId;
1904
+ let discovery;
1905
+ try {
1906
+ discovery = await discoverEndpoints(issuer);
1907
+ } catch {
1908
+ printAuthStatus({
1909
+ state: "unverified",
1910
+ issuer: meta.issuer,
1911
+ lastKnownExpiresAt: meta.expiresAt
1912
+ });
1913
+ return;
1914
+ }
1915
+ let result;
1916
+ try {
1917
+ result = await getValidAccessToken({
1918
+ discovery,
1919
+ clientId,
1920
+ tokenStore,
1921
+ resource: options.resource
1922
+ });
1923
+ } catch {
1924
+ printAuthStatus({ state: "store-unreadable" });
1925
+ return;
1926
+ }
1927
+ switch (result.status) {
1928
+ case "signed-out":
1929
+ printAuthStatus({ state: "signed-out" });
1930
+ return;
1931
+ case "rejected":
1932
+ printAuthStatus({ state: "expired" });
1933
+ return;
1934
+ case "unreachable":
1935
+ printAuthStatus({
1936
+ state: "unverified",
1937
+ issuer: meta.issuer,
1938
+ lastKnownExpiresAt: meta.expiresAt
1939
+ });
1940
+ return;
1941
+ case "refreshed": {
1942
+ const expiresAt = result.expiresIn ? Date.now() + result.expiresIn * 1e3 : null;
1943
+ await saveAuthMetadata({ issuer: discovery.issuer, clientId, expiresAt, savedToStore: true });
1944
+ printAuthStatus({ state: "signed-in", issuer: discovery.issuer, expiresAt });
1945
+ return;
1946
+ }
1947
+ }
1948
+ }
1949
+
1950
+ // src/env.ts
1951
+ import { z } from "zod/v4";
1952
+ var cliEnvSchema = z.object({
1953
+ /** Authorization server issuer URL. Defaults to production; override for local dev. */
1954
+ DAZZER_AUTH_ISSUER: z.url().default("https://auth.dazzer.io"),
1955
+ /** OAuth public client id. */
1956
+ DAZZER_AUTH_CLIENT_ID: z.string().min(1).default("dazzer-cli-local"),
1957
+ /** Resource indicator for JWT access tokens. Defaults to the production MCP server. */
1958
+ DAZZER_AUTH_RESOURCE: z.url().default("https://graph.dazzer.io/mcp"),
1959
+ /**
1960
+ * Where Dazzer itself is — the main service, as opposed to the sign-in server the other three
1961
+ * settings describe. It defaults to the same host the sign-in audience above already names,
1962
+ * because they are the same service seen from two sides; the default lives here, beside the
1963
+ * others, so no library and no command carries a production host of its own.
1964
+ */
1965
+ DAZZER_API_BASE_URL: z.url().default("https://graph.dazzer.io")
1966
+ });
1967
+ var cachedEnv = null;
1968
+ function validateEnv() {
1969
+ if (!cachedEnv) {
1970
+ cachedEnv = cliEnvSchema.parse(process.env);
1971
+ }
1972
+ return cachedEnv;
1973
+ }
1974
+ function signInServerDefaults() {
1975
+ const settings = cliEnvSchema.pick({ DAZZER_AUTH_ISSUER: true, DAZZER_AUTH_CLIENT_ID: true }).parse(process.env);
1976
+ return { issuer: settings.DAZZER_AUTH_ISSUER, clientId: settings.DAZZER_AUTH_CLIENT_ID };
1977
+ }
1978
+
1979
+ // src/index.ts
1980
+ var require2 = createRequire(import.meta.url);
1981
+ var { version } = require2("../package.json");
1982
+ var program = new Command();
15
1983
  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) => {
1984
+ program.command("add").description("Put a file, or a whole folder, into Dazzer").argument("<path>", "The file or folder to add").option("--space <name>", "The space to put it in \u2014 the name you were shown, or its number").option("--keep-to-myself", "Keep it to yourself, rather than putting it in a shared space").option("--why", "Show each document that was skipped, and why").action(async (file, opts) => {
1985
+ const env = validateEnv();
1986
+ const result = await runAdd({
1987
+ file,
1988
+ space: opts.space,
1989
+ keepToMyself: opts.keepToMyself === true,
1990
+ why: opts.why === true,
1991
+ baseUrl: env.DAZZER_API_BASE_URL,
1992
+ issuer: env.DAZZER_AUTH_ISSUER,
1993
+ clientId: env.DAZZER_AUTH_CLIENT_ID,
1994
+ resource: env.DAZZER_AUTH_RESOURCE
1995
+ });
1996
+ if (!result.ok) process.exitCode = 1;
1997
+ });
1998
+ var auth = program.command("auth").description("Manage sign-in");
1999
+ 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(
2000
+ async (opts) => {
25
2001
  const env = validateEnv();
26
2002
  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,
2003
+ issuer: opts.issuer ?? env.DAZZER_AUTH_ISSUER,
2004
+ clientId: opts.clientId ?? env.DAZZER_AUTH_CLIENT_ID,
2005
+ resource: opts.resource ?? env.DAZZER_AUTH_RESOURCE,
2006
+ noBrowser: opts.browser === false
31
2007
  });
2008
+ }
2009
+ );
2010
+ auth.command("logout").description("Cancel your sign-in and remove it from this machine").option(
2011
+ "--forget-this-machine",
2012
+ "On a machine with no credential store, remove its sign-in record without cancelling anything"
2013
+ ).action(async (opts) => {
2014
+ const result = await runLogout({
2015
+ defaults: signInServerDefaults,
2016
+ forgetThisMachine: opts.forgetThisMachine === true
2017
+ });
2018
+ if (!result.ok) process.exitCode = 1;
32
2019
  });
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
- });
2020
+ 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) => {
2021
+ await runStatus({
2022
+ issuer: opts.issuer,
2023
+ clientId: opts.clientId,
2024
+ resource: opts.resource
2025
+ });
51
2026
  });
52
2027
  program.parseAsync(process.argv).catch((err) => {
53
- process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
54
- process.exit(1);
2028
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2029
+ `);
2030
+ process.exit(1);
55
2031
  });