@nebulr-group/bridge-cli 0.1.0-beta.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +75 -18
- package/dist/auth/api-client.d.ts +48 -0
- package/dist/auth/api-client.d.ts.map +1 -0
- package/dist/auth/api-client.js +109 -0
- package/dist/auth/api-client.js.map +1 -0
- package/dist/auth/browser-open.d.ts +12 -0
- package/dist/auth/browser-open.d.ts.map +1 -0
- package/dist/auth/browser-open.js +82 -0
- package/dist/auth/browser-open.js.map +1 -0
- package/dist/auth/loopback.d.ts +63 -0
- package/dist/auth/loopback.d.ts.map +1 -0
- package/dist/auth/loopback.js +290 -0
- package/dist/auth/loopback.js.map +1 -0
- package/dist/auth/pkce.d.ts +19 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +46 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/relative-time.d.ts +2 -0
- package/dist/auth/relative-time.d.ts.map +1 -0
- package/dist/auth/relative-time.js +27 -0
- package/dist/auth/relative-time.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/auth/config.command.d.ts +8 -0
- package/dist/commands/auth/config.command.d.ts.map +1 -0
- package/dist/commands/auth/config.command.js +60 -0
- package/dist/commands/auth/config.command.js.map +1 -0
- package/dist/commands/auth/login.command.d.ts +20 -0
- package/dist/commands/auth/login.command.d.ts.map +1 -0
- package/dist/commands/auth/login.command.js +141 -0
- package/dist/commands/auth/login.command.js.map +1 -0
- package/dist/commands/auth/logout.command.d.ts +14 -0
- package/dist/commands/auth/logout.command.d.ts.map +1 -0
- package/dist/commands/auth/logout.command.js +57 -0
- package/dist/commands/auth/logout.command.js.map +1 -0
- package/dist/commands/auth/status.command.d.ts +14 -0
- package/dist/commands/auth/status.command.d.ts.map +1 -0
- package/dist/commands/auth/status.command.js +59 -0
- package/dist/commands/auth/status.command.js.map +1 -0
- package/dist/commands/auth.command.d.ts +10 -1
- package/dist/commands/auth.command.d.ts.map +1 -1
- package/dist/commands/auth.command.js +13 -55
- package/dist/commands/auth.command.js.map +1 -1
- package/dist/commands/guide.command.js +5 -5
- package/dist/commands/guide.command.js.map +1 -1
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +79 -9
- package/dist/config.js.map +1 -1
- package/dist/credentials.d.ts +57 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +136 -0
- package/dist/credentials.js.map +1 -0
- package/package.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nebulr Group
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -16,28 +16,83 @@ npx @nebulr-group/bridge-cli tenant list
|
|
|
16
16
|
|
|
17
17
|
## Authentication
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
bridge-cli supports two authentication paths.
|
|
20
|
+
|
|
21
|
+
### 1. Interactive (recommended for humans + AI agents) — `bridge auth login`
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bridge auth login
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Opens your default browser, runs through a PKCE-secured loopback flow (RFC 8252), and stores a 10-day token at `~/.config/bridge/credentials.json` (mode `0600`). After that, every subsequent `bridge` command picks the token up automatically — no env vars needed.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bridge auth status # show who you're logged in as and when the token expires
|
|
31
|
+
bridge auth logout # revoke the token and delete the credentials file
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
#### `bridge auth login` flags
|
|
35
|
+
|
|
36
|
+
| Flag | Description |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `--app <id\|name>` | Pin to a specific app, skipping the picker on the consent screen. |
|
|
39
|
+
| `--label <text>` | Friendly label stored on the token (default: `bridge-cli`). Useful when listing CLI tokens at `app.thebridge.dev/keys`. |
|
|
40
|
+
| `--no-browser` | Print the authorization URL instead of opening a browser. Use this on headless boxes or over SSH. |
|
|
41
|
+
|
|
42
|
+
When the token expires (10 days), the next command fails with a friendly `Token expired. Run `bridge auth login` to re-authenticate.` message.
|
|
43
|
+
|
|
44
|
+
The credentials file location honors `XDG_CONFIG_HOME`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
$XDG_CONFIG_HOME/bridge/credentials.json # if XDG_CONFIG_HOME is set
|
|
48
|
+
~/.config/bridge/credentials.json # otherwise
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Service-account / CI — `BRIDGE_API_KEY`
|
|
52
|
+
|
|
53
|
+
For non-interactive contexts (CI/CD pipelines, Docker images, headless agents) set the API key directly in the environment:
|
|
20
54
|
|
|
21
55
|
```bash
|
|
22
56
|
export BRIDGE_API_KEY=<your-api-token>
|
|
23
57
|
```
|
|
24
58
|
|
|
25
|
-
|
|
59
|
+
If you have logged in via `bridge auth login`, the credentials file always wins — a fresh login takes effect immediately, even when `BRIDGE_API_KEY` is still exported in your shell. `BRIDGE_API_KEY` is only used when no credentials file is present (the typical CI runner shape). To switch back to env-var auth on a developer machine, run `bridge auth logout` first.
|
|
60
|
+
|
|
61
|
+
Optional configuration (applies to both auth paths):
|
|
26
62
|
|
|
27
63
|
```bash
|
|
28
|
-
export BRIDGE_BASE_URL=https://
|
|
29
|
-
export BRIDGE_TENANT_ID=<tenant-id>
|
|
30
|
-
export BRIDGE_DEBUG=true
|
|
64
|
+
export BRIDGE_BASE_URL=https://api.thebridge.dev # default
|
|
65
|
+
export BRIDGE_TENANT_ID=<tenant-id> # for user commands
|
|
66
|
+
export BRIDGE_DEBUG=true # enable debug logging
|
|
31
67
|
```
|
|
32
68
|
|
|
33
69
|
## Usage
|
|
34
70
|
|
|
35
|
-
All output is JSON by default. AI agents parse it directly; humans can pipe through `jq`.
|
|
71
|
+
All output is JSON by default for management commands. AI agents parse it directly; humans can pipe through `jq`. The `bridge auth status` and `bridge auth login` commands print human-readable text, since their primary audience is a human in a terminal.
|
|
36
72
|
|
|
37
73
|
```bash
|
|
38
74
|
bridge <command> <subcommand> [options]
|
|
39
75
|
```
|
|
40
76
|
|
|
77
|
+
### Auth (interactive credentials)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bridge auth login # default: open browser, complete PKCE flow
|
|
81
|
+
bridge auth login --app acme # pin to a specific app
|
|
82
|
+
bridge auth login --label "work laptop"
|
|
83
|
+
bridge auth login --no-browser # print the URL (headless / SSH)
|
|
84
|
+
bridge auth status # show current login state
|
|
85
|
+
bridge auth logout # revoke token + delete local file
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Auth Configuration (app-level — separate from `auth login`)
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bridge auth config
|
|
92
|
+
bridge auth mfa --enabled true
|
|
93
|
+
bridge auth password-policy --access-token-ttl 3600
|
|
94
|
+
```
|
|
95
|
+
|
|
41
96
|
### App
|
|
42
97
|
|
|
43
98
|
```bash
|
|
@@ -86,14 +141,6 @@ bridge flag toggle --id <flag-id> --enabled true
|
|
|
86
141
|
bridge flag delete --id <flag-id>
|
|
87
142
|
```
|
|
88
143
|
|
|
89
|
-
### Auth Configuration
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
bridge auth config
|
|
93
|
-
bridge auth mfa --enabled true
|
|
94
|
-
bridge auth password-policy --access-token-ttl 3600
|
|
95
|
-
```
|
|
96
|
-
|
|
97
144
|
### Branding
|
|
98
145
|
|
|
99
146
|
```bash
|
|
@@ -176,14 +223,24 @@ bridge guide custom
|
|
|
176
223
|
}
|
|
177
224
|
```
|
|
178
225
|
|
|
226
|
+
`bridge auth login`, `bridge auth logout`, and `bridge auth status` print plain text (they're the only commands aimed primarily at humans).
|
|
227
|
+
|
|
179
228
|
## Exit Codes
|
|
180
229
|
|
|
181
230
|
| Code | Meaning |
|
|
182
231
|
|------|---------|
|
|
183
232
|
| 0 | Success |
|
|
184
|
-
| 1 | Client error (4xx) |
|
|
233
|
+
| 1 | Client error (4xx), or `bridge auth login` failed/cancelled |
|
|
185
234
|
| 2 | Server error (5xx) |
|
|
186
|
-
| 3 | Configuration error (missing API key, etc.) |
|
|
235
|
+
| 3 | Configuration error (missing API key, expired credentials, etc.) |
|
|
236
|
+
|
|
237
|
+
## Security notes
|
|
238
|
+
|
|
239
|
+
- `bridge auth login` uses RFC 8252 loopback PKCE — the loopback URL is always `http://127.0.0.1:<random-port>/callback` (never `localhost`, to avoid DNS spoofing).
|
|
240
|
+
- The loopback HTTP server handles a single request, then closes — no port stays bound.
|
|
241
|
+
- The CSRF `state` parameter is verified on the callback before the code is exchanged.
|
|
242
|
+
- The credentials file is written with mode `0600` (owner-only). Its parent directory is created with mode `0700`.
|
|
243
|
+
- The CLI never logs the JWT or the PKCE `code_verifier`.
|
|
187
244
|
|
|
188
245
|
## Development
|
|
189
246
|
|
|
@@ -197,6 +254,6 @@ npm run build
|
|
|
197
254
|
# Run locally
|
|
198
255
|
node bridge-cli/dist/bin.js --help
|
|
199
256
|
|
|
200
|
-
#
|
|
201
|
-
npm
|
|
257
|
+
# Test
|
|
258
|
+
npm test
|
|
202
259
|
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin client for the bridge-api `/v1/auth/cli/*` endpoints.
|
|
3
|
+
*
|
|
4
|
+
* - `exchangeCode(...)` calls `POST /v1/auth/cli/token` with `{code, code_verifier}`.
|
|
5
|
+
* - `revokeToken(...)` calls `POST /v1/auth/cli/revoke` with `x-api-key: <jwt>`.
|
|
6
|
+
*
|
|
7
|
+
* Errors come back RFC-style as `{ error, error_description }`. We translate
|
|
8
|
+
* the common cases to friendly messages so end-users don't see RFC tags.
|
|
9
|
+
*
|
|
10
|
+
* The CLI authenticates as `x-api-key: <jwt>` (NOT `Authorization: Bearer`) —
|
|
11
|
+
* see TBP-111 contract notes.
|
|
12
|
+
*/
|
|
13
|
+
export interface CliTokenExchangeResponse {
|
|
14
|
+
api_token: string;
|
|
15
|
+
expires_at: string;
|
|
16
|
+
app: {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
user: {
|
|
21
|
+
id: string;
|
|
22
|
+
email: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface CliApiClientOptions {
|
|
26
|
+
/** Base URL of bridge-api, e.g. `https://api.thebridge.dev`. No trailing slash. */
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
/** Used for diagnostics; do NOT log secrets. */
|
|
29
|
+
debug?: boolean;
|
|
30
|
+
/** Override fetch (for tests). Defaults to the global `fetch`. */
|
|
31
|
+
fetchImpl?: typeof fetch;
|
|
32
|
+
}
|
|
33
|
+
export declare class CliAuthApiError extends Error {
|
|
34
|
+
/** RFC error code returned by bridge-api (`invalid_request`, `invalid_grant`, ...). */
|
|
35
|
+
readonly code: string;
|
|
36
|
+
readonly status: number;
|
|
37
|
+
constructor(code: string, message: string, status: number);
|
|
38
|
+
}
|
|
39
|
+
export declare function createCliApiClient(options: CliApiClientOptions): {
|
|
40
|
+
exchangeCode: (args: {
|
|
41
|
+
code: string;
|
|
42
|
+
codeVerifier: string;
|
|
43
|
+
}) => Promise<CliTokenExchangeResponse>;
|
|
44
|
+
revokeToken: (token: string) => Promise<{
|
|
45
|
+
revoked: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=api-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../src/auth/api-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC;AAED,MAAM,WAAW,mBAAmB;IAClC,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,qBAAa,eAAgB,SAAQ,KAAK;IACxC,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBACZ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAM1D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB;yBAU3B;QAChC,IAAI,EAAE,MAAM,CAAC;QACb,YAAY,EAAE,MAAM,CAAC;KACtB,KAAG,OAAO,CAAC,wBAAwB,CAAC;yBA+BH,MAAM,KAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;EA2BzE"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin client for the bridge-api `/v1/auth/cli/*` endpoints.
|
|
3
|
+
*
|
|
4
|
+
* - `exchangeCode(...)` calls `POST /v1/auth/cli/token` with `{code, code_verifier}`.
|
|
5
|
+
* - `revokeToken(...)` calls `POST /v1/auth/cli/revoke` with `x-api-key: <jwt>`.
|
|
6
|
+
*
|
|
7
|
+
* Errors come back RFC-style as `{ error, error_description }`. We translate
|
|
8
|
+
* the common cases to friendly messages so end-users don't see RFC tags.
|
|
9
|
+
*
|
|
10
|
+
* The CLI authenticates as `x-api-key: <jwt>` (NOT `Authorization: Bearer`) —
|
|
11
|
+
* see TBP-111 contract notes.
|
|
12
|
+
*/
|
|
13
|
+
export class CliAuthApiError extends Error {
|
|
14
|
+
/** RFC error code returned by bridge-api (`invalid_request`, `invalid_grant`, ...). */
|
|
15
|
+
code;
|
|
16
|
+
status;
|
|
17
|
+
constructor(code, message, status) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'CliAuthApiError';
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.status = status;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function createCliApiClient(options) {
|
|
25
|
+
const baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
26
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
27
|
+
if (typeof fetchImpl !== 'function') {
|
|
28
|
+
throw new Error('No global `fetch` is available. Run bridge-cli on Node.js >= 18.');
|
|
29
|
+
}
|
|
30
|
+
async function exchangeCode(args) {
|
|
31
|
+
const url = `${baseUrl}/v1/auth/cli/token`;
|
|
32
|
+
const res = await fetchImpl(url, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'content-type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
code: args.code,
|
|
37
|
+
code_verifier: args.codeVerifier,
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
throw await translateError(res);
|
|
42
|
+
}
|
|
43
|
+
const body = (await res.json());
|
|
44
|
+
if (!body || typeof body.api_token !== 'string') {
|
|
45
|
+
throw new CliAuthApiError('invalid_response', 'Bridge API returned an unexpected response from /v1/auth/cli/token.', res.status);
|
|
46
|
+
}
|
|
47
|
+
return body;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Best-effort revoke. The CLI uses self-revoke (token is identified by the
|
|
51
|
+
* `x-api-key` it sends). 401 means the token is already gone — treat as
|
|
52
|
+
* success.
|
|
53
|
+
*/
|
|
54
|
+
async function revokeToken(token) {
|
|
55
|
+
const url = `${baseUrl}/v1/auth/cli/revoke`;
|
|
56
|
+
let res;
|
|
57
|
+
try {
|
|
58
|
+
res = await fetchImpl(url, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'content-type': 'application/json',
|
|
62
|
+
'x-api-key': token,
|
|
63
|
+
},
|
|
64
|
+
body: '{}',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
// Network failure — caller can still delete the local file.
|
|
69
|
+
throw new CliAuthApiError('network_error', `Could not reach bridge-api to revoke token: ${err instanceof Error ? err.message : String(err)}`, 0);
|
|
70
|
+
}
|
|
71
|
+
if (res.ok)
|
|
72
|
+
return { revoked: true };
|
|
73
|
+
if (res.status === 401)
|
|
74
|
+
return { revoked: true }; // already revoked / expired
|
|
75
|
+
throw await translateError(res);
|
|
76
|
+
}
|
|
77
|
+
return { exchangeCode, revokeToken };
|
|
78
|
+
}
|
|
79
|
+
async function translateError(res) {
|
|
80
|
+
let raw;
|
|
81
|
+
try {
|
|
82
|
+
raw = await res.json();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return new CliAuthApiError('http_error', `Bridge API returned HTTP ${res.status}.`, res.status);
|
|
86
|
+
}
|
|
87
|
+
const obj = (raw && typeof raw === 'object' ? raw : {});
|
|
88
|
+
const code = typeof obj.error === 'string' ? obj.error : 'http_error';
|
|
89
|
+
const desc = typeof obj.error_description === 'string' ? obj.error_description : undefined;
|
|
90
|
+
let friendly;
|
|
91
|
+
switch (code) {
|
|
92
|
+
case 'invalid_request':
|
|
93
|
+
friendly = desc ?? 'The login request was rejected as malformed.';
|
|
94
|
+
break;
|
|
95
|
+
case 'invalid_grant':
|
|
96
|
+
friendly = 'The authorization code has already been used or is invalid. Please run `bridge auth login` again.';
|
|
97
|
+
break;
|
|
98
|
+
case 'expired_grant':
|
|
99
|
+
friendly = 'The authorization code expired before it could be exchanged. Please run `bridge auth login` again.';
|
|
100
|
+
break;
|
|
101
|
+
case 'access_denied':
|
|
102
|
+
friendly = 'Login cancelled.';
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
friendly = desc ?? `Bridge API returned HTTP ${res.status}.`;
|
|
106
|
+
}
|
|
107
|
+
return new CliAuthApiError(code, friendly, res.status);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=api-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../../src/auth/api-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface OpenBrowserResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
/** Short reason the launch failed (only set when `ok === false`). */
|
|
4
|
+
reason?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Attempts to open `url` in the user's default browser. Resolves to
|
|
8
|
+
* `{ ok: true }` on success, `{ ok: false, reason }` on failure (caller
|
|
9
|
+
* should print the URL and ask the user to open it manually).
|
|
10
|
+
*/
|
|
11
|
+
export declare function openBrowser(url: string): Promise<OpenBrowserResult>;
|
|
12
|
+
//# sourceMappingURL=browser-open.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-open.d.ts","sourceRoot":"","sources":["../../src/auth/browser-open.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA6DzE"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform default-browser opener.
|
|
3
|
+
*
|
|
4
|
+
* Uses `child_process.spawn` to invoke the platform's "open this URL" command:
|
|
5
|
+
* - macOS: `open <url>`
|
|
6
|
+
* - Windows: `cmd /c start "" <url>` (the empty `""` is the window title;
|
|
7
|
+
* omitting it makes `start` interpret the URL as the title)
|
|
8
|
+
* - Linux: `xdg-open <url>` (most desktops; requires xdg-utils)
|
|
9
|
+
*
|
|
10
|
+
* Returns `false` if the spawn fails or the command exits non-zero, so callers
|
|
11
|
+
* can fall back to printing the URL.
|
|
12
|
+
*
|
|
13
|
+
* Implemented directly on `child_process` to avoid a heavyweight dependency
|
|
14
|
+
* for what is ~30 lines of platform detection.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from 'node:child_process';
|
|
17
|
+
/**
|
|
18
|
+
* Attempts to open `url` in the user's default browser. Resolves to
|
|
19
|
+
* `{ ok: true }` on success, `{ ok: false, reason }` on failure (caller
|
|
20
|
+
* should print the URL and ask the user to open it manually).
|
|
21
|
+
*/
|
|
22
|
+
export async function openBrowser(url) {
|
|
23
|
+
const platform = process.platform;
|
|
24
|
+
let command;
|
|
25
|
+
let args;
|
|
26
|
+
if (platform === 'darwin') {
|
|
27
|
+
command = 'open';
|
|
28
|
+
args = [url];
|
|
29
|
+
}
|
|
30
|
+
else if (platform === 'win32') {
|
|
31
|
+
// `cmd /c start "" <url>` — empty quoted string is the (ignored) window title.
|
|
32
|
+
command = 'cmd';
|
|
33
|
+
args = ['/c', 'start', '""', url];
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Treat anything else (linux, freebsd, etc.) as xdg-open.
|
|
37
|
+
command = 'xdg-open';
|
|
38
|
+
args = [url];
|
|
39
|
+
}
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let child;
|
|
42
|
+
try {
|
|
43
|
+
child = spawn(command, args, {
|
|
44
|
+
stdio: 'ignore',
|
|
45
|
+
// detached so the browser process lives independently of the CLI.
|
|
46
|
+
// unref() so the CLI can exit without waiting for the browser to close.
|
|
47
|
+
detached: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
resolve({ ok: false, reason: err instanceof Error ? err.message : String(err) });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
let settled = false;
|
|
55
|
+
const settle = (r) => {
|
|
56
|
+
if (settled)
|
|
57
|
+
return;
|
|
58
|
+
settled = true;
|
|
59
|
+
resolve(r);
|
|
60
|
+
};
|
|
61
|
+
child.on('error', (err) => {
|
|
62
|
+
settle({ ok: false, reason: err.message });
|
|
63
|
+
});
|
|
64
|
+
child.on('exit', (code) => {
|
|
65
|
+
if (code === 0 || code === null) {
|
|
66
|
+
settle({ ok: true });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
settle({ ok: false, reason: `${command} exited with code ${code}` });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Don't block CLI shutdown waiting on the browser process.
|
|
73
|
+
child.unref?.();
|
|
74
|
+
// Most platforms exit (or detach) within a fraction of a second. If we
|
|
75
|
+
// don't hear back in 2s we optimistically declare success — at worst the
|
|
76
|
+
// browser opened but the parent process hung. The user only sees a
|
|
77
|
+
// problem if no callback ever arrives, and the loopback timeout handles
|
|
78
|
+
// that case.
|
|
79
|
+
setTimeout(() => settle({ ok: true }), 2000).unref?.();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=browser-open.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-open.js","sourceRoot":"","sources":["../../src/auth/browser-open.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAQ3C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,GAAW;IAC3C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,IAAI,OAAe,CAAC;IACpB,IAAI,IAAc,CAAC;IAEnB,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,GAAG,MAAM,CAAC;QACjB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;SAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,+EAA+E;QAC/E,OAAO,GAAG,KAAK,CAAC;QAChB,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;IACpC,CAAC;SAAM,CAAC;QACN,0DAA0D;QAC1D,OAAO,GAAG,UAAU,CAAC;QACrB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;IAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,KAAK,CAAC;QACV,IAAI,CAAC;YACH,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE;gBAC3B,KAAK,EAAE,QAAQ;gBACf,kEAAkE;gBAClE,wEAAwE;gBACxE,QAAQ,EAAE,IAAI;aACf,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACjF,OAAO;QACT,CAAC;QAED,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,CAAoB,EAAE,EAAE;YACtC,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,OAAO,CAAC,CAAC,CAAC,CAAC;QACb,CAAC,CAAC;QAEF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACxB,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAChC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YACvB,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,qBAAqB,IAAI,EAAE,EAAE,CAAC,CAAC;YACvE,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,2DAA2D;QAC3D,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QAEhB,uEAAuE;QACvE,yEAAyE;QACzE,mEAAmE;QACnE,wEAAwE;QACxE,aAAa;QACb,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface LoopbackResult {
|
|
2
|
+
code: string;
|
|
3
|
+
state: string;
|
|
4
|
+
}
|
|
5
|
+
export interface RunLoopbackOptions {
|
|
6
|
+
/** The CSRF state the caller generated; must match what the IdP echoes. */
|
|
7
|
+
expectedState: string;
|
|
8
|
+
/**
|
|
9
|
+
* If set, the loopback responds 302 → this URL on success instead of
|
|
10
|
+
* rendering the inline HTML. Used to bounce the browser to a hosted,
|
|
11
|
+
* branded "you can close this tab" page on cloud-views.
|
|
12
|
+
* Falls back to inline HTML if not provided.
|
|
13
|
+
*/
|
|
14
|
+
successRedirectUrl?: string;
|
|
15
|
+
/** Body of the success page rendered to the browser when no redirect URL is set. */
|
|
16
|
+
successHtml?: string;
|
|
17
|
+
/** Total time (ms) we'll wait for the redirect before giving up. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface StartedLoopback {
|
|
21
|
+
/** The redirect URI to pass to the IdP, e.g. `http://127.0.0.1:54321/callback`. */
|
|
22
|
+
redirectUri: string;
|
|
23
|
+
/** Resolves with the verified `{ code, state }` once the redirect arrives. */
|
|
24
|
+
result: Promise<LoopbackResult>;
|
|
25
|
+
/** Force-close the server (e.g. on Ctrl-C or a token-exchange failure). */
|
|
26
|
+
close: () => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parses the loopback callback URL to extract `code`, `state`, `error`, and
|
|
30
|
+
* `error_description` query parameters.
|
|
31
|
+
*
|
|
32
|
+
* Exposed for unit testing.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseCallbackUrl(rawUrl: string): {
|
|
35
|
+
code?: string;
|
|
36
|
+
state?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
errorDescription?: string;
|
|
39
|
+
pathname: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Validates a parsed callback against the expected CSRF state. Returns the
|
|
43
|
+
* verified `{code, state}` or throws a friendly error.
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyCallback(parsed: ReturnType<typeof parseCallbackUrl>, expectedState: string): LoopbackResult;
|
|
46
|
+
/**
|
|
47
|
+
* Boots the single-shot loopback listener on a random ephemeral port.
|
|
48
|
+
*
|
|
49
|
+
* Returns immediately with `{ redirectUri, result }`. The caller passes
|
|
50
|
+
* `redirectUri` to the IdP, then awaits `result` for the verified `{code, state}`.
|
|
51
|
+
*
|
|
52
|
+
* The server:
|
|
53
|
+
* - Closes itself after the first GET to `/callback` completes its response.
|
|
54
|
+
* - Closes itself if the token-exchange caller invokes `close()` (e.g. on a
|
|
55
|
+
* later failure that wants to abort the wait).
|
|
56
|
+
* - Times out after `timeoutMs` (default 5 minutes).
|
|
57
|
+
*/
|
|
58
|
+
export declare function startLoopback(opts: RunLoopbackOptions): Promise<StartedLoopback>;
|
|
59
|
+
export declare class LoopbackError extends Error {
|
|
60
|
+
readonly kind: string;
|
|
61
|
+
constructor(message: string, kind: string);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=loopback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../../src/auth/loopback.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IAChC,2EAA2E;IAC3E,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAyHD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAUA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EAC3C,aAAa,EAAE,MAAM,GACpB,cAAc,CAkBhB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CAiGtF;AAED,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAK1C"}
|