@deque/axe-auth 1.1.0-next.6ad261c8 → 1.1.0-next.907ffbd7

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.
@@ -1,7 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OAuthCallbackError = exports.startCallbackServer = void 0;
3
+ exports.discoverOIDC = exports.STORED_BLOB_VERSION = exports.KeyringTokenStore = exports.authorize = exports.OAuthFlowError = exports.OAuthCallbackError = exports.startCallbackServer = void 0;
4
4
  var callbackServer_1 = require("./callbackServer");
5
5
  Object.defineProperty(exports, "startCallbackServer", { enumerable: true, get: function () { return callbackServer_1.startCallbackServer; } });
6
6
  var errors_1 = require("./errors");
7
7
  Object.defineProperty(exports, "OAuthCallbackError", { enumerable: true, get: function () { return errors_1.OAuthCallbackError; } });
8
+ Object.defineProperty(exports, "OAuthFlowError", { enumerable: true, get: function () { return errors_1.OAuthFlowError; } });
9
+ var authorize_1 = require("./authorize");
10
+ Object.defineProperty(exports, "authorize", { enumerable: true, get: function () { return authorize_1.authorize; } });
11
+ var tokenStore_1 = require("./tokenStore");
12
+ Object.defineProperty(exports, "KeyringTokenStore", { enumerable: true, get: function () { return tokenStore_1.KeyringTokenStore; } });
13
+ Object.defineProperty(exports, "STORED_BLOB_VERSION", { enumerable: true, get: function () { return tokenStore_1.STORED_BLOB_VERSION; } });
14
+ var discoverOIDC_1 = require("./discoverOIDC");
15
+ Object.defineProperty(exports, "discoverOIDC", { enumerable: true, get: function () { return discoverOIDC_1.discoverOIDC; } });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Canonicalizes an OIDC issuer URL for equivalence comparison. Two URLs
3
+ * that normalize to the same string refer to the same issuer, which is
4
+ * what `discoverOIDC` uses to build discovery URLs and what
5
+ * `KeyringTokenStore` uses to key keychain entries.
6
+ *
7
+ * Rules (per RFC 3986 §6.2 URI comparison):
8
+ * - Trailing slashes stripped from the path.
9
+ * - Scheme and authority (host + optional port) lowercased — both are
10
+ * case-insensitive per the RFC.
11
+ * - Default ports (80 for http, 443 for https) collapsed via `URL.host`.
12
+ * - Path preserved case-sensitively.
13
+ * - Query string and fragment dropped: OIDC issuers are defined by
14
+ * scheme + authority + path, and carrying them through would break
15
+ * downstream path concatenation (e.g. appending
16
+ * `/.well-known/openid-configuration`).
17
+ *
18
+ * If the input is not a parseable URL, the function trims trailing
19
+ * slashes and returns — discovery will surface a clearer error than
20
+ * this function could.
21
+ */
22
+ export declare function normalizeIssuerURL(url: string): string;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeIssuerURL = normalizeIssuerURL;
4
+ /**
5
+ * Canonicalizes an OIDC issuer URL for equivalence comparison. Two URLs
6
+ * that normalize to the same string refer to the same issuer, which is
7
+ * what `discoverOIDC` uses to build discovery URLs and what
8
+ * `KeyringTokenStore` uses to key keychain entries.
9
+ *
10
+ * Rules (per RFC 3986 §6.2 URI comparison):
11
+ * - Trailing slashes stripped from the path.
12
+ * - Scheme and authority (host + optional port) lowercased — both are
13
+ * case-insensitive per the RFC.
14
+ * - Default ports (80 for http, 443 for https) collapsed via `URL.host`.
15
+ * - Path preserved case-sensitively.
16
+ * - Query string and fragment dropped: OIDC issuers are defined by
17
+ * scheme + authority + path, and carrying them through would break
18
+ * downstream path concatenation (e.g. appending
19
+ * `/.well-known/openid-configuration`).
20
+ *
21
+ * If the input is not a parseable URL, the function trims trailing
22
+ * slashes and returns — discovery will surface a clearer error than
23
+ * this function could.
24
+ */
25
+ function normalizeIssuerURL(url) {
26
+ let parsed;
27
+ try {
28
+ parsed = new URL(url);
29
+ }
30
+ catch {
31
+ return url.replace(/\/+$/, "");
32
+ }
33
+ // `URL.protocol` is already lowercased by the URL parser.
34
+ // `URL.host` retains input casing, so lowercase it explicitly.
35
+ const host = parsed.host.toLowerCase();
36
+ const pathname = parsed.pathname.replace(/\/+$/, "");
37
+ return `${parsed.protocol}//${host}${pathname}`;
38
+ }
@@ -0,0 +1,19 @@
1
+ import type { ChildProcess, SpawnOptions } from "node:child_process";
2
+ /** Injection seam for `child_process.spawn`. Used by tests. */
3
+ export type SpawnFn = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
4
+ /** Options for `openBrowser`. */
5
+ export interface OpenBrowserOptions {
6
+ /** Override for `process.platform`. Used by tests. */
7
+ platform?: NodeJS.Platform;
8
+ /** Override for `child_process.spawn`. Used by tests. */
9
+ spawnFn?: SpawnFn;
10
+ }
11
+ /**
12
+ * Launches the system browser at `url` in a detached child process.
13
+ * Returns synchronously once the child is spawned — completion of the
14
+ * browser load is intentionally not awaited.
15
+ *
16
+ * @param url Absolute URL to open.
17
+ * @param options Platform/spawn overrides; only exposed for tests.
18
+ */
19
+ export declare function openBrowser(url: string, options?: OpenBrowserOptions): void;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openBrowser = openBrowser;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const errors_1 = require("./errors");
6
+ // On Windows `start` is a cmd.exe builtin, not a standalone binary.
7
+ // The empty `""` pair is a positional placeholder for the window
8
+ // title — without it `start` treats the URL as the title.
9
+ //
10
+ // The escape class covers:
11
+ // - `& | ^ < >` — cmd metacharacters that would otherwise split the
12
+ // command line.
13
+ // - `"` — would prematurely terminate the argument and break
14
+ // `start`'s quoting.
15
+ // - `%` — triggers cmd.exe environment-variable expansion (e.g.
16
+ // `%USERNAME%`), which could leak or alter the URL.
17
+ // - `\r \n` — embedded newlines let a crafted URL inject additional
18
+ // commands onto cmd.exe's line.
19
+ //
20
+ // URLs normally percent-encode most of these (so this is defense in
21
+ // depth), but we do not fully trust the authorization endpoint that
22
+ // came back from discovery.
23
+ function windowsCommand(url) {
24
+ return {
25
+ command: "cmd.exe",
26
+ args: ["/c", "start", '""', url.replace(/[&|^<>"%\r\n]/g, (c) => `^${c}`)],
27
+ };
28
+ }
29
+ function browserCommand(platform, url) {
30
+ switch (platform) {
31
+ case "darwin":
32
+ return { command: "open", args: [url] };
33
+ case "win32":
34
+ return windowsCommand(url);
35
+ default:
36
+ // linux / freebsd / openbsd — xdg-open is part of xdg-utils which
37
+ // is near-universal on desktop Linux. Environments without it
38
+ // (headless servers, containers) will report the missing binary
39
+ // via an asynchronous child-process `error` event, which this
40
+ // module deliberately swallows (see `child.once("error", ...)`
41
+ // below); `BROWSER_LAUNCH_FAILED` is only raised for synchronous
42
+ // `spawn()` throws. The caller's fallback is the URL already
43
+ // surfaced via `onAuthorizationUrl` so the user can finish the
44
+ // flow manually.
45
+ return { command: "xdg-open", args: [url] };
46
+ }
47
+ }
48
+ /**
49
+ * Launches the system browser at `url` in a detached child process.
50
+ * Returns synchronously once the child is spawned — completion of the
51
+ * browser load is intentionally not awaited.
52
+ *
53
+ * @param url Absolute URL to open.
54
+ * @param options Platform/spawn overrides; only exposed for tests.
55
+ */
56
+ function openBrowser(url, options = {}) {
57
+ const platform = options.platform ?? process.platform;
58
+ const spawnFn = options.spawnFn ?? node_child_process_1.spawn;
59
+ const { command, args } = browserCommand(platform, url);
60
+ let child;
61
+ try {
62
+ child = spawnFn(command, args, {
63
+ detached: true,
64
+ stdio: "ignore",
65
+ });
66
+ }
67
+ catch (cause) {
68
+ throw new errors_1.OAuthFlowError("BROWSER_LAUNCH_FAILED", `Failed to launch the system browser (${command}). Open this URL manually:\n${url}`, { cause });
69
+ }
70
+ // `spawn` itself can succeed (the parent fork was fine) and then emit
71
+ // `error` asynchronously if the binary isn't on PATH. We can't surface
72
+ // that synchronously, but attaching a handler prevents the default
73
+ // "unhandled error" crash. Callers get a benign no-op if the browser
74
+ // never opens; the authorize() orchestrator prints the URL alongside
75
+ // the launch so the user has a fallback.
76
+ child.once("error", () => { });
77
+ child.unref();
78
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Generates a cryptographically random PKCE `code_verifier` per RFC 7636
3
+ * §4.1. 43 base64url characters, 256 bits of entropy.
4
+ */
5
+ export declare function generateCodeVerifier(): string;
6
+ /**
7
+ * Derives the PKCE S256 `code_challenge` for the given verifier per
8
+ * RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(verifier)))`.
9
+ *
10
+ * @param verifier The PKCE verifier produced by `generateCodeVerifier`.
11
+ */
12
+ export declare function deriveCodeChallenge(verifier: string): string;
13
+ /**
14
+ * Generates a cryptographically random OAuth `state` value for CSRF
15
+ * protection. 22 base64url characters, 128 bits of entropy.
16
+ */
17
+ export declare function generateState(): string;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateCodeVerifier = generateCodeVerifier;
4
+ exports.deriveCodeChallenge = deriveCodeChallenge;
5
+ exports.generateState = generateState;
6
+ const node_crypto_1 = require("node:crypto");
7
+ // PKCE per RFC 7636. We only ever emit S256; plain is permitted by the RFC
8
+ // but explicitly disallowed by our authorization-server config so it can't
9
+ // silently fall back in the face of a buggy server.
10
+ /**
11
+ * Entropy for the PKCE `code_verifier`. 32 bytes yields 43 base64url chars
12
+ * (no padding), the minimum length RFC 7636 allows (43–128). 256 bits
13
+ * matches the S256 hash's security ceiling.
14
+ */
15
+ const VERIFIER_ENTROPY_BYTES = 32;
16
+ /**
17
+ * Entropy for the CSRF `state` parameter. 16 bytes yields 22 base64url
18
+ * chars — unguessable without bloating the authorization URL.
19
+ */
20
+ const STATE_ENTROPY_BYTES = 16;
21
+ /**
22
+ * Generates a cryptographically random PKCE `code_verifier` per RFC 7636
23
+ * §4.1. 43 base64url characters, 256 bits of entropy.
24
+ */
25
+ function generateCodeVerifier() {
26
+ return (0, node_crypto_1.randomBytes)(VERIFIER_ENTROPY_BYTES).toString("base64url");
27
+ }
28
+ /**
29
+ * Derives the PKCE S256 `code_challenge` for the given verifier per
30
+ * RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(verifier)))`.
31
+ *
32
+ * @param verifier The PKCE verifier produced by `generateCodeVerifier`.
33
+ */
34
+ function deriveCodeChallenge(verifier) {
35
+ return (0, node_crypto_1.createHash)("sha256").update(verifier, "ascii").digest("base64url");
36
+ }
37
+ /**
38
+ * Generates a cryptographically random OAuth `state` value for CSRF
39
+ * protection. 22 base64url characters, 128 bits of entropy.
40
+ */
41
+ function generateState() {
42
+ return (0, node_crypto_1.randomBytes)(STATE_ENTROPY_BYTES).toString("base64url");
43
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tokens returned by a successful authorization-code exchange.
3
+ *
4
+ * `refreshToken` is optional because not all flows return one —
5
+ * callers that did not request `offline_access` (or the provider
6
+ * equivalent) will receive only an access token. Refresh logic (issue
7
+ * #422) must handle this case.
8
+ *
9
+ * `grantedScope` reflects the authorization server's `scope` response
10
+ * field when present (RFC 6749 §5.1 says `scope` is required when the
11
+ * granted set differs from the requested set; optional otherwise).
12
+ * Callers comparing granted vs requested to surface diagnostics should
13
+ * read this field.
14
+ */
15
+ export interface TokenSet {
16
+ /** Access token for authenticated API calls. */
17
+ accessToken: string;
18
+ /** Long-lived token used to mint new access tokens without re-auth. Absent if the flow did not request it. */
19
+ refreshToken?: string;
20
+ /** Absolute timestamp (ms since epoch) when the access token expires. */
21
+ expiresAt: number;
22
+ /** Space-delimited scopes the server actually granted, if reported. */
23
+ grantedScope?: string;
24
+ }
25
+ /** Options for `exchangeCodeForTokens`. */
26
+ export interface ExchangeCodeForTokensOptions {
27
+ /** Token endpoint resolved from OIDC discovery. */
28
+ tokenEndpoint: string;
29
+ /** OAuth client identifier. */
30
+ clientId: string;
31
+ /** Authorization code received via the loopback callback. */
32
+ code: string;
33
+ /** PKCE verifier paired with the `code_challenge` sent at auth time. */
34
+ codeVerifier: string;
35
+ /** Redirect URI originally sent to the authorization endpoint. */
36
+ redirectUri: string;
37
+ /** Source of `now`. Injected for test determinism; defaults to `Date.now`. */
38
+ now?: () => number;
39
+ /** Aborts the underlying fetch when fired. */
40
+ signal?: AbortSignal;
41
+ }
42
+ /**
43
+ * Exchanges an authorization code for a `TokenSet` via the
44
+ * authorization server's token endpoint (RFC 6749 §4.1.3 + RFC 7636
45
+ * §4.5). Rejects with `OAuthFlowError("TOKEN_EXCHANGE_FAILED", ...)`
46
+ * for any failure mode, surfacing the OAuth `error` /
47
+ * `error_description` when available.
48
+ */
49
+ export declare function exchangeCodeForTokens(options: ExchangeCodeForTokensOptions): Promise<TokenSet>;
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exchangeCodeForTokens = exchangeCodeForTokens;
4
+ const errors_1 = require("./errors");
5
+ function isNonEmptyString(v) {
6
+ return typeof v === "string" && v.length > 0;
7
+ }
8
+ // RFC 6749 §5.1 describes `expires_in` as "the lifetime in seconds"
9
+ // without pinning the JSON type, and some providers historically send
10
+ // numeric strings. Accept both; reject anything non-positive or
11
+ // non-finite.
12
+ function parseExpiresIn(v) {
13
+ if (typeof v === "number" && Number.isFinite(v) && v > 0)
14
+ return v;
15
+ if (typeof v === "string") {
16
+ const n = Number(v);
17
+ if (Number.isFinite(n) && n > 0)
18
+ return n;
19
+ }
20
+ return null;
21
+ }
22
+ function parseErrorBody(body) {
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(body);
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ if (parsed === null || typeof parsed !== "object")
31
+ return {};
32
+ const raw = parsed;
33
+ return {
34
+ error: isNonEmptyString(raw.error) ? raw.error : undefined,
35
+ description: isNonEmptyString(raw.error_description)
36
+ ? raw.error_description
37
+ : undefined,
38
+ };
39
+ }
40
+ function throwFromErrorResponse(status, body) {
41
+ const { error, description } = parseErrorBody(body);
42
+ const suffix = error
43
+ ? description
44
+ ? `: ${error}: ${description}`
45
+ : `: ${error}`
46
+ : "";
47
+ const details = {};
48
+ if (error)
49
+ details.error = error;
50
+ if (description)
51
+ details.error_description = description;
52
+ throw new errors_1.OAuthFlowError("TOKEN_EXCHANGE_FAILED", `Token exchange failed with HTTP ${status}${suffix}`, Object.keys(details).length > 0 ? { details } : undefined);
53
+ }
54
+ /**
55
+ * Exchanges an authorization code for a `TokenSet` via the
56
+ * authorization server's token endpoint (RFC 6749 §4.1.3 + RFC 7636
57
+ * §4.5). Rejects with `OAuthFlowError("TOKEN_EXCHANGE_FAILED", ...)`
58
+ * for any failure mode, surfacing the OAuth `error` /
59
+ * `error_description` when available.
60
+ */
61
+ async function exchangeCodeForTokens(options) {
62
+ const now = options.now ?? Date.now;
63
+ const body = new URLSearchParams({
64
+ grant_type: "authorization_code",
65
+ client_id: options.clientId,
66
+ code: options.code,
67
+ code_verifier: options.codeVerifier,
68
+ redirect_uri: options.redirectUri,
69
+ });
70
+ // Capture `issuedAt` before the network call so we don't drift past
71
+ // expiry just because the network was slow. Slightly conservative —
72
+ // the token actually expires `expires_in` seconds from when the
73
+ // server issued it, so the effective usable window is `expires_in -
74
+ // RTT`, which errs toward "expires sooner" rather than "expires
75
+ // later." That's the safer direction for any consumer doing
76
+ // pre-expiry checks.
77
+ const issuedAt = now();
78
+ let response;
79
+ try {
80
+ response = await fetch(options.tokenEndpoint, {
81
+ method: "POST",
82
+ headers: {
83
+ "Content-Type": "application/x-www-form-urlencoded",
84
+ Accept: "application/json",
85
+ },
86
+ body,
87
+ signal: options.signal,
88
+ });
89
+ }
90
+ catch (cause) {
91
+ throw new errors_1.OAuthFlowError("TOKEN_EXCHANGE_FAILED", `Could not reach the token endpoint at ${options.tokenEndpoint}. Check your network connection.`, { cause });
92
+ }
93
+ if (!response.ok) {
94
+ const text = await response.text().catch(() => "");
95
+ throwFromErrorResponse(response.status, text);
96
+ }
97
+ let parsed;
98
+ try {
99
+ parsed = await response.json();
100
+ }
101
+ catch (cause) {
102
+ throw new errors_1.OAuthFlowError("TOKEN_EXCHANGE_FAILED", `Token endpoint at ${options.tokenEndpoint} returned a non-JSON response`, { cause });
103
+ }
104
+ if (parsed === null || typeof parsed !== "object") {
105
+ throw new errors_1.OAuthFlowError("TOKEN_EXCHANGE_FAILED", `Token endpoint at ${options.tokenEndpoint} returned a non-object response`);
106
+ }
107
+ const raw = parsed;
108
+ if (!isNonEmptyString(raw.access_token)) {
109
+ throw new errors_1.OAuthFlowError("TOKEN_EXCHANGE_FAILED", `Token response missing 'access_token'`);
110
+ }
111
+ const expiresIn = parseExpiresIn(raw.expires_in);
112
+ if (expiresIn === null) {
113
+ throw new errors_1.OAuthFlowError("TOKEN_EXCHANGE_FAILED", `Token response missing or has invalid 'expires_in'`);
114
+ }
115
+ // RFC 6749 §5.1: token_type is REQUIRED. We only speak Bearer;
116
+ // DPoP / MAC / other proof-of-possession types need request-side
117
+ // support we don't implement, and silently treating them as Bearer
118
+ // would send tokens in the wrong header with unclear semantics.
119
+ if (!isNonEmptyString(raw.token_type)) {
120
+ throw new errors_1.OAuthFlowError("TOKEN_EXCHANGE_FAILED", `Token response missing required 'token_type'`);
121
+ }
122
+ if (raw.token_type.toLowerCase() !== "bearer") {
123
+ throw new errors_1.OAuthFlowError("TOKEN_EXCHANGE_FAILED", `Unsupported token_type '${raw.token_type}'; this library only handles Bearer.`);
124
+ }
125
+ const tokens = {
126
+ accessToken: raw.access_token,
127
+ expiresAt: issuedAt + expiresIn * 1000,
128
+ };
129
+ if (isNonEmptyString(raw.refresh_token)) {
130
+ tokens.refreshToken = raw.refresh_token;
131
+ }
132
+ if (isNonEmptyString(raw.scope)) {
133
+ tokens.grantedScope = raw.scope;
134
+ }
135
+ return tokens;
136
+ }
@@ -0,0 +1,78 @@
1
+ import type { TokenSet } from "./tokenExchange";
2
+ /**
3
+ * Current on-disk blob schema version. Exported so consumers can
4
+ * display "stored v:N, expected v:M" diagnostics when `load()` returns
5
+ * a `version-mismatch` result.
6
+ */
7
+ export declare const STORED_BLOB_VERSION = 1;
8
+ /**
9
+ * Outcome of a `TokenStore.load()` call.
10
+ *
11
+ * Note on downgrades: the migrator chain only walks *forward*. A user
12
+ * who downgrades `axe-auth` to a release that predates a schema bump
13
+ * will see `version-mismatch` on any blob written by the newer
14
+ * release, even if the change was strictly additive. That is the safe
15
+ * default for a credentials blob — the older version cannot vouch for
16
+ * the meaning of fields it has never seen. Callers hitting this case
17
+ * should treat it as "re-authenticate" rather than attempting to
18
+ * parse an unknown future shape.
19
+ */
20
+ export type LoadResult = {
21
+ ok: true;
22
+ tokens: TokenSet;
23
+ } | {
24
+ ok: false;
25
+ reason: "empty";
26
+ } | {
27
+ ok: false;
28
+ reason: "corrupt";
29
+ } | {
30
+ ok: false;
31
+ reason: "version-mismatch";
32
+ storedVersion: number;
33
+ };
34
+ /** Persistence layer for an OAuth `TokenSet`. */
35
+ export interface TokenStore {
36
+ /** Write-through save. Replaces any previously stored tokens. */
37
+ save(tokens: TokenSet): Promise<void>;
38
+ /**
39
+ * Reads the stored tokens and returns a structured result.
40
+ *
41
+ * Callers should branch on `result.ok` first. When `ok` is `false`,
42
+ * `reason` tells them *why* there is no usable `TokenSet`: `empty`
43
+ * (nothing stored), `corrupt` (unparseable or shape-invalid), or
44
+ * `version-mismatch` (stored under a schema we cannot migrate from).
45
+ * The library does not emit output on these cases — surfacing them
46
+ * to the user is the caller's responsibility.
47
+ */
48
+ load(): Promise<LoadResult>;
49
+ /** Removes any stored tokens. No-op if none are present. */
50
+ clear(): Promise<void>;
51
+ }
52
+ /** Minimal keyring-entry surface consumed by `KeyringTokenStore`. */
53
+ export interface KeyringEntry {
54
+ /** Writes the password for this entry. */
55
+ setPassword(password: string): void;
56
+ /** Reads the current password, or returns `null` if none is set. */
57
+ getPassword(): string | null;
58
+ /** Deletes the password and returns `true` if one existed. */
59
+ deletePassword(): boolean;
60
+ }
61
+ /**
62
+ * Factory for `KeyringEntry` values. Injection seam for tests;
63
+ * production callers use the default that constructs
64
+ * `@napi-rs/keyring` entries lazily.
65
+ */
66
+ export type KeyringEntryFactory = (service: string, account: string) => KeyringEntry;
67
+ /**
68
+ * `TokenStore` backed by the operating system's native keychain via
69
+ * `@napi-rs/keyring` (macOS Keychain, Windows Credential Manager, Linux
70
+ * Secret Service). Account is keyed by the normalized issuer URL.
71
+ */
72
+ export declare class KeyringTokenStore implements TokenStore {
73
+ #private;
74
+ constructor(issuerURL: string, entryFactory?: KeyringEntryFactory);
75
+ save(tokens: TokenSet): Promise<void>;
76
+ load(): Promise<LoadResult>;
77
+ clear(): Promise<void>;
78
+ }