@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,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;;;;;;;;"}
|