@deque/axe-auth 1.2.1-next.a702a9f5 → 1.2.1-rc.710a04bd

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/credits.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
- "@napi-rs/keyring@1.3.0": {
2
+ "@napi-rs/keyring@1.2.0": {
3
3
  "name": "@napi-rs/keyring",
4
- "version": "1.3.0",
4
+ "version": "1.2.0",
5
5
  "licenses": "MIT",
6
- "path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/@napi-rs+keyring@1.3.0/node_modules/@napi-rs/keyring",
6
+ "path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/@napi-rs+keyring@1.2.0/node_modules/@napi-rs/keyring",
7
7
  "licenseText": "MIT License\n\nCopyright (c) 2020 N-API for Rust\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",
8
- "licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/@napi-rs+keyring@1.3.0/node_modules/@napi-rs/keyring/LICENSE",
8
+ "licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/@napi-rs+keyring@1.2.0/node_modules/@napi-rs/keyring/LICENSE",
9
9
  "copyright": "Copyright (c) 2020 N-API for Rust"
10
10
  },
11
- "@napi-rs/keyring-linux-x64-gnu@1.3.0": {
11
+ "@napi-rs/keyring-linux-x64-gnu@1.2.0": {
12
12
  "name": "@napi-rs/keyring-linux-x64-gnu",
13
- "version": "1.3.0",
13
+ "version": "1.2.0",
14
14
  "licenses": "MIT",
15
- "path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/@napi-rs+keyring-linux-x64-gnu@1.3.0/node_modules/@napi-rs/keyring-linux-x64-gnu",
15
+ "path": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/@napi-rs+keyring-linux-x64-gnu@1.2.0/node_modules/@napi-rs/keyring-linux-x64-gnu",
16
16
  "licenseText": "# `@napi-rs/keyring-linux-x64-gnu`\n\nThis is the **x86_64-unknown-linux-gnu** binary for `@napi-rs/keyring`\n",
17
- "licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/@napi-rs+keyring-linux-x64-gnu@1.3.0/node_modules/@napi-rs/keyring-linux-x64-gnu/README.md"
17
+ "licenseFile": "/home/runner/work/axe-mcp-server/axe-mcp-server/node_modules/.pnpm/@napi-rs+keyring-linux-x64-gnu@1.2.0/node_modules/@napi-rs/keyring-linux-x64-gnu/README.md"
18
18
  },
19
19
  "remove-trailing-slash@0.1.1": {
20
20
  "name": "remove-trailing-slash",
@@ -88,7 +88,7 @@ const loginCommand = {
88
88
  scopes: ["offline_access"],
89
89
  allowInsecureIssuer: args.allowInsecureIssuer,
90
90
  tokenStore,
91
- onAuthorizationURL: (url) => {
91
+ onAuthorizationUrl: (url) => {
92
92
  deps.stderr.write(`Authorization URL: ${url}\n`);
93
93
  },
94
94
  onWarning: (msg) => {
package/dist/index.js CHANGED
@@ -13,7 +13,6 @@ const login_1 = __importDefault(require("./commands/login"));
13
13
  const logout_1 = __importDefault(require("./commands/logout"));
14
14
  const token_1 = __importDefault(require("./commands/token"));
15
15
  const errors_1 = require("./cli/errors");
16
- const safeExit_1 = require("./cli/safeExit");
17
16
  const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8"));
18
17
  const COMMANDS = [
19
18
  login_1.default,
@@ -127,7 +126,7 @@ async function dispatch(argv) {
127
126
  return 2;
128
127
  }
129
128
  }
130
- dispatch(process.argv.slice(2)).then((code) => (0, safeExit_1.safeExit)(code), (err) => {
129
+ dispatch(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
131
130
  process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
132
- return (0, safeExit_1.safeExit)(1);
131
+ process.exit(1);
133
132
  });
@@ -5,7 +5,7 @@ export interface BuildAuthorizationURLOptions {
5
5
  /** OAuth client identifier registered with the authorization server. */
6
6
  clientId: string;
7
7
  /** Loopback redirect URI the callback server is listening on. */
8
- redirectURI: string;
8
+ redirectUri: string;
9
9
  /** PKCE `code_challenge` derived via S256. */
10
10
  codeChallenge: string;
11
11
  /** CSRF `state` value, echoed by the auth server and validated on callback. */
@@ -39,7 +39,7 @@ function buildAuthorizationURL(options) {
39
39
  // `set` just reads more conventionally.
40
40
  url.searchParams.set("response_type", "code");
41
41
  url.searchParams.set("client_id", options.clientId);
42
- url.searchParams.set("redirect_uri", options.redirectURI);
42
+ url.searchParams.set("redirect_uri", options.redirectUri);
43
43
  url.searchParams.set("code_challenge", options.codeChallenge);
44
44
  url.searchParams.set("code_challenge_method", "S256");
45
45
  url.searchParams.set("state", options.state);
@@ -19,7 +19,7 @@ export interface AuthorizeOptions {
19
19
  /** Override for the system browser launcher. Injected for tests. */
20
20
  openBrowser?: (url: string) => void;
21
21
  /** Called with the authorization URL just before the browser launch. Default prints to stderr only when stderr is a TTY. */
22
- onAuthorizationURL?: (url: string) => void;
22
+ onAuthorizationUrl?: (url: string) => void;
23
23
  /**
24
24
  * Called for soft warnings (e.g. requested `offline_access` but the
25
25
  * server returned no refresh token, or the browser failed to
@@ -9,7 +9,7 @@ const openBrowser_1 = require("./openBrowser");
9
9
  const tokenExchange_1 = require("./tokenExchange");
10
10
  const tokenStore_1 = require("./tokenStore");
11
11
  const errors_1 = require("./errors");
12
- function defaultOnAuthorizationURL(url) {
12
+ function defaultOnAuthorizationUrl(url) {
13
13
  if (process.stderr.isTTY) {
14
14
  console.error(`Authorization URL: ${url}`);
15
15
  }
@@ -38,7 +38,7 @@ function defaultOnWarning(message) {
38
38
  * @throws {OAuthCallbackError} For loopback/callback-server failures.
39
39
  */
40
40
  async function authorize(options) {
41
- const { issuerURL, clientId, walnutURL, scopes, timeoutMs, signal, tokenStore = new tokenStore_1.KeyringTokenStore(), openBrowser = openBrowser_1.openBrowser, onAuthorizationURL = defaultOnAuthorizationURL, onWarning = defaultOnWarning, allowInsecureIssuer, } = options;
41
+ const { issuerURL, clientId, walnutURL, scopes, timeoutMs, signal, tokenStore = new tokenStore_1.KeyringTokenStore(), openBrowser = openBrowser_1.openBrowser, onAuthorizationUrl = defaultOnAuthorizationUrl, onWarning = defaultOnWarning, allowInsecureIssuer, } = options;
42
42
  // Discovery before browser-launch so a bad URL surfaces as a
43
43
  // throw rather than a wrong/unreachable browser tab.
44
44
  const config = await (0, discoverOIDC_1.discoverOIDC)(issuerURL, {
@@ -57,7 +57,7 @@ async function authorize(options) {
57
57
  const authURL = (0, authorizationURL_1.buildAuthorizationURL)({
58
58
  authorizationEndpoint: config.authorizationEndpoint,
59
59
  clientId,
60
- redirectURI: callback.redirectURI,
60
+ redirectUri: callback.redirectUri,
61
61
  codeChallenge,
62
62
  state,
63
63
  scopes,
@@ -65,13 +65,13 @@ async function authorize(options) {
65
65
  // Surface before launch so the URL is always visible even if the
66
66
  // browser spawn fails (or never does anything useful, e.g. on a
67
67
  // headless box).
68
- onAuthorizationURL(authURL);
68
+ onAuthorizationUrl(authURL);
69
69
  try {
70
70
  openBrowser(authURL);
71
71
  }
72
72
  catch (err) {
73
73
  // Only swallow the "could not launch browser" case: the URL was
74
- // already surfaced via onAuthorizationURL so the user can
74
+ // already surfaced via onAuthorizationUrl so the user can
75
75
  // complete the flow manually. Any other error (a bug in an
76
76
  // injected openBrowser, an unexpected throw) must propagate so
77
77
  // callers and tests can see it.
@@ -89,7 +89,7 @@ async function authorize(options) {
89
89
  clientId,
90
90
  code,
91
91
  codeVerifier,
92
- redirectURI: callback.redirectURI,
92
+ redirectUri: callback.redirectUri,
93
93
  signal,
94
94
  });
95
95
  // If the caller requested offline_access but no refresh_token
@@ -1,34 +1,23 @@
1
1
  import { IncomingMessage, Server, ServerResponse } from "node:http";
2
- /** Configures the loopback callback server. */
3
- export interface CallbackServerOptions {
2
+ export type CallbackServerOptions = {
4
3
  expectedState: string;
5
4
  timeoutMs?: number;
6
5
  signal?: AbortSignal;
7
- }
8
- /** Authorization-code and state pair captured from a successful redirect. */
9
- export interface CallbackResult {
6
+ };
7
+ export type CallbackResult = {
10
8
  code: string;
11
9
  state: string;
12
- }
13
- /** Live handle to a running callback server. */
14
- export interface CallbackServerHandle {
15
- redirectURI: string;
10
+ };
11
+ export type CallbackServerHandle = {
12
+ redirectUri: string;
16
13
  result: Promise<CallbackResult>;
17
14
  close: () => Promise<void>;
18
- }
15
+ };
19
16
  type RequestHandler = (req: IncomingMessage, res: ServerResponse) => void;
20
17
  type LoopbackListener = (handler: RequestHandler, host: string) => Promise<Server>;
21
- /**
22
- * Binds an HTTP server to the loopback interface, preferring IPv4.
23
- *
24
- * RFC 8252 §7.3: "use whichever is available". Prefer IPv4; fall back to
25
- * IPv6 only when the IPv4 loopback isn't configured on this host. `listenFn`
26
- * is an injection seam for tests — production callers use the default.
27
- */
28
- export declare function bindLoopback(handler: RequestHandler, listenFn?: LoopbackListener): Promise<{
18
+ export declare const bindLoopback: (handler: RequestHandler, listenFn?: LoopbackListener) => Promise<{
29
19
  server: Server;
30
20
  host: "127.0.0.1" | "::1";
31
21
  }>;
32
- /** Starts a loopback HTTP server that captures the OAuth redirect. */
33
- export declare function startCallbackServer(opts: CallbackServerOptions): Promise<CallbackServerHandle>;
22
+ export declare const startCallbackServer: (opts: CallbackServerOptions) => Promise<CallbackServerHandle>;
34
23
  export {};
@@ -1,16 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.bindLoopback = bindLoopback;
4
- exports.startCallbackServer = startCallbackServer;
3
+ exports.startCallbackServer = exports.bindLoopback = void 0;
5
4
  const node_events_1 = require("node:events");
6
5
  const node_http_1 = require("node:http");
7
6
  const errors_1 = require("./errors");
8
- const renderHTML_1 = require("./renderHTML");
7
+ const renderHtml_1 = require("./renderHtml");
9
8
  const DEFAULT_TIMEOUT_MS = 120_000;
10
9
  const LOOPBACK_ADDRESSES = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
11
- function isLoopback(addr) {
12
- return !!addr && LOOPBACK_ADDRESSES.has(addr);
13
- }
10
+ const isLoopback = (addr) => !!addr && LOOPBACK_ADDRESSES.has(addr);
14
11
  // Errors from bind(2) that mean "this address family isn't configured on this
15
12
  // host" — the signal to fall back to the other loopback family rather than
16
13
  // fail the caller.
@@ -22,14 +19,10 @@ const listen = async (handler, host) => {
22
19
  await (0, node_events_1.once)(server, "listening");
23
20
  return server;
24
21
  };
25
- /**
26
- * Binds an HTTP server to the loopback interface, preferring IPv4.
27
- *
28
- * RFC 8252 §7.3: "use whichever is available". Prefer IPv4; fall back to
29
- * IPv6 only when the IPv4 loopback isn't configured on this host. `listenFn`
30
- * is an injection seam for tests — production callers use the default.
31
- */
32
- async function bindLoopback(handler, listenFn = listen) {
22
+ // RFC 8252 §7.3: "use whichever is available". Prefer IPv4; fall back to
23
+ // IPv6 only when the IPv4 loopback isn't configured on this host. `listenFn`
24
+ // is an injection seam for tests — production callers use the default.
25
+ const bindLoopback = async (handler, listenFn = listen) => {
33
26
  try {
34
27
  return {
35
28
  server: await listenFn(handler, "127.0.0.1"),
@@ -51,32 +44,32 @@ async function bindLoopback(handler, listenFn = listen) {
51
44
  throw new errors_1.OAuthCallbackError("BIND_FAILED", `Failed to bind loopback server on 127.0.0.1 (${code}) and [::1]: ${ipv6Err.message}`, { cause: ipv6Err });
52
45
  }
53
46
  }
54
- }
55
- async function closeServer(server) {
47
+ };
48
+ exports.bindLoopback = bindLoopback;
49
+ const closeServer = async (server) => {
56
50
  if (!server.listening)
57
51
  return;
58
52
  server.close();
59
53
  server.closeAllConnections?.();
60
54
  await (0, node_events_1.once)(server, "close");
61
- }
62
- function writeHTML(res, status, html) {
55
+ };
56
+ const writeHtml = (res, status, html) => {
63
57
  res.writeHead(status, {
64
58
  "Content-Type": "text/html; charset=utf-8",
65
- "Content-Security-Policy": renderHTML_1.CSP_HEADER,
59
+ "Content-Security-Policy": renderHtml_1.CSP_HEADER,
66
60
  "X-Content-Type-Options": "nosniff",
67
61
  });
68
62
  res.end(html);
69
- }
70
- function writeText(res, status, body, extraHeaders = {}) {
63
+ };
64
+ const writeText = (res, status, body, extraHeaders = {}) => {
71
65
  res.writeHead(status, {
72
66
  "Content-Type": "text/plain; charset=utf-8",
73
67
  "X-Content-Type-Options": "nosniff",
74
68
  ...extraHeaders,
75
69
  });
76
70
  res.end(body + "\n");
77
- }
78
- /** Starts a loopback HTTP server that captures the OAuth redirect. */
79
- async function startCallbackServer(opts) {
71
+ };
72
+ const startCallbackServer = async (opts) => {
80
73
  const { expectedState, timeoutMs = DEFAULT_TIMEOUT_MS, signal } = opts;
81
74
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
82
75
  throw new TypeError(`timeoutMs must be a positive finite number (received ${timeoutMs}).`);
@@ -171,7 +164,7 @@ async function startCallbackServer(opts) {
171
164
  // up front means once we start composing the response, no concurrent
172
165
  // settlement (timer/abort) can race us into a dropped connection.
173
166
  if (!tryConsume()) {
174
- writeHTML(res, 409, (0, renderHTML_1.renderHTML)({
167
+ writeHtml(res, 409, (0, renderHtml_1.renderHtml)({
175
168
  kind: "error",
176
169
  reason: "This callback server has already handled a response.",
177
170
  }));
@@ -186,7 +179,7 @@ async function startCallbackServer(opts) {
186
179
  : { error: providerError },
187
180
  });
188
181
  deferSettleOnClose(res, () => rejectResult(error));
189
- writeHTML(res, 400, (0, renderHTML_1.renderHTML)({
182
+ writeHtml(res, 400, (0, renderHtml_1.renderHtml)({
190
183
  kind: "error",
191
184
  reason: providerError,
192
185
  description,
@@ -198,7 +191,7 @@ async function startCallbackServer(opts) {
198
191
  if (!code) {
199
192
  const error = new errors_1.OAuthCallbackError("MISSING_CODE", "Authorization response missing 'code' parameter.");
200
193
  deferSettleOnClose(res, () => rejectResult(error));
201
- writeHTML(res, 400, (0, renderHTML_1.renderHTML)({
194
+ writeHtml(res, 400, (0, renderHtml_1.renderHtml)({
202
195
  kind: "error",
203
196
  reason: "The authorization response was missing the 'code' parameter.",
204
197
  }));
@@ -207,19 +200,19 @@ async function startCallbackServer(opts) {
207
200
  if (state !== expectedState) {
208
201
  const error = new errors_1.OAuthCallbackError("STATE_MISMATCH", "Authorization response 'state' did not match expected value.");
209
202
  deferSettleOnClose(res, () => rejectResult(error));
210
- writeHTML(res, 400, (0, renderHTML_1.renderHTML)({
203
+ writeHtml(res, 400, (0, renderHtml_1.renderHtml)({
211
204
  kind: "error",
212
205
  reason: "The authorization response failed state validation.",
213
206
  }));
214
207
  return;
215
208
  }
216
209
  deferSettleOnClose(res, () => resolveResult({ code, state }));
217
- writeHTML(res, 200, (0, renderHTML_1.renderHTML)({ kind: "success" }));
210
+ writeHtml(res, 200, (0, renderHtml_1.renderHtml)({ kind: "success" }));
218
211
  };
219
- const bound = await bindLoopback(handler);
212
+ const bound = await (0, exports.bindLoopback)(handler);
220
213
  server = bound.server;
221
214
  const port = server.address().port;
222
- const redirectURI = bound.host === "::1"
215
+ const redirectUri = bound.host === "::1"
223
216
  ? `http://[::1]:${port}/callback`
224
217
  : `http://127.0.0.1:${port}/callback`;
225
218
  timeoutHandle = setTimeout(() => {
@@ -236,5 +229,6 @@ async function startCallbackServer(opts) {
236
229
  signal.addEventListener("abort", abortListener, { once: true });
237
230
  }
238
231
  }
239
- return { redirectURI, result, close };
240
- }
232
+ return { redirectUri, result, close };
233
+ };
234
+ exports.startCallbackServer = startCallbackServer;
@@ -45,7 +45,7 @@ function browserCommand(platform, url, browserOverride) {
45
45
  // module deliberately swallows (see `child.once("error", ...)`
46
46
  // below); `BROWSER_LAUNCH_FAILED` is only raised for synchronous
47
47
  // `spawn()` throws. The caller's fallback is the URL already
48
- // surfaced via `onAuthorizationURL` so the user can finish the
48
+ // surfaced via `onAuthorizationUrl` so the user can finish the
49
49
  // flow manually.
50
50
  return { command: "xdg-open", args: [url] };
51
51
  }
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.refreshTokens = refreshTokens;
4
4
  const errors_1 = require("./errors");
5
- const retry_1 = require("./retry");
6
5
  const tokenResponse_1 = require("./tokenResponse");
7
6
  const userAgent_1 = require("../userAgent");
8
7
  /**
@@ -32,11 +31,11 @@ async function refreshTokens(options) {
32
31
  grant_type: "refresh_token",
33
32
  client_id: options.clientId,
34
33
  refresh_token: options.refreshToken,
35
- }).toString();
34
+ });
36
35
  const issuedAt = now();
37
36
  let response;
38
37
  try {
39
- response = await (0, retry_1.fetchWithRetry)(options.tokenEndpoint, {
38
+ response = await fetch(options.tokenEndpoint, {
40
39
  method: "POST",
41
40
  headers: {
42
41
  "Content-Type": "application/x-www-form-urlencoded",
@@ -0,0 +1,9 @@
1
+ export type HtmlInput = {
2
+ kind: "success";
3
+ } | {
4
+ kind: "error";
5
+ reason: string;
6
+ description?: string;
7
+ };
8
+ export declare const CSP_HEADER: string;
9
+ export declare const renderHtml: (input: HtmlInput) => string;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CSP_HEADER = void 0;
4
- exports.renderHTML = renderHTML;
3
+ exports.renderHtml = exports.CSP_HEADER = void 0;
5
4
  const node_crypto_1 = require("node:crypto");
6
5
  const logo_generated_1 = require("./logo.generated");
7
6
  const CSS = `:root{--off-black:#666;--off-white:#fdfdfe;--white:#fff;--pink:#d71ef7;--blue:#3c7aae;--space-small:12px;--space-medium:24px;--space-large:36px;--space-huge:48px;--border-radius:3px}
@@ -20,21 +19,17 @@ main{margin:var(--space-huge) auto;padding-left:var(--space-medium);padding-righ
20
19
  // 'unsafe-inline'. Any drift between this digest and the rendered <style> tag
21
20
  // causes the browser to refuse the stylesheet — see docs/callback-page.md.
22
21
  const STYLE_HASH = (0, node_crypto_1.createHash)("sha256").update(CSS, "utf8").digest("base64");
23
- /** Content-Security-Policy header value sent with every callback page. */
24
22
  exports.CSP_HEADER = `default-src 'none'; img-src data:; style-src 'sha256-${STYLE_HASH}'; frame-ancestors 'none'`;
25
23
  // OWASP-recommended 5-character set for HTML body/attribute contexts;
26
24
  // & must come first to avoid double-escaping. Not safe for JS/CSS/URL contexts.
27
25
  // https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-for-html-contexts
28
- function escape(s) {
29
- return s
30
- .replace(/&/g, "&amp;")
31
- .replace(/</g, "&lt;")
32
- .replace(/>/g, "&gt;")
33
- .replace(/"/g, "&quot;")
34
- .replace(/'/g, "&#39;");
35
- }
36
- function page(title, body) {
37
- return `<!doctype html>
26
+ const escape = (s) => s
27
+ .replace(/&/g, "&amp;")
28
+ .replace(/</g, "&lt;")
29
+ .replace(/>/g, "&gt;")
30
+ .replace(/"/g, "&quot;")
31
+ .replace(/'/g, "&#39;");
32
+ const page = (title, body) => `<!doctype html>
38
33
  <html lang="en">
39
34
  <head>
40
35
  <meta charset="UTF-8">
@@ -49,9 +44,7 @@ ${body}
49
44
  </main>
50
45
  </body>
51
46
  </html>`;
52
- }
53
- /** Renders the loopback callback page shown in the user's browser. */
54
- function renderHTML(input) {
47
+ const renderHtml = (input) => {
55
48
  if (input.kind === "success") {
56
49
  return page("Authenticated", `<h1>Authenticated</h1>
57
50
  <p class="close">You can close this tab and return to your terminal.</p>`);
@@ -63,4 +56,5 @@ function renderHTML(input) {
63
56
  <p class="reason">${escape(input.reason)}</p>
64
57
  ${descriptionBlock}
65
58
  <p class="close">You can close this tab and return to your terminal.</p>`);
66
- }
59
+ };
60
+ exports.renderHtml = renderHtml;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.revokeRefreshToken = revokeRefreshToken;
4
- const retry_1 = require("./retry");
5
4
  const userAgent_1 = require("../userAgent");
6
5
  /**
7
6
  * Revokes a refresh token via RFC 7009. Servers SHOULD return 200
@@ -24,10 +23,10 @@ async function revokeRefreshToken(options) {
24
23
  token: options.refreshToken,
25
24
  token_type_hint: "refresh_token",
26
25
  client_id: options.clientId,
27
- }).toString();
26
+ });
28
27
  let response;
29
28
  try {
30
- response = await (0, retry_1.fetchWithRetry)(options.revocationEndpoint, {
29
+ response = await fetch(options.revocationEndpoint, {
31
30
  method: "POST",
32
31
  headers: {
33
32
  "Content-Type": "application/x-www-form-urlencoded",
@@ -10,7 +10,7 @@ export interface ExchangeCodeForTokensOptions {
10
10
  /** PKCE verifier paired with the `code_challenge` sent at auth time. */
11
11
  codeVerifier: string;
12
12
  /** Redirect URI originally sent to the authorization endpoint. */
13
- redirectURI: string;
13
+ redirectUri: string;
14
14
  /** Source of `now`. Injected for test determinism; defaults to `Date.now`. */
15
15
  now?: () => number;
16
16
  /** Aborts the underlying fetch when fired. */
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.exchangeCodeForTokens = exchangeCodeForTokens;
4
4
  const errors_1 = require("./errors");
5
- const retry_1 = require("./retry");
6
5
  const tokenResponse_1 = require("./tokenResponse");
7
6
  const userAgent_1 = require("../userAgent");
8
7
  /**
@@ -19,12 +18,12 @@ async function exchangeCodeForTokens(options) {
19
18
  client_id: options.clientId,
20
19
  code: options.code,
21
20
  code_verifier: options.codeVerifier,
22
- redirect_uri: options.redirectURI,
23
- }).toString();
21
+ redirect_uri: options.redirectUri,
22
+ });
24
23
  const issuedAt = now();
25
24
  let response;
26
25
  try {
27
- response = await (0, retry_1.fetchWithRetry)(options.tokenEndpoint, {
26
+ response = await fetch(options.tokenEndpoint, {
28
27
  method: "POST",
29
28
  headers: {
30
29
  "Content-Type": "application/x-www-form-urlencoded",
@@ -1,6 +1,6 @@
1
1
  # Callback response page
2
2
 
3
- The HTML the browser renders after Keycloak redirects to the loopback URL, produced by `src/oauth/renderHTML.ts`.
3
+ The HTML the browser renders after Keycloak redirects to the loopback URL, produced by `src/oauth/renderHtml.ts`.
4
4
 
5
5
  ## Approach
6
6
 
@@ -17,7 +17,7 @@ Content-Security-Policy: default-src 'none'; img-src data:; style-src 'sha256-<d
17
17
  X-Content-Type-Options: nosniff
18
18
  ```
19
19
 
20
- `default-src 'none'` blocks everything by default. `img-src data:` allows only the inlined logo. `style-src 'sha256-...'` allows only a `<style>` block whose contents match a digest computed at module load — stricter than `'unsafe-inline'`, and any drift between the rendered CSS and the committed hash causes the browser to refuse the stylesheet. Inline `style=""` attributes are avoided entirely (they require separate `style-src-attr` / `'unsafe-hashes'` machinery). The `renderHTML` test suite asserts the hash matches the rendered `<style>` contents to prevent silent drift.
20
+ `default-src 'none'` blocks everything by default. `img-src data:` allows only the inlined logo. `style-src 'sha256-...'` allows only a `<style>` block whose contents match a digest computed at module load — stricter than `'unsafe-inline'`, and any drift between the rendered CSS and the committed hash causes the browser to refuse the stylesheet. Inline `style=""` attributes are avoided entirely (they require separate `style-src-attr` / `'unsafe-hashes'` machinery). The `renderHtml` test suite asserts the hash matches the rendered `<style>` contents to prevent silent drift.
21
21
 
22
22
  **Auth code not echoed.** The success page deliberately does not render the received `code` in the HTML (RFC 8252 §8.1 interception mitigation).
23
23
 
@@ -4,7 +4,7 @@ The loopback HTTP listener that receives the OAuth authorization code redirect,
4
4
 
5
5
  ## Loopback bind
6
6
 
7
- RFC 8252 §7.3 says clients SHOULD NOT assume a particular IP version is available and RECOMMENDS trying both. Implementation: bind `127.0.0.1` on an ephemeral port; on `EAFNOSUPPORT` / `EADDRNOTAVAIL` (no IPv4 loopback configured on this host) fall back to `[::1]` on a fresh ephemeral port. The returned `redirectURI` uses whichever literal actually got bound, so the browser connects to exactly what the authorization server redirects it to — no reliance on `localhost` DNS resolution.
7
+ RFC 8252 §7.3 says clients SHOULD NOT assume a particular IP version is available and RECOMMENDS trying both. Implementation: bind `127.0.0.1` on an ephemeral port; on `EAFNOSUPPORT` / `EADDRNOTAVAIL` (no IPv4 loopback configured on this host) fall back to `[::1]` on a fresh ephemeral port. The returned `redirectUri` uses whichever literal actually got bound, so the browser connects to exactly what the authorization server redirects it to — no reliance on `localhost` DNS resolution.
8
8
 
9
9
  Request handling additionally rejects non-loopback `remoteAddress` values with `403` as defense in depth against DNS-rebinding-style pivots — the listener only binds to a loopback interface, but enforcing it at the handler costs nothing.
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deque/axe-auth",
3
- "version": "1.2.1-next.a702a9f5",
3
+ "version": "1.2.1-rc.710a04bd",
4
4
  "description": "CLI authentication utility for Deque services",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -27,18 +27,18 @@
27
27
  "registry": "https://registry.npmjs.org/"
28
28
  },
29
29
  "engines": {
30
- "node": ">=24.11.0"
30
+ "node": ">=22.13.0"
31
31
  },
32
32
  "dependencies": {
33
- "@napi-rs/keyring": "^1.3.0",
33
+ "@napi-rs/keyring": "^1.2.0",
34
34
  "remove-trailing-slash": "^0.1.1",
35
35
  "shlex": "^3.0.0",
36
36
  "ts-dedent": "^2.2.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@hono/node-server": "^1.19.14",
40
- "@types/node": "^24.0.0",
41
- "c8": "^11.0.0",
40
+ "@types/node": "^22.13.10",
41
+ "c8": "^10.1.3",
42
42
  "hono": "^4.12.16",
43
43
  "tsx": "^4.20.6",
44
44
  "typescript": "^6.0.3"
@@ -1,8 +0,0 @@
1
- /**
2
- * Force-exit the process to avoid hanging on open connections.
3
- *
4
- * On Windows, briefly yield first so undici can finish closing pending sockets;
5
- * otherwise the process aborts with a libuv UV_HANDLE_CLOSING assertion in
6
- * `src\win\async.c`. See https://github.com/nodejs/node/issues/56645.
7
- */
8
- export declare function safeExit(code: number): Promise<never>;
@@ -1,16 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.safeExit = safeExit;
4
- /**
5
- * Force-exit the process to avoid hanging on open connections.
6
- *
7
- * On Windows, briefly yield first so undici can finish closing pending sockets;
8
- * otherwise the process aborts with a libuv UV_HANDLE_CLOSING assertion in
9
- * `src\win\async.c`. See https://github.com/nodejs/node/issues/56645.
10
- */
11
- async function safeExit(code) {
12
- if (process.platform === "win32") {
13
- await new Promise((resolve) => setTimeout(resolve, 100));
14
- }
15
- process.exit(code);
16
- }
@@ -1,12 +0,0 @@
1
- /** Discriminates the success vs. error variant of the callback page. */
2
- export type HTMLInput = {
3
- kind: "success";
4
- } | {
5
- kind: "error";
6
- reason: string;
7
- description?: string;
8
- };
9
- /** Content-Security-Policy header value sent with every callback page. */
10
- export declare const CSP_HEADER: string;
11
- /** Renders the loopback callback page shown in the user's browser. */
12
- export declare function renderHTML(input: HTMLInput): string;
@@ -1,2 +0,0 @@
1
- /** Wraps `fetch` with bounded retries on transient connection errors. */
2
- export declare function fetchWithRetry(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
@@ -1,50 +0,0 @@
1
- "use strict";
2
- // undici's `RetryAgent` cannot retry POSTs through `fetch()` — its retry
3
- // handler aborts once the fetch ReadableStream body is consumed. Re-invoking
4
- // `fetch()` per attempt with a string body sidesteps that. POST replay is
5
- // safe for our OAuth endpoints by spec: single-use codes (RFC 6749 §4.1.2),
6
- // refresh requests that never reached the server (§6), no-op revocation
7
- // (RFC 7009 §2.2).
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.fetchWithRetry = fetchWithRetry;
10
- const promises_1 = require("node:timers/promises");
11
- const MAX_RETRIES = 3;
12
- const CONNECTION_ERROR_CODES = new Set([
13
- "ECONNRESET",
14
- "ECONNREFUSED",
15
- "ENOTFOUND",
16
- "ENETDOWN",
17
- "ENETUNREACH",
18
- "EHOSTDOWN",
19
- "UND_ERR_SOCKET",
20
- ]);
21
- function isConnectionError(err) {
22
- const seen = new Set();
23
- let current = err;
24
- while (current && typeof current === "object" && !seen.has(current)) {
25
- seen.add(current);
26
- const e = current;
27
- if (typeof e.code === "string" && CONNECTION_ERROR_CODES.has(e.code)) {
28
- return true;
29
- }
30
- current = e.cause;
31
- }
32
- return false;
33
- }
34
- /** Wraps `fetch` with bounded retries on transient connection errors. */
35
- async function fetchWithRetry(input, init) {
36
- for (let attempt = 0;; attempt++) {
37
- try {
38
- return await fetch(input, init);
39
- }
40
- catch (err) {
41
- if (attempt >= MAX_RETRIES || !isConnectionError(err)) {
42
- throw err;
43
- }
44
- // Exponential backoff: 500ms, 1s, 2s, capped at 30s.
45
- // `sleep` honors `init.signal` so an aborted request interrupts the wait.
46
- const delay = Math.min(500 * Math.pow(2, attempt), 30_000);
47
- await (0, promises_1.setTimeout)(delay, undefined, { signal: init?.signal ?? undefined });
48
- }
49
- }
50
- }