@roarkanalytics/cli 0.1.1 → 0.3.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/CHANGELOG.md +36 -0
- package/commands.js +4 -4
- package/man/roark-customer-flow-update.1 +1 -1
- package/man/roark-metric-definition-create.1 +3 -3
- package/npm-shrinkwrap.json +54 -0
- package/package.json +7 -8
- package/runtime/browser-login.d.ts +26 -0
- package/runtime/browser-login.js +132 -0
- package/runtime/commands/auth.d.ts +11 -4
- package/runtime/commands/auth.js +112 -15
- package/runtime/program.js +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.3.0](https://github.com/roarkhq/cli-roark-analytics/compare/v0.2.0...v0.3.0) (2026-09-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **auth:** browser-based login (OAuth authorization-code + PKCE) ([#6](https://github.com/roarkhq/cli-roark-analytics/issues/6)) ([62c476e](https://github.com/roarkhq/cli-roark-analytics/commit/62c476ea4d383842bf57fe9645ba65a77297d503))
|
|
9
|
+
* **auth:** verify token on login, warn on env-var shadow, clearer errors ([#5](https://github.com/roarkhq/cli-roark-analytics/issues/5)) ([5b7a256](https://github.com/roarkhq/cli-roark-analytics/commit/5b7a2568149cf36d1cabe2cfe84afc63b4ef7939))
|
|
10
|
+
* **cli:** add the roark command line interface ([9ad4541](https://github.com/roarkhq/cli-roark-analytics/commit/9ad45413fca0e0864db5827c9511b4c2757103eb))
|
|
11
|
+
* **cli:** cli update ([e28294f](https://github.com/roarkhq/cli-roark-analytics/commit/e28294fec4df79eb40a184b6996a31f2ed45a7db))
|
|
12
|
+
* **cli:** cli update ([8c16aed](https://github.com/roarkhq/cli-roark-analytics/commit/8c16aed1a2b5fdebfd0303f0905b8d596b4d2acb))
|
|
13
|
+
* **cli:** cli update ([131c7c3](https://github.com/roarkhq/cli-roark-analytics/commit/131c7c37e05b28ea31934708c774c4c512191a60))
|
|
14
|
+
* **cli:** cli update ([c46b935](https://github.com/roarkhq/cli-roark-analytics/commit/c46b935e23f0e40e290a2430879e3c5bb9e7fe77))
|
|
15
|
+
* **cli:** cli update ([35b9b0b](https://github.com/roarkhq/cli-roark-analytics/commit/35b9b0b5eee942b4ed24e4ca6e6aedf5fafa5a54))
|
|
16
|
+
* **cli:** config diff/apply for config-as-code directories ([#1143](https://github.com/roarkhq/cli-roark-analytics/issues/1143)) ([d4ff899](https://github.com/roarkhq/cli-roark-analytics/commit/d4ff8999e223e9c3c0e36c6f910d08058507841b))
|
|
17
|
+
* **cli:** install with a script, not a global npm install ([6db9132](https://github.com/roarkhq/cli-roark-analytics/commit/6db91327c257d38de08dcf816cd103eb13ee3aaa))
|
|
18
|
+
* **cli:** ship a shrinkwrap so an install resolves the same tree twice ([415cc82](https://github.com/roarkhq/cli-roark-analytics/commit/415cc8269939265d9b3a3a9827c4a09a568144bb))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* **cli:** do not send a stored credential to a base URL a project file chose ([1d50a94](https://github.com/roarkhq/cli-roark-analytics/commit/1d50a9447e1a5bc988e9d2e2d641f58ed5865b87))
|
|
24
|
+
* **cli:** make a truncated download do nothing ([9a52f2f](https://github.com/roarkhq/cli-roark-analytics/commit/9a52f2fb6dc1938bbd6d5919fae2a4673622f15f))
|
|
25
|
+
* **cli:** only read stdin when there is something to read from ([66c7061](https://github.com/roarkhq/cli-roark-analytics/commit/66c706157bee14727b96a1970fa49474755f0812))
|
|
26
|
+
* **cli:** report the version the package actually publishes ([bbaf3ac](https://github.com/roarkhq/cli-roark-analytics/commit/bbaf3ac48a3e39c6f626810547caf9b7329fb4bd))
|
|
27
|
+
* **cli:** warn instead of failing when the shrinkwrap cannot be built ([09f0a2c](https://github.com/roarkhq/cli-roark-analytics/commit/09f0a2c793ec5ab3efdc55608229a7da9bb1f414))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Chores
|
|
31
|
+
|
|
32
|
+
* **cli:** 0.2.0 ([cd35004](https://github.com/roarkhq/cli-roark-analytics/commit/cd350040d4dcbbb4eaeaa714ce5a759ffe627c88))
|
|
33
|
+
* **cli:** drop the inert x-release-please-version marker ([e3747f7](https://github.com/roarkhq/cli-roark-analytics/commit/e3747f7b30bb9b345020debc8475a685576a4c60))
|
|
34
|
+
* **cli:** pin shell scripts to LF line endings ([1a65bea](https://github.com/roarkhq/cli-roark-analytics/commit/1a65bea0f823abfa84818646b738031d018cf73c))
|
|
35
|
+
* **cli:** start the CLI at 0.1.0 rather than mirroring the SDK ([e9dad1d](https://github.com/roarkhq/cli-roark-analytics/commit/e9dad1d3e8672a71bfda639599361ef1e2d70c3a))
|
|
36
|
+
* stand the package up on its own ([068005f](https://github.com/roarkhq/cli-roark-analytics/commit/068005f5f6154e7131926328246ec96ae10e0626))
|
package/commands.js
CHANGED
|
@@ -1375,7 +1375,7 @@ exports.COMMANDS = [
|
|
|
1375
1375
|
path: ['branchingMode'],
|
|
1376
1376
|
location: 'body',
|
|
1377
1377
|
required: false,
|
|
1378
|
-
description: 'Scripted flows only.',
|
|
1378
|
+
description: 'Scripted flows only. How a run walks the graph. DETERMINISTIC ("Simulate every path" in the app) places one call per variant, each following its path exactly whatever the agent says. ADAPTIVE ("Adapt to your agent") collapses the paths into one call PER PERSONA, on which the simulated customer picks a branch from what the agent actually said. Both modes speak the exact authored lines, and neither changes how metrics or expectations grade.',
|
|
1379
1379
|
valueKind: 'string',
|
|
1380
1380
|
enumValues: ['DETERMINISTIC', 'ADAPTIVE'],
|
|
1381
1381
|
repeatable: false,
|
|
@@ -1756,15 +1756,15 @@ exports.COMMANDS = [
|
|
|
1756
1756
|
bodyVariants: [
|
|
1757
1757
|
{
|
|
1758
1758
|
name: 'PromptMetricInput',
|
|
1759
|
-
required: ['calculationType', 'name', 'outputType'
|
|
1759
|
+
required: ['calculationType', 'name', 'outputType'],
|
|
1760
1760
|
},
|
|
1761
1761
|
{
|
|
1762
1762
|
name: 'FormulaMetricInput',
|
|
1763
|
-
required: ['calculationType', 'name', 'outputType', 'formula', 'sources'
|
|
1763
|
+
required: ['calculationType', 'name', 'outputType', 'formula', 'sources'],
|
|
1764
1764
|
},
|
|
1765
1765
|
{
|
|
1766
1766
|
name: 'PatternMetricInput',
|
|
1767
|
-
required: ['calculationType', 'name', 'operation', 'outcome'
|
|
1767
|
+
required: ['calculationType', 'name', 'operation', 'outcome'],
|
|
1768
1768
|
},
|
|
1769
1769
|
],
|
|
1770
1770
|
acceptsBody: true,
|
|
@@ -22,7 +22,7 @@ Optional.
|
|
|
22
22
|
Optional.
|
|
23
23
|
.TP
|
|
24
24
|
\fB\-\-branching\-mode\fR \fI<value>\fR
|
|
25
|
-
Scripted flows only. One of: DETERMINISTIC, ADAPTIVE. Optional.
|
|
25
|
+
Scripted flows only. How a run walks the graph. DETERMINISTIC ("Simulate every path" in the app) places one call per variant, each following its path exactly whatever the agent says. ADAPTIVE ("Adapt to your agent") collapses the paths into one call PER PERSONA, on which the simulated customer picks a branch from what the agent actually said. Both modes speak the exact authored lines, and neither changes how metrics or expectations grade. One of: DETERMINISTIC, ADAPTIVE. Optional.
|
|
26
26
|
.TP
|
|
27
27
|
\fB\-\-agent\-ids\fR \fI<value>\fR
|
|
28
28
|
Replaces the linked agents. Omit to leave them unchanged. An improv flow must keep at least one; a scripted flow can be left with none. Repeatable. Optional.
|
|
@@ -13,13 +13,13 @@ POST /v1/metric/definitions
|
|
|
13
13
|
This endpoint accepts one of several request shapes, so the body is supplied as JSON through \e\-\e\-data or on standard input rather than as individual flags.
|
|
14
14
|
.TP
|
|
15
15
|
\fBPromptMetricInput\fR
|
|
16
|
-
Requires calculationType, name, outputType
|
|
16
|
+
Requires calculationType, name, outputType.
|
|
17
17
|
.TP
|
|
18
18
|
\fBFormulaMetricInput\fR
|
|
19
|
-
Requires calculationType, name, outputType, formula, sources
|
|
19
|
+
Requires calculationType, name, outputType, formula, sources.
|
|
20
20
|
.TP
|
|
21
21
|
\fBPatternMetricInput\fR
|
|
22
|
-
Requires calculationType, name, operation, outcome
|
|
22
|
+
Requires calculationType, name, operation, outcome.
|
|
23
23
|
.SH "GLOBAL OPTIONS"
|
|
24
24
|
.TP
|
|
25
25
|
\fB\-\-data\fR \fI<json>\fR
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@roarkanalytics/cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "@roarkanalytics/cli",
|
|
9
|
+
"version": "0.3.0",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@roarkanalytics/sdk": "^3.2.1",
|
|
13
|
+
"commander": "^14.0.3",
|
|
14
|
+
"yaml": "^2.9.0"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"roark": "bin.js"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"node_modules/@roarkanalytics/sdk": {
|
|
24
|
+
"version": "3.2.1",
|
|
25
|
+
"resolved": "https://registry.npmjs.org/@roarkanalytics/sdk/-/sdk-3.2.1.tgz",
|
|
26
|
+
"integrity": "sha512-5K8cMDUBWD4Id1BGHSiHe71tJSW9kn119XgFsSAckgpnaYtsP8SooRuz6XOuMXortSTUgLyqVrtBnExuw36jyw==",
|
|
27
|
+
"license": "Apache-2.0"
|
|
28
|
+
},
|
|
29
|
+
"node_modules/commander": {
|
|
30
|
+
"version": "14.0.3",
|
|
31
|
+
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
|
|
32
|
+
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"node_modules/yaml": {
|
|
39
|
+
"version": "2.9.0",
|
|
40
|
+
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
|
41
|
+
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
|
42
|
+
"license": "ISC",
|
|
43
|
+
"bin": {
|
|
44
|
+
"yaml": "bin.mjs"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">= 14.6"
|
|
48
|
+
},
|
|
49
|
+
"funding": {
|
|
50
|
+
"url": "https://github.com/sponsors/eemeli"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roarkanalytics/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The official command line interface for the Roark API",
|
|
5
5
|
"author": "Roark <james@roark.ai>",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/roarkhq/
|
|
10
|
-
"directory": "packages/cli"
|
|
9
|
+
"url": "git+https://github.com/roarkhq/cli-roark-analytics.git"
|
|
11
10
|
},
|
|
12
|
-
"homepage": "https://github.com/roarkhq/
|
|
11
|
+
"homepage": "https://github.com/roarkhq/cli-roark-analytics#readme",
|
|
13
12
|
"license": "Apache-2.0",
|
|
14
13
|
"packageManager": "pnpm@10.30.1",
|
|
15
14
|
"private": false,
|
|
@@ -26,11 +25,11 @@
|
|
|
26
25
|
"build": "./build",
|
|
27
26
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
28
27
|
"test": "jest",
|
|
29
|
-
"lint": "eslint .",
|
|
30
|
-
"fix": "eslint --fix ."
|
|
28
|
+
"lint": "eslint . && prettier --check .",
|
|
29
|
+
"fix": "eslint --fix . && prettier --write ."
|
|
31
30
|
},
|
|
32
31
|
"dependencies": {
|
|
33
|
-
"@roarkanalytics/sdk": "^3.1
|
|
32
|
+
"@roarkanalytics/sdk": "^3.2.1",
|
|
34
33
|
"commander": "^14.0.3",
|
|
35
34
|
"yaml": "^2.9.0"
|
|
36
35
|
},
|
|
@@ -100,4 +99,4 @@
|
|
|
100
99
|
"./man/roark-webhook-get.1",
|
|
101
100
|
"./man/roark-webhook-list.1"
|
|
102
101
|
]
|
|
103
|
-
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-based `auth login` (OAuth authorization-code + PKCE over a loopback redirect).
|
|
3
|
+
*
|
|
4
|
+
* The CLI starts a throwaway `127.0.0.1` listener, opens the platform's consent page, and waits for
|
|
5
|
+
* the approved code to come back to that listener. It then exchanges the code (with its PKCE
|
|
6
|
+
* verifier) for a freshly minted API key at the public token endpoint. No token is ever typed or
|
|
7
|
+
* pasted; the secret only exists in the final exchange response.
|
|
8
|
+
*/
|
|
9
|
+
export declare const generatePkce: () => {
|
|
10
|
+
verifier: string;
|
|
11
|
+
challenge: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Where the consent page lives, derived from the API base URL. `api.roark.ai` -> `platform.roark.ai`.
|
|
15
|
+
* `ROARK_PLATFORM_URL` overrides for local/self-hosted stages where the mapping doesn't hold.
|
|
16
|
+
*/
|
|
17
|
+
export declare const platformOriginFor: (apiBaseUrl: string) => string;
|
|
18
|
+
export interface BrowserLoginOptions {
|
|
19
|
+
apiBaseUrl: string;
|
|
20
|
+
platformOrigin: string;
|
|
21
|
+
clientName: string;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
onAuthorizeUrl?: (url: string) => void;
|
|
24
|
+
open?: (url: string) => void;
|
|
25
|
+
}
|
|
26
|
+
export declare const browserLogin: (options: BrowserLoginOptions) => Promise<string>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Browser-based `auth login` (OAuth authorization-code + PKCE over a loopback redirect).
|
|
4
|
+
*
|
|
5
|
+
* The CLI starts a throwaway `127.0.0.1` listener, opens the platform's consent page, and waits for
|
|
6
|
+
* the approved code to come back to that listener. It then exchanges the code (with its PKCE
|
|
7
|
+
* verifier) for a freshly minted API key at the public token endpoint. No token is ever typed or
|
|
8
|
+
* pasted; the secret only exists in the final exchange response.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.browserLogin = exports.platformOriginFor = exports.generatePkce = void 0;
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const node_http_1 = require("node:http");
|
|
15
|
+
const base64url = (buf) => buf.toString('base64url');
|
|
16
|
+
// A PKCE pair: a random verifier and its S256 challenge (base64url(sha256(verifier))).
|
|
17
|
+
const generatePkce = () => {
|
|
18
|
+
const verifier = base64url((0, node_crypto_1.randomBytes)(32));
|
|
19
|
+
const challenge = base64url((0, node_crypto_1.createHash)('sha256').update(verifier).digest());
|
|
20
|
+
return { verifier, challenge };
|
|
21
|
+
};
|
|
22
|
+
exports.generatePkce = generatePkce;
|
|
23
|
+
/**
|
|
24
|
+
* Where the consent page lives, derived from the API base URL. `api.roark.ai` -> `platform.roark.ai`.
|
|
25
|
+
* `ROARK_PLATFORM_URL` overrides for local/self-hosted stages where the mapping doesn't hold.
|
|
26
|
+
*/
|
|
27
|
+
const platformOriginFor = (apiBaseUrl) => {
|
|
28
|
+
const override = process.env['ROARK_PLATFORM_URL'];
|
|
29
|
+
if (override && override.trim().length > 0)
|
|
30
|
+
return override.replace(/\/+$/, '');
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(apiBaseUrl);
|
|
33
|
+
if (url.hostname === 'api.roark.ai')
|
|
34
|
+
return 'https://platform.roark.ai';
|
|
35
|
+
if (url.hostname.startsWith('api.'))
|
|
36
|
+
return `${url.protocol}//platform.${url.hostname.slice('api.'.length)}`;
|
|
37
|
+
// localhost / an unrecognised host: assume the web app shares the origin.
|
|
38
|
+
return url.origin;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return 'https://platform.roark.ai';
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
exports.platformOriginFor = platformOriginFor;
|
|
45
|
+
const closeHtml = (title, body) => `<!doctype html><meta charset="utf-8"><title>${title}</title>` +
|
|
46
|
+
`<body style="font:15px system-ui;margin:4rem auto;max-width:28rem;text-align:center;color:#222">` +
|
|
47
|
+
`<h2 style="margin:0 0 .5rem">${title}</h2><p style="color:#666">${body}</p></body>`;
|
|
48
|
+
// Best-effort browser open. If it fails, the caller has already printed the URL to open by hand.
|
|
49
|
+
const openBrowser = (url) => {
|
|
50
|
+
const [command, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
51
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
|
|
52
|
+
: ['xdg-open', [url]];
|
|
53
|
+
try {
|
|
54
|
+
const child = (0, node_child_process_1.spawn)(command, [...args], { stdio: 'ignore', detached: true });
|
|
55
|
+
child.on('error', () => undefined);
|
|
56
|
+
child.unref();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* the printed URL is the fallback */
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
// Run the full loopback + PKCE flow and return the minted bearer token.
|
|
63
|
+
const browserLogin = async (options) => {
|
|
64
|
+
const { verifier, challenge } = (0, exports.generatePkce)();
|
|
65
|
+
const state = base64url((0, node_crypto_1.randomBytes)(16));
|
|
66
|
+
const code = await new Promise((resolve, reject) => {
|
|
67
|
+
let settled = false;
|
|
68
|
+
const finish = (fn) => {
|
|
69
|
+
if (settled)
|
|
70
|
+
return;
|
|
71
|
+
settled = true;
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
server.close();
|
|
74
|
+
// This is a one-shot listener; drop any keep-alive socket so nothing lingers after we're done.
|
|
75
|
+
server.closeAllConnections?.();
|
|
76
|
+
fn();
|
|
77
|
+
};
|
|
78
|
+
const server = (0, node_http_1.createServer)((req, res) => {
|
|
79
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
80
|
+
if (url.pathname !== '/callback') {
|
|
81
|
+
res.writeHead(404);
|
|
82
|
+
res.end();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const returnedCode = url.searchParams.get('code');
|
|
86
|
+
const returnedState = url.searchParams.get('state');
|
|
87
|
+
const errorParam = url.searchParams.get('error');
|
|
88
|
+
const ok = !errorParam && returnedCode && returnedState === state;
|
|
89
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
90
|
+
res.end(ok ?
|
|
91
|
+
closeHtml('Logged in', 'You can close this tab and return to your terminal.')
|
|
92
|
+
: closeHtml('Login failed', 'You can close this tab and return to your terminal.'));
|
|
93
|
+
if (ok)
|
|
94
|
+
finish(() => resolve(returnedCode));
|
|
95
|
+
else if (errorParam)
|
|
96
|
+
finish(() => reject(new Error(`authorization was ${errorParam}`)));
|
|
97
|
+
else
|
|
98
|
+
finish(() => reject(new Error('authorization response was invalid')));
|
|
99
|
+
});
|
|
100
|
+
const timer = setTimeout(() => finish(() => reject(new Error('timed out waiting for authorization in the browser'))), options.timeoutMs ?? 300_000);
|
|
101
|
+
timer.unref?.();
|
|
102
|
+
server.on('error', (error) => finish(() => reject(error)));
|
|
103
|
+
server.listen(0, '127.0.0.1', () => {
|
|
104
|
+
const { port } = server.address();
|
|
105
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
106
|
+
const authorizeUrl = `${options.platformOrigin}/cli/authorize?` +
|
|
107
|
+
new URLSearchParams({
|
|
108
|
+
code_challenge: challenge,
|
|
109
|
+
state,
|
|
110
|
+
redirect_uri: redirectUri,
|
|
111
|
+
name: options.clientName,
|
|
112
|
+
}).toString();
|
|
113
|
+
options.onAuthorizeUrl?.(authorizeUrl);
|
|
114
|
+
(options.open ?? openBrowser)(authorizeUrl);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, '')}/v1/cli/auth/token`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ code, code_verifier: verifier }),
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const detail = await response.text().catch(() => '');
|
|
124
|
+
throw new Error(`token exchange failed (${response.status})${detail ? `: ${detail}` : ''}`);
|
|
125
|
+
}
|
|
126
|
+
const body = (await response.json());
|
|
127
|
+
const token = body.data?.token;
|
|
128
|
+
if (!token)
|
|
129
|
+
throw new Error('token exchange returned no token');
|
|
130
|
+
return token;
|
|
131
|
+
};
|
|
132
|
+
exports.browserLogin = browserLogin;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `roark auth login | logout | status`
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `login` defaults to a browser flow (OAuth authorization-code + PKCE): it opens the platform's
|
|
5
|
+
* consent page, mints a key on approval, and stores it. Non-interactive/piped input, `--paste`, or
|
|
6
|
+
* `--no-browser` fall back to taking a bearer token. A token is never accepted as a flag value:
|
|
7
|
+
* flags land in shell history and the process table, where a bearer token has no business being; it
|
|
8
|
+
* is read from a prompt with echo off, or from stdin so CI can pipe it in.
|
|
7
9
|
*/
|
|
8
10
|
import { Command } from 'commander';
|
|
9
|
-
|
|
11
|
+
import type Roark from '@roarkanalytics/sdk';
|
|
12
|
+
/** How the program builds an authenticated client; injected so tests can stub it (and skip the
|
|
13
|
+
* network entirely by omitting it). Matches the factory the other commands receive. */
|
|
14
|
+
type ClientFor = (options: never) => Roark;
|
|
15
|
+
export declare const registerAuthCommands: (root: Command, binaryName: string, clientFor?: ClientFor) => void;
|
|
16
|
+
export {};
|
package/runtime/commands/auth.js
CHANGED
|
@@ -2,18 +2,67 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `roark auth login | logout | status`
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `login` defaults to a browser flow (OAuth authorization-code + PKCE): it opens the platform's
|
|
6
|
+
* consent page, mints a key on approval, and stores it. Non-interactive/piped input, `--paste`, or
|
|
7
|
+
* `--no-browser` fall back to taking a bearer token. A token is never accepted as a flag value:
|
|
8
|
+
* flags land in shell history and the process table, where a bearer token has no business being; it
|
|
9
|
+
* is read from a prompt with echo off, or from stdin so CI can pipe it in.
|
|
8
10
|
*/
|
|
9
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
12
|
exports.registerAuthCommands = void 0;
|
|
11
13
|
const commander_1 = require("commander");
|
|
14
|
+
const node_os_1 = require("node:os");
|
|
12
15
|
const node_readline_1 = require("node:readline");
|
|
16
|
+
const browser_login_1 = require("../browser-login");
|
|
13
17
|
const errors_1 = require("../errors");
|
|
14
18
|
const config_1 = require("../config");
|
|
15
19
|
const output_1 = require("../output");
|
|
16
20
|
const confirm_1 = require("../confirm");
|
|
21
|
+
// The SDK's default when no base URL is configured; the token exchange targets the same host.
|
|
22
|
+
const DEFAULT_BASE_URL = 'https://api.roark.ai';
|
|
23
|
+
// Any authenticated endpoint works as a liveness probe: `requireAuth` runs first, so a bad token is
|
|
24
|
+
// a 401 while a valid one that merely lacks this endpoint's permission is a 403 — both prove the
|
|
25
|
+
// token authenticates. `/v1/agent` is a stable GET that every project has.
|
|
26
|
+
const AUTH_PROBE_PATH = '/v1/agent';
|
|
27
|
+
/**
|
|
28
|
+
* Confirm the stored credential actually authenticates, so a mistyped or half-pasted token is caught
|
|
29
|
+
* here instead of on the user's next real command. Best-effort: it never fails the login (the token
|
|
30
|
+
* is already saved) and stays quiet-but-honest when offline.
|
|
31
|
+
*/
|
|
32
|
+
const verifyCredential = async (clientFor, binaryName, color) => {
|
|
33
|
+
let client;
|
|
34
|
+
try {
|
|
35
|
+
// Resolve the client the same way a real command would (no forced --token), so the base-url
|
|
36
|
+
// trust guard still applies: we must not send the credential to a host a checked-in .roark.json
|
|
37
|
+
// chose. This verifies the effective credential, which is exactly what the next command will use.
|
|
38
|
+
client = clientFor({});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// e.g. the trust guard refused, or config is malformed. Don't fail the login, but say the check
|
|
42
|
+
// was skipped so it doesn't look like the feature silently didn't run.
|
|
43
|
+
const reason = error.message.split('\n')[0];
|
|
44
|
+
(0, output_1.write)((0, output_1.paint)(`Saved, but skipped the token check (${reason}).`, 'dim', color), process.stderr);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const verified = () => (0, output_1.write)(`${(0, output_1.paint)('Verified', 'green', color)} the token authenticates.`, process.stderr);
|
|
48
|
+
try {
|
|
49
|
+
await client.get(AUTH_PROBE_PATH, { query: { limit: '1' } });
|
|
50
|
+
verified();
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const status = error.status;
|
|
54
|
+
if (status === 401) {
|
|
55
|
+
(0, output_1.write)((0, output_1.paint)(`Warning: the token was rejected (401). It may be wrong, expired, or revoked. It is still saved; run \`${binaryName} auth login\` again to replace it.`, 'yellow', color), process.stderr);
|
|
56
|
+
}
|
|
57
|
+
else if (status !== undefined && status < 500) {
|
|
58
|
+
// 403 (authenticated, lacks agent:read) or any other client-level response: auth succeeded.
|
|
59
|
+
verified();
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
(0, output_1.write)((0, output_1.paint)(`Saved, but could not verify it right now (${error.message}). It will be used as-is.`, 'dim', color), process.stderr);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
17
66
|
/** Reads a secret without echoing it, so it is not left on screen or in a scrollback. */
|
|
18
67
|
const promptSecret = async (prompt) => {
|
|
19
68
|
const input = process.stdin;
|
|
@@ -39,7 +88,7 @@ const promptSecret = async (prompt) => {
|
|
|
39
88
|
output.write('\n');
|
|
40
89
|
}
|
|
41
90
|
};
|
|
42
|
-
const readToken = async () => {
|
|
91
|
+
const readToken = async (binaryName) => {
|
|
43
92
|
if (!(0, confirm_1.isInteractive)()) {
|
|
44
93
|
const piped = await new Promise((resolve, reject) => {
|
|
45
94
|
let buffer = '';
|
|
@@ -50,33 +99,81 @@ const readToken = async () => {
|
|
|
50
99
|
});
|
|
51
100
|
const token = piped.trim();
|
|
52
101
|
if (token.length === 0) {
|
|
53
|
-
throw new errors_1.UsageError(
|
|
102
|
+
throw new errors_1.UsageError(`No token on stdin. Pipe one in (echo "$ROARK_API_BEARER_TOKEN" | ${binaryName} auth login), or run this in a terminal to be prompted.`);
|
|
54
103
|
}
|
|
55
104
|
return token;
|
|
56
105
|
}
|
|
57
106
|
const token = (await promptSecret('Bearer token: ')).trim();
|
|
58
|
-
if (token.length === 0)
|
|
59
|
-
|
|
107
|
+
if (token.length === 0) {
|
|
108
|
+
// The most common cause: a paste that the hidden prompt did not capture. Point at the reliable
|
|
109
|
+
// path rather than just saying "nothing entered".
|
|
110
|
+
throw new errors_1.UsageError(`No token entered. If you pasted and nothing happened, pipe it instead: echo "$ROARK_API_BEARER_TOKEN" | ${binaryName} auth login`);
|
|
111
|
+
}
|
|
60
112
|
return token;
|
|
61
113
|
};
|
|
62
|
-
|
|
114
|
+
// Save the token and report where it went (masked). Shared by the browser and paste paths.
|
|
115
|
+
const persistToken = (token) => {
|
|
116
|
+
const path = (0, config_1.writeUserConfig)({ ...(0, config_1.readUserConfig)(), bearerToken: token });
|
|
117
|
+
const color = (0, output_1.supportsColor)(process.stderr);
|
|
118
|
+
(0, output_1.write)(`${(0, output_1.paint)('Saved', 'green', color)} ${(0, config_1.maskToken)(token)} to ${path}`, process.stderr);
|
|
119
|
+
return { color };
|
|
120
|
+
};
|
|
121
|
+
// A set environment variable outranks the stored token (see the precedence in config.ts), so without
|
|
122
|
+
// this a login looks like it "did nothing" when a stale export shadows what we just saved.
|
|
123
|
+
const warnEnvShadow = (token, color) => {
|
|
124
|
+
const fromEnv = process.env['ROARK_API_BEARER_TOKEN'];
|
|
125
|
+
if (fromEnv && fromEnv !== token) {
|
|
126
|
+
(0, output_1.write)((0, output_1.paint)(`Note: ROARK_API_BEARER_TOKEN is set and overrides this stored token. Unset it to use the credential you just saved.`, 'yellow', color), process.stderr);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const registerAuthCommands = (root, binaryName, clientFor) => {
|
|
63
130
|
const auth = new commander_1.Command('auth').description('Manage the stored credential').showHelpAfterError();
|
|
64
131
|
auth.action(() => auth.outputHelp());
|
|
65
132
|
auth
|
|
66
133
|
.command('login')
|
|
67
|
-
.description('
|
|
134
|
+
.description('Authorize this CLI in your browser, or store a bearer token')
|
|
135
|
+
.option('--no-browser', 'do not open a browser; paste or pipe a token instead')
|
|
136
|
+
.option('--paste', 'paste or pipe a bearer token instead of using the browser')
|
|
68
137
|
.addHelpText('after', [
|
|
69
138
|
'',
|
|
70
|
-
'
|
|
139
|
+
'By default an interactive terminal opens your browser to approve access and stores the',
|
|
140
|
+
'minted key. Non-interactive (piped) input always takes a token, so CI keeps working:',
|
|
71
141
|
'',
|
|
72
142
|
` ${binaryName} auth login`,
|
|
143
|
+
` ${binaryName} auth login --paste`,
|
|
73
144
|
` echo "$ROARK_API_BEARER_TOKEN" | ${binaryName} auth login`,
|
|
74
145
|
].join('\n'))
|
|
75
|
-
.action(async () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
146
|
+
.action(async (options) => {
|
|
147
|
+
// Browser flow when we're an interactive terminal and it wasn't opted out. A piped /
|
|
148
|
+
// non-interactive invocation always uses the token path so `echo "$TOKEN" | auth login` works.
|
|
149
|
+
const useBrowser = options.browser !== false && options.paste !== true && (0, confirm_1.isInteractive)();
|
|
150
|
+
if (useBrowser) {
|
|
151
|
+
const color = (0, output_1.supportsColor)(process.stderr);
|
|
152
|
+
const apiBaseUrl = (0, config_1.resolveConfig)({}).config.baseURL ?? DEFAULT_BASE_URL;
|
|
153
|
+
(0, output_1.write)((0, output_1.paint)('Opening your browser to authorize this CLI…', 'dim', color), process.stderr);
|
|
154
|
+
let token;
|
|
155
|
+
try {
|
|
156
|
+
token = await (0, browser_login_1.browserLogin)({
|
|
157
|
+
apiBaseUrl,
|
|
158
|
+
platformOrigin: (0, browser_login_1.platformOriginFor)(apiBaseUrl),
|
|
159
|
+
clientName: `CLI on ${(0, node_os_1.hostname)()}`,
|
|
160
|
+
onAuthorizeUrl: (url) => (0, output_1.write)((0, output_1.paint)(`If your browser didn't open, visit:\n ${url}`, 'dim', color), process.stderr),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
throw new errors_1.UsageError(`Browser login failed: ${error.message}\n` +
|
|
165
|
+
`Run \`${binaryName} auth login --paste\` to paste or pipe a token instead.`);
|
|
166
|
+
}
|
|
167
|
+
const { color: savedColor } = persistToken(token);
|
|
168
|
+
warnEnvShadow(token, savedColor);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const token = await readToken(binaryName);
|
|
172
|
+
const { color } = persistToken(token);
|
|
173
|
+
// Confirm the token actually works, so a bad paste surfaces now rather than on the next command.
|
|
174
|
+
if (clientFor)
|
|
175
|
+
await verifyCredential(clientFor, binaryName, color);
|
|
176
|
+
warnEnvShadow(token, color);
|
|
80
177
|
});
|
|
81
178
|
auth
|
|
82
179
|
.command('logout')
|
package/runtime/program.js
CHANGED
|
@@ -258,7 +258,7 @@ const createProgram = (options) => {
|
|
|
258
258
|
continue;
|
|
259
259
|
addApiCommand(root, options, definition);
|
|
260
260
|
}
|
|
261
|
-
(0, auth_1.registerAuthCommands)(root, options.binaryName);
|
|
261
|
+
(0, auth_1.registerAuthCommands)(root, options.binaryName, clientFor);
|
|
262
262
|
(0, config_as_code_1.registerConfigAsCodeCommands)(root, options.binaryName, clientFor, exports.resolveOutput);
|
|
263
263
|
(0, config_1.registerConfigCommands)(root, options.binaryName);
|
|
264
264
|
(0, api_1.registerApiCommand)(root, options.binaryName, clientFor, exports.resolveOutput);
|
package/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "0.
|
|
1
|
+
export declare const version = "0.3.0";
|
package/version.js
CHANGED
|
@@ -8,4 +8,4 @@ exports.version = void 0;
|
|
|
8
8
|
// It exists at all because the CLI is not in release-please-config.json's
|
|
9
9
|
// `extra-files` - it ships from publish-cli.yml rather than the vendor's
|
|
10
10
|
// release train - so nothing else keeps this in step.
|
|
11
|
-
exports.version = '0.
|
|
11
|
+
exports.version = '0.3.0';
|