@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.
- package/README.md +75 -0
- package/dist/cjs/auth/api-key.js +159 -0
- package/dist/cjs/auth/api-key.js.map +1 -0
- package/dist/cjs/auth/detect.js +173 -0
- package/dist/cjs/auth/detect.js.map +1 -0
- package/dist/cjs/auth/device-flow.js +135 -0
- package/dist/cjs/auth/device-flow.js.map +1 -0
- package/dist/cjs/auth/keyring.js +118 -0
- package/dist/cjs/auth/keyring.js.map +1 -0
- package/dist/cjs/auth/pkce.js +243 -0
- package/dist/cjs/auth/pkce.js.map +1 -0
- package/dist/cjs/auth/refresh.js +203 -0
- package/dist/cjs/auth/refresh.js.map +1 -0
- package/dist/cjs/config/env.js +44 -0
- package/dist/cjs/config/env.js.map +1 -0
- package/dist/cjs/config/store.js +178 -0
- package/dist/cjs/config/store.js.map +1 -0
- package/dist/cjs/index.js +48 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cli.cjs +34372 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/esm/auth/api-key.js +154 -0
- package/dist/esm/auth/api-key.js.map +1 -0
- package/dist/esm/auth/detect.js +150 -0
- package/dist/esm/auth/detect.js.map +1 -0
- package/dist/esm/auth/device-flow.js +132 -0
- package/dist/esm/auth/device-flow.js.map +1 -0
- package/dist/esm/auth/keyring.js +116 -0
- package/dist/esm/auth/keyring.js.map +1 -0
- package/dist/esm/auth/pkce.js +220 -0
- package/dist/esm/auth/pkce.js.map +1 -0
- package/dist/esm/auth/refresh.js +198 -0
- package/dist/esm/auth/refresh.js.map +1 -0
- package/dist/esm/config/env.js +38 -0
- package/dist/esm/config/env.js.map +1 -0
- package/dist/esm/config/store.js +166 -0
- package/dist/esm/config/store.js.map +1 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/mcp.cjs +8 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/types/index.d.ts +369 -0
- package/package.json +62 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import * as crypto from 'node:crypto';
|
|
3
|
+
import { getApiUrl } from '../config/env.js';
|
|
4
|
+
import { getActiveProfile } from '../config/store.js';
|
|
5
|
+
import { createCredentialStore } from './keyring.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* pkce.ts
|
|
9
|
+
*
|
|
10
|
+
* OAuth 2.0 PKCE (RFC 7636) interactive login flow for the Doow CLI.
|
|
11
|
+
*
|
|
12
|
+
* Steps:
|
|
13
|
+
* 1. Generate PKCE code_verifier + code_challenge
|
|
14
|
+
* 2. Generate CSRF state token
|
|
15
|
+
* 3. Start a localhost HTTP server on a random port
|
|
16
|
+
* 4. Open browser to the authorize URL
|
|
17
|
+
* 5. Wait for the OAuth callback (120 s default timeout)
|
|
18
|
+
* 6. Exchange authorization code for tokens
|
|
19
|
+
* 7. Store tokens via CredentialStore
|
|
20
|
+
* 8. Close server and return result
|
|
21
|
+
*/
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// HTML responses
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
const SUCCESS_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:40px">
|
|
26
|
+
<h2>Logged in to Doow</h2><p>You can close this tab.</p>
|
|
27
|
+
</body></html>`;
|
|
28
|
+
const ERROR_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;text-align:center;padding:40px">
|
|
29
|
+
<h2>Login failed</h2><p>CSRF state mismatch. Please try again.</p>
|
|
30
|
+
</body></html>`;
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// PKCE helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Generates a PKCE code_verifier and code_challenge pair.
|
|
36
|
+
* - code_verifier: 32 random bytes, base64url-encoded (43 chars)
|
|
37
|
+
* - code_challenge: SHA-256 of verifier, base64url-encoded
|
|
38
|
+
*/
|
|
39
|
+
function generatePkcePair() {
|
|
40
|
+
const verifierBytes = crypto.randomBytes(32);
|
|
41
|
+
const codeVerifier = verifierBytes
|
|
42
|
+
.toString('base64')
|
|
43
|
+
.replace(/\+/g, '-')
|
|
44
|
+
.replace(/\//g, '_')
|
|
45
|
+
.replace(/=/g, '');
|
|
46
|
+
const challengeBytes = crypto.createHash('sha256').update(codeVerifier).digest();
|
|
47
|
+
const codeChallenge = challengeBytes
|
|
48
|
+
.toString('base64')
|
|
49
|
+
.replace(/\+/g, '-')
|
|
50
|
+
.replace(/\//g, '_')
|
|
51
|
+
.replace(/=/g, '');
|
|
52
|
+
return { codeVerifier, codeChallenge };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generates a random CSRF state token (32 bytes, base64url).
|
|
56
|
+
*/
|
|
57
|
+
function generateState() {
|
|
58
|
+
return crypto
|
|
59
|
+
.randomBytes(32)
|
|
60
|
+
.toString('base64')
|
|
61
|
+
.replace(/\+/g, '-')
|
|
62
|
+
.replace(/\//g, '_')
|
|
63
|
+
.replace(/=/g, '');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Starts a localhost HTTP server on a random port and waits for the OAuth
|
|
67
|
+
* callback. Resolves with { code, state } on success.
|
|
68
|
+
* Rejects if the CSRF state doesn't match.
|
|
69
|
+
*/
|
|
70
|
+
function startCallbackServer(expectedState, timeoutMs) {
|
|
71
|
+
return new Promise((resolveServer, rejectServer) => {
|
|
72
|
+
const server = http.createServer();
|
|
73
|
+
const callbackPromise = new Promise((resolveCallback, rejectCallback) => {
|
|
74
|
+
let timeoutHandle;
|
|
75
|
+
// Set the timeout for the callback wait — server.close() is handled
|
|
76
|
+
// by the finally block in executePkceFlow, not here.
|
|
77
|
+
timeoutHandle = setTimeout(() => {
|
|
78
|
+
rejectCallback(new Error('Login timed out after 120 seconds. Try doow login --device for headless environments.'));
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
server.on('request', (req, res) => {
|
|
81
|
+
// Only handle /callback path
|
|
82
|
+
if (!req.url?.startsWith('/callback')) {
|
|
83
|
+
res.writeHead(404);
|
|
84
|
+
res.end('Not found');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
88
|
+
const code = url.searchParams.get('code');
|
|
89
|
+
const state = url.searchParams.get('state');
|
|
90
|
+
const errorParam = url.searchParams.get('error');
|
|
91
|
+
// Handle OAuth error from server
|
|
92
|
+
if (errorParam) {
|
|
93
|
+
clearTimeout(timeoutHandle);
|
|
94
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
95
|
+
res.end(ERROR_HTML);
|
|
96
|
+
rejectCallback(new Error(`Authorization failed: ${errorParam}`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Validate required params
|
|
100
|
+
if (!code || !state) {
|
|
101
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
102
|
+
res.end(ERROR_HTML);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// CSRF state validation
|
|
106
|
+
if (state !== expectedState) {
|
|
107
|
+
clearTimeout(timeoutHandle);
|
|
108
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
109
|
+
res.end(ERROR_HTML);
|
|
110
|
+
rejectCallback(new Error('CSRF state mismatch — possible attack. Try again.'));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Success
|
|
114
|
+
clearTimeout(timeoutHandle);
|
|
115
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
116
|
+
res.end(SUCCESS_HTML);
|
|
117
|
+
resolveCallback({ code, state });
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
server.on('error', (err) => {
|
|
121
|
+
rejectServer(new Error(`Failed to start local callback server: ${err.message}. Try doow login --device for headless environments.`));
|
|
122
|
+
});
|
|
123
|
+
// Bind to port 0 — OS assigns a random available port
|
|
124
|
+
server.listen(0, '127.0.0.1', () => {
|
|
125
|
+
const addr = server.address();
|
|
126
|
+
if (!addr || typeof addr === 'string') {
|
|
127
|
+
server.close();
|
|
128
|
+
rejectServer(new Error('Failed to determine callback server port. Try doow login --device for headless environments.'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
resolveServer({ server, port: addr.port, callbackPromise });
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Token exchange
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
async function exchangeCodeForTokens(apiUrl, code, codeVerifier, state) {
|
|
139
|
+
const response = await fetch(`${apiUrl}/v1/auth/cli/token`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
grant_type: 'authorization_code',
|
|
144
|
+
code,
|
|
145
|
+
code_verifier: codeVerifier,
|
|
146
|
+
state,
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const body = await response.text().catch(() => '(no body)');
|
|
151
|
+
throw new Error(`Token exchange failed: HTTP ${response.status} — ${body}`);
|
|
152
|
+
}
|
|
153
|
+
return response.json();
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Main flow
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Executes the full PKCE browser-based OAuth 2.0 login flow.
|
|
160
|
+
*
|
|
161
|
+
* Opens the system browser to the Doow authorize endpoint, waits for the
|
|
162
|
+
* localhost callback, exchanges the authorization code for tokens, and
|
|
163
|
+
* persists the tokens via the credential store.
|
|
164
|
+
*/
|
|
165
|
+
async function executePkceFlow(options = {}) {
|
|
166
|
+
// Resolve options
|
|
167
|
+
const profile = await getActiveProfile();
|
|
168
|
+
const apiUrl = options.apiUrl ?? getApiUrl(profile);
|
|
169
|
+
const profileName = options.profileName ?? profile.name;
|
|
170
|
+
const credentialStore = options.credentialStore ?? (await createCredentialStore());
|
|
171
|
+
const timeout = options.timeout ?? 120_000;
|
|
172
|
+
// Step 1 & 2: Generate PKCE pair and CSRF state
|
|
173
|
+
const { codeVerifier, codeChallenge } = generatePkcePair();
|
|
174
|
+
const state = generateState();
|
|
175
|
+
// Step 3: Start callback server
|
|
176
|
+
const { server, port, callbackPromise } = await startCallbackServer(state, timeout);
|
|
177
|
+
try {
|
|
178
|
+
// Step 4: Build authorize URL
|
|
179
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
180
|
+
const authorizeUrl = `${apiUrl}/v1/auth/cli/authorize` +
|
|
181
|
+
`?response_type=code` +
|
|
182
|
+
`&code_challenge=${codeChallenge}` +
|
|
183
|
+
`&code_challenge_method=S256` +
|
|
184
|
+
`&state=${state}` +
|
|
185
|
+
`&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
186
|
+
// Step 5: Open browser
|
|
187
|
+
try {
|
|
188
|
+
const { default: open } = await import('open');
|
|
189
|
+
await open(authorizeUrl);
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
193
|
+
throw new Error(`Failed to open browser: ${msg}. Try doow login --device for headless environments.`);
|
|
194
|
+
}
|
|
195
|
+
// Step 6: Wait for callback
|
|
196
|
+
const { code } = await callbackPromise;
|
|
197
|
+
// Step 7: Exchange code for tokens
|
|
198
|
+
const tokenResponse = await exchangeCodeForTokens(apiUrl, code, codeVerifier, state);
|
|
199
|
+
// Step 8: Store tokens
|
|
200
|
+
const expiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString();
|
|
201
|
+
const result = {
|
|
202
|
+
accessToken: tokenResponse.access_token,
|
|
203
|
+
refreshToken: tokenResponse.refresh_token,
|
|
204
|
+
expiresAt,
|
|
205
|
+
};
|
|
206
|
+
await credentialStore.set(profileName, {
|
|
207
|
+
accessToken: result.accessToken,
|
|
208
|
+
refreshToken: result.refreshToken,
|
|
209
|
+
expiresAt: result.expiresAt,
|
|
210
|
+
});
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
// Always close the server — success, error, or timeout
|
|
215
|
+
server.close();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export { executePkceFlow, generatePkcePair };
|
|
220
|
+
//# sourceMappingURL=pkce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","sources":["../../../../src/auth/pkce.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { writeFile, unlink, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getConfigDir } from '../config/store.js';
|
|
4
|
+
import { getApiUrl } from '../config/env.js';
|
|
5
|
+
import { createCredentialStore } from './keyring.js';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Constants
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const REFRESH_BUFFER_SECONDS = 60;
|
|
11
|
+
const STALE_LOCK_THRESHOLD_MS = 60_000;
|
|
12
|
+
const LOCK_RETRY_DELAY_MS = 500;
|
|
13
|
+
const LOCK_MAX_RETRIES = 10;
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// needsRefresh
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/**
|
|
18
|
+
* Returns true when the credential token needs a refresh:
|
|
19
|
+
* - no expiresAt present, OR
|
|
20
|
+
* - token expires within the 60-second buffer window
|
|
21
|
+
*
|
|
22
|
+
* Pure function — no I/O.
|
|
23
|
+
*/
|
|
24
|
+
function needsRefresh(creds) {
|
|
25
|
+
if (!creds.expiresAt)
|
|
26
|
+
return true;
|
|
27
|
+
const expiresAt = new Date(creds.expiresAt).getTime();
|
|
28
|
+
if (isNaN(expiresAt))
|
|
29
|
+
return true;
|
|
30
|
+
const bufferMs = REFRESH_BUFFER_SECONDS * 1_000;
|
|
31
|
+
return Date.now() >= expiresAt - bufferMs;
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Lockfile helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Returns true when the process identified by `pid` is still running.
|
|
38
|
+
* Uses kill(pid, 0) which sends no signal but checks for process existence.
|
|
39
|
+
*/
|
|
40
|
+
function isPidAlive(pid) {
|
|
41
|
+
try {
|
|
42
|
+
process.kill(pid, 0);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Acquires an exclusive per-profile lock file.
|
|
51
|
+
*
|
|
52
|
+
* Uses O_EXCL (writeFile flag:'wx') for atomic exclusive creation.
|
|
53
|
+
* If a lock exists, checks staleness (>60s or dead PID) and cleans up if stale.
|
|
54
|
+
* Retries up to 10 times with 500ms back-off before throwing.
|
|
55
|
+
*/
|
|
56
|
+
async function acquireLock(profileName) {
|
|
57
|
+
const configDir = await getConfigDir();
|
|
58
|
+
const lockPath = join(configDir, `.${profileName}.refresh.lock`);
|
|
59
|
+
for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
60
|
+
try {
|
|
61
|
+
const content = { pid: process.pid, timestamp: Date.now() };
|
|
62
|
+
await writeFile(lockPath, JSON.stringify(content), { flag: 'wx', mode: 0o600 });
|
|
63
|
+
// Acquired — return handle with inline release
|
|
64
|
+
return {
|
|
65
|
+
lockPath,
|
|
66
|
+
async release() {
|
|
67
|
+
await unlink(lockPath).catch(() => undefined);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// Only handle EEXIST — lock file already exists
|
|
73
|
+
if (!isEexist(err))
|
|
74
|
+
throw err;
|
|
75
|
+
// Read existing lock and decide whether it's stale
|
|
76
|
+
const isStale = await checkAndCleanStaleLock(lockPath);
|
|
77
|
+
if (isStale) {
|
|
78
|
+
// Stale lock deleted — retry immediately (don't sleep)
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Live lock — wait before retrying
|
|
82
|
+
if (attempt < LOCK_MAX_RETRIES - 1) {
|
|
83
|
+
await sleep(LOCK_RETRY_DELAY_MS);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error('Could not acquire refresh lock. Another process may be refreshing.');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reads the lock file at `lockPath`, checks whether it's stale, and
|
|
91
|
+
* if so deletes it. Returns true if the lock was stale (and deleted).
|
|
92
|
+
*/
|
|
93
|
+
async function checkAndCleanStaleLock(lockPath) {
|
|
94
|
+
let content;
|
|
95
|
+
try {
|
|
96
|
+
const raw = await readFile(lockPath, 'utf-8');
|
|
97
|
+
content = JSON.parse(raw);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// File vanished between our EEXIST and this read — treat as stale
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
const isOlderThanThreshold = Date.now() - content.timestamp > STALE_LOCK_THRESHOLD_MS;
|
|
104
|
+
const isProcDead = !isPidAlive(content.pid);
|
|
105
|
+
if (isOlderThanThreshold || isProcDead) {
|
|
106
|
+
await unlink(lockPath).catch(() => undefined);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Releases a previously acquired lock handle. Best-effort — never throws.
|
|
113
|
+
*/
|
|
114
|
+
async function releaseLock(handle) {
|
|
115
|
+
await handle.release();
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// refreshToken
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* Performs a transparent token refresh with double-checked locking.
|
|
122
|
+
*
|
|
123
|
+
* 1. Acquires a per-profile lockfile
|
|
124
|
+
* 2. Re-reads credentials — another process may have already refreshed
|
|
125
|
+
* 3. If tokens are still fresh, skips the network call (wasRefreshed: false)
|
|
126
|
+
* 4. Otherwise calls POST /v1/auth/refresh and stores the new tokens
|
|
127
|
+
* 5. Always releases the lock in a finally block
|
|
128
|
+
*
|
|
129
|
+
* Throws with a user-friendly message if the session has expired.
|
|
130
|
+
*/
|
|
131
|
+
async function refreshToken(options = {}) {
|
|
132
|
+
const profileName = options.profileName ?? 'default';
|
|
133
|
+
const store = options.credentialStore ?? (await createCredentialStore());
|
|
134
|
+
const apiUrl = options.apiUrl ?? getApiUrl();
|
|
135
|
+
const lock = await acquireLock(profileName);
|
|
136
|
+
try {
|
|
137
|
+
// Double-checked locking: re-read credentials now that we hold the lock.
|
|
138
|
+
// Another process may have already refreshed while we waited.
|
|
139
|
+
const latestCreds = await store.get(profileName);
|
|
140
|
+
if (latestCreds && !needsRefresh(latestCreds)) {
|
|
141
|
+
// Already refreshed by another process — return current tokens.
|
|
142
|
+
return {
|
|
143
|
+
accessToken: latestCreds.accessToken ?? '',
|
|
144
|
+
refreshToken: latestCreds.refreshToken ?? '',
|
|
145
|
+
expiresAt: latestCreds.expiresAt ?? '',
|
|
146
|
+
wasRefreshed: false,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// We need to refresh — verify we have a refresh token to use.
|
|
150
|
+
if (!latestCreds?.refreshToken) {
|
|
151
|
+
await store.clear(profileName);
|
|
152
|
+
throw new Error('Session expired. Run doow login to re-authenticate.');
|
|
153
|
+
}
|
|
154
|
+
// Call the refresh endpoint.
|
|
155
|
+
const response = await fetch(`${apiUrl}/v1/auth/refresh`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
body: JSON.stringify({ refresh_token: latestCreds.refreshToken }),
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
// 401 or any non-2xx → session expired
|
|
162
|
+
await store.clear(profileName);
|
|
163
|
+
throw new Error('Session expired. Run doow login to re-authenticate.');
|
|
164
|
+
}
|
|
165
|
+
const data = (await response.json());
|
|
166
|
+
const expiresAt = new Date(Date.now() + data.expires_in * 1_000).toISOString();
|
|
167
|
+
const newCreds = {
|
|
168
|
+
accessToken: data.access_token,
|
|
169
|
+
refreshToken: data.refresh_token,
|
|
170
|
+
expiresAt,
|
|
171
|
+
};
|
|
172
|
+
await store.set(profileName, newCreds);
|
|
173
|
+
return {
|
|
174
|
+
accessToken: data.access_token,
|
|
175
|
+
refreshToken: data.refresh_token,
|
|
176
|
+
expiresAt,
|
|
177
|
+
wasRefreshed: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await lock.release();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Utilities
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
function isEexist(err) {
|
|
188
|
+
return (typeof err === 'object' &&
|
|
189
|
+
err !== null &&
|
|
190
|
+
'code' in err &&
|
|
191
|
+
err.code === 'EEXIST');
|
|
192
|
+
}
|
|
193
|
+
function sleep(ms) {
|
|
194
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export { acquireLock, needsRefresh, refreshToken, releaseLock };
|
|
198
|
+
//# sourceMappingURL=refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.js","sources":["../../../../src/auth/refresh.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** True when stdout is an interactive terminal. */
|
|
2
|
+
function isTTY() {
|
|
3
|
+
return process.stdout.isTTY === true;
|
|
4
|
+
}
|
|
5
|
+
/** True when running inside a CI environment (any common CI sets $CI). */
|
|
6
|
+
function isCI() {
|
|
7
|
+
return !!process.env['CI'];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* True when the CLI is invoked by an automated agent rather than a human.
|
|
11
|
+
* Detected via $DOOW_AGENT_MODE env var or the --agent CLI flag (which
|
|
12
|
+
* Commander stores on the global options object as `process.env` is the
|
|
13
|
+
* canonical signal here — Commander integration is wired up in cli.ts).
|
|
14
|
+
*/
|
|
15
|
+
function isAgentMode() {
|
|
16
|
+
return !!process.env['DOOW_AGENT_MODE'];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* True when interactive UI (spinners, prompts, color) should be shown.
|
|
20
|
+
* Requires a real TTY, no CI env, and not running in agent mode.
|
|
21
|
+
*/
|
|
22
|
+
function shouldShowUI() {
|
|
23
|
+
return isTTY() && !isCI() && !isAgentMode();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the API base URL.
|
|
27
|
+
* Precedence: profile.apiUrl → $DOOW_API_URL → hardcoded default.
|
|
28
|
+
*/
|
|
29
|
+
function getApiUrl(profile) {
|
|
30
|
+
if (profile?.apiUrl)
|
|
31
|
+
return profile.apiUrl;
|
|
32
|
+
if (process.env['DOOW_API_URL'])
|
|
33
|
+
return process.env['DOOW_API_URL'];
|
|
34
|
+
return 'https://api.doow.com';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { getApiUrl, isAgentMode, isCI, isTTY, shouldShowUI };
|
|
38
|
+
//# 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;;;;"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { mkdir, chmod, readFile, writeFile, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Constants / defaults
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const DEFAULT_PROFILE_NAME = 'default';
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
activeProfile: DEFAULT_PROFILE_NAME,
|
|
11
|
+
profiles: {
|
|
12
|
+
[DEFAULT_PROFILE_NAME]: { name: DEFAULT_PROFILE_NAME },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
const DEFAULT_CREDENTIALS = {
|
|
16
|
+
profiles: {},
|
|
17
|
+
};
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Config directory
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Returns the Doow config directory path.
|
|
23
|
+
* Prefers $DOOW_CONFIG_DIR when set; falls back to ~/.doow.
|
|
24
|
+
* Creates the directory (mode 0o700) if it does not yet exist.
|
|
25
|
+
*/
|
|
26
|
+
async function getConfigDir() {
|
|
27
|
+
const dir = process.env['DOOW_CONFIG_DIR'] ?? join(homedir(), '.doow');
|
|
28
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
29
|
+
await chmod(dir, 0o700);
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Atomic write helper
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
async function atomicWrite(filePath, data) {
|
|
36
|
+
const tmp = `${filePath}.tmp`;
|
|
37
|
+
try {
|
|
38
|
+
await writeFile(tmp, data, { encoding: 'utf-8', mode: 0o600 });
|
|
39
|
+
await rename(tmp, filePath);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
// Clean up temp file on failure, best-effort
|
|
43
|
+
await rm(tmp, { force: true }).catch(() => undefined);
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// config.json
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
/** Reads ~/.doow/config.json. Returns the default config if the file is absent. */
|
|
51
|
+
async function readConfig() {
|
|
52
|
+
const dir = await getConfigDir();
|
|
53
|
+
const filePath = join(dir, 'config.json');
|
|
54
|
+
try {
|
|
55
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (isEnoent(err))
|
|
60
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Writes ~/.doow/config.json atomically with 0o600 perms. */
|
|
65
|
+
async function writeConfig(config) {
|
|
66
|
+
const dir = await getConfigDir();
|
|
67
|
+
const filePath = join(dir, 'config.json');
|
|
68
|
+
await atomicWrite(filePath, JSON.stringify(config, null, 2));
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// credentials.json
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
/** Reads ~/.doow/credentials.json. Returns empty credentials if absent. */
|
|
74
|
+
async function readCredentials() {
|
|
75
|
+
const dir = await getConfigDir();
|
|
76
|
+
const filePath = join(dir, 'credentials.json');
|
|
77
|
+
try {
|
|
78
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
79
|
+
return JSON.parse(raw);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (isEnoent(err))
|
|
83
|
+
return structuredClone(DEFAULT_CREDENTIALS);
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Writes ~/.doow/credentials.json atomically with 0o600 perms. */
|
|
88
|
+
async function writeCredentials(creds) {
|
|
89
|
+
const dir = await getConfigDir();
|
|
90
|
+
const filePath = join(dir, 'credentials.json');
|
|
91
|
+
await atomicWrite(filePath, JSON.stringify(creds, null, 2));
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Active profile
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
/** Returns the currently active Profile object. */
|
|
97
|
+
async function getActiveProfile() {
|
|
98
|
+
const config = await readConfig();
|
|
99
|
+
const profile = config.profiles[config.activeProfile];
|
|
100
|
+
if (!profile) {
|
|
101
|
+
// Graceful degradation: return synthetic default
|
|
102
|
+
return { name: config.activeProfile };
|
|
103
|
+
}
|
|
104
|
+
return profile;
|
|
105
|
+
}
|
|
106
|
+
/** Sets the active profile name and persists config. Throws if the profile doesn't exist. */
|
|
107
|
+
async function setActiveProfile(name) {
|
|
108
|
+
const config = await readConfig();
|
|
109
|
+
if (!config.profiles[name]) {
|
|
110
|
+
throw new Error(`Profile "${name}" does not exist.`);
|
|
111
|
+
}
|
|
112
|
+
config.activeProfile = name;
|
|
113
|
+
await writeConfig(config);
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Profile credentials
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
/**
|
|
119
|
+
* Returns credentials for the given profile name.
|
|
120
|
+
* Defaults to the currently active profile if name is omitted.
|
|
121
|
+
*/
|
|
122
|
+
async function getProfileCredentials(profileName) {
|
|
123
|
+
const name = profileName ?? (await readConfig()).activeProfile;
|
|
124
|
+
const creds = await readCredentials();
|
|
125
|
+
return creds.profiles[name];
|
|
126
|
+
}
|
|
127
|
+
/** Stores credentials for the given profile, merging with existing. */
|
|
128
|
+
async function setProfileCredentials(profileName, creds) {
|
|
129
|
+
const existing = await readCredentials();
|
|
130
|
+
existing.profiles[profileName] = creds;
|
|
131
|
+
await writeCredentials(existing);
|
|
132
|
+
}
|
|
133
|
+
/** Removes all credentials for the given profile. */
|
|
134
|
+
async function clearProfileCredentials(profileName) {
|
|
135
|
+
const creds = await readCredentials();
|
|
136
|
+
delete creds.profiles[profileName];
|
|
137
|
+
await writeCredentials(creds);
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Profile lifecycle
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
/**
|
|
143
|
+
* Deletes a profile from config + credentials.
|
|
144
|
+
* Throws if the profile is currently active — caller must switch first.
|
|
145
|
+
*/
|
|
146
|
+
async function deleteProfile(name) {
|
|
147
|
+
const config = await readConfig();
|
|
148
|
+
if (config.activeProfile === name) {
|
|
149
|
+
throw new Error(`Cannot delete the active profile "${name}". Switch to another profile first.`);
|
|
150
|
+
}
|
|
151
|
+
delete config.profiles[name];
|
|
152
|
+
await writeConfig(config);
|
|
153
|
+
await clearProfileCredentials(name);
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Utility
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
function isEnoent(err) {
|
|
159
|
+
return (typeof err === 'object' &&
|
|
160
|
+
err !== null &&
|
|
161
|
+
'code' in err &&
|
|
162
|
+
err.code === 'ENOENT');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { clearProfileCredentials, deleteProfile, getActiveProfile, getConfigDir, getProfileCredentials, readConfig, readCredentials, setActiveProfile, setProfileCredentials, writeConfig, writeCredentials };
|
|
166
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sources":["../../../../src/config/store.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|