@deque/axe-auth 1.1.0-next.6fbca4dd → 1.1.0-next.71013e07

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 (44) hide show
  1. package/README.md +13 -17
  2. package/credits.json +12 -1
  3. package/dist/cli/commonArgs.d.ts +20 -51
  4. package/dist/cli/commonArgs.help.d.ts +1 -1
  5. package/dist/cli/commonArgs.help.js +12 -11
  6. package/dist/cli/commonArgs.js +20 -76
  7. package/dist/cli/confirm.js +0 -3
  8. package/dist/cli/errors.d.ts +2 -19
  9. package/dist/cli/errors.js +3 -25
  10. package/dist/cli/testUtils.js +3 -3
  11. package/dist/cli/types.d.ts +10 -53
  12. package/dist/commands/login.d.ts +4 -4
  13. package/dist/commands/login.help.d.ts +1 -1
  14. package/dist/commands/login.help.js +11 -5
  15. package/dist/commands/login.js +33 -18
  16. package/dist/commands/logout.d.ts +1 -1
  17. package/dist/commands/logout.help.d.ts +1 -1
  18. package/dist/commands/logout.help.js +5 -4
  19. package/dist/commands/logout.js +1 -17
  20. package/dist/commands/token.d.ts +2 -7
  21. package/dist/commands/token.help.d.ts +1 -1
  22. package/dist/commands/token.help.js +5 -5
  23. package/dist/commands/token.js +6 -22
  24. package/dist/index.js +17 -52
  25. package/dist/oauth/authorizationURL.d.ts +1 -6
  26. package/dist/oauth/authorizationURL.js +2 -6
  27. package/dist/oauth/authorize.d.ts +13 -44
  28. package/dist/oauth/authorize.js +4 -5
  29. package/dist/oauth/discoverOIDC.d.ts +10 -27
  30. package/dist/oauth/discoverOIDC.js +17 -46
  31. package/dist/oauth/discoverSSOConfig.d.ts +37 -0
  32. package/dist/oauth/discoverSSOConfig.js +105 -0
  33. package/dist/oauth/errors.d.ts +2 -0
  34. package/dist/oauth/getValidAccessToken.d.ts +9 -44
  35. package/dist/oauth/getValidAccessToken.js +8 -16
  36. package/dist/oauth/openBrowser.d.ts +14 -3
  37. package/dist/oauth/openBrowser.js +22 -5
  38. package/dist/oauth/refreshTokens.js +0 -3
  39. package/dist/oauth/tokenResponse.d.ts +6 -38
  40. package/dist/oauth/tokenResponse.js +7 -27
  41. package/dist/oauth/tokenStore.d.ts +75 -3
  42. package/dist/oauth/tokenStore.js +394 -32
  43. package/docs/architecture.md +27 -18
  44. package/package.json +7 -3
package/README.md CHANGED
@@ -32,21 +32,17 @@ Run `axe-auth <command> --help` for command-specific options.
32
32
 
33
33
  ### Common configuration
34
34
 
35
- `axe-auth login` accepts the Keycloak coordinates as flags or environment variables (flag wins over env):
35
+ `axe-auth` discovers its OAuth coordinates by calling `<server>/api/sso-config` on the axe server. Users only supply (or default to) the axe server URL — never the underlying Keycloak URL, realm, or client ID.
36
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`. |
37
+ | Flag | Env var | Notes |
38
+ | ---------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39
+ | `--server` | `AXE_SERVER_URL` | axe server URL. Defaults to `https://axe.deque.com` (SaaS prod) when neither flag nor env var is set, so SaaS users pass no flags at all. Customers on other deployments override with their own axe server URL. |
40
+ | `--allow-insecure-issuer` | — | Permit non-loopback http URLs (default is https only; loopback http is always allowed). Applies to `login` only; `token` and `logout` use the policy persisted at login. |
41
+ | `--no-allow-insecure-issuer` | — | Force `allowInsecureIssuer=false` for the new `login` (and the entry it persists). Mutually exclusive with `--allow-insecure-issuer`. `token` and `logout` ignore this flag. |
44
42
 
45
- The issuer URL is built as `${server}/realms/${realm}`.
43
+ `axe-auth` stores one set of credentials per machine. On a successful `login`, the discovered 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)`.
46
44
 
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.
45
+ There is no concurrent multi-issuer support. Logging in to a second deployment overwrites the previous tokens; an interactive prompt confirms the switch before destroying the existing session, and `--force` skips the prompt.
50
46
 
51
47
  ### Exit codes
52
48
 
@@ -60,11 +56,11 @@ There is no concurrent multi-issuer support. Logging in to a second Keycloak ove
60
56
  ### Examples
61
57
 
62
58
  ```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
