opensteer 0.6.0 → 0.6.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.
@@ -0,0 +1,1235 @@
1
+ import {
2
+ DEFAULT_CLOUD_BASE_URL,
3
+ createKeychainStore,
4
+ normalizeCloudBaseUrl,
5
+ resolveCloudSelection,
6
+ resolveConfigWithEnv,
7
+ stripTrailingSlashes
8
+ } from "./chunk-WDRMHPWL.js";
9
+
10
+ // src/cli/auth.ts
11
+ import open from "open";
12
+
13
+ // src/auth/credential-resolver.ts
14
+ function resolveCloudCredential(options) {
15
+ const flagApiKey = normalizeToken(options.apiKeyFlag);
16
+ const flagAccessToken = normalizeToken(options.accessTokenFlag);
17
+ if (flagApiKey && flagAccessToken) {
18
+ throw new Error("--api-key and --access-token are mutually exclusive.");
19
+ }
20
+ if (flagAccessToken) {
21
+ return {
22
+ kind: "access-token",
23
+ source: "flag",
24
+ token: flagAccessToken,
25
+ authScheme: "bearer"
26
+ };
27
+ }
28
+ if (flagApiKey) {
29
+ return {
30
+ kind: "api-key",
31
+ source: "flag",
32
+ token: flagApiKey,
33
+ authScheme: "api-key"
34
+ };
35
+ }
36
+ const envAuthScheme = parseEnvAuthScheme(options.env.OPENSTEER_AUTH_SCHEME);
37
+ const envApiKey = normalizeToken(options.env.OPENSTEER_API_KEY);
38
+ const envAccessToken = normalizeToken(options.env.OPENSTEER_ACCESS_TOKEN);
39
+ if (envApiKey && envAccessToken) {
40
+ throw new Error(
41
+ "OPENSTEER_API_KEY and OPENSTEER_ACCESS_TOKEN are mutually exclusive. Set only one."
42
+ );
43
+ }
44
+ if (envAccessToken) {
45
+ return {
46
+ kind: "access-token",
47
+ source: "env",
48
+ token: envAccessToken,
49
+ authScheme: "bearer"
50
+ };
51
+ }
52
+ if (envApiKey) {
53
+ if (envAuthScheme === "bearer") {
54
+ return {
55
+ kind: "access-token",
56
+ source: "env",
57
+ token: envApiKey,
58
+ authScheme: "bearer",
59
+ compatibilityBearerApiKey: true
60
+ };
61
+ }
62
+ return {
63
+ kind: "api-key",
64
+ source: "env",
65
+ token: envApiKey,
66
+ authScheme: envAuthScheme ?? "api-key"
67
+ };
68
+ }
69
+ return null;
70
+ }
71
+ function applyCloudCredentialToEnv(env, credential) {
72
+ if (credential.kind === "access-token") {
73
+ env.OPENSTEER_ACCESS_TOKEN = credential.token;
74
+ delete env.OPENSTEER_API_KEY;
75
+ env.OPENSTEER_AUTH_SCHEME = "bearer";
76
+ return;
77
+ }
78
+ env.OPENSTEER_API_KEY = credential.token;
79
+ delete env.OPENSTEER_ACCESS_TOKEN;
80
+ env.OPENSTEER_AUTH_SCHEME = credential.authScheme;
81
+ }
82
+ function parseEnvAuthScheme(value) {
83
+ const normalized = normalizeToken(value);
84
+ if (!normalized) return void 0;
85
+ if (normalized === "api-key" || normalized === "bearer") {
86
+ return normalized;
87
+ }
88
+ throw new Error(
89
+ `Invalid OPENSTEER_AUTH_SCHEME value "${value}". Use "api-key" or "bearer".`
90
+ );
91
+ }
92
+ function normalizeToken(value) {
93
+ if (typeof value !== "string") return void 0;
94
+ const normalized = value.trim();
95
+ return normalized.length ? normalized : void 0;
96
+ }
97
+
98
+ // src/auth/machine-credential-store.ts
99
+ import { createHash } from "crypto";
100
+ import fs from "fs";
101
+ import os from "os";
102
+ import path from "path";
103
+ var METADATA_VERSION = 1;
104
+ var ACTIVE_TARGET_VERSION = 1;
105
+ var KEYCHAIN_SERVICE = "com.opensteer.cli.cloud";
106
+ var KEYCHAIN_ACCOUNT_PREFIX = "machine:";
107
+ var LEGACY_KEYCHAIN_ACCOUNT = "machine";
108
+ var LEGACY_METADATA_FILE_NAME = "cli-login.json";
109
+ var LEGACY_FALLBACK_SECRET_FILE_NAME = "cli-login.secret.json";
110
+ var ACTIVE_TARGET_FILE_NAME = "cli-target.json";
111
+ var MachineCredentialStore = class {
112
+ authDir;
113
+ warn;
114
+ keychain = createKeychainStore();
115
+ warnedFallback = false;
116
+ constructor(options = {}) {
117
+ const appName = options.appName || "opensteer";
118
+ const env = options.env ?? process.env;
119
+ const configDir = resolveConfigDir(appName, env);
120
+ this.authDir = path.join(configDir, "auth");
121
+ this.warn = options.warn ?? (() => void 0);
122
+ }
123
+ readCloudCredential(target) {
124
+ const slot = resolveCredentialSlot(this.authDir, target);
125
+ return this.readCredentialSlot(slot, target) ?? this.readAndMigrateLegacyCredential(target);
126
+ }
127
+ writeCloudCredential(args) {
128
+ const accessToken = args.accessToken.trim();
129
+ const refreshToken2 = args.refreshToken.trim();
130
+ if (!accessToken || !refreshToken2) {
131
+ throw new Error("Cannot persist empty machine credential secrets.");
132
+ }
133
+ const baseUrl = normalizeCredentialUrl(args.baseUrl, "baseUrl");
134
+ const siteUrl = normalizeCredentialUrl(args.siteUrl, "siteUrl");
135
+ const slot = resolveCredentialSlot(this.authDir, {
136
+ baseUrl,
137
+ siteUrl
138
+ });
139
+ ensureDirectory(this.authDir);
140
+ const secretPayload = {
141
+ accessToken,
142
+ refreshToken: refreshToken2
143
+ };
144
+ let secretBackend = "file";
145
+ if (this.keychain) {
146
+ try {
147
+ this.keychain.set(
148
+ KEYCHAIN_SERVICE,
149
+ slot.keychainAccount,
150
+ JSON.stringify(secretPayload)
151
+ );
152
+ secretBackend = "keychain";
153
+ removeFileIfExists(slot.fallbackSecretPath);
154
+ } catch {
155
+ this.writeFallbackSecret(slot, secretPayload);
156
+ secretBackend = "file";
157
+ }
158
+ } else {
159
+ this.writeFallbackSecret(slot, secretPayload);
160
+ }
161
+ const metadata = {
162
+ version: METADATA_VERSION,
163
+ secretBackend,
164
+ baseUrl,
165
+ siteUrl,
166
+ scope: args.scope,
167
+ obtainedAt: args.obtainedAt,
168
+ expiresAt: args.expiresAt,
169
+ updatedAt: Date.now()
170
+ };
171
+ writeJsonFile(slot.metadataPath, metadata);
172
+ }
173
+ readActiveCloudTarget() {
174
+ return readActiveCloudTargetMetadata(resolveActiveTargetPath(this.authDir));
175
+ }
176
+ writeActiveCloudTarget(target) {
177
+ const baseUrl = normalizeCredentialUrl(target.baseUrl, "baseUrl");
178
+ const siteUrl = normalizeCredentialUrl(target.siteUrl, "siteUrl");
179
+ ensureDirectory(this.authDir);
180
+ writeJsonFile(resolveActiveTargetPath(this.authDir), {
181
+ version: ACTIVE_TARGET_VERSION,
182
+ baseUrl,
183
+ siteUrl,
184
+ updatedAt: Date.now()
185
+ });
186
+ }
187
+ clearCloudCredential(target) {
188
+ this.clearCredentialSlot(resolveCredentialSlot(this.authDir, target));
189
+ const legacySlot = resolveLegacyCredentialSlot(this.authDir);
190
+ const legacyMetadata = readMetadata(legacySlot.metadataPath);
191
+ if (legacyMetadata && matchesCredentialTarget(legacyMetadata, target)) {
192
+ this.clearCredentialSlot(legacySlot);
193
+ }
194
+ }
195
+ readCredentialSlot(slot, target) {
196
+ const metadata = readMetadata(slot.metadataPath);
197
+ if (!metadata) {
198
+ return null;
199
+ }
200
+ if (target && !matchesCredentialTarget(metadata, target)) {
201
+ return null;
202
+ }
203
+ const secret = this.readSecret(slot, metadata.secretBackend);
204
+ if (!secret) {
205
+ return null;
206
+ }
207
+ return {
208
+ baseUrl: metadata.baseUrl,
209
+ siteUrl: metadata.siteUrl,
210
+ scope: metadata.scope,
211
+ accessToken: secret.accessToken,
212
+ refreshToken: secret.refreshToken,
213
+ obtainedAt: metadata.obtainedAt,
214
+ expiresAt: metadata.expiresAt
215
+ };
216
+ }
217
+ readAndMigrateLegacyCredential(target) {
218
+ const legacySlot = resolveLegacyCredentialSlot(this.authDir);
219
+ const legacyCredential = this.readCredentialSlot(legacySlot, target);
220
+ if (!legacyCredential) {
221
+ return null;
222
+ }
223
+ this.writeCloudCredential(legacyCredential);
224
+ this.clearCredentialSlot(legacySlot);
225
+ return legacyCredential;
226
+ }
227
+ readSecret(slot, backend) {
228
+ if (backend === "keychain" && this.keychain) {
229
+ try {
230
+ const secret = this.keychain.get(
231
+ KEYCHAIN_SERVICE,
232
+ slot.keychainAccount
233
+ );
234
+ if (!secret) return null;
235
+ return parseSecretPayload(secret);
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+ return readSecretFile(slot.fallbackSecretPath);
241
+ }
242
+ writeFallbackSecret(slot, secretPayload) {
243
+ writeJsonFile(slot.fallbackSecretPath, secretPayload, {
244
+ mode: 384
245
+ });
246
+ if (!this.warnedFallback) {
247
+ this.warn({
248
+ code: "fallback_file_store",
249
+ path: slot.fallbackSecretPath,
250
+ message: "Secure keychain is unavailable. Falling back to file-based credential storage with mode 0600."
251
+ });
252
+ this.warnedFallback = true;
253
+ }
254
+ }
255
+ clearCredentialSlot(slot) {
256
+ removeFileIfExists(slot.metadataPath);
257
+ removeFileIfExists(slot.fallbackSecretPath);
258
+ if (this.keychain) {
259
+ this.keychain.delete(KEYCHAIN_SERVICE, slot.keychainAccount);
260
+ }
261
+ }
262
+ };
263
+ function createMachineCredentialStore(options = {}) {
264
+ return new MachineCredentialStore(options);
265
+ }
266
+ function resolveCredentialSlot(authDir, target) {
267
+ const normalizedBaseUrl = normalizeCredentialUrl(target.baseUrl, "baseUrl");
268
+ const normalizedSiteUrl = normalizeCredentialUrl(target.siteUrl, "siteUrl");
269
+ const storageKey = createHash("sha256").update(`${normalizedBaseUrl}\0${normalizedSiteUrl}`).digest("hex").slice(0, 24);
270
+ return {
271
+ keychainAccount: `${KEYCHAIN_ACCOUNT_PREFIX}${storageKey}`,
272
+ metadataPath: path.join(authDir, `cli-login.${storageKey}.json`),
273
+ fallbackSecretPath: path.join(
274
+ authDir,
275
+ `cli-login.${storageKey}.secret.json`
276
+ )
277
+ };
278
+ }
279
+ function resolveLegacyCredentialSlot(authDir) {
280
+ return {
281
+ keychainAccount: LEGACY_KEYCHAIN_ACCOUNT,
282
+ metadataPath: path.join(authDir, LEGACY_METADATA_FILE_NAME),
283
+ fallbackSecretPath: path.join(authDir, LEGACY_FALLBACK_SECRET_FILE_NAME)
284
+ };
285
+ }
286
+ function resolveActiveTargetPath(authDir) {
287
+ return path.join(authDir, ACTIVE_TARGET_FILE_NAME);
288
+ }
289
+ function matchesCredentialTarget(value, target) {
290
+ return normalizeCredentialUrl(value.baseUrl, "baseUrl") === normalizeCredentialUrl(target.baseUrl, "baseUrl") && normalizeCredentialUrl(value.siteUrl, "siteUrl") === normalizeCredentialUrl(target.siteUrl, "siteUrl");
291
+ }
292
+ function normalizeCredentialUrl(value, field) {
293
+ const normalized = stripTrailingSlashes(value.trim());
294
+ if (!normalized) {
295
+ throw new Error(`Cannot persist machine credential without ${field}.`);
296
+ }
297
+ return normalized;
298
+ }
299
+ function resolveConfigDir(appName, env) {
300
+ if (process.platform === "win32") {
301
+ const appData = env.APPDATA?.trim() || path.join(os.homedir(), "AppData", "Roaming");
302
+ return path.join(appData, appName);
303
+ }
304
+ if (process.platform === "darwin") {
305
+ return path.join(
306
+ os.homedir(),
307
+ "Library",
308
+ "Application Support",
309
+ appName
310
+ );
311
+ }
312
+ const xdgConfigHome = env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config");
313
+ return path.join(xdgConfigHome, appName);
314
+ }
315
+ function ensureDirectory(directoryPath) {
316
+ fs.mkdirSync(directoryPath, { recursive: true, mode: 448 });
317
+ }
318
+ function removeFileIfExists(filePath) {
319
+ try {
320
+ fs.rmSync(filePath, { force: true });
321
+ } catch {
322
+ return;
323
+ }
324
+ }
325
+ function readMetadata(filePath) {
326
+ if (!fs.existsSync(filePath)) {
327
+ return null;
328
+ }
329
+ try {
330
+ const raw = fs.readFileSync(filePath, "utf8");
331
+ const parsed = JSON.parse(raw);
332
+ if (parsed.version !== METADATA_VERSION) return null;
333
+ if (parsed.secretBackend !== "keychain" && parsed.secretBackend !== "file") {
334
+ return null;
335
+ }
336
+ if (typeof parsed.baseUrl !== "string" || !parsed.baseUrl.trim()) return null;
337
+ if (typeof parsed.siteUrl !== "string" || !parsed.siteUrl.trim()) return null;
338
+ if (!Array.isArray(parsed.scope)) return null;
339
+ if (typeof parsed.obtainedAt !== "number") return null;
340
+ if (typeof parsed.expiresAt !== "number") return null;
341
+ if (typeof parsed.updatedAt !== "number") return null;
342
+ return {
343
+ version: parsed.version,
344
+ secretBackend: parsed.secretBackend,
345
+ baseUrl: parsed.baseUrl,
346
+ siteUrl: parsed.siteUrl,
347
+ scope: parsed.scope.filter(
348
+ (value) => typeof value === "string"
349
+ ),
350
+ obtainedAt: parsed.obtainedAt,
351
+ expiresAt: parsed.expiresAt,
352
+ updatedAt: parsed.updatedAt
353
+ };
354
+ } catch {
355
+ return null;
356
+ }
357
+ }
358
+ function readActiveCloudTargetMetadata(filePath) {
359
+ if (!fs.existsSync(filePath)) {
360
+ return null;
361
+ }
362
+ try {
363
+ const raw = fs.readFileSync(filePath, "utf8");
364
+ const parsed = JSON.parse(raw);
365
+ if (parsed.version !== ACTIVE_TARGET_VERSION) {
366
+ return null;
367
+ }
368
+ if (typeof parsed.baseUrl !== "string" || !parsed.baseUrl.trim()) {
369
+ return null;
370
+ }
371
+ if (typeof parsed.siteUrl !== "string" || !parsed.siteUrl.trim()) {
372
+ return null;
373
+ }
374
+ return {
375
+ baseUrl: parsed.baseUrl,
376
+ siteUrl: parsed.siteUrl
377
+ };
378
+ } catch {
379
+ return null;
380
+ }
381
+ }
382
+ function parseSecretPayload(raw) {
383
+ try {
384
+ const parsed = JSON.parse(raw);
385
+ if (typeof parsed.accessToken !== "string" || !parsed.accessToken.trim() || typeof parsed.refreshToken !== "string" || !parsed.refreshToken.trim()) {
386
+ return null;
387
+ }
388
+ return {
389
+ accessToken: parsed.accessToken,
390
+ refreshToken: parsed.refreshToken
391
+ };
392
+ } catch {
393
+ return null;
394
+ }
395
+ }
396
+ function readSecretFile(filePath) {
397
+ if (!fs.existsSync(filePath)) {
398
+ return null;
399
+ }
400
+ try {
401
+ const raw = fs.readFileSync(filePath, "utf8");
402
+ return parseSecretPayload(raw);
403
+ } catch {
404
+ return null;
405
+ }
406
+ }
407
+ function writeJsonFile(filePath, payload, options = {}) {
408
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), {
409
+ encoding: "utf8",
410
+ mode: options.mode ?? 384
411
+ });
412
+ if (typeof options.mode === "number") {
413
+ fs.chmodSync(filePath, options.mode);
414
+ }
415
+ }
416
+
417
+ // src/cli/auth.ts
418
+ var CliAuthHttpError = class extends Error {
419
+ status;
420
+ body;
421
+ constructor(message, status, body) {
422
+ super(message);
423
+ this.name = "CliAuthHttpError";
424
+ this.status = status;
425
+ this.body = body;
426
+ }
427
+ };
428
+ var HELP_TEXT = `Usage: opensteer auth <command> [options]
429
+
430
+ Authenticate Opensteer CLI with Opensteer Cloud.
431
+
432
+ Commands:
433
+ login Start device login flow in browser
434
+ status Show saved machine login state for the selected cloud host
435
+ logout Revoke and remove saved machine login for the selected cloud host
436
+
437
+ Options:
438
+ --base-url <url> Cloud API base URL (defaults to env or the last selected host)
439
+ --site-url <url> Cloud site URL for browser/device auth (defaults to env or the last selected host)
440
+ --json JSON output (login prompts go to stderr)
441
+ --no-browser Do not auto-open your default browser during login
442
+ -h, --help Show this help
443
+ `;
444
+ function createDefaultDeps() {
445
+ const env = process.env;
446
+ return {
447
+ env,
448
+ store: createMachineCredentialStore({
449
+ env,
450
+ warn: (warning) => {
451
+ process.stderr.write(`${warning.message} (${warning.path})
452
+ `);
453
+ }
454
+ }),
455
+ fetchFn: fetch,
456
+ writeStdout: (message) => process.stdout.write(message),
457
+ writeStderr: (message) => process.stderr.write(message),
458
+ isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
459
+ sleep: async (ms) => {
460
+ await new Promise((resolve) => setTimeout(resolve, ms));
461
+ },
462
+ now: () => Date.now(),
463
+ openExternalUrl: openDefaultBrowser
464
+ };
465
+ }
466
+ function readFlagValue(args, index, flag) {
467
+ const value = args[index + 1];
468
+ if (value === void 0 || value.startsWith("-")) {
469
+ return {
470
+ ok: false,
471
+ error: `${flag} requires a value.`
472
+ };
473
+ }
474
+ return {
475
+ ok: true,
476
+ value,
477
+ nextIndex: index + 1
478
+ };
479
+ }
480
+ function parseAuthCommonArgs(rawArgs) {
481
+ const args = {};
482
+ for (let i = 0; i < rawArgs.length; i++) {
483
+ const arg = rawArgs[i];
484
+ if (arg === "--json") {
485
+ args.json = true;
486
+ continue;
487
+ }
488
+ if (arg === "--base-url") {
489
+ const value = readFlagValue(rawArgs, i, "--base-url");
490
+ if (!value.ok) return { args, error: value.error };
491
+ args.baseUrl = value.value;
492
+ i = value.nextIndex;
493
+ continue;
494
+ }
495
+ if (arg === "--site-url") {
496
+ const value = readFlagValue(rawArgs, i, "--site-url");
497
+ if (!value.ok) return { args, error: value.error };
498
+ args.siteUrl = value.value;
499
+ i = value.nextIndex;
500
+ continue;
501
+ }
502
+ return {
503
+ args,
504
+ error: `Unsupported option "${arg}".`
505
+ };
506
+ }
507
+ return { args };
508
+ }
509
+ function parseOpensteerAuthArgs(rawArgs) {
510
+ if (!rawArgs.length) {
511
+ return { mode: "help" };
512
+ }
513
+ const [subcommand, ...rest] = rawArgs;
514
+ if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
515
+ return { mode: "help" };
516
+ }
517
+ if (subcommand === "login") {
518
+ let openBrowser = true;
519
+ const filtered = [];
520
+ for (const arg of rest) {
521
+ if (arg === "--no-browser") {
522
+ openBrowser = false;
523
+ continue;
524
+ }
525
+ filtered.push(arg);
526
+ }
527
+ const parsed = parseAuthCommonArgs(filtered);
528
+ if (parsed.error) return { mode: "error", error: parsed.error };
529
+ return {
530
+ mode: "login",
531
+ args: {
532
+ ...parsed.args,
533
+ openBrowser
534
+ }
535
+ };
536
+ }
537
+ if (subcommand === "status") {
538
+ const parsed = parseAuthCommonArgs(rest);
539
+ if (parsed.error) return { mode: "error", error: parsed.error };
540
+ return { mode: "status", args: parsed.args };
541
+ }
542
+ if (subcommand === "logout") {
543
+ const parsed = parseAuthCommonArgs(rest);
544
+ if (parsed.error) return { mode: "error", error: parsed.error };
545
+ return { mode: "logout", args: parsed.args };
546
+ }
547
+ return {
548
+ mode: "error",
549
+ error: `Unsupported auth subcommand "${subcommand}".`
550
+ };
551
+ }
552
+ function printHelp(deps) {
553
+ deps.writeStdout(`${HELP_TEXT}
554
+ `);
555
+ }
556
+ function writeHumanLine(deps, message) {
557
+ deps.writeStdout(`${message}
558
+ `);
559
+ }
560
+ function writeJsonLine(deps, payload) {
561
+ deps.writeStdout(`${JSON.stringify(payload)}
562
+ `);
563
+ }
564
+ function resolveBaseUrl(provided, env) {
565
+ const baseUrl = normalizeCloudBaseUrl(
566
+ (provided || env.OPENSTEER_BASE_URL || DEFAULT_CLOUD_BASE_URL).trim()
567
+ );
568
+ assertSecureUrl(baseUrl, "--base-url");
569
+ return baseUrl;
570
+ }
571
+ function resolveSiteUrl(provided, baseUrl, env) {
572
+ const siteUrl = normalizeCloudBaseUrl(
573
+ (provided || env.OPENSTEER_CLOUD_SITE_URL || deriveSiteUrlFromBaseUrl(baseUrl)).trim()
574
+ );
575
+ assertSecureUrl(siteUrl, "--site-url");
576
+ return siteUrl;
577
+ }
578
+ function hasExplicitCloudTargetSelection(providedBaseUrl, providedSiteUrl, env) {
579
+ return Boolean(
580
+ providedBaseUrl?.trim() || providedSiteUrl?.trim() || env.OPENSTEER_BASE_URL?.trim() || env.OPENSTEER_CLOUD_SITE_URL?.trim()
581
+ );
582
+ }
583
+ function readRememberedCloudTarget(store) {
584
+ const activeTarget = store.readActiveCloudTarget();
585
+ if (!activeTarget) {
586
+ return null;
587
+ }
588
+ try {
589
+ const baseUrl = normalizeCloudBaseUrl(activeTarget.baseUrl);
590
+ const siteUrl = normalizeCloudBaseUrl(activeTarget.siteUrl);
591
+ assertSecureUrl(baseUrl, "--base-url");
592
+ assertSecureUrl(siteUrl, "--site-url");
593
+ return {
594
+ baseUrl,
595
+ siteUrl
596
+ };
597
+ } catch {
598
+ return null;
599
+ }
600
+ }
601
+ function resolveCloudTarget(args, env, store) {
602
+ if (!hasExplicitCloudTargetSelection(args.baseUrl, args.siteUrl, env)) {
603
+ const rememberedTarget = readRememberedCloudTarget(store);
604
+ if (rememberedTarget) {
605
+ return rememberedTarget;
606
+ }
607
+ }
608
+ const baseUrl = resolveBaseUrl(args.baseUrl, env);
609
+ const siteUrl = resolveSiteUrl(args.siteUrl, baseUrl, env);
610
+ return {
611
+ baseUrl,
612
+ siteUrl
613
+ };
614
+ }
615
+ function deriveSiteUrlFromBaseUrl(baseUrl) {
616
+ let parsed;
617
+ try {
618
+ parsed = new URL(baseUrl);
619
+ } catch {
620
+ return "https://opensteer.com";
621
+ }
622
+ const hostname = parsed.hostname.toLowerCase();
623
+ if (hostname.startsWith("api.")) {
624
+ parsed.hostname = hostname.slice("api.".length);
625
+ parsed.pathname = "";
626
+ parsed.search = "";
627
+ parsed.hash = "";
628
+ return normalizeCloudBaseUrl(parsed.toString());
629
+ }
630
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") {
631
+ parsed.port = "3001";
632
+ parsed.pathname = "";
633
+ parsed.search = "";
634
+ parsed.hash = "";
635
+ return normalizeCloudBaseUrl(parsed.toString());
636
+ }
637
+ parsed.pathname = "";
638
+ parsed.search = "";
639
+ parsed.hash = "";
640
+ return normalizeCloudBaseUrl(parsed.toString());
641
+ }
642
+ function assertSecureUrl(value, flag) {
643
+ let parsed;
644
+ try {
645
+ parsed = new URL(value);
646
+ } catch {
647
+ throw new Error(`Invalid ${flag} "${value}".`);
648
+ }
649
+ if (parsed.protocol === "https:") {
650
+ return;
651
+ }
652
+ if (parsed.protocol === "http:") {
653
+ const host = parsed.hostname.toLowerCase();
654
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
655
+ return;
656
+ }
657
+ }
658
+ throw new Error(
659
+ `Insecure URL "${value}". Use HTTPS, or HTTP only for localhost.`
660
+ );
661
+ }
662
+ async function postJson(fetchFn, url, body) {
663
+ const response = await fetchFn(url, {
664
+ method: "POST",
665
+ headers: {
666
+ "content-type": "application/json"
667
+ },
668
+ body: JSON.stringify(body)
669
+ });
670
+ let payload = null;
671
+ try {
672
+ payload = await response.json();
673
+ } catch {
674
+ payload = null;
675
+ }
676
+ if (!response.ok) {
677
+ throw new CliAuthHttpError(
678
+ `Auth request failed with status ${response.status}.`,
679
+ response.status,
680
+ payload
681
+ );
682
+ }
683
+ return payload;
684
+ }
685
+ function parseScope(rawScope) {
686
+ if (!rawScope) return ["cloud:browser"];
687
+ const values = rawScope.split(" ").map((value) => value.trim()).filter(Boolean);
688
+ return values.length ? values : ["cloud:browser"];
689
+ }
690
+ function parseCliTokenResponse(payload) {
691
+ const accessToken = typeof payload.access_token === "string" ? payload.access_token.trim() : "";
692
+ const refreshToken2 = typeof payload.refresh_token === "string" ? payload.refresh_token.trim() : "";
693
+ const expiresInSec = typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) && payload.expires_in > 0 ? Math.trunc(payload.expires_in) : 0;
694
+ if (!accessToken || !refreshToken2 || !expiresInSec) {
695
+ throw new Error("Invalid token response from cloud auth endpoint.");
696
+ }
697
+ return {
698
+ accessToken,
699
+ refreshToken: refreshToken2,
700
+ expiresInSec,
701
+ scope: parseScope(payload.scope)
702
+ };
703
+ }
704
+ function parseCliOauthError(error) {
705
+ if (!error || typeof error !== "object" || Array.isArray(error)) {
706
+ return null;
707
+ }
708
+ const root = error;
709
+ return {
710
+ error: typeof root.error === "string" ? root.error : void 0,
711
+ error_description: typeof root.error_description === "string" ? root.error_description : void 0,
712
+ interval: typeof root.interval === "number" ? root.interval : void 0
713
+ };
714
+ }
715
+ async function startDeviceAuthorization(siteUrl, fetchFn) {
716
+ const response = await postJson(
717
+ fetchFn,
718
+ `${siteUrl}/api/cli-auth/device/start`,
719
+ {
720
+ scope: ["cloud:browser"]
721
+ }
722
+ );
723
+ if (!response || typeof response.device_code !== "string" || !response.device_code.trim() || typeof response.user_code !== "string" || !response.user_code.trim() || typeof response.verification_uri_complete !== "string" || !response.verification_uri_complete.trim() || typeof response.expires_in !== "number" || response.expires_in <= 0 || typeof response.interval !== "number" || response.interval <= 0) {
724
+ throw new Error("Invalid device authorization response from cloud.");
725
+ }
726
+ return response;
727
+ }
728
+ async function pollDeviceToken(siteUrl, deviceCode, fetchFn) {
729
+ return await postJson(
730
+ fetchFn,
731
+ `${siteUrl}/api/cli-auth/device/token`,
732
+ {
733
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
734
+ device_code: deviceCode
735
+ }
736
+ );
737
+ }
738
+ async function refreshToken(siteUrl, refreshTokenValue, fetchFn) {
739
+ return await postJson(fetchFn, `${siteUrl}/api/cli-auth/token`, {
740
+ grant_type: "refresh_token",
741
+ refresh_token: refreshTokenValue
742
+ });
743
+ }
744
+ async function revokeToken(siteUrl, refreshTokenValue, fetchFn) {
745
+ await postJson(fetchFn, `${siteUrl}/api/cli-auth/revoke`, {
746
+ token: refreshTokenValue
747
+ });
748
+ }
749
+ async function openDefaultBrowser(url) {
750
+ try {
751
+ const child = await open(url, {
752
+ wait: false
753
+ });
754
+ child.on("error", () => void 0);
755
+ child.unref();
756
+ return true;
757
+ } catch {
758
+ return false;
759
+ }
760
+ }
761
+ async function runDeviceLoginFlow(args) {
762
+ const start = await startDeviceAuthorization(args.siteUrl, args.fetchFn);
763
+ if (args.openBrowser) {
764
+ args.writeProgress(
765
+ "Opening your default browser for Opensteer CLI authentication.\n"
766
+ );
767
+ args.writeProgress(
768
+ `If nothing opens, use this URL:
769
+ ${start.verification_uri_complete}
770
+ `
771
+ );
772
+ } else {
773
+ if (args.openBrowserDisabledReason) {
774
+ args.writeProgress(
775
+ `Automatic browser open is disabled (${args.openBrowserDisabledReason}).
776
+ `
777
+ );
778
+ }
779
+ args.writeProgress(
780
+ `Open this URL to authenticate Opensteer CLI:
781
+ ${start.verification_uri_complete}
782
+ `
783
+ );
784
+ }
785
+ args.writeProgress(`Verification code: ${start.user_code}
786
+ `);
787
+ if (args.openBrowser) {
788
+ const browserOpened = await args.openExternalUrl(
789
+ start.verification_uri_complete
790
+ );
791
+ if (browserOpened) {
792
+ args.writeProgress(
793
+ "Opened your default browser. Finish authentication there; this terminal will continue automatically.\n"
794
+ );
795
+ } else {
796
+ args.writeProgress(
797
+ "Could not open your default browser automatically. Paste the URL above into a browser to continue.\n"
798
+ );
799
+ }
800
+ }
801
+ const deadline = args.now() + start.expires_in * 1e3;
802
+ let pollIntervalMs = Math.max(1, Math.trunc(start.interval)) * 1e3;
803
+ while (args.now() <= deadline) {
804
+ await args.sleep(pollIntervalMs);
805
+ try {
806
+ const tokenPayload = await pollDeviceToken(
807
+ args.siteUrl,
808
+ start.device_code,
809
+ args.fetchFn
810
+ );
811
+ const parsed = parseCliTokenResponse(tokenPayload);
812
+ return {
813
+ accessToken: parsed.accessToken,
814
+ refreshToken: parsed.refreshToken,
815
+ expiresAt: args.now() + parsed.expiresInSec * 1e3,
816
+ scope: parsed.scope
817
+ };
818
+ } catch (error) {
819
+ if (error instanceof CliAuthHttpError) {
820
+ const oauthError = parseCliOauthError(error.body);
821
+ if (!oauthError?.error) {
822
+ throw error;
823
+ }
824
+ if (oauthError.error === "authorization_pending") {
825
+ continue;
826
+ }
827
+ if (oauthError.error === "slow_down") {
828
+ const hintedInterval = typeof oauthError.interval === "number" && oauthError.interval > 0 ? Math.trunc(oauthError.interval) * 1e3 : pollIntervalMs + 5e3;
829
+ pollIntervalMs = Math.max(hintedInterval, pollIntervalMs + 1e3);
830
+ continue;
831
+ }
832
+ if (oauthError.error === "expired_token") {
833
+ throw new Error(
834
+ 'Device authorization expired before approval. Run "opensteer auth login" again.'
835
+ );
836
+ }
837
+ if (oauthError.error === "access_denied") {
838
+ throw new Error(
839
+ 'Cloud login was denied. Run "opensteer auth login" to retry.'
840
+ );
841
+ }
842
+ throw new Error(
843
+ oauthError.error_description || `Cloud login failed: ${oauthError.error}.`
844
+ );
845
+ }
846
+ throw error;
847
+ }
848
+ }
849
+ throw new Error(
850
+ 'Timed out waiting for cloud login approval. Run "opensteer auth login" again.'
851
+ );
852
+ }
853
+ async function refreshSavedCredential(saved, deps) {
854
+ const tokenPayload = await refreshToken(
855
+ saved.siteUrl,
856
+ saved.refreshToken,
857
+ deps.fetchFn
858
+ );
859
+ const parsed = parseCliTokenResponse(tokenPayload);
860
+ const updated = {
861
+ accessToken: parsed.accessToken,
862
+ refreshToken: parsed.refreshToken,
863
+ expiresAt: deps.now() + parsed.expiresInSec * 1e3,
864
+ scope: parsed.scope
865
+ };
866
+ deps.store.writeCloudCredential({
867
+ baseUrl: saved.baseUrl,
868
+ siteUrl: saved.siteUrl,
869
+ scope: updated.scope,
870
+ accessToken: updated.accessToken,
871
+ refreshToken: updated.refreshToken,
872
+ obtainedAt: deps.now(),
873
+ expiresAt: updated.expiresAt
874
+ });
875
+ return updated;
876
+ }
877
+ async function ensureSavedCredentialIsFresh(saved, deps) {
878
+ const refreshSkewMs = 6e4;
879
+ if (saved.expiresAt > deps.now() + refreshSkewMs) {
880
+ return saved;
881
+ }
882
+ try {
883
+ const refreshed = await refreshSavedCredential(saved, deps);
884
+ return {
885
+ ...saved,
886
+ accessToken: refreshed.accessToken,
887
+ refreshToken: refreshed.refreshToken,
888
+ expiresAt: refreshed.expiresAt,
889
+ scope: refreshed.scope,
890
+ obtainedAt: deps.now()
891
+ };
892
+ } catch (error) {
893
+ if (error instanceof CliAuthHttpError) {
894
+ const oauth = parseCliOauthError(error.body);
895
+ if (oauth?.error === "invalid_grant" || oauth?.error === "expired_token") {
896
+ deps.store.clearCloudCredential({
897
+ baseUrl: saved.baseUrl,
898
+ siteUrl: saved.siteUrl
899
+ });
900
+ return null;
901
+ }
902
+ }
903
+ deps.writeStderr(
904
+ `Unable to refresh saved cloud login: ${error instanceof Error ? error.message : "unknown error"}
905
+ `
906
+ );
907
+ return null;
908
+ }
909
+ }
910
+ function toAuthMissingMessage(commandName) {
911
+ return [
912
+ `${commandName} requires cloud authentication.`,
913
+ 'Use --api-key, --access-token, OPENSTEER_API_KEY, OPENSTEER_ACCESS_TOKEN, or run "opensteer auth login".'
914
+ ].join(" ");
915
+ }
916
+ function describeBrowserOpenMode(args, deps) {
917
+ if (!args.openBrowser) {
918
+ return {
919
+ enabled: false,
920
+ disabledReason: "--no-browser"
921
+ };
922
+ }
923
+ if (!deps.isInteractive()) {
924
+ return {
925
+ enabled: false,
926
+ disabledReason: "this shell is not interactive"
927
+ };
928
+ }
929
+ if (isCiEnvironment(deps.env)) {
930
+ return {
931
+ enabled: false,
932
+ disabledReason: "CI"
933
+ };
934
+ }
935
+ return {
936
+ enabled: true
937
+ };
938
+ }
939
+ function isCiEnvironment(env) {
940
+ const value = env.CI?.trim().toLowerCase();
941
+ return Boolean(value && value !== "0" && value !== "false");
942
+ }
943
+ function isCloudModeEnabledForRootDir(rootDir, env) {
944
+ const resolved = resolveConfigWithEnv(
945
+ {
946
+ storage: { rootDir }
947
+ },
948
+ {
949
+ env
950
+ }
951
+ );
952
+ return resolveCloudSelection(
953
+ {
954
+ cloud: resolved.config.cloud
955
+ },
956
+ resolved.env
957
+ ).cloud;
958
+ }
959
+ async function ensureCloudCredentialsForOpenCommand(options) {
960
+ const env = options.env ?? process.env;
961
+ if (!isCloudModeEnabledForRootDir(options.scopeDir, env)) {
962
+ return null;
963
+ }
964
+ const writeStderr = options.writeStderr ?? ((message) => process.stderr.write(message));
965
+ return await ensureCloudCredentialsForCommand({
966
+ commandName: "opensteer open",
967
+ env,
968
+ store: options.store,
969
+ apiKeyFlag: options.apiKeyFlag,
970
+ accessTokenFlag: options.accessTokenFlag,
971
+ interactive: options.interactive,
972
+ autoLoginIfNeeded: true,
973
+ writeProgress: options.writeProgress ?? writeStderr,
974
+ writeStderr,
975
+ fetchFn: options.fetchFn,
976
+ sleep: options.sleep,
977
+ now: options.now,
978
+ openExternalUrl: options.openExternalUrl
979
+ });
980
+ }
981
+ async function ensureCloudCredentialsForCommand(options) {
982
+ const env = options.env ?? process.env;
983
+ const writeProgress = options.writeProgress ?? options.writeStdout ?? ((message) => process.stdout.write(message));
984
+ const writeStderr = options.writeStderr ?? ((message) => process.stderr.write(message));
985
+ const fetchFn = options.fetchFn ?? fetch;
986
+ const sleep = options.sleep ?? (async (ms) => {
987
+ await new Promise((resolve) => setTimeout(resolve, ms));
988
+ });
989
+ const now = options.now ?? Date.now;
990
+ const openExternalUrl = options.openExternalUrl ?? openDefaultBrowser;
991
+ const store = options.store ?? createMachineCredentialStore({
992
+ env,
993
+ warn: (warning) => {
994
+ writeStderr(`${warning.message} (${warning.path})
995
+ `);
996
+ }
997
+ });
998
+ const { baseUrl, siteUrl } = resolveCloudTarget(options, env, store);
999
+ const initialCredential = resolveCloudCredential({
1000
+ env,
1001
+ apiKeyFlag: options.apiKeyFlag,
1002
+ accessTokenFlag: options.accessTokenFlag
1003
+ });
1004
+ let credential = initialCredential;
1005
+ if (!credential) {
1006
+ const saved = store.readCloudCredential({
1007
+ baseUrl,
1008
+ siteUrl
1009
+ });
1010
+ const freshSaved = saved ? await ensureSavedCredentialIsFresh(saved, {
1011
+ fetchFn,
1012
+ store,
1013
+ now,
1014
+ writeStderr
1015
+ }) : null;
1016
+ if (freshSaved) {
1017
+ credential = {
1018
+ kind: "access-token",
1019
+ source: "saved",
1020
+ token: freshSaved.accessToken,
1021
+ authScheme: "bearer"
1022
+ };
1023
+ }
1024
+ }
1025
+ if (!credential) {
1026
+ if (options.autoLoginIfNeeded && (options.interactive ?? false)) {
1027
+ const loggedIn = await runDeviceLoginFlow({
1028
+ siteUrl,
1029
+ fetchFn,
1030
+ writeProgress,
1031
+ openExternalUrl,
1032
+ sleep,
1033
+ now,
1034
+ openBrowser: true
1035
+ });
1036
+ store.writeCloudCredential({
1037
+ baseUrl,
1038
+ siteUrl,
1039
+ scope: loggedIn.scope,
1040
+ accessToken: loggedIn.accessToken,
1041
+ refreshToken: loggedIn.refreshToken,
1042
+ obtainedAt: now(),
1043
+ expiresAt: loggedIn.expiresAt
1044
+ });
1045
+ credential = {
1046
+ kind: "access-token",
1047
+ source: "saved",
1048
+ token: loggedIn.accessToken,
1049
+ authScheme: "bearer"
1050
+ };
1051
+ writeProgress("Cloud login complete.\n");
1052
+ } else {
1053
+ throw new Error(toAuthMissingMessage(options.commandName));
1054
+ }
1055
+ }
1056
+ store.writeActiveCloudTarget({
1057
+ baseUrl,
1058
+ siteUrl
1059
+ });
1060
+ applyCloudCredentialToEnv(env, credential);
1061
+ env.OPENSTEER_BASE_URL = baseUrl;
1062
+ env.OPENSTEER_CLOUD_SITE_URL = siteUrl;
1063
+ return {
1064
+ token: credential.token,
1065
+ authScheme: credential.authScheme,
1066
+ source: credential.source,
1067
+ kind: credential.kind,
1068
+ baseUrl,
1069
+ siteUrl
1070
+ };
1071
+ }
1072
+ async function runLogin(args, deps) {
1073
+ const { baseUrl, siteUrl } = resolveCloudTarget(args, deps.env, deps.store);
1074
+ const writeProgress = args.json ? deps.writeStderr : deps.writeStdout;
1075
+ const browserOpenMode = describeBrowserOpenMode(args, deps);
1076
+ const login = await runDeviceLoginFlow({
1077
+ siteUrl,
1078
+ fetchFn: deps.fetchFn,
1079
+ writeProgress,
1080
+ openExternalUrl: deps.openExternalUrl,
1081
+ sleep: deps.sleep,
1082
+ now: deps.now,
1083
+ openBrowser: browserOpenMode.enabled,
1084
+ openBrowserDisabledReason: browserOpenMode.disabledReason
1085
+ });
1086
+ deps.store.writeCloudCredential({
1087
+ baseUrl,
1088
+ siteUrl,
1089
+ scope: login.scope,
1090
+ accessToken: login.accessToken,
1091
+ refreshToken: login.refreshToken,
1092
+ obtainedAt: deps.now(),
1093
+ expiresAt: login.expiresAt
1094
+ });
1095
+ deps.store.writeActiveCloudTarget({
1096
+ baseUrl,
1097
+ siteUrl
1098
+ });
1099
+ if (args.json) {
1100
+ writeJsonLine(deps, {
1101
+ loggedIn: true,
1102
+ baseUrl,
1103
+ siteUrl,
1104
+ expiresAt: login.expiresAt,
1105
+ scope: login.scope,
1106
+ authSource: "device"
1107
+ });
1108
+ return 0;
1109
+ }
1110
+ writeHumanLine(deps, "Opensteer CLI login successful.");
1111
+ writeHumanLine(deps, ` Site URL: ${siteUrl}`);
1112
+ writeHumanLine(deps, ` API Base URL: ${baseUrl}`);
1113
+ writeHumanLine(deps, ` Expires At: ${new Date(login.expiresAt).toISOString()}`);
1114
+ return 0;
1115
+ }
1116
+ async function runStatus(args, deps) {
1117
+ const { baseUrl, siteUrl } = resolveCloudTarget(args, deps.env, deps.store);
1118
+ deps.store.writeActiveCloudTarget({
1119
+ baseUrl,
1120
+ siteUrl
1121
+ });
1122
+ const saved = deps.store.readCloudCredential({
1123
+ baseUrl,
1124
+ siteUrl
1125
+ });
1126
+ if (!saved) {
1127
+ if (args.json) {
1128
+ writeJsonLine(deps, {
1129
+ loggedIn: false,
1130
+ baseUrl,
1131
+ siteUrl
1132
+ });
1133
+ } else {
1134
+ writeHumanLine(
1135
+ deps,
1136
+ `Opensteer CLI is not logged in for ${siteUrl}.`
1137
+ );
1138
+ }
1139
+ return 0;
1140
+ }
1141
+ const now = deps.now();
1142
+ const expired = saved.expiresAt <= now;
1143
+ if (args.json) {
1144
+ writeJsonLine(deps, {
1145
+ loggedIn: true,
1146
+ expired,
1147
+ baseUrl: saved.baseUrl,
1148
+ siteUrl: saved.siteUrl,
1149
+ expiresAt: saved.expiresAt,
1150
+ scope: saved.scope
1151
+ });
1152
+ return 0;
1153
+ }
1154
+ writeHumanLine(
1155
+ deps,
1156
+ expired ? "Opensteer CLI has a saved login, but the access token is expired." : "Opensteer CLI is logged in."
1157
+ );
1158
+ writeHumanLine(deps, ` Site URL: ${saved.siteUrl}`);
1159
+ writeHumanLine(deps, ` API Base URL: ${saved.baseUrl}`);
1160
+ writeHumanLine(deps, ` Expires At: ${new Date(saved.expiresAt).toISOString()}`);
1161
+ return 0;
1162
+ }
1163
+ async function runLogout(args, deps) {
1164
+ const { baseUrl, siteUrl } = resolveCloudTarget(args, deps.env, deps.store);
1165
+ deps.store.writeActiveCloudTarget({
1166
+ baseUrl,
1167
+ siteUrl
1168
+ });
1169
+ const saved = deps.store.readCloudCredential({
1170
+ baseUrl,
1171
+ siteUrl
1172
+ });
1173
+ if (saved) {
1174
+ try {
1175
+ await revokeToken(saved.siteUrl, saved.refreshToken, deps.fetchFn);
1176
+ } catch {
1177
+ }
1178
+ }
1179
+ deps.store.clearCloudCredential({
1180
+ baseUrl,
1181
+ siteUrl
1182
+ });
1183
+ if (args.json) {
1184
+ writeJsonLine(deps, {
1185
+ loggedOut: true,
1186
+ baseUrl,
1187
+ siteUrl
1188
+ });
1189
+ return 0;
1190
+ }
1191
+ writeHumanLine(
1192
+ deps,
1193
+ `Opensteer CLI login removed for ${siteUrl}.`
1194
+ );
1195
+ return 0;
1196
+ }
1197
+ async function runOpensteerAuthCli(rawArgs, overrideDeps = {}) {
1198
+ const deps = {
1199
+ ...createDefaultDeps(),
1200
+ ...overrideDeps
1201
+ };
1202
+ const parsed = parseOpensteerAuthArgs(rawArgs);
1203
+ if (parsed.mode === "help") {
1204
+ printHelp(deps);
1205
+ return 0;
1206
+ }
1207
+ if (parsed.mode === "error") {
1208
+ deps.writeStderr(`${parsed.error}
1209
+ `);
1210
+ deps.writeStderr('Run "opensteer auth --help" for usage.\n');
1211
+ return 1;
1212
+ }
1213
+ try {
1214
+ if (parsed.mode === "login") {
1215
+ return await runLogin(parsed.args, deps);
1216
+ }
1217
+ if (parsed.mode === "status") {
1218
+ return await runStatus(parsed.args, deps);
1219
+ }
1220
+ return await runLogout(parsed.args, deps);
1221
+ } catch (error) {
1222
+ const message = error instanceof Error ? error.message : "Auth command failed.";
1223
+ deps.writeStderr(`${message}
1224
+ `);
1225
+ return 1;
1226
+ }
1227
+ }
1228
+
1229
+ export {
1230
+ parseOpensteerAuthArgs,
1231
+ isCloudModeEnabledForRootDir,
1232
+ ensureCloudCredentialsForOpenCommand,
1233
+ ensureCloudCredentialsForCommand,
1234
+ runOpensteerAuthCli
1235
+ };