@deque/axe-auth 1.1.0-next.97bcb8e6 → 1.1.0-next.f082f98a
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 +68 -7
- package/dist/cli/commonArgs.d.ts +66 -0
- package/dist/cli/commonArgs.help.d.ts +2 -0
- package/dist/cli/commonArgs.help.js +19 -0
- package/dist/cli/commonArgs.js +119 -0
- package/dist/cli/confirm.d.ts +17 -0
- package/dist/cli/confirm.js +56 -0
- package/dist/cli/errors.d.ts +30 -0
- package/dist/cli/errors.js +52 -0
- package/dist/cli/testUtils.d.ts +52 -0
- package/dist/cli/testUtils.js +100 -0
- package/dist/cli/types.d.ts +82 -0
- package/dist/cli/types.js +2 -0
- package/dist/commands/login.d.ts +41 -0
- package/dist/commands/login.help.d.ts +2 -0
- package/dist/commands/login.help.js +35 -0
- package/dist/commands/login.js +93 -0
- package/dist/commands/logout.d.ts +24 -0
- package/dist/commands/logout.help.d.ts +2 -0
- package/dist/commands/logout.help.js +37 -0
- package/dist/commands/logout.js +84 -0
- package/dist/commands/token.d.ts +26 -0
- package/dist/commands/token.help.d.ts +2 -0
- package/dist/commands/token.help.js +41 -0
- package/dist/commands/token.js +56 -0
- package/dist/index.js +154 -27
- package/dist/oauth/authorizationURL.d.ts +29 -0
- package/dist/oauth/authorizationURL.js +52 -0
- package/dist/oauth/authorize.d.ts +84 -0
- package/dist/oauth/authorize.js +118 -0
- package/dist/oauth/callbackServer.d.ts +23 -0
- package/dist/oauth/callbackServer.js +234 -0
- package/dist/oauth/discoverOIDC.d.ts +50 -0
- package/dist/oauth/discoverOIDC.js +173 -0
- package/dist/oauth/errors.d.ts +75 -0
- package/dist/oauth/errors.js +48 -0
- package/dist/oauth/getValidAccessToken.d.ts +89 -0
- package/dist/oauth/getValidAccessToken.js +139 -0
- package/dist/oauth/index.d.ts +16 -0
- package/dist/oauth/index.js +19 -0
- package/dist/oauth/issuerURL.d.ts +22 -0
- package/dist/oauth/issuerURL.js +38 -0
- package/dist/oauth/keyringBinding.d.ts +22 -0
- package/dist/oauth/keyringBinding.js +41 -0
- package/dist/oauth/logo.generated.d.ts +1 -0
- package/dist/oauth/logo.generated.js +7 -0
- package/dist/oauth/openBrowser.d.ts +19 -0
- package/dist/oauth/openBrowser.js +78 -0
- package/dist/oauth/pkce.d.ts +17 -0
- package/dist/oauth/pkce.js +43 -0
- package/dist/oauth/predicates.d.ts +7 -0
- package/dist/oauth/predicates.js +15 -0
- package/dist/oauth/refreshTokens.d.ts +30 -0
- package/dist/oauth/refreshTokens.js +63 -0
- package/dist/oauth/renderHtml.d.ts +9 -0
- package/dist/oauth/renderHtml.js +60 -0
- package/dist/oauth/revokeToken.d.ts +28 -0
- package/dist/oauth/revokeToken.js +63 -0
- package/dist/oauth/testUtils.d.ts +35 -0
- package/dist/oauth/testUtils.js +61 -0
- package/dist/oauth/tokenExchange.d.ts +26 -0
- package/dist/oauth/tokenExchange.js +44 -0
- package/dist/oauth/tokenResponse.d.ts +54 -0
- package/dist/oauth/tokenResponse.js +121 -0
- package/dist/oauth/tokenStore.d.ts +111 -0
- package/dist/oauth/tokenStore.js +198 -0
- package/dist/userAgent.d.ts +12 -0
- package/dist/userAgent.js +18 -0
- package/docs/architecture.md +192 -0
- package/docs/callback-page.md +24 -0
- package/docs/callback-server.md +21 -0
- package/docs/oauth-flow.md +15 -0
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @deque/axe-auth
|
|
2
2
|
|
|
3
|
-
CLI for authenticating with Deque
|
|
3
|
+
CLI for authenticating with Deque services via the OAuth 2.0 Authorization Code + PKCE flow (RFC 6749, RFC 7636, RFC 8252 §7.3). Tokens are persisted to the OS keychain so subsequent invocations can refresh silently.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -17,12 +17,73 @@ npx @deque/axe-auth
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
axe-auth [options]
|
|
20
|
+
axe-auth <command> [options]
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Commands:
|
|
24
24
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
25
|
+
| Command | Description |
|
|
26
|
+
| -------- | ------------------------------------------------------------------------------- |
|
|
27
|
+
| `login` | Open a browser, complete the OAuth flow, persist tokens to the OS keychain. |
|
|
28
|
+
| `logout` | Revoke the stored refresh token server-side and clear the local keychain entry. |
|
|
29
|
+
| `token` | Print a currently-valid access token to stdout, refreshing silently if needed. |
|
|
30
|
+
|
|
31
|
+
Run `axe-auth <command> --help` for command-specific options.
|
|
32
|
+
|
|
33
|
+
### Common configuration
|
|
34
|
+
|
|
35
|
+
`axe-auth login` accepts the Keycloak coordinates as flags or environment variables (flag wins over env):
|
|
36
|
+
|
|
37
|
+
| Flag | Env var | Notes |
|
|
38
|
+
| ---------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `--server` | `AXE_OAUTH_SERVER` | Authorization-server base URL. |
|
|
40
|
+
| `--realm` | `AXE_OAUTH_REALM` | Keycloak realm. |
|
|
41
|
+
| `--client-id` | `AXE_OAUTH_CLIENT_ID` | OAuth client ID registered with Keycloak. |
|
|
42
|
+
| `--allow-insecure-issuer` | — | Permit non-loopback http issuers (default is https only; loopback http is always allowed). |
|
|
43
|
+
| `--no-allow-insecure-issuer` | — | Force `allowInsecureIssuer=false` for this call, overriding the stored value. Mutually exclusive with `--allow-insecure-issuer`. |
|
|
44
|
+
|
|
45
|
+
The issuer URL is built as `${server}/realms/${realm}`.
|
|
46
|
+
|
|
47
|
+
`axe-auth` stores one set of credentials per machine. On a successful `login`, the resolved issuer / client / insecure-issuer values are persisted alongside the tokens, so subsequent `axe-auth token` and `axe-auth logout` invocations work flag-free — a typical scripted call is just `$(axe-auth token)`. The same flags accepted by `login` work on the other verbs too, but `logout` always operates on the stored entry (mismatched flags trigger a warning on stderr and are ignored).
|
|
48
|
+
|
|
49
|
+
There is no concurrent multi-issuer support. Logging in to a second Keycloak overwrites the previous tokens; an interactive prompt confirms the switch before destroying the existing session, and `--force` skips the prompt.
|
|
50
|
+
|
|
51
|
+
### Exit codes
|
|
52
|
+
|
|
53
|
+
| Code | Meaning |
|
|
54
|
+
| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `0` | Success. |
|
|
56
|
+
| `1` | Not authenticated: `axe-auth token` with no stored credentials, or stored credentials are unusable (corrupt, version-mismatch, expired without a usable refresh token, or refresh rejected by the server). Branch on this in scripts that need to trigger a `login`. |
|
|
57
|
+
| `2` | Usage or runtime error: unknown command, bad flag, missing required configuration, OAuth flow failure, or keychain failure. Details written to stderr. |
|
|
58
|
+
| `3` | Cancelled: `axe-auth login` declined at the re-authentication prompt. Distinct from `1` so scripts can tell "needs login" from "user bailed." |
|
|
59
|
+
|
|
60
|
+
### Examples
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
# First-time login (opens your browser)
|
|
64
|
+
axe-auth login \
|
|
65
|
+
--server https://auth.customer.dequecloud.com \
|
|
66
|
+
--realm customer \
|
|
67
|
+
--client-id axe-auth
|
|
68
|
+
|
|
69
|
+
# Pull a fresh access token for use in shell substitution
|
|
70
|
+
docker run -e AXE_ACCESS_TOKEN="$(axe-auth token)" axe-mcp-server
|
|
71
|
+
|
|
72
|
+
# Sign out
|
|
73
|
+
axe-auth logout
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Architecture
|
|
77
|
+
|
|
78
|
+
See [`docs/architecture.md`](./docs/architecture.md) for the system architecture: components, per-verb data flow, communication security, and persisted data.
|
|
79
|
+
|
|
80
|
+
Deeper design notes:
|
|
81
|
+
|
|
82
|
+
- [`oauth-flow.md`](./docs/oauth-flow.md) — protocol-level walkthrough of the OAuth 2.0 + PKCE flow.
|
|
83
|
+
- [`callback-server.md`](./docs/callback-server.md) — `startCallbackServer` API and RFC 8252 conformance.
|
|
84
|
+
- [`callback-page.md`](./docs/callback-page.md) — HTML response design, branding, and CSP rationale.
|
|
85
|
+
|
|
86
|
+
## Caveats
|
|
87
|
+
|
|
88
|
+
- **`axe-auth token` exposes the access token in the shell process list and terminal scrollback.** When used as `$(axe-auth token)` the token briefly appears in the parent process's argument list (observable via `ps`); printed directly to a terminal it also persists in the scrollback buffer (iTerm2, Terminal.app, tmux all retain output by default), which can outlast the token's TTL on a shared machine. OAuth access tokens are short-lived (typically minutes), which limits exposure compared to a static API key. Prefer redirecting into a file (`axe-auth token > /tmp/tok && chmod 600 /tmp/tok`) or directly into an env var (`export AXE_ACCESS_TOKEN=$(axe-auth token)`) on platforms where this matters.
|
|
89
|
+
- **Linux keychain support is untested.** `@napi-rs/keyring` requires a working D-Bus Secret Service (GNOME Keyring, KWallet, etc.). Users on headless or minimal-desktop Linux environments may see `KEYRING_UNAVAILABLE`; a file-backed `TokenStore` fallback is tracked as a follow-up (internal issue #464).
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ParseArgsConfig } from "node:util";
|
|
2
|
+
/**
|
|
3
|
+
* The subset of a `StoredEntry` that `parseCommonArgs` accepts as a
|
|
4
|
+
* fallback when no flags or env vars supply the common fields.
|
|
5
|
+
* Sourced from `KeyringTokenStore.load()` by the dispatcher.
|
|
6
|
+
*/
|
|
7
|
+
export interface StoredConfig {
|
|
8
|
+
issuerURL: string;
|
|
9
|
+
clientId: string;
|
|
10
|
+
allowInsecureIssuer: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Common configuration the three CLI verbs share. */
|
|
13
|
+
export interface CommonArgs {
|
|
14
|
+
/** OIDC issuer URL, built as `${server}/realms/${realm}`. */
|
|
15
|
+
issuerURL: string;
|
|
16
|
+
/** OAuth client ID. */
|
|
17
|
+
clientId: string;
|
|
18
|
+
/**
|
|
19
|
+
* Whether to permit non-loopback http issuers. Loopback hosts
|
|
20
|
+
* (`localhost` / `127.0.0.1` / `[::1]`) are always allowed over
|
|
21
|
+
* http; this flag is the opt-in for non-loopback http issuers.
|
|
22
|
+
*/
|
|
23
|
+
allowInsecureIssuer: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `parseArgs`-shaped options describing the flags every CLI verb
|
|
27
|
+
* accepts (server / realm / client-id / allow-insecure-issuer and
|
|
28
|
+
* its negation). Subcommands spread this into their own `options` so
|
|
29
|
+
* they can add verb-specific flags alongside.
|
|
30
|
+
*
|
|
31
|
+
* Node's `parseArgs` doesn't support `--no-` boolean negation
|
|
32
|
+
* natively, so the opt-out is registered as its own flag. Passing
|
|
33
|
+
* both `--allow-insecure-issuer` and `--no-allow-insecure-issuer` is
|
|
34
|
+
* treated as user error and rejected; `--no-allow-insecure-issuer`
|
|
35
|
+
* is the only way to force `allowInsecureIssuer: false` for a single
|
|
36
|
+
* invocation when the stored entry has it set to `true`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const COMMON_OPTIONS: NonNullable<ParseArgsConfig["options"]>;
|
|
39
|
+
/** Subset of `parseArgs(...).values` this helper consumes. */
|
|
40
|
+
export interface ParsedCommonValues {
|
|
41
|
+
server?: string;
|
|
42
|
+
realm?: string;
|
|
43
|
+
"client-id"?: string;
|
|
44
|
+
"allow-insecure-issuer"?: boolean;
|
|
45
|
+
"no-allow-insecure-issuer"?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolves the common Keycloak coordinates from already-parsed CLI
|
|
49
|
+
* flag values, falling back to `AXE_OAUTH_SERVER`,
|
|
50
|
+
* `AXE_OAUTH_REALM`, and `AXE_OAUTH_CLIENT_ID` env vars when a flag
|
|
51
|
+
* is absent, then to a `StoredConfig` from the keychain when none
|
|
52
|
+
* of those are set at all. Flag wins over env wins over stored.
|
|
53
|
+
*
|
|
54
|
+
* The keychain fallback is all-or-nothing: it kicks in only when no
|
|
55
|
+
* flag and no env var supplies any common field. Mixed input (e.g.
|
|
56
|
+
* `--server X` without `--realm`) is treated as deliberate override,
|
|
57
|
+
* so the missing-field error fires instead of silently filling gaps
|
|
58
|
+
* from an unrelated stored issuer.
|
|
59
|
+
*
|
|
60
|
+
* @param values The `values` object returned from `parseArgs`.
|
|
61
|
+
* @param env Environment to consult for fallback. Defaults to
|
|
62
|
+
* `process.env`; injected for test determinism.
|
|
63
|
+
* @param defaults Stored issuer/client config. Pass `null` (or
|
|
64
|
+
* omit) when nothing is stored.
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseCommonArgs(values: ParsedCommonValues, env?: NodeJS.ProcessEnv, defaults?: StoredConfig | null): CommonArgs;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Help-text fragment describing the flags every CLI verb shares. */
|
|
2
|
+
export declare const HELP_COMMON_OPTIONS = " --server <url> Authorization-server base URL.\n Falls back to AXE_OAUTH_SERVER.\n --realm <name> Keycloak realm name.\n Falls back to AXE_OAUTH_REALM.\n --client-id <id> OAuth client ID.\n Falls back to AXE_OAUTH_CLIENT_ID.\n --allow-insecure-issuer Permit non-loopback http issuers (default\n is https only; loopback http is always\n allowed).\n --no-allow-insecure-issuer\n Force allowInsecureIssuer=false for this\n call, overriding the stored value if any.\n Mutually exclusive with\n --allow-insecure-issuer.\n -h, --help Show this help.";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP_COMMON_OPTIONS = void 0;
|
|
4
|
+
/** Help-text fragment describing the flags every CLI verb shares. */
|
|
5
|
+
exports.HELP_COMMON_OPTIONS = ` --server <url> Authorization-server base URL.
|
|
6
|
+
Falls back to AXE_OAUTH_SERVER.
|
|
7
|
+
--realm <name> Keycloak realm name.
|
|
8
|
+
Falls back to AXE_OAUTH_REALM.
|
|
9
|
+
--client-id <id> OAuth client ID.
|
|
10
|
+
Falls back to AXE_OAUTH_CLIENT_ID.
|
|
11
|
+
--allow-insecure-issuer Permit non-loopback http issuers (default
|
|
12
|
+
is https only; loopback http is always
|
|
13
|
+
allowed).
|
|
14
|
+
--no-allow-insecure-issuer
|
|
15
|
+
Force allowInsecureIssuer=false for this
|
|
16
|
+
call, overriding the stored value if any.
|
|
17
|
+
Mutually exclusive with
|
|
18
|
+
--allow-insecure-issuer.
|
|
19
|
+
-h, --help Show this help.`;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.COMMON_OPTIONS = void 0;
|
|
7
|
+
exports.parseCommonArgs = parseCommonArgs;
|
|
8
|
+
const remove_trailing_slash_1 = __importDefault(require("remove-trailing-slash"));
|
|
9
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
/**
|
|
12
|
+
* `parseArgs`-shaped options describing the flags every CLI verb
|
|
13
|
+
* accepts (server / realm / client-id / allow-insecure-issuer and
|
|
14
|
+
* its negation). Subcommands spread this into their own `options` so
|
|
15
|
+
* they can add verb-specific flags alongside.
|
|
16
|
+
*
|
|
17
|
+
* Node's `parseArgs` doesn't support `--no-` boolean negation
|
|
18
|
+
* natively, so the opt-out is registered as its own flag. Passing
|
|
19
|
+
* both `--allow-insecure-issuer` and `--no-allow-insecure-issuer` is
|
|
20
|
+
* treated as user error and rejected; `--no-allow-insecure-issuer`
|
|
21
|
+
* is the only way to force `allowInsecureIssuer: false` for a single
|
|
22
|
+
* invocation when the stored entry has it set to `true`.
|
|
23
|
+
*/
|
|
24
|
+
exports.COMMON_OPTIONS = {
|
|
25
|
+
server: { type: "string" },
|
|
26
|
+
realm: { type: "string" },
|
|
27
|
+
"client-id": { type: "string" },
|
|
28
|
+
"allow-insecure-issuer": { type: "boolean" },
|
|
29
|
+
"no-allow-insecure-issuer": { type: "boolean" },
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Resolves the common Keycloak coordinates from already-parsed CLI
|
|
33
|
+
* flag values, falling back to `AXE_OAUTH_SERVER`,
|
|
34
|
+
* `AXE_OAUTH_REALM`, and `AXE_OAUTH_CLIENT_ID` env vars when a flag
|
|
35
|
+
* is absent, then to a `StoredConfig` from the keychain when none
|
|
36
|
+
* of those are set at all. Flag wins over env wins over stored.
|
|
37
|
+
*
|
|
38
|
+
* The keychain fallback is all-or-nothing: it kicks in only when no
|
|
39
|
+
* flag and no env var supplies any common field. Mixed input (e.g.
|
|
40
|
+
* `--server X` without `--realm`) is treated as deliberate override,
|
|
41
|
+
* so the missing-field error fires instead of silently filling gaps
|
|
42
|
+
* from an unrelated stored issuer.
|
|
43
|
+
*
|
|
44
|
+
* @param values The `values` object returned from `parseArgs`.
|
|
45
|
+
* @param env Environment to consult for fallback. Defaults to
|
|
46
|
+
* `process.env`; injected for test determinism.
|
|
47
|
+
* @param defaults Stored issuer/client config. Pass `null` (or
|
|
48
|
+
* omit) when nothing is stored.
|
|
49
|
+
*/
|
|
50
|
+
function parseCommonArgs(values, env = process.env, defaults = null) {
|
|
51
|
+
const server = values.server ?? env.AXE_OAUTH_SERVER;
|
|
52
|
+
const realm = values.realm ?? env.AXE_OAUTH_REALM;
|
|
53
|
+
const clientId = values["client-id"] ?? env.AXE_OAUTH_CLIENT_ID;
|
|
54
|
+
const allowInsecureIssuer = resolveAllowInsecureIssuer(values, defaults);
|
|
55
|
+
// All-or-nothing fallback to the keychain default. Any flag or env
|
|
56
|
+
// input opts out, so partial overrides still surface the
|
|
57
|
+
// missing-field error rather than mixing flags with stored config.
|
|
58
|
+
// The `--allow-insecure-issuer` / `--no-allow-insecure-issuer`
|
|
59
|
+
// pair does NOT count toward this check: it modifies the security
|
|
60
|
+
// posture of the call without identifying a different issuer, so
|
|
61
|
+
// it should compose with the stored issuer/client fallback.
|
|
62
|
+
const noFlagOrEnv = !server && !realm && !clientId;
|
|
63
|
+
if (noFlagOrEnv && defaults) {
|
|
64
|
+
return {
|
|
65
|
+
issuerURL: defaults.issuerURL,
|
|
66
|
+
clientId: defaults.clientId,
|
|
67
|
+
allowInsecureIssuer,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const missing = [];
|
|
71
|
+
if (!server)
|
|
72
|
+
missing.push("--server (or AXE_OAUTH_SERVER)");
|
|
73
|
+
if (!realm)
|
|
74
|
+
missing.push("--realm (or AXE_OAUTH_REALM)");
|
|
75
|
+
if (!clientId)
|
|
76
|
+
missing.push("--client-id (or AXE_OAUTH_CLIENT_ID)");
|
|
77
|
+
if (missing.length > 0) {
|
|
78
|
+
throw new errors_1.MissingConfigError(missing);
|
|
79
|
+
}
|
|
80
|
+
// The `missing.length` check above already threw when any of these
|
|
81
|
+
// were absent. The `assert` calls narrow `server` / `realm` /
|
|
82
|
+
// `clientId` from `string | undefined` to `string` for the return
|
|
83
|
+
// expression, replacing the non-null `!` shortcut with a runtime
|
|
84
|
+
// check that gives a real error if the invariant is ever broken.
|
|
85
|
+
(0, strict_1.default)(server);
|
|
86
|
+
(0, strict_1.default)(realm);
|
|
87
|
+
(0, strict_1.default)(clientId);
|
|
88
|
+
const serverBase = (0, remove_trailing_slash_1.default)(server);
|
|
89
|
+
return {
|
|
90
|
+
issuerURL: `${serverBase}/realms/${realm}`,
|
|
91
|
+
clientId,
|
|
92
|
+
allowInsecureIssuer,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolves `allowInsecureIssuer` from the positive flag, its
|
|
97
|
+
* negation, and the keychain default — in that precedence order.
|
|
98
|
+
* Throws when both flags are passed together.
|
|
99
|
+
*/
|
|
100
|
+
function resolveAllowInsecureIssuer(values, defaults) {
|
|
101
|
+
// Truthy checks (rather than `!== undefined`) are deliberate:
|
|
102
|
+
// `parseArgs` with `type: "boolean"` only ever produces `true` or
|
|
103
|
+
// `undefined`, but `ParsedCommonValues` types these as
|
|
104
|
+
// `boolean | undefined` so a programmatic caller could thread in
|
|
105
|
+
// `false`. Treating `false` as "flag not set" lets such a caller
|
|
106
|
+
// mix `{ "allow-insecure-issuer": false, "no-allow-insecure-issuer":
|
|
107
|
+
// true }` without tripping the mutex — `false` here is equivalent
|
|
108
|
+
// to "absent", which is what parseArgs would have produced anyway.
|
|
109
|
+
const allow = values["allow-insecure-issuer"];
|
|
110
|
+
const deny = values["no-allow-insecure-issuer"];
|
|
111
|
+
if (allow && deny) {
|
|
112
|
+
throw new Error("--allow-insecure-issuer and --no-allow-insecure-issuer are mutually exclusive.");
|
|
113
|
+
}
|
|
114
|
+
if (allow)
|
|
115
|
+
return true;
|
|
116
|
+
if (deny)
|
|
117
|
+
return false;
|
|
118
|
+
return defaults?.allowInsecureIssuer ?? false;
|
|
119
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
/** Options for `confirm`. */
|
|
3
|
+
export interface ConfirmOptions {
|
|
4
|
+
/** Question to display. A trailing space is added if absent. */
|
|
5
|
+
question: string;
|
|
6
|
+
/** Stream to read the answer from. Defaults to `process.stdin`. */
|
|
7
|
+
input?: Readable;
|
|
8
|
+
/** Stream to print the question on. Defaults to `process.stderr`. */
|
|
9
|
+
output?: Writable;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Reads a single line from `input` and returns `true` for an
|
|
13
|
+
* affirmative answer (`y` / `yes`, case-insensitive), `false`
|
|
14
|
+
* otherwise. Empty / EOF / Ctrl-C are all treated as "no" so the
|
|
15
|
+
* default action stays conservative.
|
|
16
|
+
*/
|
|
17
|
+
export declare function confirm(options: ConfirmOptions): Promise<boolean>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.confirm = confirm;
|
|
4
|
+
const node_readline_1 = require("node:readline");
|
|
5
|
+
/**
|
|
6
|
+
* Reads a single line from `input` and returns `true` for an
|
|
7
|
+
* affirmative answer (`y` / `yes`, case-insensitive), `false`
|
|
8
|
+
* otherwise. Empty / EOF / Ctrl-C are all treated as "no" so the
|
|
9
|
+
* default action stays conservative.
|
|
10
|
+
*/
|
|
11
|
+
async function confirm(options) {
|
|
12
|
+
// Annotate after the ?? fallbacks so the union of `Writable |
|
|
13
|
+
// NodeJS.WriteStream` (from `process.stderr`) collapses to the
|
|
14
|
+
// base class — otherwise `output.write(...)` is ambiguous.
|
|
15
|
+
const input = options.input ?? process.stdin;
|
|
16
|
+
const output = options.output ?? process.stderr;
|
|
17
|
+
const question = options.question.endsWith(" ")
|
|
18
|
+
? options.question
|
|
19
|
+
: `${options.question} `;
|
|
20
|
+
// Use createInterface rather than the higher-level `rl.question`
|
|
21
|
+
// promise API because the latter wires up SIGINT handling that
|
|
22
|
+
// doesn't compose well when this is called from a CLI dispatcher
|
|
23
|
+
// that owns its own signals.
|
|
24
|
+
const rl = (0, node_readline_1.createInterface)({ input, output });
|
|
25
|
+
try {
|
|
26
|
+
output.write(question);
|
|
27
|
+
const line = await waitForLineOrClose(rl);
|
|
28
|
+
if (line === null)
|
|
29
|
+
return false;
|
|
30
|
+
const trimmed = line.trim().toLowerCase();
|
|
31
|
+
return trimmed === "y" || trimmed === "yes";
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
rl.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolves with the next line emitted by `rl`, or `null` if the
|
|
39
|
+
* underlying stream closes first (EOF / Ctrl-D / Ctrl-C). Both
|
|
40
|
+
* listeners detach themselves on the other event so we never hold
|
|
41
|
+
* references to a closed interface.
|
|
42
|
+
*/
|
|
43
|
+
function waitForLineOrClose(rl) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const onLine = (line) => {
|
|
46
|
+
rl.off("close", onClose);
|
|
47
|
+
resolve(line);
|
|
48
|
+
};
|
|
49
|
+
const onClose = () => {
|
|
50
|
+
rl.off("line", onLine);
|
|
51
|
+
resolve(null);
|
|
52
|
+
};
|
|
53
|
+
rl.once("line", onLine);
|
|
54
|
+
rl.once("close", onClose);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Discriminator for `CLIError`, used by tests and the dispatcher. */
|
|
2
|
+
export type CLIErrorCode = "NOT_AUTHENTICATED" | "USER_CANCELLED" | "ALREADY_AUTHENTICATED" | "OAUTH_FAILED" | "KEYCHAIN_FAILURE";
|
|
3
|
+
/**
|
|
4
|
+
* Thrown from a verb's `run` to signal a known failure. The
|
|
5
|
+
* dispatcher in `src/index.ts` writes `message` to stderr and exits
|
|
6
|
+
* with `exitCode`. The `code` field is the load-bearing
|
|
7
|
+
* discriminator; `exitCode` is derived for shell scripts and is
|
|
8
|
+
* documented in the README.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CLIError extends Error {
|
|
11
|
+
readonly code: CLIErrorCode;
|
|
12
|
+
readonly exitCode: number;
|
|
13
|
+
constructor(code: CLIErrorCode, message: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Thrown by `parseCommonArgs` when one or more of `server`, `realm`,
|
|
17
|
+
* or `clientId` cannot be resolved from flags, env vars, or the
|
|
18
|
+
* keychain default. Carries `missing` so the dispatcher can pair the
|
|
19
|
+
* list with extra context (e.g. a keychain-load failure).
|
|
20
|
+
*/
|
|
21
|
+
export declare class MissingConfigError extends Error {
|
|
22
|
+
readonly missing: readonly string[];
|
|
23
|
+
constructor(missing: readonly string[]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns the `message` of an `Error`-shaped value, or its `String`
|
|
27
|
+
* coercion otherwise. Used in user-facing error templates so
|
|
28
|
+
* callers don't inline the `instanceof` ternary every time.
|
|
29
|
+
*/
|
|
30
|
+
export declare function describeError(err: unknown): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MissingConfigError = exports.CLIError = void 0;
|
|
4
|
+
exports.describeError = describeError;
|
|
5
|
+
const EXIT_CODE_BY_ERROR_CODE = {
|
|
6
|
+
NOT_AUTHENTICATED: 1,
|
|
7
|
+
USER_CANCELLED: 3,
|
|
8
|
+
ALREADY_AUTHENTICATED: 2,
|
|
9
|
+
OAUTH_FAILED: 2,
|
|
10
|
+
KEYCHAIN_FAILURE: 2,
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Thrown from a verb's `run` to signal a known failure. The
|
|
14
|
+
* dispatcher in `src/index.ts` writes `message` to stderr and exits
|
|
15
|
+
* with `exitCode`. The `code` field is the load-bearing
|
|
16
|
+
* discriminator; `exitCode` is derived for shell scripts and is
|
|
17
|
+
* documented in the README.
|
|
18
|
+
*/
|
|
19
|
+
class CLIError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
exitCode;
|
|
22
|
+
constructor(code, message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "CLIError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.exitCode = EXIT_CODE_BY_ERROR_CODE[code];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.CLIError = CLIError;
|
|
30
|
+
/**
|
|
31
|
+
* Thrown by `parseCommonArgs` when one or more of `server`, `realm`,
|
|
32
|
+
* or `clientId` cannot be resolved from flags, env vars, or the
|
|
33
|
+
* keychain default. Carries `missing` so the dispatcher can pair the
|
|
34
|
+
* list with extra context (e.g. a keychain-load failure).
|
|
35
|
+
*/
|
|
36
|
+
class MissingConfigError extends Error {
|
|
37
|
+
missing;
|
|
38
|
+
constructor(missing) {
|
|
39
|
+
super(`Missing required configuration: ${missing.join(", ")}.`);
|
|
40
|
+
this.name = "MissingConfigError";
|
|
41
|
+
this.missing = missing;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.MissingConfigError = MissingConfigError;
|
|
45
|
+
/**
|
|
46
|
+
* Returns the `message` of an `Error`-shaped value, or its `String`
|
|
47
|
+
* coercion otherwise. Used in user-facing error templates so
|
|
48
|
+
* callers don't inline the `instanceof` ternary every time.
|
|
49
|
+
*/
|
|
50
|
+
function describeError(err) {
|
|
51
|
+
return err instanceof Error ? err.message : String(err);
|
|
52
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Readable, Writable } from "node:stream";
|
|
2
|
+
import type { CommonArgs } from "./commonArgs";
|
|
3
|
+
import type { CommandDeps } from "./types";
|
|
4
|
+
import type { LoadResult, StoredEntry, TokenStore } from "../oauth/tokenStore";
|
|
5
|
+
import type { TokenSet } from "../oauth/tokenResponse";
|
|
6
|
+
/** A `Writable` that accumulates everything written into a string. */
|
|
7
|
+
export interface CapturedStream extends Writable {
|
|
8
|
+
/** Concatenation of every chunk written to the stream so far. */
|
|
9
|
+
readonly value: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Returns a `Writable` that records every write into a string for
|
|
13
|
+
* assertion. Reads via `.value`. Defined as a real `Writable`
|
|
14
|
+
* subclass so it can be passed straight into `CommandDeps.stdout` /
|
|
15
|
+
* `CommandDeps.stderr` without casts.
|
|
16
|
+
*/
|
|
17
|
+
export declare function captureStream(): CapturedStream;
|
|
18
|
+
/** Convenience: an empty `Readable` to stand in for stdin in tests. */
|
|
19
|
+
export declare function emptyStdin(): Readable;
|
|
20
|
+
/** A `TokenStore` plus instrumentation for assertions. */
|
|
21
|
+
export interface FakeStore extends TokenStore {
|
|
22
|
+
/** Number of times `load()` was called. */
|
|
23
|
+
readonly loadedTimes: number;
|
|
24
|
+
/** Number of times `clear()` was called. */
|
|
25
|
+
readonly clearedTimes: number;
|
|
26
|
+
/** Current state, observable from tests. */
|
|
27
|
+
readonly current: LoadResult;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* In-memory `TokenStore` that starts in `initial`, accepts
|
|
31
|
+
* `save()` / `clear()`, and exposes `loadedTimes` / `clearedTimes`
|
|
32
|
+
* / `current` for assertions.
|
|
33
|
+
*/
|
|
34
|
+
export declare function makeStore(initial: LoadResult): FakeStore;
|
|
35
|
+
/**
|
|
36
|
+
* Builds a `StoredEntry` for the standard test issuer/client. Pass a
|
|
37
|
+
* `TokenSet` (from a per-test fixture) and override the
|
|
38
|
+
* issuer/client/insecure fields when a test cares about them.
|
|
39
|
+
*/
|
|
40
|
+
export declare function entry(tokens: TokenSet, overrides?: Partial<Omit<StoredEntry, "tokens">>): StoredEntry;
|
|
41
|
+
/** A `CommandDeps` shape with the captured streams exposed. */
|
|
42
|
+
export interface CapturedDeps extends CommandDeps {
|
|
43
|
+
stdout: CapturedStream;
|
|
44
|
+
stderr: CapturedStream;
|
|
45
|
+
}
|
|
46
|
+
/** Returns a `CommandDeps` populated with capturing streams. */
|
|
47
|
+
export declare function captureDeps(overrides?: Partial<CapturedDeps>): CapturedDeps;
|
|
48
|
+
/**
|
|
49
|
+
* Returns a fully-resolved `CommonArgs` for the standard test
|
|
50
|
+
* issuer/client. Override individual fields as needed.
|
|
51
|
+
*/
|
|
52
|
+
export declare function commonArgs(overrides?: Partial<CommonArgs>): CommonArgs;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared test helpers for the command tests. Excluded from c8
|
|
3
|
+
// coverage in `.c8rc.json` since this is test-only code.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.captureStream = captureStream;
|
|
6
|
+
exports.emptyStdin = emptyStdin;
|
|
7
|
+
exports.makeStore = makeStore;
|
|
8
|
+
exports.entry = entry;
|
|
9
|
+
exports.captureDeps = captureDeps;
|
|
10
|
+
exports.commonArgs = commonArgs;
|
|
11
|
+
const node_stream_1 = require("node:stream");
|
|
12
|
+
/**
|
|
13
|
+
* Returns a `Writable` that records every write into a string for
|
|
14
|
+
* assertion. Reads via `.value`. Defined as a real `Writable`
|
|
15
|
+
* subclass so it can be passed straight into `CommandDeps.stdout` /
|
|
16
|
+
* `CommandDeps.stderr` without casts.
|
|
17
|
+
*/
|
|
18
|
+
function captureStream() {
|
|
19
|
+
let value = "";
|
|
20
|
+
const stream = new node_stream_1.Writable({
|
|
21
|
+
write(chunk, _encoding, cb) {
|
|
22
|
+
value +=
|
|
23
|
+
typeof chunk === "string" ? chunk : Buffer.from(chunk).toString();
|
|
24
|
+
cb();
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(stream, "value", { get: () => value });
|
|
28
|
+
return stream;
|
|
29
|
+
}
|
|
30
|
+
/** Convenience: an empty `Readable` to stand in for stdin in tests. */
|
|
31
|
+
function emptyStdin() {
|
|
32
|
+
return node_stream_1.Readable.from([]);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* In-memory `TokenStore` that starts in `initial`, accepts
|
|
36
|
+
* `save()` / `clear()`, and exposes `loadedTimes` / `clearedTimes`
|
|
37
|
+
* / `current` for assertions.
|
|
38
|
+
*/
|
|
39
|
+
function makeStore(initial) {
|
|
40
|
+
let current = initial;
|
|
41
|
+
let loadedTimes = 0;
|
|
42
|
+
let clearedTimes = 0;
|
|
43
|
+
return {
|
|
44
|
+
save: async (entry) => {
|
|
45
|
+
current = { ok: true, entry };
|
|
46
|
+
},
|
|
47
|
+
load: async () => {
|
|
48
|
+
loadedTimes++;
|
|
49
|
+
return current;
|
|
50
|
+
},
|
|
51
|
+
clear: async () => {
|
|
52
|
+
clearedTimes++;
|
|
53
|
+
current = { ok: false, reason: "empty" };
|
|
54
|
+
},
|
|
55
|
+
get loadedTimes() {
|
|
56
|
+
return loadedTimes;
|
|
57
|
+
},
|
|
58
|
+
get clearedTimes() {
|
|
59
|
+
return clearedTimes;
|
|
60
|
+
},
|
|
61
|
+
get current() {
|
|
62
|
+
return current;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Builds a `StoredEntry` for the standard test issuer/client. Pass a
|
|
68
|
+
* `TokenSet` (from a per-test fixture) and override the
|
|
69
|
+
* issuer/client/insecure fields when a test cares about them.
|
|
70
|
+
*/
|
|
71
|
+
function entry(tokens, overrides = {}) {
|
|
72
|
+
return {
|
|
73
|
+
tokens,
|
|
74
|
+
issuerURL: "https://auth.example.com/realms/prod",
|
|
75
|
+
clientId: "axe-auth",
|
|
76
|
+
allowInsecureIssuer: false,
|
|
77
|
+
...overrides,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Returns a `CommandDeps` populated with capturing streams. */
|
|
81
|
+
function captureDeps(overrides = {}) {
|
|
82
|
+
return {
|
|
83
|
+
stdin: emptyStdin(),
|
|
84
|
+
stdout: captureStream(),
|
|
85
|
+
stderr: captureStream(),
|
|
86
|
+
...overrides,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns a fully-resolved `CommonArgs` for the standard test
|
|
91
|
+
* issuer/client. Override individual fields as needed.
|
|
92
|
+
*/
|
|
93
|
+
function commonArgs(overrides = {}) {
|
|
94
|
+
return {
|
|
95
|
+
issuerURL: "https://auth.example.com/realms/prod",
|
|
96
|
+
clientId: "axe-auth",
|
|
97
|
+
allowInsecureIssuer: false,
|
|
98
|
+
...overrides,
|
|
99
|
+
};
|
|
100
|
+
}
|