59
+ # First-time login on Deque SaaS prod (opens your browser, no flags needed)
60
+ axe-auth login
61
+
62
+ # First-time login on a non-SaaS-prod deployment
63
+ axe-auth login --server https://axe.customer.dequecloud.com
68
64
 
69
65
  # Pull a fresh access token for use in shell substitution
70
66
  docker run -e AXE_ACCESS_TOKEN="$(axe-auth token)" axe-mcp-server
package/credits.json CHANGED
@@ -27,6 +27,17 @@
27
27
  "publisher": "Stephen Mathieson",
28
28
  "email": "me@stephenmathieson.com"
29
29
  },
30
+ "shlex@3.0.0": {
31
+ "name": "shlex",
32
+ "version": "3.0.0",
33
+ "licenses": "MIT",
34
+ "path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/shlex@3.0.0/node_modules/shlex",
35
+ "licenseText": "MIT License\n\nCopyright (c) 2018 Ryan Govostes\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
36
+ "licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/shlex@3.0.0/node_modules/shlex/LICENSE",
37
+ "repository": "https://github.com/rgov/node-shlex",
38
+ "publisher": "Ryan Govostes",
39
+ "copyright": "Copyright (c) 2018 Ryan Govostes"
40
+ },
30
41
  "ts-dedent@2.2.0": {
31
42
  "name": "ts-dedent",
32
43
  "version": "2.2.0",
@@ -39,4 +50,4 @@
39
50
  "email": "dev@zaku.eu",
40
51
  "copyright": "Copyright (c) 2018 Tamino Martinius"
41
52
  }
42
- }
53
+ }
@@ -1,66 +1,35 @@
1
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. */
2
+ /** Default axe server URL for Deque SaaS prod. */
3
+ export declare const DEFAULT_WALNUT_URL = "https://axe.deque.com";
4
+ /** Common configuration the CLI verbs share. */
13
5
  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
- */
6
+ /** axe server URL (walnut). */
7
+ walnutURL: string;
8
+ /** Whether non-loopback http walnut/issuer URLs are permitted. */
23
9
  allowInsecureIssuer: boolean;
24
10
  }
25
11
  /**
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`.
12
+ * `parseArgs`-shaped options shared by every CLI verb. `parseArgs`
13
+ * doesn't support `--no-` boolean negation natively, so the opt-out
14
+ * is registered as its own flag and `parseCommonArgs` rejects passing
15
+ * both together.
37
16
  */
38
17
  export declare const COMMON_OPTIONS: NonNullable<ParseArgsConfig["options"]>;
39
18
  /** Subset of `parseArgs(...).values` this helper consumes. */
40
19
  export interface ParsedCommonValues {
41
20
  server?: string;
42
- realm?: string;
43
- "client-id"?: string;
44
21
  "allow-insecure-issuer"?: boolean;
45
22
  "no-allow-insecure-issuer"?: boolean;
46
23
  }
24
+ /** Stored fallback for `allowInsecureIssuer` + the `walnutURL` it was minted against. */
25
+ export interface StoredCommonDefaults {
26
+ walnutURL: string;
27
+ allowInsecureIssuer: boolean;
28
+ }
47
29
  /**
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.
30
+ * Resolves common configuration from parsed flag values, falling
31
+ * back to `AXE_SERVER_URL` and finally to `DEFAULT_WALNUT_URL`.
32
+ * `allowInsecureIssuer` falls back to `defaults` only when the
33
+ * resolved walnut URL matches `defaults.walnutURL`.
65
34
  */
