@doow/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +75 -0
  2. package/dist/cjs/auth/api-key.js +159 -0
  3. package/dist/cjs/auth/api-key.js.map +1 -0
  4. package/dist/cjs/auth/detect.js +173 -0
  5. package/dist/cjs/auth/detect.js.map +1 -0
  6. package/dist/cjs/auth/device-flow.js +135 -0
  7. package/dist/cjs/auth/device-flow.js.map +1 -0
  8. package/dist/cjs/auth/keyring.js +118 -0
  9. package/dist/cjs/auth/keyring.js.map +1 -0
  10. package/dist/cjs/auth/pkce.js +243 -0
  11. package/dist/cjs/auth/pkce.js.map +1 -0
  12. package/dist/cjs/auth/refresh.js +203 -0
  13. package/dist/cjs/auth/refresh.js.map +1 -0
  14. package/dist/cjs/config/env.js +44 -0
  15. package/dist/cjs/config/env.js.map +1 -0
  16. package/dist/cjs/config/store.js +178 -0
  17. package/dist/cjs/config/store.js.map +1 -0
  18. package/dist/cjs/index.js +48 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cli.cjs +34372 -0
  21. package/dist/cli.cjs.map +1 -0
  22. package/dist/esm/auth/api-key.js +154 -0
  23. package/dist/esm/auth/api-key.js.map +1 -0
  24. package/dist/esm/auth/detect.js +150 -0
  25. package/dist/esm/auth/detect.js.map +1 -0
  26. package/dist/esm/auth/device-flow.js +132 -0
  27. package/dist/esm/auth/device-flow.js.map +1 -0
  28. package/dist/esm/auth/keyring.js +116 -0
  29. package/dist/esm/auth/keyring.js.map +1 -0
  30. package/dist/esm/auth/pkce.js +220 -0
  31. package/dist/esm/auth/pkce.js.map +1 -0
  32. package/dist/esm/auth/refresh.js +198 -0
  33. package/dist/esm/auth/refresh.js.map +1 -0
  34. package/dist/esm/config/env.js +38 -0
  35. package/dist/esm/config/env.js.map +1 -0
  36. package/dist/esm/config/store.js +166 -0
  37. package/dist/esm/config/store.js.map +1 -0
  38. package/dist/esm/index.js +15 -0
  39. package/dist/esm/index.js.map +1 -0
  40. package/dist/mcp.cjs +8 -0
  41. package/dist/mcp.cjs.map +1 -0
  42. package/dist/types/index.d.ts +369 -0
  43. package/package.json +62 -0
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ var store = require('../config/store.js');
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Constants
7
+ // ---------------------------------------------------------------------------
8
+ const SERVICE_NAME = 'doow-cli';
9
+ const PROBE_TIMEOUT_MS = 3_000;
10
+ const OP_TIMEOUT_MS = 3_000;
11
+ // Module-level flag — only warn once per process lifetime.
12
+ let hasPrintedFallbackWarning = false;
13
+ // ---------------------------------------------------------------------------
14
+ // Timeout helper
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Races `promise` against a rejection that fires after `ms` milliseconds.
18
+ * Avoids AbortSignal.timeout which requires Node >=17.3.
19
+ */
20
+ function withTimeout(promise, ms) {
21
+ return Promise.race([
22
+ promise,
23
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms)),
24
+ ]);
25
+ }
26
+ // ---------------------------------------------------------------------------
27
+ // One-time fallback warning
28
+ // ---------------------------------------------------------------------------
29
+ function printFallbackWarning() {
30
+ if (hasPrintedFallbackWarning)
31
+ return;
32
+ hasPrintedFallbackWarning = true;
33
+ process.stderr.write('⚠ System keyring unavailable — credentials stored in ~/.doow/credentials.json (chmod 0600)\n');
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // File-backed store (wraps S118 store.ts functions)
37
+ // ---------------------------------------------------------------------------
38
+ function createFileStore() {
39
+ return {
40
+ async get(profileName) {
41
+ return store.getProfileCredentials(profileName);
42
+ },
43
+ async set(profileName, creds) {
44
+ return store.setProfileCredentials(profileName, creds);
45
+ },
46
+ async clear(profileName) {
47
+ return store.clearProfileCredentials(profileName);
48
+ },
49
+ backend() {
50
+ return 'file';
51
+ },
52
+ };
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Keyring-backed store
56
+ // ---------------------------------------------------------------------------
57
+ function createKeyringStore(keytar) {
58
+ return {
59
+ async get(profileName) {
60
+ try {
61
+ const raw = await withTimeout(keytar.getPassword(SERVICE_NAME, profileName), OP_TIMEOUT_MS);
62
+ if (raw == null)
63
+ return undefined;
64
+ return JSON.parse(raw);
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ },
70
+ async set(profileName, creds) {
71
+ await withTimeout(keytar.setPassword(SERVICE_NAME, profileName, JSON.stringify(creds)), OP_TIMEOUT_MS);
72
+ },
73
+ async clear(profileName) {
74
+ await withTimeout(keytar.deletePassword(SERVICE_NAME, profileName), OP_TIMEOUT_MS);
75
+ },
76
+ backend() {
77
+ return 'keyring';
78
+ },
79
+ };
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // Factory
83
+ // ---------------------------------------------------------------------------
84
+ /**
85
+ * Creates a CredentialStore backed by the system keyring when available,
86
+ * falling back to file storage silently when keytar is not installed or
87
+ * the system keyring does not respond within 3 seconds.
88
+ *
89
+ * Call this once at startup and reuse the returned instance.
90
+ */
91
+ async function createCredentialStore() {
92
+ // Step 1: Try loading keytar (optional peer dependency).
93
+ let keytar;
94
+ try {
95
+ // Dynamic import so missing keytar never crashes the module at load time.
96
+ // @ts-expect-error — keytar is an optional peer dep and may not be installed.
97
+ const mod = await import('keytar');
98
+ // Support both default-export and named-export module shapes.
99
+ keytar = (mod.default ?? mod);
100
+ }
101
+ catch {
102
+ // keytar not installed — silent, no warning needed (not an error condition).
103
+ return createFileStore();
104
+ }
105
+ // Step 2: Probe the keyring with a 3s timeout to confirm it's responsive.
106
+ try {
107
+ await withTimeout(keytar.findPassword(SERVICE_NAME), PROBE_TIMEOUT_MS);
108
+ }
109
+ catch {
110
+ printFallbackWarning();
111
+ return createFileStore();
112
+ }
113
+ // Step 3: Keyring is available and responsive — use it.
114
+ return createKeyringStore(keytar);
115
+ }
116
+
117
+ exports.createCredentialStore = createCredentialStore;
118
+ //# sourceMappingURL=keyring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyring.js","sources":["../../../../src/auth/keyring.ts"],"sourcesContent":[null],"names":["getProfileCredentials","setProfileCredentials","clearProfileCredentials"],"mappings":"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"}
@@ -0,0 +1,243 @@
1
+ 'use strict';
2
+
3
+ var http = require('node:http');
4
+ var crypto = require('node:crypto');
5
+ var env = require('../config/env.js');
6
+ var store = require('../config/store.js');
7
+ var keyring = require('./keyring.js');
8
+
9
+ function _interopNamespaceDefault(e) {
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var http__namespace = /*#__PURE__*/_interopNamespaceDefault(http);
27
+ var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
28
+
29
+ /**
30
+ * pkce.ts
31
+ *
32
+ * OAuth 2.0 PKCE (RFC 7636) interactive login flow for the Doow CLI.
33
+ *
34
+ * Steps:
35
+ * 1. Generate PKCE code_verifier + code_challenge
36
+ * 2. Generate CSRF state token
37
+ * 3. Start a localhost HTTP server on a random port
38
+ * 4. Open browser to the authorize URL
39
+ * 5. Wait for the OAuth callback (120 s default timeout)
40
+ * 6. Exchange authorization code for tokens
41
+ * 7. Store tokens via CredentialStore
42
+ * 8. Close server and return result
43
+ */
44
+ // ---------------------------------------------------------------------------
45
+ // HTML responses
46
+ // ---------------------------------------------------------------------------
47
+ const SUCCESS_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:40px">
48
+ <h2>Logged in to Doow</h2><p>You can close this tab.</p>
49
+ </body></html>`;
50
+ const ERROR_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:40px">
51
+ <h2>Login failed</h2><p>CSRF state mismatch. Please try again.</p>
52
+ </body></html>`;
53
+ // ---------------------------------------------------------------------------
54
+ // PKCE helpers
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Generates a PKCE code_verifier and code_challenge pair.
58
+ * - code_verifier: 32 random bytes, base64url-encoded (43 chars)
59
+ * - code_challenge: SHA-256 of verifier, base64url-encoded
60
+ */
61
+ function generatePkcePair() {
62
+ const verifierBytes = crypto__namespace.randomBytes(32);
63
+ const codeVerifier = verifierBytes
64
+ .toString('base64')
65
+ .replace(/\+/g, '-')
66
+ .replace(/\//g, '_')
67
+ .replace(/=/g, '');
68
+ const challengeBytes = crypto__namespace.createHash('sha256').update(codeVerifier).digest();
69
+ const codeChallenge = challengeBytes
70
+ .toString('base64')
71
+ .replace(/\+/g, '-')
72
+ .replace(/\//g, '_')
73
+ .replace(/=/g, '');
74
+ return { codeVerifier, codeChallenge };
75
+ }
76
+ /**
77
+ * Generates a random CSRF state token (32 bytes, base64url).
78
+ */
79
+ function generateState() {
80
+ return crypto__namespace
81
+ .randomBytes(32)
82
+ .toString('base64')
83
+ .replace(/\+/g, '-')
84
+ .replace(/\//g, '_')
85
+ .replace(/=/g, '');
86
+ }
87
+ /**
88
+ * Starts a localhost HTTP server on a random port and waits for the OAuth
89
+ * callback. Resolves with { code, state } on success.
90
+ * Rejects if the CSRF state doesn't match.
91
+ */
92
+ function startCallbackServer(expectedState, timeoutMs) {
93
+ return new Promise((resolveServer, rejectServer) => {
94
+ const server = http__namespace.createServer();
95
+ const callbackPromise = new Promise((resolveCallback, rejectCallback) => {
96
+ let timeoutHandle;
97
+ // Set the timeout for the callback wait — server.close() is handled
98
+ // by the finally block in executePkceFlow, not here.
99
+ timeoutHandle = setTimeout(() => {
100
+ rejectCallback(new Error('Login timed out after 120 seconds. Try doow login --device for headless environments.'));
101
+ }, timeoutMs);
102
+ server.on('request', (req, res) => {
103
+ // Only handle /callback path
104
+ if (!req.url?.startsWith('/callback')) {
105
+ res.writeHead(404);
106
+ res.end('Not found');
107
+ return;
108
+ }
109
+ const url = new URL(req.url, 'http://127.0.0.1');
110
+ const code = url.searchParams.get('code');
111
+ const state = url.searchParams.get('state');
112
+ const errorParam = url.searchParams.get('error');
113
+ // Handle OAuth error from server
114
+ if (errorParam) {
115
+ clearTimeout(timeoutHandle);
116
+ res.writeHead(400, { 'Content-Type': 'text/html' });
117
+ res.end(ERROR_HTML);
118
+ rejectCallback(new Error(`Authorization failed: ${errorParam}`));
119
+ return;
120
+ }
121
+ // Validate required params
122
+ if (!code || !state) {
123
+ res.writeHead(400, { 'Content-Type': 'text/html' });
124
+ res.end(ERROR_HTML);
125
+ return;
126
+ }
127
+ // CSRF state validation
128
+ if (state !== expectedState) {
129
+ clearTimeout(timeoutHandle);
130
+ res.writeHead(400, { 'Content-Type': 'text/html' });
131
+ res.end(ERROR_HTML);
132
+ rejectCallback(new Error('CSRF state mismatch — possible attack. Try again.'));
133
+ return;
134
+ }
135
+ // Success
136
+ clearTimeout(timeoutHandle);
137
+ res.writeHead(200, { 'Content-Type': 'text/html' });
138
+ res.end(SUCCESS_HTML);
139
+ resolveCallback({ code, state });
140
+ });
141
+ });
142
+ server.on('error', (err) => {
143
+ rejectServer(new Error(`Failed to start local callback server: ${err.message}. Try doow login --device for headless environments.`));
144
+ });
145
+ // Bind to port 0 — OS assigns a random available port
146
+ server.listen(0, '127.0.0.1', () => {
147
+ const addr = server.address();
148
+ if (!addr || typeof addr === 'string') {
149
+ server.close();
150
+ rejectServer(new Error('Failed to determine callback server port. Try doow login --device for headless environments.'));
151
+ return;
152
+ }
153
+ resolveServer({ server, port: addr.port, callbackPromise });
154
+ });
155
+ });
156
+ }
157
+ // ---------------------------------------------------------------------------
158
+ // Token exchange
159
+ // ---------------------------------------------------------------------------
160
+ async function exchangeCodeForTokens(apiUrl, code, codeVerifier, state) {
161
+ const response = await fetch(`${apiUrl}/v1/auth/cli/token`, {
162
+ method: 'POST',
163
+ headers: { 'Content-Type': 'application/json' },
164
+ body: JSON.stringify({
165
+ grant_type: 'authorization_code',
166
+ code,
167
+ code_verifier: codeVerifier,
168
+ state,
169
+ }),
170
+ });
171
+ if (!response.ok) {
172
+ const body = await response.text().catch(() => '(no body)');
173
+ throw new Error(`Token exchange failed: HTTP ${response.status} — ${body}`);
174
+ }
175
+ return response.json();
176
+ }
177
+ // ---------------------------------------------------------------------------
178
+ // Main flow
179
+ // ---------------------------------------------------------------------------
180
+ /**
181
+ * Executes the full PKCE browser-based OAuth 2.0 login flow.
182
+ *
183
+ * Opens the system browser to the Doow authorize endpoint, waits for the
184
+ * localhost callback, exchanges the authorization code for tokens, and
185
+ * persists the tokens via the credential store.
186
+ */
187
+ async function executePkceFlow(options = {}) {
188
+ // Resolve options
189
+ const profile = await store.getActiveProfile();
190
+ const apiUrl = options.apiUrl ?? env.getApiUrl(profile);
191
+ const profileName = options.profileName ?? profile.name;
192
+ const credentialStore = options.credentialStore ?? (await keyring.createCredentialStore());
193
+ const timeout = options.timeout ?? 120_000;
194
+ // Step 1 & 2: Generate PKCE pair and CSRF state
195
+ const { codeVerifier, codeChallenge } = generatePkcePair();
196
+ const state = generateState();
197
+ // Step 3: Start callback server
198
+ const { server, port, callbackPromise } = await startCallbackServer(state, timeout);
199
+ try {
200
+ // Step 4: Build authorize URL
201
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
202
+ const authorizeUrl = `${apiUrl}/v1/auth/cli/authorize` +
203
+ `?response_type=code` +
204
+ `&code_challenge=${codeChallenge}` +
205
+ `&code_challenge_method=S256` +
206
+ `&state=${state}` +
207
+ `&redirect_uri=${encodeURIComponent(redirectUri)}`;
208
+ // Step 5: Open browser
209
+ try {
210
+ const { default: open } = await import('open');
211
+ await open(authorizeUrl);
212
+ }
213
+ catch (err) {
214
+ const msg = err instanceof Error ? err.message : String(err);
215
+ throw new Error(`Failed to open browser: ${msg}. Try doow login --device for headless environments.`);
216
+ }
217
+ // Step 6: Wait for callback
218
+ const { code } = await callbackPromise;
219
+ // Step 7: Exchange code for tokens
220
+ const tokenResponse = await exchangeCodeForTokens(apiUrl, code, codeVerifier, state);
221
+ // Step 8: Store tokens
222
+ const expiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString();
223
+ const result = {
224
+ accessToken: tokenResponse.access_token,
225
+ refreshToken: tokenResponse.refresh_token,
226
+ expiresAt,
227
+ };
228
+ await credentialStore.set(profileName, {
229
+ accessToken: result.accessToken,
230
+ refreshToken: result.refreshToken,
231
+ expiresAt: result.expiresAt,
232
+ });
233
+ return result;
234
+ }
235
+ finally {
236
+ // Always close the server — success, error, or timeout
237
+ server.close();
238
+ }
239
+ }
240
+
241
+ exports.executePkceFlow = executePkceFlow;
242
+ exports.generatePkcePair = generatePkcePair;
243
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sources":["../../../../src/auth/pkce.ts"],"sourcesContent":[null],"names":["crypto","http","getActiveProfile","getApiUrl","createCredentialStore"],"mappings":"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"}
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+
3
+ var promises = require('node:fs/promises');
4
+ var node_path = require('node:path');
5
+ var store = require('../config/store.js');
6
+ var env = require('../config/env.js');
7
+ var keyring = require('./keyring.js');
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Constants
11
+ // ---------------------------------------------------------------------------
12
+ const REFRESH_BUFFER_SECONDS = 60;
13
+ const STALE_LOCK_THRESHOLD_MS = 60_000;
14
+ const LOCK_RETRY_DELAY_MS = 500;
15
+ const LOCK_MAX_RETRIES = 10;
16
+ // ---------------------------------------------------------------------------
17
+ // needsRefresh
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Returns true when the credential token needs a refresh:
21
+ * - no expiresAt present, OR
22
+ * - token expires within the 60-second buffer window
23
+ *
24
+ * Pure function — no I/O.
25
+ */
26
+ function needsRefresh(creds) {
27
+ if (!creds.expiresAt)
28
+ return true;
29
+ const expiresAt = new Date(creds.expiresAt).getTime();
30
+ if (isNaN(expiresAt))
31
+ return true;
32
+ const bufferMs = REFRESH_BUFFER_SECONDS * 1_000;
33
+ return Date.now() >= expiresAt - bufferMs;
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Lockfile helpers
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * Returns true when the process identified by `pid` is still running.
40
+ * Uses kill(pid, 0) which sends no signal but checks for process existence.
41
+ */
42
+ function isPidAlive(pid) {
43
+ try {
44
+ process.kill(pid, 0);
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ /**
52
+ * Acquires an exclusive per-profile lock file.
53
+ *
54
+ * Uses O_EXCL (writeFile flag:'wx') for atomic exclusive creation.
55
+ * If a lock exists, checks staleness (>60s or dead PID) and cleans up if stale.
56
+ * Retries up to 10 times with 500ms back-off before throwing.
57
+ */
58
+ async function acquireLock(profileName) {
59
+ const configDir = await store.getConfigDir();
60
+ const lockPath = node_path.join(configDir, `.${profileName}.refresh.lock`);
61
+ for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
62
+ try {
63
+ const content = { pid: process.pid, timestamp: Date.now() };
64
+ await promises.writeFile(lockPath, JSON.stringify(content), { flag: 'wx', mode: 0o600 });
65
+ // Acquired — return handle with inline release
66
+ return {
67
+ lockPath,
68
+ async release() {
69
+ await promises.unlink(lockPath).catch(() => undefined);
70
+ },
71
+ };
72
+ }
73
+ catch (err) {
74
+ // Only handle EEXIST — lock file already exists
75
+ if (!isEexist(err))
76
+ throw err;
77
+ // Read existing lock and decide whether it's stale
78
+ const isStale = await checkAndCleanStaleLock(lockPath);
79
+ if (isStale) {
80
+ // Stale lock deleted — retry immediately (don't sleep)
81
+ continue;
82
+ }
83
+ // Live lock — wait before retrying
84
+ if (attempt < LOCK_MAX_RETRIES - 1) {
85
+ await sleep(LOCK_RETRY_DELAY_MS);
86
+ }
87
+ }
88
+ }
89
+ throw new Error('Could not acquire refresh lock. Another process may be refreshing.');
90
+ }
91
+ /**
92
+ * Reads the lock file at `lockPath`, checks whether it's stale, and
93
+ * if so deletes it. Returns true if the lock was stale (and deleted).
94
+ */
95
+ async function checkAndCleanStaleLock(lockPath) {
96
+ let content;
97
+ try {
98
+ const raw = await promises.readFile(lockPath, 'utf-8');
99
+ content = JSON.parse(raw);
100
+ }
101
+ catch {
102
+ // File vanished between our EEXIST and this read — treat as stale
103
+ return true;
104
+ }
105
+ const isOlderThanThreshold = Date.now() - content.timestamp > STALE_LOCK_THRESHOLD_MS;
106
+ const isProcDead = !isPidAlive(content.pid);
107
+ if (isOlderThanThreshold || isProcDead) {
108
+ await promises.unlink(lockPath).catch(() => undefined);
109
+ return true;
110
+ }
111
+ return false;
112
+ }
113
+ /**
114
+ * Releases a previously acquired lock handle. Best-effort — never throws.
115
+ */
116
+ async function releaseLock(handle) {
117
+ await handle.release();
118
+ }
119
+ // ---------------------------------------------------------------------------
120
+ // refreshToken
121
+ // ---------------------------------------------------------------------------
122
+ /**
123
+ * Performs a transparent token refresh with double-checked locking.
124
+ *
125
+ * 1. Acquires a per-profile lockfile
126
+ * 2. Re-reads credentials — another process may have already refreshed
127
+ * 3. If tokens are still fresh, skips the network call (wasRefreshed: false)
128
+ * 4. Otherwise calls POST /v1/auth/refresh and stores the new tokens
129
+ * 5. Always releases the lock in a finally block
130
+ *
131
+ * Throws with a user-friendly message if the session has expired.
132
+ */
133
+ async function refreshToken(options = {}) {
134
+ const profileName = options.profileName ?? 'default';
135
+ const store = options.credentialStore ?? (await keyring.createCredentialStore());
136
+ const apiUrl = options.apiUrl ?? env.getApiUrl();
137
+ const lock = await acquireLock(profileName);
138
+ try {
139
+ // Double-checked locking: re-read credentials now that we hold the lock.
140
+ // Another process may have already refreshed while we waited.
141
+ const latestCreds = await store.get(profileName);
142
+ if (latestCreds && !needsRefresh(latestCreds)) {
143
+ // Already refreshed by another process — return current tokens.
144
+ return {
145
+ accessToken: latestCreds.accessToken ?? '',
146
+ refreshToken: latestCreds.refreshToken ?? '',
147
+ expiresAt: latestCreds.expiresAt ?? '',
148
+ wasRefreshed: false,
149
+ };
150
+ }
151
+ // We need to refresh — verify we have a refresh token to use.
152
+ if (!latestCreds?.refreshToken) {
153
+ await store.clear(profileName);
154
+ throw new Error('Session expired. Run doow login to re-authenticate.');
155
+ }
156
+ // Call the refresh endpoint.
157
+ const response = await fetch(`${apiUrl}/v1/auth/refresh`, {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({ refresh_token: latestCreds.refreshToken }),
161
+ });
162
+ if (!response.ok) {
163
+ // 401 or any non-2xx → session expired
164
+ await store.clear(profileName);
165
+ throw new Error('Session expired. Run doow login to re-authenticate.');
166
+ }
167
+ const data = (await response.json());
168
+ const expiresAt = new Date(Date.now() + data.expires_in * 1_000).toISOString();
169
+ const newCreds = {
170
+ accessToken: data.access_token,
171
+ refreshToken: data.refresh_token,
172
+ expiresAt,
173
+ };
174
+ await store.set(profileName, newCreds);
175
+ return {
176
+ accessToken: data.access_token,
177
+ refreshToken: data.refresh_token,
178
+ expiresAt,
179
+ wasRefreshed: true,
180
+ };
181
+ }
182
+ finally {
183
+ await lock.release();
184
+ }
185
+ }
186
+ // ---------------------------------------------------------------------------
187
+ // Utilities
188
+ // ---------------------------------------------------------------------------
189
+ function isEexist(err) {
190
+ return (typeof err === 'object' &&
191
+ err !== null &&
192
+ 'code' in err &&
193
+ err.code === 'EEXIST');
194
+ }
195
+ function sleep(ms) {
196
+ return new Promise((resolve) => setTimeout(resolve, ms));
197
+ }
198
+
199
+ exports.acquireLock = acquireLock;
200
+ exports.needsRefresh = needsRefresh;
201
+ exports.refreshToken = refreshToken;
202
+ exports.releaseLock = releaseLock;
203
+ //# sourceMappingURL=refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.js","sources":["../../../../src/auth/refresh.ts"],"sourcesContent":[null],"names":["getConfigDir","join","writeFile","unlink","readFile","createCredentialStore","getApiUrl"],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ /** True when stdout is an interactive terminal. */
4
+ function isTTY() {
5
+ return process.stdout.isTTY === true;
6
+ }
7
+ /** True when running inside a CI environment (any common CI sets $CI). */
8
+ function isCI() {
9
+ return !!process.env['CI'];
10
+ }
11
+ /**
12
+ * True when the CLI is invoked by an automated agent rather than a human.
13
+ * Detected via $DOOW_AGENT_MODE env var or the --agent CLI flag (which
14
+ * Commander stores on the global options object as `process.env` is the
15
+ * canonical signal here — Commander integration is wired up in cli.ts).
16
+ */
17
+ function isAgentMode() {
18
+ return !!process.env['DOOW_AGENT_MODE'];
19
+ }
20
+ /**
21
+ * True when interactive UI (spinners, prompts, color) should be shown.
22
+ * Requires a real TTY, no CI env, and not running in agent mode.
23
+ */
24
+ function shouldShowUI() {
25
+ return isTTY() && !isCI() && !isAgentMode();
26
+ }
27
+ /**
28
+ * Resolve the API base URL.
29
+ * Precedence: profile.apiUrl → $DOOW_API_URL → hardcoded default.
30
+ */
31
+ function getApiUrl(profile) {
32
+ if (profile?.apiUrl)
33
+ return profile.apiUrl;
34
+ if (process.env['DOOW_API_URL'])
35
+ return process.env['DOOW_API_URL'];
36
+ return 'https://api.doow.com';
37
+ }
38
+
39
+ exports.getApiUrl = getApiUrl;
40
+ exports.isAgentMode = isAgentMode;
41
+ exports.isCI = isCI;
42
+ exports.isTTY = isTTY;
43
+ exports.shouldShowUI = shouldShowUI;
44
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.js","sources":["../../../../src/config/env.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAEA;SACgB,KAAK,GAAA;AACnB,IAAA,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI;AACtC;AAEA;SACgB,IAAI,GAAA;IAClB,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;AAC5B;AAEA;;;;;AAKG;SACa,WAAW,GAAA;IACzB,OAAO,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;AACzC;AAEA;;;AAGG;SACa,YAAY,GAAA;IAC1B,OAAO,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE;AAC7C;AAEA;;;AAGG;AACG,SAAU,SAAS,CAAC,OAAiB,EAAA;IACzC,IAAI,OAAO,EAAE,MAAM;QAAE,OAAO,OAAO,CAAC,MAAM;AAC1C,IAAA,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;AAAE,QAAA,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;AACnE,IAAA,OAAO,sBAAsB;AAC/B;;;;;;;;"}