@deque/axe-auth 1.1.0-next.b1986c00 → 1.1.0-next.b7ba541c

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 (41) hide show
  1. package/dist/cli/commonArgs.d.ts +11 -58
  2. package/dist/cli/commonArgs.js +8 -35
  3. package/dist/cli/confirm.js +0 -3
  4. package/dist/cli/errors.d.ts +2 -9
  5. package/dist/cli/errors.js +2 -9
  6. package/dist/cli/safeExit.d.ts +8 -0
  7. package/dist/cli/safeExit.js +16 -0
  8. package/dist/cli/types.d.ts +10 -50
  9. package/dist/commands/login.d.ts +1 -4
  10. package/dist/commands/login.js +5 -14
  11. package/dist/commands/logout.js +0 -2
  12. package/dist/commands/token.js +0 -4
  13. package/dist/index.js +8 -14
  14. package/dist/oauth/authorizationURL.d.ts +2 -7
  15. package/dist/oauth/authorizationURL.js +3 -7
  16. package/dist/oauth/authorize.d.ts +13 -51
  17. package/dist/oauth/authorize.js +8 -10
  18. package/dist/oauth/callbackServer.d.ts +20 -9
  19. package/dist/oauth/callbackServer.js +33 -27
  20. package/dist/oauth/discoverOIDC.d.ts +10 -27
  21. package/dist/oauth/discoverOIDC.js +17 -46
  22. package/dist/oauth/discoverSSOConfig.d.ts +2 -12
  23. package/dist/oauth/getValidAccessToken.d.ts +9 -44
  24. package/dist/oauth/getValidAccessToken.js +7 -16
  25. package/dist/oauth/openBrowser.js +1 -1
  26. package/dist/oauth/refreshTokens.js +3 -5
  27. package/dist/oauth/renderHTML.d.ts +12 -0
  28. package/dist/oauth/{renderHtml.js → renderHTML.js} +17 -11
  29. package/dist/oauth/retry.d.ts +2 -0
  30. package/dist/oauth/retry.js +50 -0
  31. package/dist/oauth/revokeToken.js +3 -2
  32. package/dist/oauth/tokenExchange.d.ts +1 -1
  33. package/dist/oauth/tokenExchange.js +4 -3
  34. package/dist/oauth/tokenResponse.d.ts +6 -38
  35. package/dist/oauth/tokenResponse.js +7 -27
  36. package/dist/oauth/tokenStore.d.ts +11 -23
  37. package/dist/oauth/tokenStore.js +32 -72
  38. package/docs/callback-page.md +2 -2
  39. package/docs/callback-server.md +1 -1
  40. package/package.json +13 -5
  41. package/dist/oauth/renderHtml.d.ts +0 -9
@@ -0,0 +1,50 @@
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
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.revokeRefreshToken = revokeRefreshToken;
4
+ const retry_1 = require("./retry");
4
5
  const userAgent_1 = require("../userAgent");