66
- export declare function parseCommonArgs(values: ParsedCommonValues, env?: NodeJS.ProcessEnv, defaults?: StoredConfig | null): CommonArgs;
35
+ export declare function parseCommonArgs(values: ParsedCommonValues, env?: NodeJS.ProcessEnv, defaults?: StoredCommonDefaults | null): CommonArgs;
@@ -1,2 +1,2 @@
1
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.";
2
+ export declare const HELP_COMMON_OPTIONS = " --server <url> axe server URL. Used by `login` to fetch\n /api/sso-config and derive the OAuth\n coordinates. Falls back to AXE_SERVER_URL,\n then to https://axe.deque.com (SaaS prod).\n --allow-insecure-issuer Permit non-loopback http URLs (default is\n https only; loopback http is always\n allowed). Applies to `login` only;\n `token` and `logout` use the policy\n persisted at login.\n --no-allow-insecure-issuer\n Force allowInsecureIssuer=false for the new\n `login` (and the entry it persists).\n Ignored by `token` and `logout`.\n Mutually exclusive with\n --allow-insecure-issuer.\n -h, --help Show this help.";
@@ -2,18 +2,19 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HELP_COMMON_OPTIONS = void 0;
4
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).
5
+ exports.HELP_COMMON_OPTIONS = ` --server <url> axe server URL. Used by \`login\` to fetch
6
+ /api/sso-config and derive the OAuth
7
+ coordinates. Falls back to AXE_SERVER_URL,
8
+ then to https://axe.deque.com (SaaS prod).
9
+ --allow-insecure-issuer Permit non-loopback http URLs (default is
10
+ https only; loopback http is always
11
+ allowed). Applies to \`login\` only;
12
+ \`token\` and \`logout\` use the policy
13
+ persisted at login.
14
14
  --no-allow-insecure-issuer
15
- Force allowInsecureIssuer=false for this
16
- call, overriding the stored value if any.
15
+ Force allowInsecureIssuer=false for the new
16
+ \`login\` (and the entry it persists).
17
+ Ignored by \`token\` and \`logout\`.
17
18
  Mutually exclusive with
18
19
  --allow-insecure-issuer.
19
20
  -h, --help Show this help.`;
@@ -3,94 +3,38 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.COMMON_OPTIONS = void 0;
6
+ exports.COMMON_OPTIONS = exports.DEFAULT_WALNUT_URL = void 0;
7
7
  exports.parseCommonArgs = parseCommonArgs;
