@dereekb/util 13.23.0 → 13.24.0

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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@dereekb/util/oidc",
3
+ "version": "13.24.0",
4
+ "peerDependencies": {
5
+ "@dereekb/util": "13.24.0"
6
+ },
7
+ "exports": {
8
+ "./package.json": "./package.json",
9
+ ".": {
10
+ "module": "./index.esm.js",
11
+ "types": "./index.d.ts",
12
+ "import": "./index.cjs.mjs",
13
+ "default": "./index.cjs.js"
14
+ }
15
+ },
16
+ "module": "./index.esm.js",
17
+ "main": "./index.cjs.js",
18
+ "types": "./index.d.ts"
19
+ }
@@ -0,0 +1 @@
1
+ export * from './lib';
@@ -0,0 +1,2 @@
1
+ export * from './oidc.protocol';
2
+ export * from './oidc.token';
@@ -0,0 +1,96 @@
1
+ import { type DiscoverOidcMetadataInput, type ExchangeAuthorizationCodeInput, type FetchSessionInfoInput, type FetchUserInfoInput, type OidcDiscoveryMetadata, type OidcSessionInfo, type OidcTokenResponse, type RefreshAccessTokenInput, type RevokeTokenInput } from '@dereekb/util';
2
+ /**
3
+ * The fetch transport injected into every relying-party network call.
4
+ *
5
+ * Matches the global `fetch` signature so callers can pass raw `fetch`, a traced/timeout wrapper,
6
+ * or a `configureFetch(...)` result interchangeably.
7
+ *
8
+ * ⚠️ The transport passed to the token-endpoint calls (`exchangeAuthorizationCode`,
9
+ * `refreshAccessToken`, `revokeToken`, `discoverOidcMetadata`) MUST be a plain fetch — it must NOT
10
+ * inject an `Authorization: Bearer` header. Public PKCE clients authenticate via the
11
+ * `code_verifier`, not the access token; a Bearer-injecting transport belongs only on the
12
+ * resource-server (API) client.
13
+ */
14
+ export type OidcRelyingPartyFetch = typeof fetch;
15
+ /**
16
+ * Mixed into each network function's input to inject the {@link OidcRelyingPartyFetch} transport.
17
+ */
18
+ export interface OidcRelyingPartyFetchInput {
19
+ readonly fetch: OidcRelyingPartyFetch;
20
+ }
21
+ /**
22
+ * Fetches the OIDC discovery document for the given issuer, trying the candidates returned by
23
+ * {@link buildOidcDiscoveryCandidates} in order.
24
+ *
25
+ * @param input - The discovery request plus the injected fetch transport.
26
+ * @param input.issuer - The OIDC issuer URL whose `.well-known/openid-configuration` is fetched first.
27
+ * @param input.fallbackBaseUrl - Optional sibling base URL tried after the issuer-prefixed and origin-rooted candidates.
28
+ * @param input.fetch - The plain fetch transport.
29
+ * @returns The parsed {@link OidcDiscoveryMetadata}. Throws an {@link OidcRelyingPartyError} (`DISCOVERY_FAILED`) when every candidate fails.
30
+ */
31
+ export declare function discoverOidcMetadata(input: DiscoverOidcMetadataInput & OidcRelyingPartyFetchInput): Promise<OidcDiscoveryMetadata>;
32
+ /**
33
+ * Exchanges an authorization code (from the redirect) for an access token + refresh token.
34
+ *
35
+ * Sends `client_secret` only when provided — public PKCE clients (`token_endpoint_auth_method: 'none'`)
36
+ * authenticate via the `code_verifier` alone.
37
+ *
38
+ * @param input - The token exchange parameters plus the injected fetch transport.
39
+ * @param input.tokenEndpoint - The OIDC token endpoint URL discovered from the issuer.
40
+ * @param input.clientId - The OAuth client ID.
41
+ * @param input.clientSecret - Optional client secret for `client_secret_post` auth; omit for public clients.
42
+ * @param input.redirectUri - The redirect URI registered with the OAuth client.
43
+ * @param input.code - The authorization code returned to the redirect URI.
44
+ * @param input.codeVerifier - The PKCE code verifier originally paired with the code challenge.
45
+ * @param input.fetch - The plain fetch transport.
46
+ * @returns The parsed {@link OidcTokenResponse} with access/refresh tokens.
47
+ */
48
+ export declare function exchangeAuthorizationCode(input: ExchangeAuthorizationCodeInput & OidcRelyingPartyFetchInput): Promise<OidcTokenResponse>;
49
+ /**
50
+ * Exchanges a refresh token for a new access token (and possibly a rotated refresh token).
51
+ *
52
+ * @param input - The refresh request plus the injected fetch transport.
53
+ * @param input.tokenEndpoint - The OIDC token endpoint URL discovered from the issuer.
54
+ * @param input.clientId - The OAuth client ID.
55
+ * @param input.clientSecret - Optional client secret; omit for public clients.
56
+ * @param input.refreshToken - The cached refresh token to redeem.
57
+ * @param input.fetch - The plain fetch transport.
58
+ * @returns The parsed {@link OidcTokenResponse} with the refreshed access token.
59
+ */
60
+ export declare function refreshAccessToken(input: RefreshAccessTokenInput & OidcRelyingPartyFetchInput): Promise<OidcTokenResponse>;
61
+ /**
62
+ * Revokes an access or refresh token at the OIDC revocation endpoint.
63
+ *
64
+ * @param input - The revocation request plus the injected fetch transport.
65
+ * @param input.revocationEndpoint - The OIDC revocation endpoint URL.
66
+ * @param input.clientId - The OAuth client ID.
67
+ * @param input.clientSecret - Optional client secret; omit for public clients.
68
+ * @param input.token - The access or refresh token to revoke.
69
+ * @param input.tokenTypeHint - Optional hint passed as `token_type_hint` (`access_token` or `refresh_token`).
70
+ * @param input.fetch - The plain fetch transport.
71
+ * @returns Resolves when the server returns a non-error status. Throws an {@link OidcRelyingPartyError} (`TOKEN_REVOCATION_FAILED`) on error.
72
+ */
73
+ export declare function revokeToken(input: RevokeTokenInput & OidcRelyingPartyFetchInput): Promise<void>;
74
+ /**
75
+ * Fetches the OIDC `userinfo` endpoint and returns the parsed claims object.
76
+ *
77
+ * @param input - The userinfo request plus the injected fetch transport.
78
+ * @param input.userinfoEndpoint - The OIDC userinfo endpoint URL discovered from the issuer.
79
+ * @param input.accessToken - The Bearer access token sent in the `Authorization` header.
80
+ * @param input.fetch - The fetch transport.
81
+ * @returns The parsed userinfo claims. Throws an {@link OidcRelyingPartyError} (`USERINFO_FAILED`) on a non-OK response.
82
+ */
83
+ export declare function fetchUserInfo(input: FetchUserInfoInput & OidcRelyingPartyFetchInput): Promise<Record<string, unknown>>;
84
+ /**
85
+ * Fetches the dbx-components `GET /oidc/session` route and returns the parsed session lifetime metadata.
86
+ *
87
+ * Mirrors {@link fetchUserInfo}, but reads the access token's baked-in session-lifetime claims
88
+ * (`dbx_session_expires_at` / `dbx_rotation_disabled`) which userinfo does not echo.
89
+ *
90
+ * @param input - The session request plus the injected fetch transport.
91
+ * @param input.sessionEndpoint - The `GET /oidc/session` endpoint URL.
92
+ * @param input.accessToken - The Bearer access token sent in the `Authorization` header.
93
+ * @param input.fetch - The fetch transport.
94
+ * @returns The parsed {@link OidcSessionInfo}. Throws an {@link OidcRelyingPartyError} (`SESSION_INFO_FAILED`) on a non-OK response.
95
+ */
96
+ export declare function fetchSessionInfo(input: FetchSessionInfoInput & OidcRelyingPartyFetchInput): Promise<OidcSessionInfo>;
@@ -0,0 +1,175 @@
1
+ import { type AsyncValueCache, type Getter, type Maybe, type OidcTokenResponse, type UnixDateTimeMillisecondsNumber } from '@dereekb/util';
2
+ /**
3
+ * Default pre-emptive refresh buffer in milliseconds.
4
+ *
5
+ * An access token is treated as needing a refresh this far ahead of its real expiry to absorb
6
+ * clock skew and request latency.
7
+ */
8
+ export declare const DEFAULT_OIDC_TOKEN_REFRESH_BUFFER_MS = 60000;
9
+ /**
10
+ * Environment-agnostic snapshot of an OIDC relying party's current tokens.
11
+ *
12
+ * Persisted via an {@link OidcTokenStorage}. `expiresAt` is stored as an absolute unix epoch
13
+ * milliseconds value (rather than a `Date`) so the state round-trips cleanly through JSON storage
14
+ * such as `localStorage`.
15
+ */
16
+ export interface OidcTokenState {
17
+ readonly accessToken: string;
18
+ readonly refreshToken?: Maybe<string>;
19
+ readonly idToken?: Maybe<string>;
20
+ readonly tokenType?: Maybe<string>;
21
+ readonly scope?: Maybe<string>;
22
+ /**
23
+ * Absolute access-token expiry as unix epoch milliseconds. Absent ⇒ unknown (treated as expired).
24
+ */
25
+ readonly expiresAt?: Maybe<UnixDateTimeMillisecondsNumber>;
26
+ /**
27
+ * Optional decoded identity claims (e.g. from the `id_token`), surfaced to UIs.
28
+ */
29
+ readonly claims?: Maybe<Record<string, unknown>>;
30
+ }
31
+ /**
32
+ * Single-value async cache holding the current {@link OidcTokenState}.
33
+ *
34
+ * Defaults to {@link inMemoryAsyncValueCache}; environment shells supply a persistent backing
35
+ * (e.g. a `localStorage`-backed cache in the browser, a file-backed cache in a CLI).
36
+ */
37
+ export type OidcTokenStorage = AsyncValueCache<OidcTokenState>;
38
+ export interface OidcTokenStateFromResponseConfig {
39
+ /**
40
+ * The current time used to anchor `expires_in`. Defaults to the present time.
41
+ */
42
+ readonly now?: Maybe<Date | UnixDateTimeMillisecondsNumber>;
43
+ /**
44
+ * Pre-decoded identity claims. When omitted, the claims are decoded from the response `id_token`.
45
+ */
46
+ readonly claims?: Maybe<Record<string, unknown>>;
47
+ }
48
+ /**
49
+ * Normalizes an {@link OidcTokenResponse} into an {@link OidcTokenState}, converting the relative
50
+ * `expires_in` (seconds) into an absolute `expiresAt` (epoch ms) anchored at `config.now`.
51
+ *
52
+ * @param response - The token endpoint response to normalize.
53
+ * @param config - Optional anchoring time and pre-decoded claims.
54
+ * @returns The normalized {@link OidcTokenState}.
55
+ */
56
+ export declare function oidcTokenStateFromResponse(response: OidcTokenResponse, config?: OidcTokenStateFromResponseConfig): OidcTokenState;
57
+ /**
58
+ * Returns true when the access token's real expiry has already passed (no buffer applied), or when
59
+ * the state / its `expiresAt` is missing.
60
+ *
61
+ * @param state - The token state to inspect.
62
+ * @param now - Optional current time override.
63
+ * @returns Whether the access token has expired.
64
+ */
65
+ export declare function isAccessTokenExpired(state: Maybe<OidcTokenState>, now?: Maybe<Date | UnixDateTimeMillisecondsNumber>): boolean;
66
+ /**
67
+ * Returns true when the access token is expired or within `bufferMs` of expiry (pre-emptive
68
+ * refresh), or when the state / its `expiresAt` is missing.
69
+ *
70
+ * @param state - The token state to inspect.
71
+ * @param now - Optional current time override.
72
+ * @param bufferMs - Pre-emptive refresh buffer in milliseconds. Defaults to {@link DEFAULT_OIDC_TOKEN_REFRESH_BUFFER_MS}.
73
+ * @returns Whether the access token should be refreshed.
74
+ */
75
+ export declare function accessTokenNeedsRefresh(state: Maybe<OidcTokenState>, now?: Maybe<Date | UnixDateTimeMillisecondsNumber>, bufferMs?: number): boolean;
76
+ /**
77
+ * Returns the milliseconds remaining until the access token should be pre-emptively refreshed
78
+ * (its real expiry minus `bufferMs`), clamped to a minimum of `0`.
79
+ *
80
+ * @param state - The token state to inspect.
81
+ * @param now - Optional current time override.
82
+ * @param bufferMs - Pre-emptive refresh buffer in milliseconds. Defaults to {@link DEFAULT_OIDC_TOKEN_REFRESH_BUFFER_MS}.
83
+ * @returns The delay in milliseconds, or `0` when already due / unknown.
84
+ */
85
+ export declare function nextRefreshDelay(state: Maybe<OidcTokenState>, now?: Maybe<Date | UnixDateTimeMillisecondsNumber>, bufferMs?: number): number;
86
+ /**
87
+ * Performs a single refresh-token exchange and returns the new token response.
88
+ *
89
+ * Wired by the environment shell to call {@link refreshAccessToken} with the resolved endpoint +
90
+ * client credentials + injected fetch.
91
+ */
92
+ export type OidcTokenRefreshFunction = (input: OidcTokenRefreshFunctionInput) => Promise<OidcTokenResponse>;
93
+ export interface OidcTokenRefreshFunctionInput {
94
+ /**
95
+ * The current token state being refreshed.
96
+ */
97
+ readonly state: OidcTokenState;
98
+ /**
99
+ * The refresh token to redeem (non-null — the manager only calls refresh when one is present).
100
+ */
101
+ readonly refreshToken: string;
102
+ }
103
+ export interface OidcTokenManagerConfig {
104
+ /**
105
+ * Backing token store. Defaults to an in-memory cache.
106
+ */
107
+ readonly storage?: OidcTokenStorage;
108
+ /**
109
+ * Refresh-token exchange function. Required.
110
+ */
111
+ readonly refresh: OidcTokenRefreshFunction;
112
+ /**
113
+ * Current-time getter (epoch ms). Defaults to `Date.now`.
114
+ */
115
+ readonly now?: Getter<UnixDateTimeMillisecondsNumber>;
116
+ /**
117
+ * Pre-emptive refresh buffer in milliseconds. Defaults to {@link DEFAULT_OIDC_TOKEN_REFRESH_BUFFER_MS}.
118
+ */
119
+ readonly refreshBufferMs?: number;
120
+ }
121
+ /**
122
+ * The env-agnostic token "brain": resolves a valid access token, refreshing lazily and
123
+ * de-duplicating concurrent refreshes into one network call.
124
+ */
125
+ export interface OidcTokenManager {
126
+ /**
127
+ * Loads the stored state, returning a valid access token — refreshing first when the current one
128
+ * is missing/expired. Resolves to `undefined` when there is no usable token (logged out, or the
129
+ * refresh failed with `invalid_grant`, which also clears the store).
130
+ */
131
+ getValidAccessToken(): Promise<Maybe<string>>;
132
+ /**
133
+ * Returns the currently stored token state.
134
+ */
135
+ getState(): Promise<Maybe<OidcTokenState>>;
136
+ /**
137
+ * Replaces the stored token state (e.g. after an initial login exchange).
138
+ */
139
+ setState(state: OidcTokenState): Promise<void>;
140
+ /**
141
+ * Forces a refresh now (sharing any in-flight refresh), returning the new state.
142
+ */
143
+ refreshNow(): Promise<Maybe<OidcTokenState>>;
144
+ /**
145
+ * Clears the stored token state (logout).
146
+ */
147
+ clear(): Promise<void>;
148
+ }
149
+ /**
150
+ * Creates an {@link OidcTokenManager} over the given storage + refresh function.
151
+ *
152
+ * - **Lazy** — refreshes only on demand (no timer), so it works unchanged in a CLI or browser.
153
+ * - **Single-flight** — concurrent `getValidAccessToken()` / `refreshNow()` calls share one
154
+ * in-flight refresh, so N callers trigger at most one network exchange.
155
+ * - **Rotation-safe** — a rotated `refresh_token` replaces the old one; when the response omits it,
156
+ * the prior refresh token is kept. The `id_token`/`claims` are likewise carried forward when a
157
+ * refresh response omits them.
158
+ * - **invalid_grant → logout** — a refresh that fails with `TOKEN_INVALID_GRANT` clears the store
159
+ * and surfaces as "no token" (`undefined`); other errors propagate.
160
+ *
161
+ * @param config - The manager configuration.
162
+ * @returns The configured {@link OidcTokenManager}.
163
+ */
164
+ export declare function oidcTokenManager(config: OidcTokenManagerConfig): OidcTokenManager;
165
+ /**
166
+ * Decodes the (unverified) payload claims from a JWT such as an `id_token`.
167
+ *
168
+ * This does NOT verify the signature — it is purely for surfacing identity claims a relying party
169
+ * already trusts (the token was just issued to it). Returns `undefined` when the input is missing
170
+ * or not a decodable JWT.
171
+ *
172
+ * @param token - The JWT string.
173
+ * @returns The decoded claims object, or `undefined`.
174
+ */
175
+ export declare function decodeJwtClaims(token: Maybe<string>): Maybe<Record<string, unknown>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/util",
3
- "version": "13.23.0",
3
+ "version": "13.24.0",
4
4
  "sideEffects": false,