5
6
  /**
6
7
  * Revokes a refresh token via RFC 7009. Servers SHOULD return 200
@@ -23,10 +24,10 @@ async function revokeRefreshToken(options) {
23
24
  token: options.refreshToken,
24
25
  token_type_hint: "refresh_token",
25
26
  client_id: options.clientId,
26
- });
27
+ }).toString();
27
28
  let response;
28
29
  try {
29
- response = await fetch(options.revocationEndpoint, {
30
+ response = await (0, retry_1.fetchWithRetry)(options.revocationEndpoint, {
30
31
  method: "POST",
31
32
  headers: {
32
33
  "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,6 +2,7 @@
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");
5
6
  const tokenResponse_1 = require("./tokenResponse");
6
7
  const userAgent_1 = require("../userAgent");
7
8
  /**
@@ -18,12 +19,12 @@ async function exchangeCodeForTokens(options) {
18
19
  client_id: options.clientId,
19
20
  code: options.code,
20
21
  code_verifier: options.codeVerifier,
21
- redirect_uri: options.redirectUri,
22
- });
22
+ redirect_uri: options.redirectURI,
23
+ }).toString();
23
24
  const issuedAt = now();
24
25
  let response;
25
26
  try {
26
- response = await fetch(options.tokenEndpoint, {
27
+ response = await (0, retry_1.fetchWithRetry)(options.tokenEndpoint, {
27
28
  method: "POST",
28
29
  headers: {
29
30
  "Content-Type": "application/x-www-form-urlencoded",
@@ -1,18 +1,4 @@
1
- /**
2
- * Tokens returned by a successful token-endpoint call (authorization
3
- * code exchange, refresh-token grant, etc.).
4
- *
5
- * `refreshToken` is optional because not all flows return one. On
6
- * authorization-code exchange it's absent if the caller did not
7
- * request `offline_access` (or the provider equivalent); on refresh
8
- * some providers rotate tokens (return a new one) while others don't
9
- * (the caller should keep the existing refresh token).
10
- *
11
- * `grantedScope` reflects the authorization server's `scope` response
12
- * field when present. RFC 6749 §5.1 says `scope` is required in the
13
- * response when the granted set differs from the requested set; many
14
- * servers send it unconditionally.
15
- */
1
+ /** Tokens returned by a successful token-endpoint call. */
16
2
  export interface TokenSet {
17
3
  /** Access token for authenticated API calls. */
18
4
  accessToken: string;
@@ -24,31 +10,13 @@ export interface TokenSet {
24
10
  grantedScope?: string;
25
11
  }
26
12
  /**
27
- * Reads a non-2xx response body and throws
28
- * `OAuthFlowError("TOKEN_EXCHANGE_FAILED", …)` with the OAuth
29
- * `error` / `error_description` surfaced in both message and details
30
- * when present. Shared by both the authorization-code exchange and
31
- * refresh-token paths since the error contract is identical.
32
- *
33
- * @param context Short human-readable description of which call
34
- * failed ("Token exchange", "Token refresh", etc.). Appears in the
35
- * error message.
13
+ * Reads a non-2xx response body and throws `TOKEN_EXCHANGE_FAILED`
14
+ * with the OAuth `error` / `error_description` surfaced when present.
36
15
  */
37
16
  export declare function throwTokenEndpointError(response: Response, context: string): Promise<never>;
38
17
  /**
39
- * Parses a 2xx response body from an RFC 6749 §5.1 token endpoint
40
- * (authorization-code exchange, refresh-token grant, etc.) into a
41
- * `TokenSet`. Validates the required shape (`access_token`,
42
- * `expires_in`, Bearer `token_type`) and converts the relative
43
- * `expires_in` into an absolute `expiresAt` using `issuedAt`.
44
- *
45
- * @param response The HTTP response (must be 2xx; caller handles
46
- * error statuses via `throwTokenEndpointError`).
47
- * @param issuedAt The timestamp captured just before the network
48
- * call. Slightly conservative — the token actually expires
49
- * `expires_in` seconds from when the server issued it, so the
50
- * effective usable window is `expires_in - RTT`, which errs toward
51
- * "expires sooner" rather than "expires later."
52
- * @param endpointURL URL used for error messages.
18
+ * Parses a 2xx response from an RFC 6749 §5.1 token endpoint into a
19
+ * `TokenSet`. `issuedAt` is the timestamp captured just before the
20
+ * network call; the resulting `expiresAt` is conservative by ~RTT.
53
21
  */
54
22
  export declare function parseTokenResponse(response: Response, issuedAt: number, endpointURL: string): Promise<TokenSet>;
@@ -4,10 +4,8 @@ exports.throwTokenEndpointError = throwTokenEndpointError;
4
4
  exports.parseTokenResponse = parseTokenResponse;
5
5
  const errors_1 = require("./errors");
6
6
  const predicates_1 = require("./predicates");
7
- // RFC 6749 §5.1 describes `expires_in` as "the lifetime in seconds"
8
- // without pinning the JSON type, and some providers historically send
9
- // numeric strings. Accept both; reject anything non-positive or
10
- // non-finite.
7
+ // RFC 6749 §5.1 doesn't pin the JSON type and some providers send
8
+ // numeric strings; accept both, reject non-positive or non-finite.
11
9
  function parseExpiresIn(v) {
12
10
  if (typeof v === "number" && Number.isFinite(v) && v > 0)
13
11
  return v;
@@ -37,15 +35,8 @@ function parseErrorBody(body) {
37
35
  };
38
36
  }
39
37
  /**
40
- * Reads a non-2xx response body and throws
41
- * `OAuthFlowError("TOKEN_EXCHANGE_FAILED", …)` with the OAuth
42
- * `error` / `error_description` surfaced in both message and details
43
- * when present. Shared by both the authorization-code exchange and
44
- * refresh-token paths since the error contract is identical.
45
- *
46
- * @param context Short human-readable description of which call
47
- * failed ("Token exchange", "Token refresh", etc.). Appears in the
48
- * error message.
38
+ * Reads a non-2xx response body and throws `TOKEN_EXCHANGE_FAILED`
39
+ * with the OAuth `error` / `error_description` surfaced when present.
49
40
  */
50
41
  async function throwTokenEndpointError(response, context) {
51
42
  const body = await response.text().catch(() => "");
@@ -63,20 +54,9 @@ async function throwTokenEndpointError(response, context) {
63
54
  throw new errors_1.OAuthFlowError("TOKEN_EXCHANGE_FAILED", `${context} failed with HTTP ${response.status}${suffix}`, Object.keys(details).length > 0 ? { details } : undefined);
64
55
  }
65
56
  /**
66
- * Parses a 2xx response body from an RFC 6749 §5.1 token endpoint
67
- * (authorization-code exchange, refresh-token grant, etc.) into a
68
- * `TokenSet`. Validates the required shape (`access_token`,
69
- * `expires_in`, Bearer `token_type`) and converts the relative
70
- * `expires_in` into an absolute `expiresAt` using `issuedAt`.
71
- *
72
- * @param response The HTTP response (must be 2xx; caller handles
73
- * error statuses via `throwTokenEndpointError`).
74
- * @param issuedAt The timestamp captured just before the network
75
- * call. Slightly conservative — the token actually expires
76
- * `expires_in` seconds from when the server issued it, so the
77
- * effective usable window is `expires_in - RTT`, which errs toward
78
- * "expires sooner" rather than "expires later."
79
- * @param endpointURL URL used for error messages.
57
+ * Parses a 2xx response from an RFC 6749 §5.1 token endpoint into a
58
+ * `TokenSet`. `issuedAt` is the timestamp captured just before the
59
+ * network call; the resulting `expiresAt` is conservative by ~RTT.
80
60
  */
81
61
  async function parseTokenResponse(response, issuedAt, endpointURL) {
82
62
  let parsed;
@@ -2,8 +2,11 @@ import { type KeyringEntryFactory } from "./keyringBinding";
2
2
  import type { TokenSet } from "./tokenResponse";
3
3
  /**
4
4
  * Whether `KeyringTokenStore` should split the stored blob across
5
- * multiple keychain entries on this platform. Windows-only because of
6
- * Credential Manager's 2560 UTF-16 character per-entry cap. Exported
5
+ * multiple keychain entries on this platform. Windows-only: Credential
6
+ * Manager has a 2560-byte per-entry cap that large OAuth tokens
7
+ * routinely exceed. macOS Keychain and Linux libsecret have no
8
+ * comparable limit, and on macOS each entry is independently lockable
9
+ * (chunking there would multiply per-entry ACL prompts). Exported
7
10
  * (parameterized for tests) so the chunking path can be exercised
8
11
  * deterministically.
9
12
  */
@@ -109,28 +112,13 @@ export type BlobChainResult = {
109
112
  */
110
113
  export declare function parseAndMigrateBlob(raw: string | null, expectedVersion?: number, migrators?: ReadonlyMap<number, (old: unknown) => unknown | null>): BlobChainResult;
111
114
  /**
112
- * Builds the user-facing keychain error message. Platform is a
113
- * parameter (defaulting to `process.platform`) so tests can drive each
114
- * branch without mocking the runtime; mirrors the pattern in
115
+ * Builds the user-facing keychain error message: the underlying
116
+ * cause's text plus a per-platform hint. Platform is a parameter
117
+ * (defaulting to `process.platform`) so tests can drive each branch
118
+ * without mocking the runtime; mirrors the pattern in
115
119
  * `platformKeyringHint`.
116
- *
117
- * The Windows-specific size-limit message is only used when the
118
- * underlying error matches the binding's "longer than the platform
119
- * limit" wording AND the runtime is win32 — that combination is the
120
- * only way the size cap actually manifests in practice. On other
121
- * platforms (or for any other binding error) we fall back to the
122
- * generic per-platform hint.
123
120
  */
124
121
  export declare function keyringErrorMessage(op: string, cause: unknown, platform?: NodeJS.Platform): string;
125
- /**
126
- * Detects the `@napi-rs/keyring` error string for "value too large".
127
- * In practice only Windows Credential Manager triggers this — its
128
- * stored values are capped at 2560 UTF-16 chars; macOS Keychain and
129
- * Linux libsecret have no comparable limit. Exported (but not
130
- * re-exported from the package index) so tests can exercise the
131
- * detector independently of the wrap path.
132
- */
133
- export declare function isKeyringSizeError(cause: unknown): boolean;
134
122
  /**
135
123
  * Returns a per-platform hint appended to keychain error messages so
136
124
  * users see actionable guidance for their OS instead of generic or
@@ -145,8 +133,8 @@ export declare function platformKeyringHint(platform?: NodeJS.Platform): string;
145
133
  * Secret Service). On macOS and Linux the blob lives in a single entry
146
134
  * keyed by the fixed `credentials` account name. On Windows the blob
147
135
  * is split across `credentials.0`, `credentials.1`, … entries to fit
148
- * under Credential Manager's 2560 UTF-16 character per-entry cap; see
149
- * `shouldChunkForKeyring`.
136
+ * under Credential Manager's 2560-byte (1280 UTF-16 char) per-entry
137
+ * cap; see `shouldChunkForKeyring`.
150
138
  *
151
139
  * The blob carries its own issuer/client coordinates so verbs can
152
140
  * recover full config without per-issuer keying.
@@ -4,49 +4,41 @@ exports.KeyringTokenStore = exports.STORED_BLOB_VERSION = void 0;
4
4
  exports.shouldChunkForKeyring = shouldChunkForKeyring;
5
5
  exports.parseAndMigrateBlob = parseAndMigrateBlob;
6
6
  exports.keyringErrorMessage = keyringErrorMessage;
7
- exports.isKeyringSizeError = isKeyringSizeError;
8
7
  exports.platformKeyringHint = platformKeyringHint;
9
8
  exports.chunkBlobForKeyring = chunkBlobForKeyring;
10
9
  const errors_1 = require("./errors");
11
10
  const keyringBinding_1 = require("./keyringBinding");
12
- // On macOS: Keychain generic password item with the service name below.
13
- // On Windows: Credential Manager entry. On Linux: Secret Service / libsecret.
14
- // Exposed as a human-readable string because these all surface the service
15
- // name in OS UIs (Keychain Access, credmgr.exe, seahorse).
16
11
  const SERVICE_NAME = "axe-auth";
17
- // Single keychain entry per machine on macOS / Linux. (Windows splits
18
- // across `credentials.0`, `credentials.1`, see `CHUNK_LIMIT`
19
- // below.) The blob it holds is fully self-describing (issuerURL,
20
- // clientId, allowInsecureIssuer, plus the tokens), so verbs that
21
- // don't pass `--server` / `--realm` / `--client-id` can resolve their
22
- // config from the entry.
23
- //
24
- // Account name is human-readable so users investigating the entry in
25
- // macOS Keychain Access (or `secret-tool` on Linux, credmgr on
26
- // Windows) can tell what it is. Not versioned: the schema version
27
- // lives inside the blob and migrators handle the upgrade path. Note:
28
- // Windows entries hold base64-encoded JSON rather than the raw JSON
29
- // macOS / Linux store, so a Windows user inspecting their Credential
30
- // Manager will see opaque base64; that's a side effect of chunking.
12
+ /**
13
+ * Keychain account identifier. On macOS/Linux the entire blob lives at
14
+ * this single account. On Windows the blob is base64-encoded and split
15
+ * across `credentials.0`, `credentials.1`, entries (see `CHUNK_LIMIT`),
16
+ * so a Windows dev inspecting Credential Manager will see opaque base64.
17
+ */
31
18
  const ACCOUNT_NAME = "credentials";
32
- // Windows Credential Manager caps stored values at 2560 UTF-16 code
33
- // units, which large OAuth access-token JWTs (many groups/roles
34
- // claims) routinely exceed. On Windows we work around this by
35
- // splitting the JSON blob across multiple entries with account names
36
- // `credentials.0`, `credentials.1`, . `CHUNK_LIMIT` leaves margin
37
- // under the platform cap; `MAX_CHUNKS` is a safety bound — we should
38
- // never get close in practice, even with maximally-claimed tokens.
39
- //
40
- // macOS Keychain and Linux libsecret have no comparable limit, so
41
- // chunking there would just multiply per-entry ACL prompts (each
42
- // keychain entry is independently lockable on macOS) for no gain.
43
- // Chunking is therefore Windows-only, gated by `shouldChunkForKeyring`.
44
- const CHUNK_LIMIT = 2500;
19
+ /**
20
+ * Max JS string length per chunk. The limit applies to the full chunk
21
+ * including chunk 0's `<N>\n` count header, so chunk 0's data slice is
22
+ * `CHUNK_LIMIT - headerLen`. Windows Credential Manager's per-entry
23
+ * cap is `CRED_MAX_CREDENTIAL_BLOB_SIZE = 2560` bytes, and the
24
+ * `@napi-rs/keyring` Windows backend stores strings as UTF-16 (2 bytes
25
+ * per char), so 1250 chars = 2500 bytes stays safely under the cap.
26
+ */
27
+ const CHUNK_LIMIT = 1250;
28
+ /**
29
+ * Cap on chunks per stored blob. A request that would exceed this
30
+ * raises `TOKEN_TOO_LARGE` so an IDP issuing tokens with extraordinary
31
+ * claim counts fails with a clear error instead of silently consuming
32
+ * dozens of keychain entries.
33
+ */
45
34
  const MAX_CHUNKS = 32;
46
35
  /**
47
36
  * Whether `KeyringTokenStore` should split the stored blob across
48
- * multiple keychain entries on this platform. Windows-only because of
49
- * Credential Manager's 2560 UTF-16 character per-entry cap. Exported
37
+ * multiple keychain entries on this platform. Windows-only: Credential
38
+ * Manager has a 2560-byte per-entry cap that large OAuth tokens
39
+ * routinely exceed. macOS Keychain and Linux libsecret have no
40
+ * comparable limit, and on macOS each entry is independently lockable
41
+ * (chunking there would multiply per-entry ACL prompts). Exported
50
42
  * (parameterized for tests) so the chunking path can be exercised
51
43
  * deterministically.
52
44
  */
@@ -160,10 +152,6 @@ function parseAndMigrateBlob(raw, expectedVersion = exports.STORED_BLOB_VERSION,
160
152
  const storedVersion = getStoredVersion(parsed);
161
153
  if (storedVersion === null)
162
154
  return { ok: false, reason: "corrupt" };
163
- // Walk the migrator chain until we reach the expected version. A
164
- // missing or null-returning migrator means the old blob cannot be
165
- // upgraded; surface that so callers can prompt re-auth with a
166
- // clear signal instead of silently returning `empty`.
167
155
  let current = parsed;
168
156
  let currentVersion = storedVersion;
169
157
  while (currentVersion !== expectedVersion) {
@@ -177,8 +165,6 @@ function parseAndMigrateBlob(raw, expectedVersion = exports.STORED_BLOB_VERSION,
177
165
  }
178
166
  const nextVersion = getStoredVersion(next);
179
167
  if (nextVersion === null || nextVersion <= currentVersion) {
180
- // Migrator output is malformed or didn't advance. Treat the
181
- // stored blob as un-migratable rather than loop forever.
182
168
  return { ok: false, reason: "version-mismatch", storedVersion };
183
169
  }
184
170
  current = next;
@@ -201,38 +187,16 @@ function wrapKeyringError(op, cause) {
201
187
  });
202
188
  }
203
189
  /**
204
- * Builds the user-facing keychain error message. Platform is a
205
- * parameter (defaulting to `process.platform`) so tests can drive each
206
- * branch without mocking the runtime; mirrors the pattern in
190
+ * Builds the user-facing keychain error message: the underlying
191
+ * cause's text plus a per-platform hint. Platform is a parameter
192
+ * (defaulting to `process.platform`) so tests can drive each branch
193
+ * without mocking the runtime; mirrors the pattern in
207
194
  * `platformKeyringHint`.
208
- *
209
- * The Windows-specific size-limit message is only used when the
210
- * underlying error matches the binding's "longer than the platform
211
- * limit" wording AND the runtime is win32 — that combination is the
212
- * only way the size cap actually manifests in practice. On other
213
- * platforms (or for any other binding error) we fall back to the
214
- * generic per-platform hint.
215
195
  */
216
196
  function keyringErrorMessage(op, cause, platform = process.platform) {
217
- if (platform === "win32" && isKeyringSizeError(cause)) {
218
- return `System keychain ${op} failed: Windows Credential Manager limits stored values to 2560 UTF-16 characters. Large OAuth access-token JWTs (many groups/roles claims) commonly exceed this.`;
219
- }
220
197
  const causeMessage = cause instanceof Error ? cause.message : String(cause);
221
198
  return `System keychain ${op} failed: ${causeMessage}. ${platformKeyringHint(platform)}`;
222
199
  }
223
- /**
224
- * Detects the `@napi-rs/keyring` error string for "value too large".
225
- * In practice only Windows Credential Manager triggers this — its
226
- * stored values are capped at 2560 UTF-16 chars; macOS Keychain and
227
- * Linux libsecret have no comparable limit. Exported (but not
228
- * re-exported from the package index) so tests can exercise the
229
- * detector independently of the wrap path.
230
- */
231
- function isKeyringSizeError(cause) {
232
- if (!(cause instanceof Error))
233
- return false;
234
- return /longer than the platform limit/.test(cause.message);
235
- }
236
200
  /**
237
201
  * Returns a per-platform hint appended to keychain error messages so
238
202
  * users see actionable guidance for their OS instead of generic or
@@ -280,8 +244,8 @@ function parseChunkHeader(first) {
280
244
  * Secret Service). On macOS and Linux the blob lives in a single entry
281
245
  * keyed by the fixed `credentials` account name. On Windows the blob
282
246
  * is split across `credentials.0`, `credentials.1`, … entries to fit
283
- * under Credential Manager's 2560 UTF-16 character per-entry cap; see
284
- * `shouldChunkForKeyring`.
247
+ * under Credential Manager's 2560-byte (1280 UTF-16 char) per-entry
248
+ * cap; see `shouldChunkForKeyring`.
285
249
  *
286
250
  * The blob carries its own issuer/client coordinates so verbs can
287
251
  * recover full config without per-issuer keying.
@@ -424,10 +388,6 @@ class KeyringTokenStore {
424
388
  * could trip it.
425
389
  */
426
390
  #saveChunked(parts) {
427
- // Read previous N before any writes so the cleanup sweep is
428
- // bounded. If the previous chunk 0 is missing or its header is
429
- // unparseable we have no upper bound, so fall back to the full
430
- // safety range as a one-time defensive recovery.
431
391
  const previousN = this.#previousChunkN();
432
392
  for (let i = parts.length - 1; i >= 1; i--) {
433
393
  this.#entry(`${ACCOUNT_NAME}.${i}`).setPassword(parts[i]);
@@ -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,8 +1,15 @@
1
1
  {
2
2
  "name": "@deque/axe-auth",
3
- "version": "1.1.0-next.b1986c00",
3
+ "version": "1.1.0-next.b7ba541c",
4
4
  "description": "CLI authentication utility for Deque services",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/dequelabs/axe-mcp-server-public.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/dequelabs/axe-mcp-server-public/issues"
12
+ },
6
13
  "type": "commonjs",
7
14
  "main": "dist/index.js",
8
15
  "types": "dist/index.d.ts",
@@ -20,7 +27,7 @@
20
27
  "registry": "https://registry.npmjs.org/"
21
28
  },
22
29
  "engines": {
23
- "node": ">=22.13.0"
30
+ "node": ">=24.11.0"
24
31
  },
25
32
  "dependencies": {
26
33
  "@napi-rs/keyring": "^1.2.0",
@@ -30,15 +37,16 @@
30
37
  },
31
38
  "devDependencies": {
32
39
  "@hono/node-server": "^1.19.14",
33
- "@types/node": "^22.13.10",
40
+ "@types/node": "^24.0.0",
34
41
  "c8": "^10.1.3",
35
42
  "hono": "^4.12.16",
36
43
  "tsx": "^4.20.6",
37
- "typescript": "^5.9.3"
44
+ "typescript": "^6.0.3"
38
45
  },
39
46
  "scripts": {
40
47
  "build": "tsc",
41
- "test": "tsx --test 'src/**/*.test.ts'",
48
+ "typecheck:scripts": "tsc -p tsconfig.scripts.json",
49
+ "test": "tsx --test \"src/**/*.test.ts\"",
42
50
  "coverage": "c8 pnpm test",
43
51
  "register-dev-client": "tsx scripts/registerDevClient.ts",
44
52
  "smoke-authorize": "tsx scripts/smokeAuthorize.ts",
@@ -1,9 +0,0 @@
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;