@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
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @doow/cli
|
|
2
|
+
|
|
3
|
+
Doow CLI -- manage SaaS spend from your terminal and coding agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @doow/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or via Homebrew (once the tap is live):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
brew tap doow-co/doow
|
|
15
|
+
brew install doow
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
doow login # authenticate (PKCE or device flow)
|
|
22
|
+
doow whoami # verify identity
|
|
23
|
+
doow apps list # list managed applications
|
|
24
|
+
doow reports expense # generate expense report
|
|
25
|
+
doow chat # interactive chat with Derek or Mina
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## MCP server
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
doow mcp # start MCP server over stdio
|
|
32
|
+
doow mcp --list-tools # print available tools as JSON
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Configure in your MCP client:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"doow": {
|
|
41
|
+
"command": "doow",
|
|
42
|
+
"args": ["mcp"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Shell completion
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
doow completion install # auto-detect shell and install
|
|
52
|
+
doow completion bash # output bash completion script
|
|
53
|
+
doow completion zsh # output zsh completion script
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Diagnostics
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
doow doctor # check auth, config, network, API version
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Global flags
|
|
63
|
+
|
|
64
|
+
| Flag | Description |
|
|
65
|
+
|------|-------------|
|
|
66
|
+
| `--json` | JSON output (default when piped) |
|
|
67
|
+
| `--table` | Table output (default in TTY) |
|
|
68
|
+
| `--profile <name>` | Multi-org profile selection |
|
|
69
|
+
| `--yes` | Skip confirmation prompts |
|
|
70
|
+
| `--dry-run` | Preview mutations without executing |
|
|
71
|
+
| `--debug` | Verbose HTTP logging to stderr |
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var env = require('../config/env.js');
|
|
4
|
+
var keyring = require('./keyring.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* api-key.ts
|
|
8
|
+
*
|
|
9
|
+
* PAT (Personal Access Token) authentication for CI/scripting contexts.
|
|
10
|
+
*
|
|
11
|
+
* Provides:
|
|
12
|
+
* - validateApiKey — pure format check (dak_ prefix, length ≥ 20)
|
|
13
|
+
* - authenticateWithApiKey — store key + optionally verify against API
|
|
14
|
+
* - readTokenFromStdin — read a piped token from stdin
|
|
15
|
+
* - resolveAuth — precedence chain: flag > env > stdin > stored > none
|
|
16
|
+
*/
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Constants
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const DAK_PREFIX = 'dak_';
|
|
21
|
+
const DAK_MIN_LENGTH = 20;
|
|
22
|
+
const REFRESH_BUFFER_SECONDS = 60;
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// validateApiKey
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Returns true if key starts with 'dak_' (case-sensitive) and is at least
|
|
28
|
+
* 20 characters total. Pure function — no network call.
|
|
29
|
+
*/
|
|
30
|
+
function validateApiKey(key) {
|
|
31
|
+
return key.startsWith(DAK_PREFIX) && key.length >= DAK_MIN_LENGTH;
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// authenticateWithApiKey
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Validates the key format, stores it in the credential store, and optionally
|
|
38
|
+
* verifies it works by hitting GET /v1/auth/capabilities.
|
|
39
|
+
*
|
|
40
|
+
* @throws {Error} if the key does not have the dak_ prefix
|
|
41
|
+
* @throws {Error} if verify is true and the capabilities request fails
|
|
42
|
+
*/
|
|
43
|
+
async function authenticateWithApiKey(options) {
|
|
44
|
+
const { key, profileName = 'default', verify = true } = options;
|
|
45
|
+
const apiUrl = options.apiUrl ?? env.getApiUrl();
|
|
46
|
+
const store = options.credentialStore ?? (await keyring.createCredentialStore());
|
|
47
|
+
// Validate format first
|
|
48
|
+
if (!validateApiKey(key)) {
|
|
49
|
+
throw new Error(`Invalid API key format. Keys must start with '${DAK_PREFIX}' and be at least ${DAK_MIN_LENGTH} characters long.`);
|
|
50
|
+
}
|
|
51
|
+
// Optionally verify before storing
|
|
52
|
+
if (verify) {
|
|
53
|
+
const res = await fetch(`${apiUrl}/v1/auth/capabilities`, {
|
|
54
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const body = await res.text().catch(() => '');
|
|
58
|
+
throw new Error(`API key verification failed: HTTP ${res.status}${body ? ` — ${body}` : ''}`);
|
|
59
|
+
}
|
|
60
|
+
await store.set(profileName, { apiKey: key });
|
|
61
|
+
return { apiKey: key, verified: true };
|
|
62
|
+
}
|
|
63
|
+
await store.set(profileName, { apiKey: key });
|
|
64
|
+
return { apiKey: key, verified: false };
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// readTokenFromStdin
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
/**
|
|
70
|
+
* Reads a token from piped stdin input.
|
|
71
|
+
*
|
|
72
|
+
* @throws {Error} if stdin is a TTY (not piped)
|
|
73
|
+
* @throws {Error} if the resulting string is empty after trim
|
|
74
|
+
*/
|
|
75
|
+
async function readTokenFromStdin() {
|
|
76
|
+
if (process.stdin.isTTY) {
|
|
77
|
+
throw new Error('--token-stdin requires piped input (e.g., echo $TOKEN | doow login --token-stdin)');
|
|
78
|
+
}
|
|
79
|
+
const parts = [];
|
|
80
|
+
for await (const chunk of process.stdin) {
|
|
81
|
+
if (Buffer.isBuffer(chunk)) {
|
|
82
|
+
parts.push(chunk.toString('utf-8'));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
parts.push(String(chunk));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const token = parts.join('').trim();
|
|
89
|
+
if (!token) {
|
|
90
|
+
throw new Error('No token received on stdin');
|
|
91
|
+
}
|
|
92
|
+
return token;
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// resolveAuth
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
/**
|
|
98
|
+
* Resolves the active auth context by walking the precedence chain:
|
|
99
|
+
* --api-key flag > DOOW_API_KEY env > --token-stdin > stored profile > none
|
|
100
|
+
*/
|
|
101
|
+
async function resolveAuth(options = {}) {
|
|
102
|
+
const { apiKeyFlag, tokenStdin = false, profileName = 'default' } = options;
|
|
103
|
+
const store = options.credentialStore ?? (await keyring.createCredentialStore());
|
|
104
|
+
// 1. --api-key flag
|
|
105
|
+
if (apiKeyFlag !== undefined && apiKeyFlag !== '') {
|
|
106
|
+
if (!validateApiKey(apiKeyFlag)) {
|
|
107
|
+
throw new Error(`Invalid API key format. Keys must start with '${DAK_PREFIX}' and be at least ${DAK_MIN_LENGTH} characters long.`);
|
|
108
|
+
}
|
|
109
|
+
return { type: 'api-key', token: apiKeyFlag, source: '--api-key flag' };
|
|
110
|
+
}
|
|
111
|
+
// 2. DOOW_API_KEY env var
|
|
112
|
+
const envKey = process.env['DOOW_API_KEY'];
|
|
113
|
+
if (envKey && validateApiKey(envKey)) {
|
|
114
|
+
return { type: 'api-key', token: envKey, source: 'DOOW_API_KEY env' };
|
|
115
|
+
}
|
|
116
|
+
// 3. --token-stdin
|
|
117
|
+
if (tokenStdin) {
|
|
118
|
+
const token = await readTokenFromStdin();
|
|
119
|
+
const type = validateApiKey(token) ? 'api-key' : 'oauth-token';
|
|
120
|
+
return { type, token, source: 'stdin' };
|
|
121
|
+
}
|
|
122
|
+
// 4. Stored credentials for the profile
|
|
123
|
+
const creds = await store.get(profileName);
|
|
124
|
+
if (creds?.apiKey) {
|
|
125
|
+
return { type: 'api-key', token: creds.apiKey, source: 'stored profile' };
|
|
126
|
+
}
|
|
127
|
+
if (creds?.accessToken) {
|
|
128
|
+
const token = creds.accessToken;
|
|
129
|
+
const needsRefresh = isTokenExpiredOrExpiring(creds.expiresAt);
|
|
130
|
+
return {
|
|
131
|
+
type: 'oauth-token',
|
|
132
|
+
token,
|
|
133
|
+
source: 'stored profile',
|
|
134
|
+
...(needsRefresh ? { needsRefresh: true } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// 5. Nothing found
|
|
138
|
+
return { type: 'none', source: 'none' };
|
|
139
|
+
}
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Private helpers
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
/**
|
|
144
|
+
* Returns true if the ISO-8601 expiresAt string is either absent, already
|
|
145
|
+
* past, or within 60 seconds of now.
|
|
146
|
+
*/
|
|
147
|
+
function isTokenExpiredOrExpiring(expiresAt) {
|
|
148
|
+
if (!expiresAt)
|
|
149
|
+
return true;
|
|
150
|
+
const expiryMs = new Date(expiresAt).getTime();
|
|
151
|
+
const nowPlusBuffer = Date.now() + REFRESH_BUFFER_SECONDS * 1000;
|
|
152
|
+
return expiryMs <= nowPlusBuffer;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
exports.authenticateWithApiKey = authenticateWithApiKey;
|
|
156
|
+
exports.readTokenFromStdin = readTokenFromStdin;
|
|
157
|
+
exports.resolveAuth = resolveAuth;
|
|
158
|
+
exports.validateApiKey = validateApiKey;
|
|
159
|
+
//# sourceMappingURL=api-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.js","sources":["../../../../src/auth/api-key.ts"],"sourcesContent":[null],"names":["getApiUrl","createCredentialStore"],"mappings":"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"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var http = require('node:http');
|
|
4
|
+
var env = require('../config/env.js');
|
|
5
|
+
var apiKey = require('./api-key.js');
|
|
6
|
+
var pkce = require('./pkce.js');
|
|
7
|
+
var deviceFlow = require('./device-flow.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
|
+
|
|
28
|
+
/**
|
|
29
|
+
* detect.ts
|
|
30
|
+
*
|
|
31
|
+
* S122 — Auth auto-detection for the Doow CLI.
|
|
32
|
+
*
|
|
33
|
+
* Determines whether to use PKCE, device, or API-key authentication based on
|
|
34
|
+
* the current runtime environment, then executes the appropriate flow.
|
|
35
|
+
*
|
|
36
|
+
* Detection order:
|
|
37
|
+
* 1. --api-key provided + valid format → api-key
|
|
38
|
+
* 2. --device flag → device
|
|
39
|
+
* 3. Running inside CI ($CI set) → device
|
|
40
|
+
* 4. Non-interactive stdout (!isTTY) → device
|
|
41
|
+
* 5. Can bind 127.0.0.1 on a free port → pkce
|
|
42
|
+
* 6. Fallback → device
|
|
43
|
+
*/
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// canBindLocalhost
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* Tests whether the process can bind a TCP server on 127.0.0.1 using an
|
|
49
|
+
* OS-assigned ephemeral port. The server is closed immediately on success.
|
|
50
|
+
*
|
|
51
|
+
* Returns true → PKCE callback server will work.
|
|
52
|
+
* Returns false → Network stack can't bind (containers with restricted network
|
|
53
|
+
* policies, permission denied, etc.) — use device flow instead.
|
|
54
|
+
*/
|
|
55
|
+
function canBindLocalhost() {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const server = http__namespace.createServer();
|
|
58
|
+
server.once('error', () => {
|
|
59
|
+
resolve(false);
|
|
60
|
+
});
|
|
61
|
+
server.listen(0, '127.0.0.1', () => {
|
|
62
|
+
server.close(() => resolve(true));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// detectAuthMethod
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
/**
|
|
70
|
+
* Returns the best authentication method for the current environment.
|
|
71
|
+
*/
|
|
72
|
+
async function detectAuthMethod(options = {}) {
|
|
73
|
+
const { forceDevice = false, apiKey: apiKey$1 } = options;
|
|
74
|
+
// 1. API key provided and format-valid → skip OAuth entirely
|
|
75
|
+
if (apiKey$1 !== undefined && apiKey$1 !== '' && apiKey.validateApiKey(apiKey$1)) {
|
|
76
|
+
return 'api-key';
|
|
77
|
+
}
|
|
78
|
+
// 2. Explicit --device flag
|
|
79
|
+
if (forceDevice) {
|
|
80
|
+
return 'device';
|
|
81
|
+
}
|
|
82
|
+
// 3. CI environment — browser is not available
|
|
83
|
+
if (env.isCI()) {
|
|
84
|
+
return 'device';
|
|
85
|
+
}
|
|
86
|
+
// 4. Non-interactive (stdout is not a TTY) — can't drive a browser login
|
|
87
|
+
if (!env.isTTY()) {
|
|
88
|
+
return 'device';
|
|
89
|
+
}
|
|
90
|
+
// 5. Try to bind a local port — if it works, PKCE callback server will too
|
|
91
|
+
const canBind = await canBindLocalhost();
|
|
92
|
+
if (canBind) {
|
|
93
|
+
return 'pkce';
|
|
94
|
+
}
|
|
95
|
+
// 6. Safe fallback
|
|
96
|
+
return 'device';
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// isServerBindError
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
/**
|
|
102
|
+
* Returns true when an error looks like a localhost server bind failure —
|
|
103
|
+
* the only case where PKCE should automatically fall back to device flow.
|
|
104
|
+
*
|
|
105
|
+
* We intentionally do NOT fall back on application-level errors such as
|
|
106
|
+
* CSRF mismatch, token exchange failures, or user cancellation.
|
|
107
|
+
*/
|
|
108
|
+
function isServerBindError(err) {
|
|
109
|
+
if (!(err instanceof Error))
|
|
110
|
+
return false;
|
|
111
|
+
const msg = err.message.toLowerCase();
|
|
112
|
+
return (msg.includes('failed to start local callback server') ||
|
|
113
|
+
msg.includes('failed to determine callback server port') ||
|
|
114
|
+
msg.includes('failed to open browser') ||
|
|
115
|
+
msg.includes('eaddrinuse') ||
|
|
116
|
+
msg.includes('eacces') ||
|
|
117
|
+
msg.includes('permission denied'));
|
|
118
|
+
}
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// executeAutoLogin
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
/**
|
|
123
|
+
* The main login orchestrator.
|
|
124
|
+
*
|
|
125
|
+
* 1. Detects the best auth method.
|
|
126
|
+
* 2. Executes the corresponding flow.
|
|
127
|
+
* 3. If PKCE fails with a server bind error, automatically retries with device flow.
|
|
128
|
+
*/
|
|
129
|
+
async function executeAutoLogin(options = {}) {
|
|
130
|
+
const { forceDevice, apiKey: apiKey$1, apiUrl, profileName, credentialStore, timeout } = options;
|
|
131
|
+
const method = await detectAuthMethod({ forceDevice, apiKey: apiKey$1 });
|
|
132
|
+
if (method === 'api-key') {
|
|
133
|
+
// apiKey is guaranteed non-empty here (detectAuthMethod validated it)
|
|
134
|
+
await apiKey.authenticateWithApiKey({
|
|
135
|
+
key: apiKey$1,
|
|
136
|
+
apiUrl,
|
|
137
|
+
profileName,
|
|
138
|
+
credentialStore,
|
|
139
|
+
});
|
|
140
|
+
return { method: 'api-key', apiKey: apiKey$1 };
|
|
141
|
+
}
|
|
142
|
+
if (method === 'pkce') {
|
|
143
|
+
try {
|
|
144
|
+
const result = await pkce.executePkceFlow({ apiUrl, profileName, credentialStore, timeout });
|
|
145
|
+
return {
|
|
146
|
+
method: 'pkce',
|
|
147
|
+
accessToken: result.accessToken,
|
|
148
|
+
refreshToken: result.refreshToken,
|
|
149
|
+
expiresAt: result.expiresAt,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
// Only fall back on server bind errors — not on CSRF or token exchange errors
|
|
154
|
+
if (!isServerBindError(err)) {
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
// Fall through to device flow below
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// device flow (either detected or PKCE fallback)
|
|
161
|
+
const result = await deviceFlow.executeDeviceFlow({ apiUrl, profileName, credentialStore });
|
|
162
|
+
return {
|
|
163
|
+
method: 'device',
|
|
164
|
+
accessToken: result.accessToken,
|
|
165
|
+
refreshToken: result.refreshToken,
|
|
166
|
+
expiresAt: result.expiresAt,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
exports.canBindLocalhost = canBindLocalhost;
|
|
171
|
+
exports.detectAuthMethod = detectAuthMethod;
|
|
172
|
+
exports.executeAutoLogin = executeAutoLogin;
|
|
173
|
+
//# sourceMappingURL=detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.js","sources":["../../../../src/auth/detect.ts"],"sourcesContent":[null],"names":["http","apiKey","validateApiKey","isCI","isTTY","authenticateWithApiKey","executePkceFlow","executeDeviceFlow"],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var env = require('../config/env.js');
|
|
4
|
+
var keyring = require('./keyring.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* device-flow.ts
|
|
8
|
+
*
|
|
9
|
+
* RFC 8628 OAuth 2.0 Device Authorization flow for headless/SSH/container
|
|
10
|
+
* environments where a browser cannot be opened on the same machine.
|
|
11
|
+
*
|
|
12
|
+
* Steps:
|
|
13
|
+
* 1. POST /v1/auth/device/authorize → get device_code + user_code
|
|
14
|
+
* 2. Display verification URI + user_code on stderr
|
|
15
|
+
* 3. Optionally open the browser (best-effort)
|
|
16
|
+
* 4. Poll POST /v1/auth/device/token until granted or expired
|
|
17
|
+
* 5. Store tokens in the credential store
|
|
18
|
+
*/
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/** Sleep for `ms` milliseconds — real timer in production, override in tests. */
|
|
23
|
+
function sleep(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build an ISO-8601 expiry timestamp from a seconds-from-now value.
|
|
28
|
+
* Exported for test visibility only.
|
|
29
|
+
*/
|
|
30
|
+
function expiresAtFromSecondsIn(secondsIn) {
|
|
31
|
+
return new Date(Date.now() + secondsIn * 1000).toISOString();
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Core function
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Execute the RFC 8628 device authorization flow.
|
|
38
|
+
*
|
|
39
|
+
* All user-facing output goes to stderr so stdout stays clean for piping.
|
|
40
|
+
*
|
|
41
|
+
* @throws {Error} if authorization fails or the device code expires.
|
|
42
|
+
*/
|
|
43
|
+
async function executeDeviceFlow(options = {}) {
|
|
44
|
+
const apiUrl = options.apiUrl ?? env.getApiUrl();
|
|
45
|
+
const profileName = options.profileName ?? 'default';
|
|
46
|
+
const store = options.credentialStore ?? (await keyring.createCredentialStore());
|
|
47
|
+
const openUrl = options.openUrl ??
|
|
48
|
+
(async (url) => {
|
|
49
|
+
const { default: open } = await import('open');
|
|
50
|
+
await open(url);
|
|
51
|
+
});
|
|
52
|
+
// -------------------------------------------------------------------------
|
|
53
|
+
// Step 1: Request device authorization
|
|
54
|
+
// -------------------------------------------------------------------------
|
|
55
|
+
const authorizeRes = await fetch(`${apiUrl}/v1/auth/device/authorize`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ client_id: 'doow-cli' }),
|
|
59
|
+
});
|
|
60
|
+
if (!authorizeRes.ok) {
|
|
61
|
+
const body = await authorizeRes.text().catch(() => '');
|
|
62
|
+
throw new Error(`Device authorization request failed: HTTP ${authorizeRes.status}${body ? ` — ${body}` : ''}`);
|
|
63
|
+
}
|
|
64
|
+
const auth = (await authorizeRes.json());
|
|
65
|
+
// -------------------------------------------------------------------------
|
|
66
|
+
// Step 2: Display instructions on stderr
|
|
67
|
+
// -------------------------------------------------------------------------
|
|
68
|
+
process.stderr.write(`\nTo sign in, open this URL in any browser:\n ${auth.verification_uri}\n\nThen enter code: ${auth.user_code}\n\n`);
|
|
69
|
+
// Step 2b: Best-effort browser open — never throw on failure
|
|
70
|
+
if (env.shouldShowUI()) {
|
|
71
|
+
try {
|
|
72
|
+
await openUrl(auth.verification_uri);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Silently ignore — user can open manually
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// -------------------------------------------------------------------------
|
|
79
|
+
// Step 3: Poll for token
|
|
80
|
+
// -------------------------------------------------------------------------
|
|
81
|
+
let pollInterval = auth.interval; // seconds; RFC 8628 §3.5
|
|
82
|
+
while (true) {
|
|
83
|
+
await sleep(pollInterval * 1000);
|
|
84
|
+
const tokenRes = await fetch(`${apiUrl}/v1/auth/device/token`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
device_code: auth.device_code,
|
|
89
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
if (tokenRes.ok) {
|
|
93
|
+
// Success — stop polling
|
|
94
|
+
const tokens = (await tokenRes.json());
|
|
95
|
+
// -----------------------------------------------------------------------
|
|
96
|
+
// Step 4: Store tokens
|
|
97
|
+
// -----------------------------------------------------------------------
|
|
98
|
+
const expiresAt = expiresAtFromSecondsIn(tokens.expires_in);
|
|
99
|
+
await store.set(profileName, {
|
|
100
|
+
accessToken: tokens.access_token,
|
|
101
|
+
refreshToken: tokens.refresh_token,
|
|
102
|
+
expiresAt,
|
|
103
|
+
});
|
|
104
|
+
// -----------------------------------------------------------------------
|
|
105
|
+
// Step 5: Return result
|
|
106
|
+
// -----------------------------------------------------------------------
|
|
107
|
+
return {
|
|
108
|
+
accessToken: tokens.access_token,
|
|
109
|
+
refreshToken: tokens.refresh_token,
|
|
110
|
+
expiresAt,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Non-200 — parse RFC 8628 error body
|
|
114
|
+
const errBody = (await tokenRes.json().catch(() => ({ error: 'unknown' })));
|
|
115
|
+
switch (errBody.error) {
|
|
116
|
+
case 'authorization_pending':
|
|
117
|
+
// User hasn't approved yet — keep polling at current interval
|
|
118
|
+
continue;
|
|
119
|
+
case 'slow_down':
|
|
120
|
+
// RFC 8628 §3.5: increase interval by 5 seconds
|
|
121
|
+
pollInterval += 5;
|
|
122
|
+
continue;
|
|
123
|
+
case 'expired_token':
|
|
124
|
+
throw new Error('Device code expired. Run doow login --device again.');
|
|
125
|
+
case 'access_denied':
|
|
126
|
+
throw new Error('Authorization denied by user.');
|
|
127
|
+
default:
|
|
128
|
+
throw new Error(`Token polling failed: ${errBody.error}${errBody.error_description ? ` — ${errBody.error_description}` : ''}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
exports.executeDeviceFlow = executeDeviceFlow;
|
|
134
|
+
exports.expiresAtFromSecondsIn = expiresAtFromSecondsIn;
|
|
135
|
+
//# sourceMappingURL=device-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-flow.js","sources":["../../../../src/auth/device-flow.ts"],"sourcesContent":[null],"names":["getApiUrl","createCredentialStore","shouldShowUI"],"mappings":"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"}
|