5
5
  "exports": {
6
6
  "./test": {
@@ -15,6 +15,12 @@
15
15
  "import": "./fetch/index.cjs.mjs",
16
16
  "default": "./fetch/index.cjs.js"
17
17
  },
18
+ "./oidc": {
19
+ "module": "./oidc/index.esm.js",
20
+ "types": "./oidc/index.d.ts",
21
+ "import": "./oidc/index.cjs.mjs",
22
+ "default": "./oidc/index.cjs.js"
23
+ },
18
24
  "./eslint": {
19
25
  "module": "./eslint/index.esm.js",
20
26
  "types": "./eslint/index.d.ts",
@@ -1,3 +1,6 @@
1
+ import { BaseError } from 'make-error';
2
+ import { type CodedError } from '../error/error';
3
+ import { type Maybe } from '../value/maybe.type';
1
4
  /**
2
5
  * Standard "out-of-band" OAuth 2.0 redirect URI URN.
3
6
  *
@@ -10,3 +13,277 @@
10
13
  * bind a local port.
11
14
  */
12
15
  export declare const OAUTH_OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob";
16
+ /**
17
+ * Default OAuth scope requested by an OIDC relying party when no explicit scope list is provided.
18
+ *
19
+ * `openid` is the one scope every OpenID Connect flow must request to receive an ID token.
20
+ */
21
+ export declare const DEFAULT_OIDC_RELYING_PARTY_SCOPE = "openid";
22
+ /**
23
+ * Stable error code identifying a failure raised by the OIDC relying-party (token-consumer) layer.
24
+ *
25
+ * Shared by the pure helpers here and the network protocol functions in `@dereekb/util/oidc`.
26
+ * Consumers (CLI, browser) re-wrap or branch on these codes at their boundary.
27
+ */
28
+ export type OidcRelyingPartyErrorCode = 'DISCOVERY_FAILED' | 'TOKEN_EXCHANGE_FAILED' | 'TOKEN_INVALID_GRANT' | 'TOKEN_REVOCATION_FAILED' | 'USERINFO_FAILED' | 'SESSION_INFO_FAILED' | 'AUTH_NO_CODE' | 'AUTH_PROVIDER_ERROR' | 'AUTH_REDIRECT_PARSE_FAILED' | 'INVALID_STATE' | 'INVALID_SESSION_TTL';
29
+ export interface OidcRelyingPartyErrorInput {
30
+ readonly message: string;
31
+ readonly code: OidcRelyingPartyErrorCode;
32
+ /**
33
+ * The original error, if this error wraps one.
34
+ */
35
+ readonly _error?: unknown;
36
+ }
37
+ /**
38
+ * Error raised by the OIDC relying-party layer (pure helpers here + the network protocol functions
39
+ * in `@dereekb/util/oidc`). Carries a stable {@link OidcRelyingPartyErrorCode} so environment shells
40
+ * (CLI, browser) can branch on or re-wrap it without string-matching messages.
41
+ */
42
+ export declare class OidcRelyingPartyError extends BaseError implements CodedError {
43
+ readonly code: OidcRelyingPartyErrorCode;
44
+ readonly _error?: unknown;
45
+ constructor(input: OidcRelyingPartyErrorInput);
46
+ }
47
+ /**
48
+ * The subset of fields read from an OIDC discovery document.
49
+ *
50
+ * A relying party uses the authorization, token, userinfo, and revocation endpoints to drive
51
+ * the authorization-code-with-PKCE flow.
52
+ */
53
+ export interface OidcDiscoveryMetadata {
54
+ readonly issuer: string;
55
+ readonly authorization_endpoint: string;
56
+ readonly token_endpoint: string;
57
+ readonly userinfo_endpoint?: string;
58
+ readonly revocation_endpoint?: string;
59
+ readonly end_session_endpoint?: string;
60
+ readonly jwks_uri?: string;
61
+ readonly scopes_supported?: string[];
62
+ }
63
+ /**
64
+ * Token response from a successful `authorization_code` or `refresh_token` exchange.
65
+ */
66
+ export interface OidcTokenResponse {
67
+ readonly access_token: string;
68
+ readonly token_type?: string;
69
+ readonly expires_in?: number;
70
+ readonly refresh_token?: string;
71
+ readonly id_token?: string;
72
+ readonly scope?: string;
73
+ }
74
+ /**
75
+ * Session lifetime metadata returned by the dbx-components `GET /oidc/session` route.
76
+ */
77
+ export interface OidcSessionInfo {
78
+ readonly sub?: string;
79
+ readonly scope?: Maybe<string>;
80
+ /**
81
+ * Grant (session) expiry as unix epoch SECONDS, or `null` when the provider could not resolve it.
82
+ */
83
+ readonly expiresAt?: Maybe<number>;
84
+ /**
85
+ * Whether refresh-token rotation is disabled for this grant (a long-lived service token).
86
+ */
87
+ readonly rotationDisabled?: boolean;
88
+ }
89
+ export interface DiscoverOidcMetadataInput {
90
+ readonly issuer: string;
91
+ /**
92
+ * Optional sibling base URL to try after the issuer-prefixed and origin-rooted paths.
93
+ *
94
+ * Useful when the discovery doc is served at `<api-base-url>/.well-known/openid-configuration`
95
+ * — e.g. when the issuer is reached via a different host than the API and the origin-rooted
96
+ * candidate would target the wrong host.
97
+ */
98
+ readonly fallbackBaseUrl?: string;
99
+ }
100
+ export interface ExchangeAuthorizationCodeInput {
101
+ readonly tokenEndpoint: string;
102
+ readonly clientId: string;
103
+ /**
104
+ * Optional client secret. Confidential clients authenticate via `client_secret_post`; public
105
+ * PKCE clients (`token_endpoint_auth_method: 'none'`) omit it and authenticate via the
106
+ * `code_verifier` alone.
107
+ */
108
+ readonly clientSecret?: Maybe<string>;
109
+ readonly redirectUri: string;
110
+ readonly code: string;
111
+ readonly codeVerifier: string;
112
+ }
113
+ export interface RefreshAccessTokenInput {
114
+ readonly tokenEndpoint: string;
115
+ readonly clientId: string;
116
+ /**
117
+ * Optional client secret. Omitted for public PKCE clients.
118
+ */
119
+ readonly clientSecret?: Maybe<string>;
120
+ readonly refreshToken: string;
121
+ }
122
+ export interface RevokeTokenInput {
123
+ readonly revocationEndpoint: string;
124
+ readonly clientId: string;
125
+ /**
126
+ * Optional client secret. Omitted for public PKCE clients.
127
+ */
128
+ readonly clientSecret?: Maybe<string>;
129
+ readonly token: string;
130
+ readonly tokenTypeHint?: 'access_token' | 'refresh_token';
131
+ }
132
+ export interface FetchUserInfoInput {
133
+ readonly userinfoEndpoint: string;
134
+ readonly accessToken: string;
135
+ }
136
+ export interface FetchSessionInfoInput {
137
+ /**
138
+ * The `GET /oidc/session` endpoint URL (typically `<oidcIssuer>/session`).
139
+ */
140
+ readonly sessionEndpoint: string;
141
+ readonly accessToken: string;
142
+ }
143
+ /**
144
+ * Builds the ordered list of `.well-known/openid-configuration` URLs probed when discovering OIDC
145
+ * metadata.
146
+ *
147
+ * 1. `<issuer>/.well-known/openid-configuration` (OpenID Connect Discovery 1.0).
148
+ * 2. `<issuer-origin>/.well-known/openid-configuration` (host-rooted; matches projects that
149
+ * mount the discovery controller at the API/dev-server root rather than under the issuer
150
+ * sub-path — e.g. demo's `OidcWellKnownController`).
151
+ * 3. `<fallbackBaseUrl>/.well-known/openid-configuration` when supplied and not already covered.
152
+ *
153
+ * Exported so diagnostic surfaces (e.g. a CLI `doctor`) can show the exact URLs the discovery step
154
+ * tried — without re-implementing the candidate ordering.
155
+ *
156
+ * @param input - The discovery request.
157
+ * @param input.issuer - The OIDC issuer URL whose `.well-known/openid-configuration` is fetched first.
158
+ * @param input.fallbackBaseUrl - Optional sibling base URL appended after the issuer-prefixed and origin-rooted candidates.
159
+ * @returns The candidate URL list in probe order, de-duplicated.
160
+ *
161
+ * @__NO_SIDE_EFFECTS__
162
+ */
163
+ export declare function buildOidcDiscoveryCandidates(input: DiscoverOidcMetadataInput): string[];
164
+ /**
165
+ * Returns the input URL string with a single trailing slash removed, if present.
166
+ *
167
+ * @param url - The URL string to trim.
168
+ * @returns The URL without a trailing slash.
169
+ *
170
+ * @__NO_SIDE_EFFECTS__
171
+ */
172
+ export declare function trimSlash(url: string): string;
173
+ export interface BuildAuthorizationUrlInput {
174
+ readonly authorizationEndpoint: string;
175
+ /**
176
+ * Optional OIDC issuer URL (e.g. `https://api.example.com/oidc`). Used as a fallback rebase
177
+ * origin when `appClientUrl` is not provided — the origin of `oidcIssuer` is applied to the
178
+ * discovered `authorizationEndpoint`. In typical OIDC deployments the issuer and authorization
179
+ * endpoint share an origin, so this fallback is a no-op rebase that ends up at the discovered
180
+ * `/oidc/auth` URL. Use `appClientUrl` to send the user to a different origin (e.g. a frontend
181
+ * dev server that proxies `/oidc/**` back to the API).
182
+ */
183
+ readonly oidcIssuer?: Maybe<string>;
184
+ /**
185
+ * Optional API base URL. Used as a fallback rebase origin when neither `appClientUrl` nor
186
+ * `oidcIssuer` is provided — only the origin is read, so prefix paths like `/api` are ignored.
187
+ * Prefer `oidcIssuer` for new deployments; `apiBaseUrl` is kept for backwards compatibility
188
+ * with envs that only declare an API base URL.
189
+ */
190
+ readonly apiBaseUrl?: Maybe<string>;
191
+ /**
192
+ * Optional client origin to rebase the authorization endpoint onto.
193
+ *
194
+ * When set, the path + search of `authorizationEndpoint` are kept and the origin is replaced
195
+ * with this URL. Takes precedence over both `oidcIssuer` and `apiBaseUrl`. Recommended for any
196
+ * env where the user-facing URL should differ from the discovered authorization endpoint —
197
+ * e.g. a frontend dev server that proxies `/oidc/**` to the API on another port. When omitted,
198
+ * the URL is derived from `oidcIssuer`/`apiBaseUrl` origin (or used verbatim if neither is set),
199
+ * so the relying party always opens the actual authorization endpoint (typically `/oidc/auth`).
200
+ */
201
+ readonly appClientUrl?: Maybe<string>;
202
+ readonly clientId: string;
203
+ readonly redirectUri: string;
204
+ /**
205
+ * Space-separated scope list. Defaults to {@link DEFAULT_OIDC_RELYING_PARTY_SCOPE}.
206
+ */
207
+ readonly scopes?: string;
208
+ readonly state: string;
209
+ readonly codeChallenge: string;
210
+ /**
211
+ * Optional requested login duration in seconds. When set, the URL includes the
212
+ * `dbx_session_ttl=<seconds>` query param so the OIDC server applies the requested
213
+ * lifetime to the issued Session, Grant, and RefreshToken (subject to per-client and
214
+ * server caps).
215
+ */
216
+ readonly requestedSessionTtlSeconds?: Maybe<number>;
217
+ }
218
+ /**
219
+ * Builds the authorization URL the user opens in a browser to start the PKCE flow.
220
+ *
221
+ * The user-facing endpoint is the discovered `authorizationEndpoint` (typically `/oidc/auth`)
222
+ * with its origin optionally rebased. The rebase origin is the first non-empty value among
223
+ * `appClientUrl` → `oidcIssuer` origin → `apiBaseUrl` origin. In a typical single-host
224
+ * deployment all three resolve to the same origin and the rebase is a no-op; in a split-host
225
+ * setup (frontend dev server proxying `/oidc/**` to the API) `appClientUrl` redirects the user
226
+ * through the frontend. When none of the three is provided, the discovered endpoint is used
227
+ * unchanged.
228
+ *
229
+ * Always lands at the actual authorization endpoint so oidc-provider can create an interaction
230
+ * and redirect to the app login page with a `uid`.
231
+ *
232
+ * @param input - The authorization URL inputs.
233
+ * @param input.authorizationEndpoint - The authorization endpoint discovered from OIDC metadata.
234
+ * @param input.appClientUrl - Optional client origin to rebase the authorization endpoint onto. Takes precedence over `oidcIssuer` and `apiBaseUrl`.
235
+ * @param input.oidcIssuer - Optional OIDC issuer URL; falls back to its origin as the rebase target when `appClientUrl` is missing.
236
+ * @param input.apiBaseUrl - Optional API base URL; falls back to its origin as the rebase target when neither `appClientUrl` nor `oidcIssuer` is set.
237
+ * @param input.clientId - The OAuth client ID.
238
+ * @param input.redirectUri - The redirect URI registered with the OAuth client.
239
+ * @param input.scopes - Space-separated scope list. Defaults to {@link DEFAULT_OIDC_RELYING_PARTY_SCOPE}.
240
+ * @param input.state - The opaque OAuth state token used for CSRF protection.
241
+ * @param input.codeChallenge - The PKCE code challenge derived from the verifier.
242
+ * @returns The full authorization URL with all OAuth params merged in.
243
+ * @throws {OidcRelyingPartyError} When `requestedSessionTtlSeconds` is provided but is not a positive integer.
244
+ *
245
+ * @__NO_SIDE_EFFECTS__
246
+ */
247
+ export declare function buildAuthorizationUrl(input: BuildAuthorizationUrlInput): string;
248
+ export interface PkceMaterial {
249
+ readonly codeVerifier: string;
250
+ readonly codeChallenge: string;
251
+ }
252
+ /**
253
+ * Generates a fresh PKCE code verifier and the matching SHA-256 code challenge.
254
+ *
255
+ * @returns A {@link PkceMaterial} pair consisting of the random `codeVerifier` and its derived `codeChallenge`.
256
+ */
257
+ export declare function generatePkceMaterial(): Promise<PkceMaterial>;
258
+ /**
259
+ * Generates a random URL-safe state value for the OAuth state parameter.
260
+ *
261
+ * @returns A 32-character hex string derived from 16 random bytes.
262
+ */
263
+ export declare function generateOAuthState(): string;
264
+ export interface ParseAuthorizationRedirectInput {
265
+ /**
266
+ * The redirect URL (e.g. `window.location.href`) or a bare authorization code.
267
+ */
268
+ readonly url: string;
269
+ /**
270
+ * Optional state value to assert against the redirect's `state` when present.
271
+ */
272
+ readonly expectedState?: string;
273
+ }
274
+ export interface ParsedAuthorizationRedirect {
275
+ readonly code: string;
276
+ readonly state?: string;
277
+ }
278
+ /**
279
+ * Parses an authorization code out of a redirect URL or a bare code string.
280
+ *
281
+ * Validates the `state` parameter when an expected value is provided.
282
+ *
283
+ * @param input - The parse inputs.
284
+ * @param input.url - The redirect URL or bare authorization code.
285
+ * @param input.expectedState - Optional state value to assert against `state` when present in the URL.
286
+ * @returns The {@link ParsedAuthorizationRedirect} containing `code` and (when present) `state`.
287
+ * @throws {OidcRelyingPartyError} When `url` is empty, when the URL contains no `code` parameter, or when `expectedState` does not match the URL's `state`.
288
+ */
289
+ export declare function parseAuthorizationRedirect(input: ParseAuthorizationRedirectInput): ParsedAuthorizationRedirect;
package/test/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@dereekb/util/test",
3
- "version": "13.23.0",
3
+ "version": "13.24.0",
4
4
  "peerDependencies": {
5
- "@dereekb/util": "13.23.0",
5
+ "@dereekb/util": "13.24.0",
6
6
  "make-error": "^1.3.6"
7
7
  },
8
8
  "exports": {