@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -18
  3. package/dist/auth/api-client.d.ts +48 -0
  4. package/dist/auth/api-client.d.ts.map +1 -0
  5. package/dist/auth/api-client.js +109 -0
  6. package/dist/auth/api-client.js.map +1 -0
  7. package/dist/auth/browser-open.d.ts +12 -0
  8. package/dist/auth/browser-open.d.ts.map +1 -0
  9. package/dist/auth/browser-open.js +82 -0
  10. package/dist/auth/browser-open.js.map +1 -0
  11. package/dist/auth/loopback.d.ts +63 -0
  12. package/dist/auth/loopback.d.ts.map +1 -0
  13. package/dist/auth/loopback.js +290 -0
  14. package/dist/auth/loopback.js.map +1 -0
  15. package/dist/auth/pkce.d.ts +19 -0
  16. package/dist/auth/pkce.d.ts.map +1 -0
  17. package/dist/auth/pkce.js +46 -0
  18. package/dist/auth/pkce.js.map +1 -0
  19. package/dist/auth/relative-time.d.ts +2 -0
  20. package/dist/auth/relative-time.d.ts.map +1 -0
  21. package/dist/auth/relative-time.js +27 -0
  22. package/dist/auth/relative-time.js.map +1 -0
  23. package/dist/cli.d.ts.map +1 -1
  24. package/dist/cli.js +9 -1
  25. package/dist/cli.js.map +1 -1
  26. package/dist/commands/auth/config.command.d.ts +8 -0
  27. package/dist/commands/auth/config.command.d.ts.map +1 -0
  28. package/dist/commands/auth/config.command.js +60 -0
  29. package/dist/commands/auth/config.command.js.map +1 -0
  30. package/dist/commands/auth/login.command.d.ts +20 -0
  31. package/dist/commands/auth/login.command.d.ts.map +1 -0
  32. package/dist/commands/auth/login.command.js +141 -0
  33. package/dist/commands/auth/login.command.js.map +1 -0
  34. package/dist/commands/auth/logout.command.d.ts +14 -0
  35. package/dist/commands/auth/logout.command.d.ts.map +1 -0
  36. package/dist/commands/auth/logout.command.js +57 -0
  37. package/dist/commands/auth/logout.command.js.map +1 -0
  38. package/dist/commands/auth/status.command.d.ts +14 -0
  39. package/dist/commands/auth/status.command.d.ts.map +1 -0
  40. package/dist/commands/auth/status.command.js +59 -0
  41. package/dist/commands/auth/status.command.js.map +1 -0
  42. package/dist/commands/auth.command.d.ts +10 -1
  43. package/dist/commands/auth.command.d.ts.map +1 -1
  44. package/dist/commands/auth.command.js +13 -55
  45. package/dist/commands/auth.command.js.map +1 -1
  46. package/dist/commands/guide.command.js +5 -5
  47. package/dist/commands/guide.command.js.map +1 -1
  48. package/dist/config.d.ts +24 -0
  49. package/dist/config.d.ts.map +1 -1
  50. package/dist/config.js +79 -9
  51. package/dist/config.js.map +1 -1
  52. package/dist/credentials.d.ts +57 -0
  53. package/dist/credentials.d.ts.map +1 -0
  54. package/dist/credentials.js +136 -0
  55. package/dist/credentials.js.map +1 -0
  56. 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
- Set your Bridge API key as an environment variable:
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
- Optional configuration:
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://account-api.thebridge.dev # default
29
- export BRIDGE_TENANT_ID=<tenant-id> # for user commands
30
- export BRIDGE_DEBUG=true # enable debug logging
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
- # Package
201
- npm run package
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"}