@tdacorp/identity-client 0.1.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,306 @@
1
+ import { RolesClaim } from '@tdacorp/identity-authz';
2
+
3
+ /**
4
+ * A successful OAuth2/OIDC token response, camelCased from the wire's
5
+ * snake_case (RFC 6749 §5.1).
6
+ */
7
+ interface TokenSet {
8
+ accessToken: string;
9
+ tokenType: string;
10
+ refreshToken?: string;
11
+ /** Present only when the request's scope included `openid`. */
12
+ idToken?: string;
13
+ /** Lifetime of `accessToken` in seconds from the time of issuance, when
14
+ * the server reports one. */
15
+ expiresIn?: number;
16
+ /** The scope actually granted, space-separated -- may be narrower than
17
+ * what was requested; absent when the server considers it unchanged. */
18
+ scope?: string;
19
+ }
20
+ /** Thrown by `exchangeAuthorizationCode` and `clientCredentialsGrant` on any
21
+ * non-success response. Both are one-shot actions with no live session to
22
+ * protect from a bad classification, unlike `refreshTokens` below, so a
23
+ * plain throw is enough: the caller has nothing to preserve either way. */
24
+ declare class TokenRequestError extends Error {
25
+ readonly status?: number;
26
+ readonly error?: string;
27
+ readonly errorDescription?: string;
28
+ constructor(details: {
29
+ status?: number;
30
+ error?: string;
31
+ errorDescription?: string;
32
+ });
33
+ }
34
+ /** Options for `exchangeAuthorizationCode`. */
35
+ interface AuthorizationCodeExchangeOptions {
36
+ /** The identity server's issuer URL, used to look up `token_endpoint`. */
37
+ issuer: string;
38
+ clientId: string;
39
+ /** Omit for a public client authenticating via PKCE alone. */
40
+ clientSecret?: string;
41
+ /** The `code` query param from the authorization callback. */
42
+ code: string;
43
+ /** Must exactly match the `redirect_uri` used on the authorization
44
+ * request (RFC 6749 §4.1.3). */
45
+ redirectUri: string;
46
+ /** The PKCE verifier generated (and kept) alongside the code challenge
47
+ * sent on the authorization request; see `generateCodeVerifier`. */
48
+ codeVerifier: string;
49
+ }
50
+ /** RFC 6749 §4.1.3 / RFC 7636 §4.5: exchanges an authorization code for a
51
+ * token set. Throws `TokenRequestError` on any non-success response. */
52
+ declare function exchangeAuthorizationCode(options: AuthorizationCodeExchangeOptions): Promise<TokenSet>;
53
+ /** Options for `clientCredentialsGrant`. */
54
+ interface ClientCredentialsGrantOptions {
55
+ /** The identity server's issuer URL, used to look up `token_endpoint`. */
56
+ issuer: string;
57
+ clientId: string;
58
+ /** Required -- this grant has no end user and no PKCE verifier, so the
59
+ * client must authenticate with its own confidential secret. */
60
+ clientSecret: string;
61
+ /** Space-separated scopes to request for the resulting token. */
62
+ scope?: string;
63
+ }
64
+ /** RFC 6749 §4.4: mints a token with no end user, for service-to-service
65
+ * calls. Always confidential -- `clientSecret` is required, not optional,
66
+ * because this grant has no PKCE verifier to authenticate a public client
67
+ * with. Throws `TokenRequestError` on any non-success response. */
68
+ declare function clientCredentialsGrant(options: ClientCredentialsGrantOptions): Promise<TokenSet>;
69
+ /** Options for `refreshTokens`. */
70
+ interface RefreshTokensOptions {
71
+ /** The identity server's issuer URL, used to look up `token_endpoint`. */
72
+ issuer: string;
73
+ clientId: string;
74
+ /** Omit for a public client authenticating via PKCE alone. */
75
+ clientSecret?: string;
76
+ /** The refresh token to redeem. */
77
+ refreshToken: string;
78
+ }
79
+ type TokenRefreshOutcome = {
80
+ outcome: 'success';
81
+ tokens: TokenSet;
82
+ }
83
+ /** The refresh token's status is still unknown. The caller must deny the
84
+ * request this refresh was gating, but must NOT clear the refresh token
85
+ * or any session built on it -- see this function's own docblock. */
86
+ | {
87
+ outcome: 'transient';
88
+ error: unknown;
89
+ }
90
+ /** The refresh token is confirmed dead. Safe, and only now safe, for the
91
+ * caller to clear it. */
92
+ | {
93
+ outcome: 'terminal';
94
+ error: TokenRequestError;
95
+ };
96
+ /**
97
+ * RFC 6749 §6: redeems a refresh token for a new token set, classifying
98
+ * failure as `transient` (retry later, credential still unknown) or
99
+ * `terminal` (the refresh token is confirmed dead) instead of collapsing
100
+ * both into one rejection.
101
+ *
102
+ * WHY THIS DISTINCTION EXISTS, AND WHY IT IS NOT OPTIONAL
103
+ *
104
+ * An infrastructure hiccup must never be treated the same as a rejected
105
+ * credential. A caller that clears a session/cookie on ANY refresh failure
106
+ * will force-log-out every one of its users during a transient outage, or
107
+ * the moment its own client secret is rotated or its client is temporarily
108
+ * disabled -- neither of those says anything about whether the refresh
109
+ * token itself is still good. This function classifies failures against the
110
+ * identity server's own status-code contract for exactly that reason: only
111
+ * a response that is actually a verdict on the refresh token gets to clear
112
+ * it.
113
+ *
114
+ * CLASSIFICATION
115
+ *
116
+ * - `transient`: a 408/429/5xx HTTP status, `fetch` itself throwing (DNS
117
+ * failure, connection refused, TLS error, timeout), or a 401 response. A
118
+ * 401 here is the server rejecting THIS CALLER's own client credentials
119
+ * (`invalid_client`) -- a verdict on the caller, not on the refresh token
120
+ * being redeemed, so the token's status is still unknown just like the
121
+ * other cases in this bucket.
122
+ * - `terminal`: a 400 response with `error: "invalid_grant"` -- RFC 6749
123
+ * §5.2's own code for "the ... refresh token is invalid, expired,
124
+ * revoked". This is the only shape that is actually the server SAYING the
125
+ * refresh token itself is dead.
126
+ * - Anything else (an unrecognised 4xx, a 2xx with a malformed body) defaults
127
+ * to `transient`. This is the safe direction, not a shrug: misclassifying
128
+ * a live refresh token as terminal tells the caller to destroy a working
129
+ * credential and force a real user to sign in again, while misclassifying
130
+ * a dead one as transient only delays cleanup until the next refresh
131
+ * attempt, which is self-correcting and costs nothing but a little
132
+ * staleness.
133
+ */
134
+ declare function refreshTokens(options: RefreshTokensOptions): Promise<TokenRefreshOutcome>;
135
+
136
+ /**
137
+ * What a verified TDACorp Identity access token actually is: a token minted
138
+ * for an end user, or a token minted for a machine with none.
139
+ *
140
+ * Precedent: Clerk's backend SDK returns a discriminated `tokenType` /
141
+ * `acceptsToken` object from its own token verification rather than one flat
142
+ * claims bag, for the same reason this type exists -- a caller that reads a
143
+ * property off the wrong variant should get a compile error, not `undefined`
144
+ * read as "no roles" or a machine's `clientId` misread as a user's `sub`.
145
+ *
146
+ * The concrete reason this matters here: this identity server's
147
+ * `client_credentials` grant mints access tokens with no end user and no
148
+ * `roles` claim at all -- there is no subject for a roles claim to describe.
149
+ * Before this type existed, `verifyAccessToken` returned one flat claims
150
+ * object for both cases, so a caller checking `.sub` on a machine token
151
+ * silently got `undefined` instead of a type error telling it this branch
152
+ * has no subject.
153
+ */
154
+ type VerifiedAccessToken = {
155
+ kind: 'user';
156
+ sub: string;
157
+ /** Present only when the token was minted with the `roles` scope.
158
+ * Pass to `permits()` from `@tdacorp/identity-authz` to decide a
159
+ * permission locally. */
160
+ roles?: RolesClaim;
161
+ claims: Record<string, unknown>;
162
+ } | {
163
+ kind: 'machine';
164
+ clientId: string;
165
+ claims: Record<string, unknown>;
166
+ };
167
+
168
+ /**
169
+ * Allowed clock skew between this process and the identity server, applied
170
+ * via jose's `clockTolerance`.
171
+ *
172
+ * 60 seconds, matching Auth0's own documented default leeway for ID token
173
+ * validation (60000ms, e.g. Auth0's Lock.swift and Auth0.js configuration
174
+ * docs) and the conventional default several JWT libraries ship
175
+ * (`jsonwebtoken`'s own README recommendation, Spring Security's
176
+ * `JwtTimestampValidator`). Clerk's backend SDK is tighter, defaulting
177
+ * `clockSkewInMs` to 5000 (5s) -- verified against Clerk's own docs. 60s is
178
+ * chosen over Clerk's tighter figure deliberately: this is a first-party,
179
+ * low-latency deployment (the identity server and every relying party share
180
+ * infrastructure-grade NTP-synced clocks), so the risk a looser tolerance
181
+ * trades away -- a stolen, still-live token being replayed slightly past its
182
+ * `exp` -- is small next to the cost of a false rejection from ordinary
183
+ * clock drift, and 60s is not a novel number invented for this package: it
184
+ * is the figure Auth0 itself ships as a default.
185
+ */
186
+ declare const CLOCK_SKEW_TOLERANCE_SECONDS = 60;
187
+ /**
188
+ * Why `verifyIdToken`/`verifyAccessToken` could not produce a verified
189
+ * claims payload. Every value here is a property of the TOKEN itself, not a
190
+ * caller programming error -- a missing `options.issuer`/`options.audience`
191
+ * throws instead (see `requireConfig`).
192
+ *
193
+ * - `'expired'`: the token's `exp` has passed, even after
194
+ * `CLOCK_SKEW_TOLERANCE_SECONDS` of leeway.
195
+ * - `'invalid-signature'`: the signature does not verify against any key in
196
+ * the issuer's JWKS (wrong key, tampered token, or an ambiguous match).
197
+ * - `'invalid-issuer'`: the token's `iss` does not match `options.issuer`.
198
+ * - `'invalid-audience'`: the token's `aud` does not match `options.audience`.
199
+ * - `'unauthorized-party'`: `options.authorizedParties` was given and the
200
+ * token's `azp` (or single-string `aud` fallback) is not in it -- see
201
+ * `VerifyTokenOptions.authorizedParties`'s own docblock for why this
202
+ * check exists alongside the plain audience check.
203
+ * - `'malformed-claims'`: the token verified but is missing a claim this
204
+ * package requires (e.g. `sub` on an id_token).
205
+ * - `'malformed-token'`: the token is not a well-formed JWT at all.
206
+ * - `'jwks-unavailable'`: the issuer's discovery document or JWKS could not
207
+ * be fetched -- infrastructure, not a verdict on the token.
208
+ */
209
+ type TokenVerificationErrorCode = 'expired' | 'invalid-signature' | 'invalid-issuer' | 'invalid-audience' | 'unauthorized-party' | 'malformed-claims' | 'malformed-token' | 'jwks-unavailable';
210
+ /** One verification failure: a stable `code` to branch on, plus a
211
+ * human-readable `message` for logs. `verifyIdToken`/`verifyAccessToken`
212
+ * return a list of these rather than throwing -- see `VerificationResult`. */
213
+ interface TokenVerificationError {
214
+ code: TokenVerificationErrorCode;
215
+ message: string;
216
+ }
217
+ /**
218
+ * The outcome of `verifyIdToken`/`verifyAccessToken`: either the verified,
219
+ * typed claims (`success: true`), or the reasons verification failed
220
+ * (`success: false`). A discriminated union rather than a thrown error, on
221
+ * the same reasoning as `Permit` in `@tdacorp/identity-authz`: an expected
222
+ * verification failure (expired, bad signature, wrong audience) is data a
223
+ * caller branches on, not an exceptional condition -- only a genuine
224
+ * programming error (missing `options.issuer`/`options.audience`) throws.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * const result = await verifyIdToken(idToken, { issuer, audience })
229
+ * if (result.success) {
230
+ * console.log(result.data.sub)
231
+ * } else {
232
+ * console.warn(result.errors.map((e) => e.code))
233
+ * }
234
+ * ```
235
+ */
236
+ type VerificationResult<T> = {
237
+ success: true;
238
+ data: T;
239
+ } | {
240
+ success: false;
241
+ errors: TokenVerificationError[];
242
+ };
243
+ /** Configuration shared by `verifyIdToken` and `verifyAccessToken`: which
244
+ * issuer and audience a token must match, plus an optional
245
+ * `authorizedParties` allowlist (see that field's own docblock below). */
246
+ interface VerifyTokenOptions {
247
+ issuer: string;
248
+ audience: string;
249
+ /**
250
+ * Origins/client ids this caller trusts a token to have been issued to,
251
+ * checked against the token's `azp` claim when present, falling back to a
252
+ * string `aud` otherwise.
253
+ *
254
+ * WHY `azp` FIRST: this identity server always sets `azp` to the
255
+ * requesting client's own id, even for an access token whose `aud` was
256
+ * overridden to an RFC 8707 resource indicator -- `azp` keeps naming who
257
+ * the token was issued to regardless of what `aud` also claims to be valid
258
+ * at. `aud` is only a safe fallback when it is a single string naming one
259
+ * party; this package does not attempt the allowlist check against a
260
+ * multi-valued `aud`.
261
+ *
262
+ * WHY THIS CHECK EXISTS AT ALL: every TDACorp product shares the
263
+ * `tdacorp.in` registrable domain, and this server's `aud`/`azp` values
264
+ * are opaque client ids, not origins that a browser's same-origin policy
265
+ * already separates. A token minted for one product is not otherwise
266
+ * prevented from being replayed against another product that shares this
267
+ * issuer -- both the plain `audience` check above and this allowlist are
268
+ * needed, and they check different things: one asks whether the token is
269
+ * FOR this resource server, the other asks whether it was issued THROUGH
270
+ * a client this caller has chosen to trust.
271
+ */
272
+ authorizedParties?: string[];
273
+ }
274
+ /**
275
+ * The verified claims of an OIDC id_token, as returned by `verifyIdToken`.
276
+ *
277
+ * Only the claims every id_token carries per OpenID Connect Core 1.0 §2 are
278
+ * typed here (`sub`, `iss`, `aud`, `exp`, `iat`); anything else the identity
279
+ * server includes (e.g. `email`, `name`) still comes through on the object,
280
+ * covered by the index signature, just not individually typed.
281
+ */
282
+ interface IdTokenClaims extends Record<string, unknown> {
283
+ sub: string;
284
+ iss: string;
285
+ aud: string | string[];
286
+ exp: number;
287
+ iat: number;
288
+ }
289
+ /** Verifies an OIDC id_token's signature, issuer, audience and expiry (with
290
+ * the clock-skew tolerance documented on `CLOCK_SKEW_TOLERANCE_SECONDS`).
291
+ * Never throws for an expected verification failure -- expired, bad
292
+ * signature, wrong issuer/audience -- returning `{ success: false, errors
293
+ * }` instead. Throws only for missing required config (`options.issuer` /
294
+ * `options.audience`), which is a programming error, not a property of the
295
+ * token. */
296
+ declare function verifyIdToken(idToken: string, options: VerifyTokenOptions): Promise<VerificationResult<IdTokenClaims>>;
297
+ /** Verifies an access token the same way `verifyIdToken` does, then shapes
298
+ * the result as a `VerifiedAccessToken` (see that type's own docblock for
299
+ * why user and machine tokens are a discriminated union rather than one
300
+ * flat claims bag) with the `roles` claim, if present, typed as
301
+ * `RolesClaim` from `@tdacorp/identity-authz` rather than left as an
302
+ * unparsed field the caller has to know to reach for. Same throw/return
303
+ * contract as `verifyIdToken`. */
304
+ declare function verifyAccessToken(accessToken: string, options: VerifyTokenOptions): Promise<VerificationResult<VerifiedAccessToken>>;
305
+
306
+ export { type AuthorizationCodeExchangeOptions as A, CLOCK_SKEW_TOLERANCE_SECONDS as C, type IdTokenClaims as I, type RefreshTokensOptions as R, type TokenRefreshOutcome as T, type VerificationResult as V, type ClientCredentialsGrantOptions as a, TokenRequestError as b, type TokenSet as c, type TokenVerificationError as d, type TokenVerificationErrorCode as e, type VerifiedAccessToken as f, type VerifyTokenOptions as g, clientCredentialsGrant as h, exchangeAuthorizationCode as i, verifyIdToken as j, refreshTokens as r, verifyAccessToken as v };
@@ -0,0 +1,306 @@
1
+ import { RolesClaim } from '@tdacorp/identity-authz';
2
+
3
+ /**
4
+ * A successful OAuth2/OIDC token response, camelCased from the wire's
5
+ * snake_case (RFC 6749 §5.1).
6
+ */
7
+ interface TokenSet {
8
+ accessToken: string;
9
+ tokenType: string;
10
+ refreshToken?: string;
11
+ /** Present only when the request's scope included `openid`. */
12
+ idToken?: string;
13
+ /** Lifetime of `accessToken` in seconds from the time of issuance, when
14
+ * the server reports one. */
15
+ expiresIn?: number;
16
+ /** The scope actually granted, space-separated -- may be narrower than
17
+ * what was requested; absent when the server considers it unchanged. */
18
+ scope?: string;
19
+ }
20
+ /** Thrown by `exchangeAuthorizationCode` and `clientCredentialsGrant` on any
21
+ * non-success response. Both are one-shot actions with no live session to
22
+ * protect from a bad classification, unlike `refreshTokens` below, so a
23
+ * plain throw is enough: the caller has nothing to preserve either way. */
24
+ declare class TokenRequestError extends Error {
25
+ readonly status?: number;
26
+ readonly error?: string;
27
+ readonly errorDescription?: string;
28
+ constructor(details: {
29
+ status?: number;
30
+ error?: string;
31
+ errorDescription?: string;
32
+ });
33
+ }
34
+ /** Options for `exchangeAuthorizationCode`. */
35
+ interface AuthorizationCodeExchangeOptions {
36
+ /** The identity server's issuer URL, used to look up `token_endpoint`. */
37
+ issuer: string;
38
+ clientId: string;
39
+ /** Omit for a public client authenticating via PKCE alone. */
40
+ clientSecret?: string;
41
+ /** The `code` query param from the authorization callback. */
42
+ code: string;
43
+ /** Must exactly match the `redirect_uri` used on the authorization
44
+ * request (RFC 6749 §4.1.3). */
45
+ redirectUri: string;
46
+ /** The PKCE verifier generated (and kept) alongside the code challenge
47
+ * sent on the authorization request; see `generateCodeVerifier`. */
48
+ codeVerifier: string;
49
+ }
50
+ /** RFC 6749 §4.1.3 / RFC 7636 §4.5: exchanges an authorization code for a
51
+ * token set. Throws `TokenRequestError` on any non-success response. */
52
+ declare function exchangeAuthorizationCode(options: AuthorizationCodeExchangeOptions): Promise<TokenSet>;
53
+ /** Options for `clientCredentialsGrant`. */
54
+ interface ClientCredentialsGrantOptions {
55
+ /** The identity server's issuer URL, used to look up `token_endpoint`. */
56
+ issuer: string;
57
+ clientId: string;
58
+ /** Required -- this grant has no end user and no PKCE verifier, so the
59
+ * client must authenticate with its own confidential secret. */
60
+ clientSecret: string;
61
+ /** Space-separated scopes to request for the resulting token. */
62
+ scope?: string;
63
+ }
64
+ /** RFC 6749 §4.4: mints a token with no end user, for service-to-service
65
+ * calls. Always confidential -- `clientSecret` is required, not optional,
66
+ * because this grant has no PKCE verifier to authenticate a public client
67
+ * with. Throws `TokenRequestError` on any non-success response. */
68
+ declare function clientCredentialsGrant(options: ClientCredentialsGrantOptions): Promise<TokenSet>;
69
+ /** Options for `refreshTokens`. */
70
+ interface RefreshTokensOptions {
71
+ /** The identity server's issuer URL, used to look up `token_endpoint`. */
72
+ issuer: string;
73
+ clientId: string;
74
+ /** Omit for a public client authenticating via PKCE alone. */
75
+ clientSecret?: string;
76
+ /** The refresh token to redeem. */
77
+ refreshToken: string;
78
+ }
79
+ type TokenRefreshOutcome = {
80
+ outcome: 'success';
81
+ tokens: TokenSet;
82
+ }
83
+ /** The refresh token's status is still unknown. The caller must deny the
84
+ * request this refresh was gating, but must NOT clear the refresh token
85
+ * or any session built on it -- see this function's own docblock. */
86
+ | {
87
+ outcome: 'transient';
88
+ error: unknown;
89
+ }
90
+ /** The refresh token is confirmed dead. Safe, and only now safe, for the
91
+ * caller to clear it. */
92
+ | {
93
+ outcome: 'terminal';
94
+ error: TokenRequestError;
95
+ };
96
+ /**
97
+ * RFC 6749 §6: redeems a refresh token for a new token set, classifying
98
+ * failure as `transient` (retry later, credential still unknown) or
99
+ * `terminal` (the refresh token is confirmed dead) instead of collapsing
100
+ * both into one rejection.
101
+ *
102
+ * WHY THIS DISTINCTION EXISTS, AND WHY IT IS NOT OPTIONAL
103
+ *
104
+ * An infrastructure hiccup must never be treated the same as a rejected
105
+ * credential. A caller that clears a session/cookie on ANY refresh failure
106
+ * will force-log-out every one of its users during a transient outage, or
107
+ * the moment its own client secret is rotated or its client is temporarily
108
+ * disabled -- neither of those says anything about whether the refresh
109
+ * token itself is still good. This function classifies failures against the
110
+ * identity server's own status-code contract for exactly that reason: only
111
+ * a response that is actually a verdict on the refresh token gets to clear
112
+ * it.
113
+ *
114
+ * CLASSIFICATION
115
+ *
116
+ * - `transient`: a 408/429/5xx HTTP status, `fetch` itself throwing (DNS
117
+ * failure, connection refused, TLS error, timeout), or a 401 response. A
118
+ * 401 here is the server rejecting THIS CALLER's own client credentials
119
+ * (`invalid_client`) -- a verdict on the caller, not on the refresh token
120
+ * being redeemed, so the token's status is still unknown just like the
121
+ * other cases in this bucket.
122
+ * - `terminal`: a 400 response with `error: "invalid_grant"` -- RFC 6749
123
+ * §5.2's own code for "the ... refresh token is invalid, expired,
124
+ * revoked". This is the only shape that is actually the server SAYING the
125
+ * refresh token itself is dead.
126
+ * - Anything else (an unrecognised 4xx, a 2xx with a malformed body) defaults
127
+ * to `transient`. This is the safe direction, not a shrug: misclassifying
128
+ * a live refresh token as terminal tells the caller to destroy a working
129
+ * credential and force a real user to sign in again, while misclassifying
130
+ * a dead one as transient only delays cleanup until the next refresh
131
+ * attempt, which is self-correcting and costs nothing but a little
132
+ * staleness.
133
+ */
134
+ declare function refreshTokens(options: RefreshTokensOptions): Promise<TokenRefreshOutcome>;
135
+
136
+ /**
137
+ * What a verified TDACorp Identity access token actually is: a token minted
138
+ * for an end user, or a token minted for a machine with none.
139
+ *
140
+ * Precedent: Clerk's backend SDK returns a discriminated `tokenType` /
141
+ * `acceptsToken` object from its own token verification rather than one flat
142
+ * claims bag, for the same reason this type exists -- a caller that reads a
143
+ * property off the wrong variant should get a compile error, not `undefined`
144
+ * read as "no roles" or a machine's `clientId` misread as a user's `sub`.
145
+ *
146
+ * The concrete reason this matters here: this identity server's
147
+ * `client_credentials` grant mints access tokens with no end user and no
148
+ * `roles` claim at all -- there is no subject for a roles claim to describe.
149
+ * Before this type existed, `verifyAccessToken` returned one flat claims
150
+ * object for both cases, so a caller checking `.sub` on a machine token
151
+ * silently got `undefined` instead of a type error telling it this branch
152
+ * has no subject.
153
+ */
154
+ type VerifiedAccessToken = {
155
+ kind: 'user';
156
+ sub: string;
157
+ /** Present only when the token was minted with the `roles` scope.
158
+ * Pass to `permits()` from `@tdacorp/identity-authz` to decide a
159
+ * permission locally. */
160
+ roles?: RolesClaim;
161
+ claims: Record<string, unknown>;
162
+ } | {
163
+ kind: 'machine';
164
+ clientId: string;
165
+ claims: Record<string, unknown>;
166
+ };
167
+
168
+ /**
169
+ * Allowed clock skew between this process and the identity server, applied
170
+ * via jose's `clockTolerance`.
171
+ *
172
+ * 60 seconds, matching Auth0's own documented default leeway for ID token
173
+ * validation (60000ms, e.g. Auth0's Lock.swift and Auth0.js configuration
174
+ * docs) and the conventional default several JWT libraries ship
175
+ * (`jsonwebtoken`'s own README recommendation, Spring Security's
176
+ * `JwtTimestampValidator`). Clerk's backend SDK is tighter, defaulting
177
+ * `clockSkewInMs` to 5000 (5s) -- verified against Clerk's own docs. 60s is
178
+ * chosen over Clerk's tighter figure deliberately: this is a first-party,
179
+ * low-latency deployment (the identity server and every relying party share
180
+ * infrastructure-grade NTP-synced clocks), so the risk a looser tolerance
181
+ * trades away -- a stolen, still-live token being replayed slightly past its
182
+ * `exp` -- is small next to the cost of a false rejection from ordinary
183
+ * clock drift, and 60s is not a novel number invented for this package: it
184
+ * is the figure Auth0 itself ships as a default.
185
+ */
186
+ declare const CLOCK_SKEW_TOLERANCE_SECONDS = 60;
187
+ /**
188
+ * Why `verifyIdToken`/`verifyAccessToken` could not produce a verified
189
+ * claims payload. Every value here is a property of the TOKEN itself, not a
190
+ * caller programming error -- a missing `options.issuer`/`options.audience`
191
+ * throws instead (see `requireConfig`).
192
+ *
193
+ * - `'expired'`: the token's `exp` has passed, even after
194
+ * `CLOCK_SKEW_TOLERANCE_SECONDS` of leeway.
195
+ * - `'invalid-signature'`: the signature does not verify against any key in
196
+ * the issuer's JWKS (wrong key, tampered token, or an ambiguous match).
197
+ * - `'invalid-issuer'`: the token's `iss` does not match `options.issuer`.
198
+ * - `'invalid-audience'`: the token's `aud` does not match `options.audience`.
199
+ * - `'unauthorized-party'`: `options.authorizedParties` was given and the
200
+ * token's `azp` (or single-string `aud` fallback) is not in it -- see
201
+ * `VerifyTokenOptions.authorizedParties`'s own docblock for why this
202
+ * check exists alongside the plain audience check.
203
+ * - `'malformed-claims'`: the token verified but is missing a claim this
204
+ * package requires (e.g. `sub` on an id_token).
205
+ * - `'malformed-token'`: the token is not a well-formed JWT at all.
206
+ * - `'jwks-unavailable'`: the issuer's discovery document or JWKS could not
207
+ * be fetched -- infrastructure, not a verdict on the token.
208
+ */
209
+ type TokenVerificationErrorCode = 'expired' | 'invalid-signature' | 'invalid-issuer' | 'invalid-audience' | 'unauthorized-party' | 'malformed-claims' | 'malformed-token' | 'jwks-unavailable';
210
+ /** One verification failure: a stable `code` to branch on, plus a
211
+ * human-readable `message` for logs. `verifyIdToken`/`verifyAccessToken`
212
+ * return a list of these rather than throwing -- see `VerificationResult`. */
213
+ interface TokenVerificationError {
214
+ code: TokenVerificationErrorCode;
215
+ message: string;
216
+ }
217
+ /**
218
+ * The outcome of `verifyIdToken`/`verifyAccessToken`: either the verified,
219
+ * typed claims (`success: true`), or the reasons verification failed
220
+ * (`success: false`). A discriminated union rather than a thrown error, on
221
+ * the same reasoning as `Permit` in `@tdacorp/identity-authz`: an expected
222
+ * verification failure (expired, bad signature, wrong audience) is data a
223
+ * caller branches on, not an exceptional condition -- only a genuine
224
+ * programming error (missing `options.issuer`/`options.audience`) throws.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * const result = await verifyIdToken(idToken, { issuer, audience })
229
+ * if (result.success) {
230
+ * console.log(result.data.sub)
231
+ * } else {
232
+ * console.warn(result.errors.map((e) => e.code))
233
+ * }
234
+ * ```
235
+ */
236
+ type VerificationResult<T> = {
237
+ success: true;
238
+ data: T;
239
+ } | {
240
+ success: false;
241
+ errors: TokenVerificationError[];
242
+ };
243
+ /** Configuration shared by `verifyIdToken` and `verifyAccessToken`: which
244
+ * issuer and audience a token must match, plus an optional
245
+ * `authorizedParties` allowlist (see that field's own docblock below). */
246
+ interface VerifyTokenOptions {
247
+ issuer: string;
248
+ audience: string;
249
+ /**
250
+ * Origins/client ids this caller trusts a token to have been issued to,
251
+ * checked against the token's `azp` claim when present, falling back to a
252
+ * string `aud` otherwise.
253
+ *
254
+ * WHY `azp` FIRST: this identity server always sets `azp` to the
255
+ * requesting client's own id, even for an access token whose `aud` was
256
+ * overridden to an RFC 8707 resource indicator -- `azp` keeps naming who
257
+ * the token was issued to regardless of what `aud` also claims to be valid
258
+ * at. `aud` is only a safe fallback when it is a single string naming one
259
+ * party; this package does not attempt the allowlist check against a
260
+ * multi-valued `aud`.
261
+ *
262
+ * WHY THIS CHECK EXISTS AT ALL: every TDACorp product shares the
263
+ * `tdacorp.in` registrable domain, and this server's `aud`/`azp` values
264
+ * are opaque client ids, not origins that a browser's same-origin policy
265
+ * already separates. A token minted for one product is not otherwise
266
+ * prevented from being replayed against another product that shares this
267
+ * issuer -- both the plain `audience` check above and this allowlist are
268
+ * needed, and they check different things: one asks whether the token is
269
+ * FOR this resource server, the other asks whether it was issued THROUGH
270
+ * a client this caller has chosen to trust.
271
+ */
272
+ authorizedParties?: string[];
273
+ }
274
+ /**
275
+ * The verified claims of an OIDC id_token, as returned by `verifyIdToken`.
276
+ *
277
+ * Only the claims every id_token carries per OpenID Connect Core 1.0 §2 are
278
+ * typed here (`sub`, `iss`, `aud`, `exp`, `iat`); anything else the identity
279
+ * server includes (e.g. `email`, `name`) still comes through on the object,
280
+ * covered by the index signature, just not individually typed.
281
+ */
282
+ interface IdTokenClaims extends Record<string, unknown> {
283
+ sub: string;
284
+ iss: string;
285
+ aud: string | string[];
286
+ exp: number;
287
+ iat: number;
288
+ }
289
+ /** Verifies an OIDC id_token's signature, issuer, audience and expiry (with
290
+ * the clock-skew tolerance documented on `CLOCK_SKEW_TOLERANCE_SECONDS`).
291
+ * Never throws for an expected verification failure -- expired, bad
292
+ * signature, wrong issuer/audience -- returning `{ success: false, errors
293
+ * }` instead. Throws only for missing required config (`options.issuer` /
294
+ * `options.audience`), which is a programming error, not a property of the
295
+ * token. */
296
+ declare function verifyIdToken(idToken: string, options: VerifyTokenOptions): Promise<VerificationResult<IdTokenClaims>>;
297
+ /** Verifies an access token the same way `verifyIdToken` does, then shapes
298
+ * the result as a `VerifiedAccessToken` (see that type's own docblock for
299
+ * why user and machine tokens are a discriminated union rather than one
300
+ * flat claims bag) with the `roles` claim, if present, typed as
301
+ * `RolesClaim` from `@tdacorp/identity-authz` rather than left as an
302
+ * unparsed field the caller has to know to reach for. Same throw/return
303
+ * contract as `verifyIdToken`. */
304
+ declare function verifyAccessToken(accessToken: string, options: VerifyTokenOptions): Promise<VerificationResult<VerifiedAccessToken>>;
305
+
306
+ export { type AuthorizationCodeExchangeOptions as A, CLOCK_SKEW_TOLERANCE_SECONDS as C, type IdTokenClaims as I, type RefreshTokensOptions as R, type TokenRefreshOutcome as T, type VerificationResult as V, type ClientCredentialsGrantOptions as a, TokenRequestError as b, type TokenSet as c, type TokenVerificationError as d, type TokenVerificationErrorCode as e, type VerifiedAccessToken as f, type VerifyTokenOptions as g, clientCredentialsGrant as h, exchangeAuthorizationCode as i, verifyIdToken as j, refreshTokens as r, verifyAccessToken as v };