@doow/cli 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/auth/api-key.js +2 -1
- package/dist/cjs/auth/api-key.js.map +1 -1
- package/dist/cjs/auth/device-flow.js +18 -2
- package/dist/cjs/auth/device-flow.js.map +1 -1
- package/dist/cjs/auth/keyring.js +36 -8
- package/dist/cjs/auth/keyring.js.map +1 -1
- package/dist/cjs/auth/pkce.js +30 -11
- package/dist/cjs/auth/pkce.js.map +1 -1
- package/dist/cjs/config/env.js +1 -1
- package/dist/cjs/config/env.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cli.cjs +147 -45
- package/dist/cli.cjs.map +1 -1
- package/dist/esm/auth/api-key.js +2 -1
- package/dist/esm/auth/api-key.js.map +1 -1
- package/dist/esm/auth/device-flow.js +18 -2
- package/dist/esm/auth/device-flow.js.map +1 -1
- package/dist/esm/auth/keyring.js +36 -8
- package/dist/esm/auth/keyring.js.map +1 -1
- package/dist/esm/auth/pkce.js +30 -11
- package/dist/esm/auth/pkce.js.map +1 -1
- package/dist/esm/config/env.js +1 -1
- package/dist/esm/config/env.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -231,7 +231,7 @@ function getApiUrl(profile) {
|
|
|
231
231
|
return profile.apiUrl;
|
|
232
232
|
if (process.env['DOOW_API_URL'])
|
|
233
233
|
return process.env['DOOW_API_URL'];
|
|
234
|
-
return 'https://api.doow.
|
|
234
|
+
return 'https://dev-api.doow.co';
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
// ---------------------------------------------------------------------------
|
|
@@ -287,26 +287,55 @@ function createFileStore() {
|
|
|
287
287
|
// Keyring-backed store
|
|
288
288
|
// ---------------------------------------------------------------------------
|
|
289
289
|
function createKeyringStore(keytar) {
|
|
290
|
+
const fileStore = createFileStore();
|
|
291
|
+
let currentBackend = 'keyring';
|
|
290
292
|
return {
|
|
291
293
|
async get(profileName) {
|
|
292
294
|
try {
|
|
293
295
|
const raw = await withTimeout(keytar.getPassword(SERVICE_NAME, profileName), OP_TIMEOUT_MS);
|
|
294
|
-
if (raw
|
|
295
|
-
|
|
296
|
-
|
|
296
|
+
if (raw != null) {
|
|
297
|
+
currentBackend = 'keyring';
|
|
298
|
+
return JSON.parse(raw);
|
|
299
|
+
}
|
|
300
|
+
currentBackend = 'keyring';
|
|
297
301
|
}
|
|
298
302
|
catch {
|
|
299
|
-
|
|
303
|
+
printFallbackWarning();
|
|
304
|
+
currentBackend = 'file';
|
|
300
305
|
}
|
|
306
|
+
const fallback = await fileStore.get(profileName);
|
|
307
|
+
if (fallback !== undefined) {
|
|
308
|
+
currentBackend = 'file';
|
|
309
|
+
return fallback;
|
|
310
|
+
}
|
|
311
|
+
return undefined;
|
|
301
312
|
},
|
|
302
313
|
async set(profileName, creds) {
|
|
303
|
-
|
|
314
|
+
try {
|
|
315
|
+
await withTimeout(keytar.setPassword(SERVICE_NAME, profileName, JSON.stringify(creds)), OP_TIMEOUT_MS);
|
|
316
|
+
currentBackend = 'keyring';
|
|
317
|
+
// Remove stale fallback data once the keyring becomes the source of truth.
|
|
318
|
+
await fileStore.clear(profileName);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
printFallbackWarning();
|
|
322
|
+
currentBackend = 'file';
|
|
323
|
+
await fileStore.set(profileName, creds);
|
|
324
|
+
}
|
|
304
325
|
},
|
|
305
326
|
async clear(profileName) {
|
|
306
|
-
|
|
327
|
+
try {
|
|
328
|
+
await withTimeout(keytar.deletePassword(SERVICE_NAME, profileName), OP_TIMEOUT_MS);
|
|
329
|
+
currentBackend = 'keyring';
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
printFallbackWarning();
|
|
333
|
+
currentBackend = 'file';
|
|
334
|
+
}
|
|
335
|
+
await fileStore.clear(profileName);
|
|
307
336
|
},
|
|
308
337
|
backend() {
|
|
309
|
-
return
|
|
338
|
+
return currentBackend;
|
|
310
339
|
},
|
|
311
340
|
};
|
|
312
341
|
}
|
|
@@ -325,7 +354,6 @@ async function createCredentialStore() {
|
|
|
325
354
|
let keytar;
|
|
326
355
|
try {
|
|
327
356
|
// Dynamic import so missing keytar never crashes the module at load time.
|
|
328
|
-
// @ts-expect-error — keytar is an optional peer dep and may not be installed.
|
|
329
357
|
const mod = await import('keytar');
|
|
330
358
|
// Support both default-export and named-export module shapes.
|
|
331
359
|
keytar = (mod.default ?? mod);
|
|
@@ -469,7 +497,8 @@ async function resolveAuth(options = {}) {
|
|
|
469
497
|
}
|
|
470
498
|
if (creds?.accessToken) {
|
|
471
499
|
const token = creds.accessToken;
|
|
472
|
-
const
|
|
500
|
+
const canRefresh = typeof creds.refreshToken === 'string' && creds.refreshToken.trim().length > 0;
|
|
501
|
+
const needsRefresh = canRefresh ? isTokenExpiredOrExpiring(creds.expiresAt) : false;
|
|
473
502
|
return {
|
|
474
503
|
type: 'oauth-token',
|
|
475
504
|
token,
|
|
@@ -626,13 +655,12 @@ function startCallbackServer(expectedState, timeoutMs) {
|
|
|
626
655
|
// ---------------------------------------------------------------------------
|
|
627
656
|
// Token exchange
|
|
628
657
|
// ---------------------------------------------------------------------------
|
|
629
|
-
async function exchangeCodeForTokens(apiUrl,
|
|
658
|
+
async function exchangeCodeForTokens(apiUrl, codeVerifier, state) {
|
|
630
659
|
const response = await fetch(`${apiUrl}/v1/auth/cli/token`, {
|
|
631
660
|
method: 'POST',
|
|
632
661
|
headers: { 'Content-Type': 'application/json' },
|
|
633
662
|
body: JSON.stringify({
|
|
634
663
|
grant_type: 'authorization_code',
|
|
635
|
-
code,
|
|
636
664
|
code_verifier: codeVerifier,
|
|
637
665
|
state,
|
|
638
666
|
}),
|
|
@@ -643,6 +671,31 @@ async function exchangeCodeForTokens(apiUrl, code, codeVerifier, state) {
|
|
|
643
671
|
}
|
|
644
672
|
return response.json();
|
|
645
673
|
}
|
|
674
|
+
async function requestAuthorizationUrl(apiUrl, codeChallenge, state, redirectUri) {
|
|
675
|
+
const response = await fetch(`${apiUrl}/v1/auth/cli/authorize`, {
|
|
676
|
+
method: 'POST',
|
|
677
|
+
headers: { 'Content-Type': 'application/json' },
|
|
678
|
+
body: JSON.stringify({
|
|
679
|
+
client_id: 'doow-cli',
|
|
680
|
+
code_challenge: codeChallenge,
|
|
681
|
+
code_challenge_method: 'S256',
|
|
682
|
+
redirect_uri: redirectUri,
|
|
683
|
+
state,
|
|
684
|
+
}),
|
|
685
|
+
});
|
|
686
|
+
if (!response.ok) {
|
|
687
|
+
const body = await response.text().catch(() => '(no body)');
|
|
688
|
+
throw new Error(`Authorize request failed: HTTP ${response.status} — ${body}`);
|
|
689
|
+
}
|
|
690
|
+
const payload = (await response.json());
|
|
691
|
+
if (payload.state !== state) {
|
|
692
|
+
throw new Error('Authorize response state mismatch — possible server bug. Try again.');
|
|
693
|
+
}
|
|
694
|
+
if (!payload.authorization_url) {
|
|
695
|
+
throw new Error('Authorize response did not include authorization_url.');
|
|
696
|
+
}
|
|
697
|
+
return payload.authorization_url;
|
|
698
|
+
}
|
|
646
699
|
// ---------------------------------------------------------------------------
|
|
647
700
|
// Main flow
|
|
648
701
|
// ---------------------------------------------------------------------------
|
|
@@ -666,14 +719,9 @@ async function executePkceFlow(options = {}) {
|
|
|
666
719
|
// Step 3: Start callback server
|
|
667
720
|
const { server, port, callbackPromise } = await startCallbackServer(state, timeout);
|
|
668
721
|
try {
|
|
669
|
-
// Step 4:
|
|
722
|
+
// Step 4: Request an authorization URL from the API
|
|
670
723
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
671
|
-
const authorizeUrl =
|
|
672
|
-
`?response_type=code` +
|
|
673
|
-
`&code_challenge=${codeChallenge}` +
|
|
674
|
-
`&code_challenge_method=S256` +
|
|
675
|
-
`&state=${state}` +
|
|
676
|
-
`&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
724
|
+
const authorizeUrl = await requestAuthorizationUrl(apiUrl, codeChallenge, state, redirectUri);
|
|
677
725
|
// Step 5: Open browser
|
|
678
726
|
try {
|
|
679
727
|
const { default: open } = await import('open');
|
|
@@ -684,9 +732,9 @@ async function executePkceFlow(options = {}) {
|
|
|
684
732
|
throw new Error(`Failed to open browser: ${msg}. Try doow login --device for headless environments.`);
|
|
685
733
|
}
|
|
686
734
|
// Step 6: Wait for callback
|
|
687
|
-
|
|
735
|
+
await callbackPromise;
|
|
688
736
|
// Step 7: Exchange code for tokens
|
|
689
|
-
const tokenResponse = await exchangeCodeForTokens(apiUrl,
|
|
737
|
+
const tokenResponse = await exchangeCodeForTokens(apiUrl, codeVerifier, state);
|
|
690
738
|
// Step 8: Store tokens
|
|
691
739
|
const expiresAt = new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString();
|
|
692
740
|
const result = {
|
|
@@ -714,7 +762,7 @@ async function executePkceFlow(options = {}) {
|
|
|
714
762
|
* environments where a browser cannot be opened on the same machine.
|
|
715
763
|
*
|
|
716
764
|
* Steps:
|
|
717
|
-
* 1. POST /v1/auth/device
|
|
765
|
+
* 1. POST /v1/auth/cli/device → get device_code + user_code
|
|
718
766
|
* 2. Display verification URI + user_code on stderr
|
|
719
767
|
* 3. Optionally open the browser (best-effort)
|
|
720
768
|
* 4. Poll POST /v1/auth/device/token until granted or expired
|
|
@@ -734,6 +782,15 @@ function sleep$1(ms) {
|
|
|
734
782
|
function expiresAtFromSecondsIn(secondsIn) {
|
|
735
783
|
return new Date(Date.now() + secondsIn * 1000).toISOString();
|
|
736
784
|
}
|
|
785
|
+
function isLegacyPendingPollResponse(status, body) {
|
|
786
|
+
const legacyBody = body;
|
|
787
|
+
return (status === 400 &&
|
|
788
|
+
body != null &&
|
|
789
|
+
typeof body === 'object' &&
|
|
790
|
+
legacyBody.name === 'HttpException' &&
|
|
791
|
+
legacyBody.message === 'Bad Request Exception' &&
|
|
792
|
+
legacyBody.response === 'Bad Request Exception');
|
|
793
|
+
}
|
|
737
794
|
// ---------------------------------------------------------------------------
|
|
738
795
|
// Core function
|
|
739
796
|
// ---------------------------------------------------------------------------
|
|
@@ -756,7 +813,7 @@ async function executeDeviceFlow(options = {}) {
|
|
|
756
813
|
// -------------------------------------------------------------------------
|
|
757
814
|
// Step 1: Request device authorization
|
|
758
815
|
// -------------------------------------------------------------------------
|
|
759
|
-
const authorizeRes = await fetch(`${apiUrl}/v1/auth/device
|
|
816
|
+
const authorizeRes = await fetch(`${apiUrl}/v1/auth/cli/device`, {
|
|
760
817
|
method: 'POST',
|
|
761
818
|
headers: { 'Content-Type': 'application/json' },
|
|
762
819
|
body: JSON.stringify({ client_id: 'doow-cli' }),
|
|
@@ -766,6 +823,7 @@ async function executeDeviceFlow(options = {}) {
|
|
|
766
823
|
throw new Error(`Device authorization request failed: HTTP ${authorizeRes.status}${body ? ` — ${body}` : ''}`);
|
|
767
824
|
}
|
|
768
825
|
const auth = (await authorizeRes.json());
|
|
826
|
+
const deviceCodeExpiresAt = Date.now() + auth.expires_in * 1000;
|
|
769
827
|
// -------------------------------------------------------------------------
|
|
770
828
|
// Step 2: Display instructions on stderr
|
|
771
829
|
// -------------------------------------------------------------------------
|
|
@@ -785,6 +843,9 @@ async function executeDeviceFlow(options = {}) {
|
|
|
785
843
|
let pollInterval = auth.interval; // seconds; RFC 8628 §3.5
|
|
786
844
|
while (true) {
|
|
787
845
|
await sleep$1(pollInterval * 1000);
|
|
846
|
+
if (Date.now() > deviceCodeExpiresAt) {
|
|
847
|
+
throw new Error('Device code expired. Run doow login --device again.');
|
|
848
|
+
}
|
|
788
849
|
const tokenRes = await fetch(`${apiUrl}/v1/auth/device/token`, {
|
|
789
850
|
method: 'POST',
|
|
790
851
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -829,6 +890,9 @@ async function executeDeviceFlow(options = {}) {
|
|
|
829
890
|
case 'access_denied':
|
|
830
891
|
throw new Error('Authorization denied by user.');
|
|
831
892
|
default:
|
|
893
|
+
if (isLegacyPendingPollResponse(tokenRes.status, errBody)) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
832
896
|
throw new Error(`Token polling failed: ${errBody.error}${errBody.error_description ? ` — ${errBody.error_description}` : ''}`);
|
|
833
897
|
}
|
|
834
898
|
}
|
|
@@ -9137,29 +9201,45 @@ async function checkNetwork(apiUrl) {
|
|
|
9137
9201
|
}
|
|
9138
9202
|
}
|
|
9139
9203
|
async function checkApiVersion(apiUrl) {
|
|
9204
|
+
const healthPaths = ['/health', '/v1/health'];
|
|
9140
9205
|
try {
|
|
9141
9206
|
const controller = new AbortController();
|
|
9142
9207
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9208
|
+
let lastPath = healthPaths[0];
|
|
9209
|
+
let lastStatus;
|
|
9210
|
+
for (const path of healthPaths) {
|
|
9211
|
+
lastPath = path;
|
|
9212
|
+
const res = await fetch(`${apiUrl}${path}`, { signal: controller.signal });
|
|
9213
|
+
if (res.ok) {
|
|
9214
|
+
clearTimeout(timeoutId);
|
|
9215
|
+
let version;
|
|
9216
|
+
try {
|
|
9217
|
+
const body = await res.json();
|
|
9218
|
+
if (typeof body['version'] === 'string')
|
|
9219
|
+
version = body['version'];
|
|
9220
|
+
else if (typeof body['server_version'] === 'string')
|
|
9221
|
+
version = body['server_version'];
|
|
9222
|
+
}
|
|
9223
|
+
catch {
|
|
9224
|
+
// non-JSON body is acceptable
|
|
9225
|
+
}
|
|
9226
|
+
return {
|
|
9227
|
+
name: 'api_version',
|
|
9228
|
+
status: 'pass',
|
|
9229
|
+
detail: version ? `server ${version}` : 'reachable (no version in response)',
|
|
9230
|
+
};
|
|
9231
|
+
}
|
|
9232
|
+
lastStatus = res.status;
|
|
9233
|
+
if (res.status !== 404 && res.status !== 410) {
|
|
9234
|
+
clearTimeout(timeoutId);
|
|
9235
|
+
return { name: 'api_version', status: 'warn', detail: `HTTP ${res.status} from ${path}` };
|
|
9236
|
+
}
|
|
9158
9237
|
}
|
|
9238
|
+
clearTimeout(timeoutId);
|
|
9159
9239
|
return {
|
|
9160
9240
|
name: 'api_version',
|
|
9161
|
-
status: '
|
|
9162
|
-
detail:
|
|
9241
|
+
status: 'warn',
|
|
9242
|
+
detail: `HTTP ${lastStatus ?? 404} from ${lastPath}`,
|
|
9163
9243
|
};
|
|
9164
9244
|
}
|
|
9165
9245
|
catch (err) {
|
|
@@ -33863,7 +33943,7 @@ async function startStdioServer(options) {
|
|
|
33863
33943
|
await server.connect(transport);
|
|
33864
33944
|
}
|
|
33865
33945
|
|
|
33866
|
-
const VERSION = "0.1.
|
|
33946
|
+
const VERSION = "0.1.5" ;
|
|
33867
33947
|
const ASCII_LOGO = `
|
|
33868
33948
|
_
|
|
33869
33949
|
__| | ___ ___ __ __
|
|
@@ -33880,6 +33960,10 @@ function printLogo() {
|
|
|
33880
33960
|
process.stdout.write(' Not logged in.\n\n');
|
|
33881
33961
|
process.stdout.write(' Run \x1b[36mdoow login\x1b[0m to get started.\n\n');
|
|
33882
33962
|
}
|
|
33963
|
+
function readString(data, key) {
|
|
33964
|
+
const value = data[key];
|
|
33965
|
+
return typeof value === 'string' ? value : undefined;
|
|
33966
|
+
}
|
|
33883
33967
|
// ---------------------------------------------------------------------------
|
|
33884
33968
|
// loginHandler — exported for testability
|
|
33885
33969
|
// ---------------------------------------------------------------------------
|
|
@@ -33989,14 +34073,28 @@ async function whoamiHandler(globalOpts, credentialStore) {
|
|
|
33989
34073
|
process.exit(1);
|
|
33990
34074
|
}
|
|
33991
34075
|
const data = (await res.json());
|
|
33992
|
-
const email = data
|
|
33993
|
-
const
|
|
34076
|
+
const email = readString(data, 'email') ?? '(unknown)';
|
|
34077
|
+
const organizationId = readString(data, 'organization_id');
|
|
34078
|
+
const org = readString(data, 'organization') ?? organizationId ?? '(unknown)';
|
|
34079
|
+
const role = readString(data, 'role');
|
|
34080
|
+
const capabilities = Array.isArray(data['capabilities'])
|
|
34081
|
+
? data['capabilities'].filter((value) => typeof value === 'string')
|
|
34082
|
+
: undefined;
|
|
33994
34083
|
if (isJson) {
|
|
33995
|
-
process.stdout.write(JSON.stringify({
|
|
34084
|
+
process.stdout.write(JSON.stringify({
|
|
34085
|
+
email,
|
|
34086
|
+
organization: org,
|
|
34087
|
+
organizationId,
|
|
34088
|
+
role,
|
|
34089
|
+
capabilities,
|
|
34090
|
+
profile: profileName,
|
|
34091
|
+
}) + '\n');
|
|
33996
34092
|
}
|
|
33997
34093
|
else {
|
|
33998
34094
|
process.stderr.write(`Logged in as ${email}\n`);
|
|
33999
34095
|
process.stderr.write(`Organization: ${org}\n`);
|
|
34096
|
+
if (role)
|
|
34097
|
+
process.stderr.write(`Role: ${role}\n`);
|
|
34000
34098
|
process.stderr.write(`Auth: ${resolved.source}\n`);
|
|
34001
34099
|
process.stderr.write(`Profile: ${profileName}\n`);
|
|
34002
34100
|
}
|
|
@@ -34071,8 +34169,12 @@ program
|
|
|
34071
34169
|
.option('--token-stdin', 'Read raw access token from stdin')
|
|
34072
34170
|
.action(async (opts) => {
|
|
34073
34171
|
const globalOpts = program.opts();
|
|
34172
|
+
const mergedOpts = {
|
|
34173
|
+
...opts,
|
|
34174
|
+
apiKey: opts.apiKey ?? globalOpts.apiKey,
|
|
34175
|
+
};
|
|
34074
34176
|
try {
|
|
34075
|
-
await loginHandler(
|
|
34177
|
+
await loginHandler(mergedOpts, globalOpts);
|
|
34076
34178
|
}
|
|
34077
34179
|
catch (err) {
|
|
34078
34180
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|