8
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");
9
+ /** Default axe server URL for Deque SaaS prod. */
10
+ exports.DEFAULT_WALNUT_URL = "https://axe.deque.com";
11
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`.
12
+ * `parseArgs`-shaped options shared by every CLI verb. `parseArgs`
13
+ * doesn't support `--no-` boolean negation natively, so the opt-out
14
+ * is registered as its own flag and `parseCommonArgs` rejects passing
15
+ * both together.
23
16
  */
24
17
  exports.COMMON_OPTIONS = {
25
18
  server: { type: "string" },
26
- realm: { type: "string" },
27
- "client-id": { type: "string" },
28
19
  "allow-insecure-issuer": { type: "boolean" },
29
20
  "no-allow-insecure-issuer": { type: "boolean" },
30
21
  };
31
22
  /**
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.
23
+ * Resolves common configuration from parsed flag values, falling
24
+ * back to `AXE_SERVER_URL` and finally to `DEFAULT_WALNUT_URL`.
25
+ * `allowInsecureIssuer` falls back to `defaults` only when the
26
+ * resolved walnut URL matches `defaults.walnutURL`.
49
27
  */
50
28
  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
- };
29
+ const walnutURL = (0, remove_trailing_slash_1.default)(values.server ?? env.AXE_SERVER_URL ?? exports.DEFAULT_WALNUT_URL);
30
+ // Only inherit the stored `allowInsecureIssuer` when the incoming
31
+ // walnut URL matches the stored one. A user logging in to a
32
+ // different deployment must opt back in via `--allow-insecure-issuer`
33
+ // explicitly; otherwise a dev-time HTTP-allow setting would silently
34
+ // carry over to a prod login.
35
+ const matchingDefaults = defaults && defaults.walnutURL === walnutURL ? defaults : null;
36
+ const allowInsecureIssuer = resolveAllowInsecureIssuer(values, matchingDefaults);
37
+ return { walnutURL, allowInsecureIssuer };
94
38
  }
95
39
  /**
96
40
  * Resolves `allowInsecureIssuer` from the positive flag, its
@@ -9,9 +9,6 @@ const node_readline_1 = require("node:readline");
9
9
  * default action stays conservative.
10
10
  */
11
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
12
  const input = options.input ?? process.stdin;
16
13
  const output = options.output ?? process.stderr;
17
14
  const question = options.question.endsWith(" ")
@@ -2,29 +2,12 @@
2
2
  export type CLIErrorCode = "NOT_AUTHENTICATED" | "USER_CANCELLED" | "ALREADY_AUTHENTICATED" | "OAUTH_FAILED" | "KEYCHAIN_FAILURE";
3
3
  /**
4
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.
5
+ * dispatcher writes `message` to stderr and exits with `exitCode`.
9
6
  */
10
7
  export declare class CLIError extends Error {
11
8
  readonly code: CLIErrorCode;
12
9
  readonly exitCode: number;
13
10
  constructor(code: CLIErrorCode, message: string);
14
11
  }
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
- */
12
+ /** Returns the `message` of an `Error`, or its `String` coercion otherwise. */
30
13
  export declare function describeError(err: unknown): string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MissingConfigError = exports.CLIError = void 0;
3
+ exports.CLIError = void 0;
4
4
  exports.describeError = describeError;
5
5
  const EXIT_CODE_BY_ERROR_CODE = {
6
6
  NOT_AUTHENTICATED: 1,
@@ -11,10 +11,7 @@ const EXIT_CODE_BY_ERROR_CODE = {
11
11
  };
12
12
  /**
13
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.
14
+ * dispatcher writes `message` to stderr and exits with `exitCode`.
18
15
  */
19
16
  class CLIError extends Error {
20
17
  code;
@@ -27,26 +24,7 @@ class CLIError extends Error {
27
24
  }
28
25
  }
29
26
  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
- */
27
+ /** Returns the `message` of an `Error`, or its `String` coercion otherwise. */
50
28
  function describeError(err) {
51
29
  return err instanceof Error ? err.message : String(err);
52
30
  }
@@ -71,9 +71,10 @@ function makeStore(initial) {
71
71
  function entry(tokens, overrides = {}) {
72
72
  return {
73
73
  tokens,
74
- issuerURL: "https://auth.example.com/realms/prod",
74
+ issuerURL: "https://auth.example.com/auth/realms/prod",
75
75
  clientId: "axe-auth",
76
76
  allowInsecureIssuer: false,
77
+ walnutURL: "https://axe.example.com",
77
78
  ...overrides,
78
79
  };
79
80
  }
@@ -92,8 +93,7 @@ function captureDeps(overrides = {}) {
92
93
  */
93
94
  function commonArgs(overrides = {}) {
94
95
  return {
95
- issuerURL: "https://auth.example.com/realms/prod",
96
- clientId: "axe-auth",
96
+ walnutURL: "https://axe.example.com",
97
97
  allowInsecureIssuer: false,
98
98
  ...overrides,
99
99
  };
@@ -4,48 +4,22 @@ import type { LoadResult } from "../oauth/tokenStore";
4
4
  import type { CommonArgs } from "./commonArgs";
5
5
  /** Mapping passed to `parseArgs` for verb-specific flags. */
6
6
  export type VerbOptions = NonNullable<ParseArgsConfig["options"]>;
7
- /**
8
- * The injectables every CLI verb sees. The dispatcher fills these in
9
- * with the real `process.*` streams; tests pass synthetic values.
10
- * Verb-specific overrides (e.g. `getToken`, `authorize`) extend this
11
- * interface in the relevant verb's module — TS allows the dispatcher
12
- * to pass a plain `CommandDeps` because the verb-specific extras are
13
- * declared optional.
14
- */
7
+ /** Injectables every CLI verb sees. Dispatcher fills with `process.*`; tests pass synthetic values. */
15
8
  export interface CommandDeps {
16
- /**
17
- * Standard input. Carries an optional `isTTY` so verbs like
18
- * `login` can branch on interactivity. Real `process.stdin`
19
- * satisfies the shape; test fixtures using `Readable.from([])`
20
- * leave `isTTY` undefined, which coerces to `false`.
21
- */
9
+ /** `isTTY` is optional so `Readable.from([])` test fixtures coerce to non-TTY. */
22
10
  stdin: Readable & {
23
11
  isTTY?: boolean;
24
12
  };
25
13
  stdout: Writable;
26
14
  stderr: Writable;
27
15
  /**
28
- * Pre-loaded result of `KeyringTokenStore.load()`, populated by the
29
- * dispatcher when it ran the keychain read to derive the
30
- * `parseCommonArgs` fallback. Verbs prefer this over loading
31
- * again so a single `axe-auth token` invocation hits the keychain
32
- * once instead of three times. Tests typically leave this
33
- * undefined and inject a fake `tokenStore` instead.
16
+ * Pre-loaded result of `KeyringTokenStore.load()` from the
17
+ * dispatcher's keychain read, so a single CLI invocation hits the
18
+ * keychain once instead of N times.
34
19
  */
35
20
  loadedEntry?: LoadResult;
36
21
  }
37
- /**
38
- * Specification of a single CLI verb. The dispatcher in
39
- * `src/index.ts` consumes one of these per registered command:
40
- * parses argv, resolves common args, prints `helpText` for `--help`,
41
- * runs `run`, and translates thrown `CLIError`s (see `./errors`)
42
- * into exit codes.
43
- *
44
- * Each verb narrows the `run` parameters to its own
45
- * `CommonArgs & <Verb>Flags` and `<Verb>Deps`. Method-shorthand
46
- * bivariance lets that narrower signature satisfy this interface
47
- * without casts at the verb definition.
48
- */
22
+ /** Specification of a single CLI verb consumed by the dispatcher. */
49
23
  export interface CommandSpec {
50
24
  /** Verb name, e.g. `"login"`. */
51
25
  readonly name: string;
@@ -53,30 +27,13 @@ export interface CommandSpec {
53
27
  readonly summary: string;
54
28
  /** Full help text printed for `axe-auth <verb> --help`. */
55
29
  readonly helpText: string;
56
- /**
57
- * Verb-specific `parseArgs` options. Common options
58
- * (`--server` / `--realm` / `--client-id` / `--allow-insecure-issuer`)
59
- * are added by the dispatcher; do not duplicate them here.
60
- */
30
+ /** Verb-specific `parseArgs` options; common options are added by the dispatcher. */
61
31
  readonly options: VerbOptions;
62
- /**
63
- * `true` if this verb cannot run without resolved
64
- * `--server` / `--realm` / `--client-id` (login). `false` for
65
- * verbs that operate on the stored entry alone (token, logout)
66
- * and have their own "not authenticated" / "already logged out"
67
- * handling for the empty-entry case.
68
- *
69
- * When `false` and `parseCommonArgs` throws `MissingConfigError`,
70
- * the dispatcher passes a sentinel-empty `CommonArgs` to `run()`
71
- * instead of erroring, so the verb's own empty/corrupt-entry
72
- * branch fires.
73
- */
32
+ /** `true` if this verb cannot run without a usable walnut URL (login). */
74
33
  readonly requiresConfig: boolean;
75
34
  /**
76
- * Run the verb with already-resolved args and the dispatcher's
77
- * deps. Throw a `CLIError` to signal a known failure with an
78
- * explicit exit code; throw any other error for a generic exit
79
- * code 2 plus the error message on stderr.
35
+ * Throw `CLIError` for a known failure with an explicit exit code;
36
+ * any other error becomes exit code 2 with the message on stderr.
80
37
  */
81
38
  run(args: CommonArgs, deps: CommandDeps): Promise<void>;
82
39
  }
@@ -1,4 +1,5 @@
1
1
  import { authorize } from "../oauth/authorize";
2
+ import { discoverSSOConfig } from "../oauth/discoverSSOConfig";
2
3
  import { type TokenStore } from "../oauth/tokenStore";
3
4
  import type { CommonArgs } from "../cli/commonArgs";
4
5
  import type { CommandDeps } from "../cli/types";
@@ -8,16 +9,15 @@ export interface LoginDeps extends CommandDeps {
8
9
  isInteractive?: boolean;
9
10
  /** Override `authorize` (for tests). */
10
11
  authorize?: typeof authorize;
12
+ /** Override the SSO discovery helper (for tests). */
13
+ discoverSSOConfig?: typeof discoverSSOConfig;
11
14
  /**
12
15
  * Override the token store. Defaults to a fresh
13
16
  * `KeyringTokenStore()`. The same instance is passed to `authorize`
14
17
  * so the pre-check and post-flow save agree on the keychain entry.
15
18
  */
16
19
  tokenStore?: TokenStore;
17
- /**
18
- * Override the confirmation prompt (for tests). Receives the
19
- * issuer URL and returns whether the user wants to proceed.
20
- */
20
+ /** Override the confirmation prompt (for tests). */
21
21
  confirm?: (prompt: string) => Promise<boolean>;
22
22
  }
23
23
  /** Verb-specific flags for `axe-auth login`. */
@@ -1,2 +1,2 @@
1
1
  /** Help text for `axe-auth login --help`. */
2
- export declare const HELP_LOGIN = "axe-auth login\n\nOpen a browser, complete the OAuth 2.0 Authorization Code + PKCE\nflow against your Keycloak realm, and persist the resulting tokens\nto the OS keychain.\n\nUsage:\n axe-auth login --server <url> --realm <name> --client-id <id> [--force]\n\n After a first successful login, --server / --realm / --client-id\n can be omitted; the previously stored values are reused.\n\nOptions:\n --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.\n --force Re-authenticate without prompting even if\n a valid token is already stored.\n\nBehavior when already authenticated:\n axe-auth stores one entry per machine. If a valid entry already\n exists, an interactive session prompts for confirmation before\n overwriting it \u2014 even when the new login targets a different\n issuer or client (logging in to B destroys A's tokens). Pass\n --force to skip the prompt. In a non-interactive session (no TTY)\n --force is required; otherwise the command refuses to overwrite\n stored tokens and exits non-zero.\n\nExit codes:\n 0 Success; tokens persisted to the keychain.\n 2 Configuration error or flow failure.\n 3 Login cancelled at the prompt.";
2
+ export declare const HELP_LOGIN = "axe-auth login\n\nOpen a browser, complete the OAuth 2.0 Authorization Code + PKCE\nflow against the customer's Keycloak realm, and persist the\nresulting tokens to the OS keychain.\n\nThe CLI discovers the OAuth coordinates by calling\n`<server>/api/sso-config` on the axe server, so users only need to\nsupply (or default to) the axe server URL \u2014 never the underlying\nKeycloak URL, realm, or client ID directly.\n\nUsage:\n axe-auth login [--server <url>] [--force]\n\n With no flags, the SaaS prod axe server URL (https://axe.deque.com)\n is used. Customers on other deployments pass --server (or set\n AXE_SERVER_URL).\n\nOptions:\n --server <url> axe server URL. Used by `login` to fetch\n /api/sso-config and derive the OAuth\n coordinates. Falls back to AXE_SERVER_URL,\n then to https://axe.deque.com (SaaS prod).\n --allow-insecure-issuer Permit non-loopback http URLs (default is\n https only; loopback http is always\n allowed). Applies to `login` only;\n `token` and `logout` use the policy\n persisted at login.\n --no-allow-insecure-issuer\n Force allowInsecureIssuer=false for the new\n `login` (and the entry it persists).\n Ignored by `token` and `logout`.\n Mutually exclusive with\n --allow-insecure-issuer.\n -h, --help Show this help.\n --force Re-authenticate without prompting even if\n a valid token is already stored.\n\nBehavior when already authenticated:\n axe-auth stores one entry per machine. If a valid entry already\n exists, an interactive session prompts for confirmation before\n overwriting it \u2014 even when the new login targets a different\n issuer or client (logging in to B destroys A's tokens). Pass\n --force to skip the prompt. In a non-interactive session (no TTY)\n --force is required; otherwise the command refuses to overwrite\n stored tokens and exits non-zero.\n\nExit codes:\n 0 Success; tokens persisted to the keychain.\n 2 Configuration error or flow failure.\n 3 Login cancelled at the prompt.";
@@ -6,14 +6,20 @@ const commonArgs_help_1 = require("../cli/commonArgs.help");
6
6
  exports.HELP_LOGIN = `axe-auth login
7
7
 
8
8
  Open a browser, complete the OAuth 2.0 Authorization Code + PKCE
9
- flow against your Keycloak realm, and persist the resulting tokens
10
- to the OS keychain.
9
+ flow against the customer's Keycloak realm, and persist the
10
+ resulting tokens to the OS keychain.
11
+
12
+ The CLI discovers the OAuth coordinates by calling
13
+ \`<server>/api/sso-config\` on the axe server, so users only need to
14
+ supply (or default to) the axe server URL — never the underlying
15
+ Keycloak URL, realm, or client ID directly.
11
16
 
12
17
  Usage:
13
- axe-auth login --server <url> --realm <name> --client-id <id> [--force]
18
+ axe-auth login [--server <url>] [--force]
14
19
 
15
- After a first successful login, --server / --realm / --client-id
16
- can be omitted; the previously stored values are reused.
20
+ With no flags, the SaaS prod axe server URL (https://axe.deque.com)
21
+ is used. Customers on other deployments pass --server (or set
22
+ AXE_SERVER_URL).
17
23
 
18
24
  Options:
19
25
  ${commonArgs_help_1.HELP_COMMON_OPTIONS}