oauth4webapi 2.4.5 → 2.6.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.
- package/README.md +3 -3
- package/build/index.d.ts +208 -5
- package/build/index.js +209 -62
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ import * as oauth2 from 'oauth4webapi'
|
|
|
43
43
|
**`example`** Deno import
|
|
44
44
|
|
|
45
45
|
```js
|
|
46
|
-
import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.
|
|
46
|
+
import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.6.0/mod.ts'
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
- Authorization Code Flow - OpenID Connect [source](examples/code.ts), or plain OAuth 2 [source](examples/oauth.ts)
|
|
@@ -53,7 +53,8 @@ import * as oauth2 from 'https://deno.land/x/oauth4webapi@v2.4.5/mod.ts'
|
|
|
53
53
|
- Pushed Authorization Request (PAR) - [source](examples/par.ts) | [diff from code flow](examples/par.diff)
|
|
54
54
|
- Client Credentials Grant - [source](examples/client_credentials.ts)
|
|
55
55
|
- Device Authorization Grant - [source](examples/device_authorization_grant.ts)
|
|
56
|
-
- FAPI
|
|
56
|
+
- FAPI 1.0 Advanced (Private Key JWT, MTLS, JAR) - [source](examples/fapi1-advanced.ts)
|
|
57
|
+
- FAPI 2.0 Security Profile (Private Key JWT, PAR, DPoP) - [source](examples/fapi2.ts)
|
|
57
58
|
- FAPI 2.0 Message Signing (Private Key JWT, PAR, DPoP, JAR, JARM) - [source](examples/fapi2-message-signing.ts) | [diff](examples/fapi2-message-signing.diff)
|
|
58
59
|
|
|
59
60
|
## Supported Runtimes
|
|
@@ -74,6 +75,5 @@ The following features are currently out of scope:
|
|
|
74
75
|
|
|
75
76
|
- CommonJS
|
|
76
77
|
- Implicit, Hybrid, and Resource Owner Password Credentials Flows
|
|
77
|
-
- Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
|
|
78
78
|
- JSON Web Encryption (JWE)
|
|
79
79
|
- Automatic polyfills of any kind
|
package/build/index.d.ts
CHANGED
|
@@ -129,6 +129,166 @@ export type ClientAuthenticationMethod = 'client_secret_basic' | 'client_secret_
|
|
|
129
129
|
export type JWSAlgorithm = 'PS256' | 'ES256' | 'RS256' | 'EdDSA' | 'ES384' | 'PS384' | 'RS384' | 'ES512' | 'PS512' | 'RS512';
|
|
130
130
|
export declare const clockSkew: unique symbol;
|
|
131
131
|
export declare const clockTolerance: unique symbol;
|
|
132
|
+
/**
|
|
133
|
+
* This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
|
|
134
|
+
* compatible changes or removal may occur in any future release.
|
|
135
|
+
*
|
|
136
|
+
* When configured on an interface that extends {@link HttpRequestOptions}, that's every `options`
|
|
137
|
+
* parameter for functions that trigger HTTP Requests, this replaces the use of global fetch. As a
|
|
138
|
+
* fetch replacement the arguments and expected return are the same as fetch.
|
|
139
|
+
*
|
|
140
|
+
* In theory any module that claims to be compatible with the Fetch API can be used but your mileage
|
|
141
|
+
* may vary. No workarounds to allow use of non-conform {@link Response}s will be considered.
|
|
142
|
+
*
|
|
143
|
+
* If you only need to update the {@link Request} properties you do not need to use a Fetch API
|
|
144
|
+
* module, just change what you need and pass it to globalThis.fetch just like this module would
|
|
145
|
+
* normally do.
|
|
146
|
+
*
|
|
147
|
+
* Its intended use cases are:
|
|
148
|
+
*
|
|
149
|
+
* - {@link Request}/{@link Response} tracing and logging
|
|
150
|
+
* - Custom caching strategies for responses of Authorization Server Metadata and JSON Web Key Set
|
|
151
|
+
* (JWKS) endpoints
|
|
152
|
+
* - Changing the {@link Request} properties like headers, body, credentials, mode before it is passed
|
|
153
|
+
* to fetch
|
|
154
|
+
*
|
|
155
|
+
* Known caveats:
|
|
156
|
+
*
|
|
157
|
+
* - Expect Type-related issues when passing the inputs through to fetch-like modules, they hardly
|
|
158
|
+
* ever get their typings inline with actual fetch, you should `@ts-expect-error` them.
|
|
159
|
+
* - Returning self-constructed {@link Response} instances prohibits AS-signalled DPoP Nonce caching.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
*
|
|
163
|
+
* Using [sindresorhus/ky](https://github.com/sindresorhus/ky) hooks feature for logging outgoing
|
|
164
|
+
* requests and their responses.
|
|
165
|
+
*
|
|
166
|
+
* ```js
|
|
167
|
+
* import ky from 'ky'
|
|
168
|
+
* import * as oauth from 'oauth4webapi'
|
|
169
|
+
*
|
|
170
|
+
* // example use
|
|
171
|
+
* await oauth.discoveryRequest(new URL('https://as.example.com'), {
|
|
172
|
+
* [oauth.experimental_customFetch]: (...args) =>
|
|
173
|
+
* ky(args[0], {
|
|
174
|
+
* ...args[1],
|
|
175
|
+
* hooks: {
|
|
176
|
+
* beforeRequest: [
|
|
177
|
+
* (request) => {
|
|
178
|
+
* logRequest(request)
|
|
179
|
+
* },
|
|
180
|
+
* ],
|
|
181
|
+
* beforeRetry: [
|
|
182
|
+
* ({ request, error, retryCount }) => {
|
|
183
|
+
* logRetry(request, error, retryCount)
|
|
184
|
+
* },
|
|
185
|
+
* ],
|
|
186
|
+
* afterResponse: [
|
|
187
|
+
* (request, _, response) => {
|
|
188
|
+
* logResponse(request, response)
|
|
189
|
+
* },
|
|
190
|
+
* ],
|
|
191
|
+
* },
|
|
192
|
+
* }),
|
|
193
|
+
* })
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
*
|
|
198
|
+
* Using [nodejs/undici](https://github.com/nodejs/undici) for mocking.
|
|
199
|
+
*
|
|
200
|
+
* ```js
|
|
201
|
+
* import * as undici from 'undici'
|
|
202
|
+
* import * as oauth from 'oauth4webapi'
|
|
203
|
+
*
|
|
204
|
+
* const mockAgent = new undici.MockAgent()
|
|
205
|
+
* mockAgent.disableNetConnect()
|
|
206
|
+
* undici.setGlobalDispatcher(mockAgent)
|
|
207
|
+
*
|
|
208
|
+
* // continue as per undici documentation
|
|
209
|
+
* // https://github.com/nodejs/undici/blob/v6.2.1/docs/api/MockAgent.md#example---basic-mocked-request
|
|
210
|
+
*
|
|
211
|
+
* // example use
|
|
212
|
+
* await oauth.discoveryRequest(new URL('https://as.example.com'), {
|
|
213
|
+
* [oauth.experimental_customFetch]: undici.fetch,
|
|
214
|
+
* })
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* @group Experimental
|
|
218
|
+
*/
|
|
219
|
+
export declare const experimental_customFetch: unique symbol;
|
|
220
|
+
/** @ignore */
|
|
221
|
+
export declare const experimentalCustomFetch: symbol;
|
|
222
|
+
/**
|
|
223
|
+
* This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
|
|
224
|
+
* compatible changes or removal may occur in any future release.
|
|
225
|
+
*
|
|
226
|
+
* When combined with {@link experimental_customFetch} (to use a Fetch API implementation that
|
|
227
|
+
* supports client certificates) this can be used to target FAPI 2.0 profiles that utilize
|
|
228
|
+
* Mutual-TLS for either client authentication or sender constraining. FAPI 1.0 Advanced profiles
|
|
229
|
+
* that use PAR and JARM can also be targetted.
|
|
230
|
+
*
|
|
231
|
+
* When configured on an interface that extends {@link ExperimentalUseMTLSAliasOptions} this makes
|
|
232
|
+
* the client prioritize an endpoint URL present in
|
|
233
|
+
* {@link AuthorizationServer.mtls_endpoint_aliases `as.mtls_endpoint_aliases`}.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
*
|
|
237
|
+
* (Node.js) Using [nodejs/undici](https://github.com/nodejs/undici) for Mutual-TLS Client
|
|
238
|
+
* Authentication and Certificate-Bound Access Tokens support.
|
|
239
|
+
*
|
|
240
|
+
* ```js
|
|
241
|
+
* import * as undici from 'undici'
|
|
242
|
+
* import * as oauth from 'oauth4webapi'
|
|
243
|
+
*
|
|
244
|
+
* const response = await oauth.pushedAuthorizationRequest(as, client, params, {
|
|
245
|
+
* [oauth.experimental_useMtlsAlias]: true,
|
|
246
|
+
* [oauth.experimental_customFetch]: (...args) => {
|
|
247
|
+
* return undici.fetch(args[0], {
|
|
248
|
+
* ...args[1],
|
|
249
|
+
* dispatcher: new undici.Agent({
|
|
250
|
+
* connect: {
|
|
251
|
+
* key: clientKey,
|
|
252
|
+
* cert: clientCertificate,
|
|
253
|
+
* },
|
|
254
|
+
* }),
|
|
255
|
+
* })
|
|
256
|
+
* },
|
|
257
|
+
* })
|
|
258
|
+
* ```
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
*
|
|
262
|
+
* (Deno) Using Deno.createHttpClient API for Mutual-TLS Client Authentication and Certificate-Bound
|
|
263
|
+
* Access Tokens support. This is currently (Jan 2023) locked behind the --unstable command line
|
|
264
|
+
* flag.
|
|
265
|
+
*
|
|
266
|
+
* ```js
|
|
267
|
+
* import * as oauth from 'oauth4webapi'
|
|
268
|
+
*
|
|
269
|
+
* const agent = Deno.createHttpClient({
|
|
270
|
+
* certChain: clientCertificate,
|
|
271
|
+
* privateKey: clientKey,
|
|
272
|
+
* })
|
|
273
|
+
*
|
|
274
|
+
* const response = await oauth.pushedAuthorizationRequest(as, client, params, {
|
|
275
|
+
* [oauth.experimental_useMtlsAlias]: true,
|
|
276
|
+
* [oauth.experimental_customFetch]: (...args) => {
|
|
277
|
+
* return fetch(args[0], {
|
|
278
|
+
* ...args[1],
|
|
279
|
+
* client: agent,
|
|
280
|
+
* })
|
|
281
|
+
* },
|
|
282
|
+
* })
|
|
283
|
+
* ```
|
|
284
|
+
*
|
|
285
|
+
* @group Experimental
|
|
286
|
+
*
|
|
287
|
+
* @see [RFC 8705 - OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens](https://www.rfc-editor.org/rfc/rfc8705.html)
|
|
288
|
+
*/
|
|
289
|
+
export declare const experimental_useMtlsAlias: unique symbol;
|
|
290
|
+
/** @ignore */
|
|
291
|
+
export declare const experimentalUseMtlsAlias: symbol;
|
|
132
292
|
/**
|
|
133
293
|
* Authorization Server Metadata
|
|
134
294
|
*
|
|
@@ -522,11 +682,17 @@ export interface HttpRequestOptions {
|
|
|
522
682
|
* ```
|
|
523
683
|
*/
|
|
524
684
|
signal?: (() => AbortSignal) | AbortSignal;
|
|
685
|
+
/** Headers to additionally send with the HTTP Request(s) triggered by this function's invocation. */
|
|
686
|
+
headers?: [string, string][] | Record<string, string> | Headers;
|
|
525
687
|
/**
|
|
526
|
-
*
|
|
527
|
-
*
|
|
688
|
+
* This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
|
|
689
|
+
* compatible changes or removal may occur in any future release.
|
|
690
|
+
*
|
|
691
|
+
* See {@link experimental_customFetch} for its documentation.
|
|
692
|
+
*
|
|
693
|
+
* @group Experimental
|
|
528
694
|
*/
|
|
529
|
-
|
|
695
|
+
[experimental_customFetch]?: typeof fetch;
|
|
530
696
|
}
|
|
531
697
|
export interface DiscoveryRequestOptions extends HttpRequestOptions {
|
|
532
698
|
/** The issuer transformation algorithm to use. */
|
|
@@ -625,7 +791,18 @@ export interface DPoPRequestOptions {
|
|
|
625
791
|
/** DPoP-related options. */
|
|
626
792
|
DPoP?: DPoPOptions;
|
|
627
793
|
}
|
|
628
|
-
export interface
|
|
794
|
+
export interface ExperimentalUseMTLSAliasOptions {
|
|
795
|
+
/**
|
|
796
|
+
* This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
|
|
797
|
+
* compatible changes or removal may occur in any future release.
|
|
798
|
+
*
|
|
799
|
+
* See {@link experimental_useMtlsAlias} for its documentation.
|
|
800
|
+
*
|
|
801
|
+
* @group Experimental
|
|
802
|
+
*/
|
|
803
|
+
[experimental_useMtlsAlias]?: boolean;
|
|
804
|
+
}
|
|
805
|
+
export interface AuthenticatedRequestOptions extends ExperimentalUseMTLSAliasOptions {
|
|
629
806
|
/**
|
|
630
807
|
* Private key to use for `private_key_jwt`
|
|
631
808
|
* {@link ClientAuthenticationMethod client authentication}. Its algorithm must be compatible with
|
|
@@ -767,7 +944,7 @@ export interface ProtectedResourceRequestOptions extends Omit<HttpRequestOptions
|
|
|
767
944
|
* @see [RFC 9449 - OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)](https://www.rfc-editor.org/rfc/rfc9449.html#name-protected-resource-access)
|
|
768
945
|
*/
|
|
769
946
|
export declare function protectedResourceRequest(accessToken: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | string, url: URL, headers: Headers, body?: ReadableStream | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | string | null, options?: ProtectedResourceRequestOptions): Promise<Response>;
|
|
770
|
-
export interface UserInfoRequestOptions extends HttpRequestOptions, DPoPRequestOptions {
|
|
947
|
+
export interface UserInfoRequestOptions extends HttpRequestOptions, DPoPRequestOptions, ExperimentalUseMTLSAliasOptions {
|
|
771
948
|
}
|
|
772
949
|
/**
|
|
773
950
|
* Performs a UserInfo Request at the
|
|
@@ -1184,6 +1361,32 @@ export declare function processIntrospectionResponse(as: AuthorizationServer, cl
|
|
|
1184
1361
|
* @see [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM)](https://openid.net/specs/openid-financial-api-jarm.html)
|
|
1185
1362
|
*/
|
|
1186
1363
|
export declare function validateJwtAuthResponse(as: AuthorizationServer, client: Client, parameters: URLSearchParams | URL, expectedState?: string | typeof expectNoState | typeof skipStateCheck, options?: HttpRequestOptions): Promise<URLSearchParams | OAuth2Error>;
|
|
1364
|
+
/**
|
|
1365
|
+
* This is an experimental feature, it is not subject to semantic versioning rules. Non-backward
|
|
1366
|
+
* compatible changes or removal may occur in any future release.
|
|
1367
|
+
*
|
|
1368
|
+
* Same as {@link validateAuthResponse} but for FAPI 1.0 Advanced Detached Signature authorization
|
|
1369
|
+
* responses.
|
|
1370
|
+
*
|
|
1371
|
+
* @param as Authorization Server Metadata.
|
|
1372
|
+
* @param client Client Metadata.
|
|
1373
|
+
* @param parameters Authorization Response.
|
|
1374
|
+
* @param expectedNonce Expected ID Token `nonce` claim value.
|
|
1375
|
+
* @param expectedState Expected `state` parameter value. Default is {@link expectNoState}.
|
|
1376
|
+
* @param maxAge ID Token {@link IDToken.auth_time `auth_time`} claim value will be checked to be
|
|
1377
|
+
* present and conform to the `maxAge` value. Use of this option is required if you sent a
|
|
1378
|
+
* `max_age` parameter in an authorization request. Default is
|
|
1379
|
+
* {@link Client.default_max_age `client.default_max_age`} and falls back to
|
|
1380
|
+
* {@link skipAuthTimeCheck}.
|
|
1381
|
+
*
|
|
1382
|
+
* @returns Validated Authorization Response parameters or Authorization Error Response.
|
|
1383
|
+
*
|
|
1384
|
+
* @group FAPI 1.0 Advanced
|
|
1385
|
+
* @group Experimental
|
|
1386
|
+
*
|
|
1387
|
+
* @see [Financial-grade API Security Profile 1.0 - Part 2: Advanced](https://openid.net/specs/openid-financial-api-part-2-1_0.html#id-token-as-detached-signature)
|
|
1388
|
+
*/
|
|
1389
|
+
export declare function experimental_validateDetachedSignatureResponse(as: AuthorizationServer, client: Client, parameters: URLSearchParams, expectedNonce: string, expectedState?: string | typeof expectNoState, maxAge?: number | typeof skipAuthTimeCheck, options?: HttpRequestOptions): Promise<URLSearchParams | OAuth2Error>;
|
|
1187
1390
|
/**
|
|
1188
1391
|
* DANGER ZONE
|
|
1189
1392
|
*
|
package/build/index.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
let USER_AGENT;
|
|
2
2
|
if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
|
|
3
3
|
const NAME = 'oauth4webapi';
|
|
4
|
-
const VERSION = 'v2.
|
|
4
|
+
const VERSION = 'v2.6.0';
|
|
5
5
|
USER_AGENT = `${NAME}/${VERSION}`;
|
|
6
6
|
}
|
|
7
|
+
function looseInstanceOf(input, expected) {
|
|
8
|
+
if (input == null) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return (input instanceof expected ||
|
|
13
|
+
Object.getPrototypeOf(input)[Symbol.toStringTag] === expected.prototype[Symbol.toStringTag]);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
7
19
|
export const clockSkew = Symbol();
|
|
8
20
|
export const clockTolerance = Symbol();
|
|
21
|
+
export const experimental_customFetch = Symbol();
|
|
22
|
+
export const experimentalCustomFetch = experimental_customFetch;
|
|
23
|
+
export const experimental_useMtlsAlias = Symbol();
|
|
24
|
+
export const experimentalUseMtlsAlias = experimental_useMtlsAlias;
|
|
9
25
|
const encoder = new TextEncoder();
|
|
10
26
|
const decoder = new TextDecoder();
|
|
11
27
|
function buf(input) {
|
|
@@ -148,8 +164,8 @@ function isJsonObject(input) {
|
|
|
148
164
|
return true;
|
|
149
165
|
}
|
|
150
166
|
function prepareHeaders(input) {
|
|
151
|
-
if (
|
|
152
|
-
|
|
167
|
+
if (looseInstanceOf(input, Headers)) {
|
|
168
|
+
input = Object.fromEntries(input.entries());
|
|
153
169
|
}
|
|
154
170
|
const headers = new Headers(input);
|
|
155
171
|
if (USER_AGENT && !headers.has('user-agent')) {
|
|
@@ -198,13 +214,12 @@ export async function discoveryRequest(issuerIdentifier, options) {
|
|
|
198
214
|
}
|
|
199
215
|
const headers = prepareHeaders(options?.headers);
|
|
200
216
|
headers.set('accept', 'application/json');
|
|
201
|
-
|
|
202
|
-
headers,
|
|
217
|
+
return (options?.[experimental_customFetch] || fetch)(url.href, {
|
|
218
|
+
headers: Object.fromEntries(headers.entries()),
|
|
203
219
|
method: 'GET',
|
|
204
220
|
redirect: 'manual',
|
|
205
221
|
signal: options?.signal ? signal(options.signal) : null,
|
|
206
|
-
});
|
|
207
|
-
return fetch(request).then(processDpopNonce);
|
|
222
|
+
}).then(processDpopNonce);
|
|
208
223
|
}
|
|
209
224
|
function validateString(input) {
|
|
210
225
|
return typeof input === 'string' && input.length !== 0;
|
|
@@ -213,7 +228,7 @@ export async function processDiscoveryResponse(expectedIssuerIdentifier, respons
|
|
|
213
228
|
if (!(expectedIssuerIdentifier instanceof URL)) {
|
|
214
229
|
throw new TypeError('"expectedIssuer" must be an instance of URL');
|
|
215
230
|
}
|
|
216
|
-
if (!(response
|
|
231
|
+
if (!looseInstanceOf(response, Response)) {
|
|
217
232
|
throw new TypeError('"response" must be an instance of Response');
|
|
218
233
|
}
|
|
219
234
|
if (response.status !== 200) {
|
|
@@ -427,9 +442,11 @@ async function clientAuthentication(as, client, body, headers, clientPrivateKey)
|
|
|
427
442
|
body.set('client_assertion', await privateKeyJwt(as, client, key, kid));
|
|
428
443
|
break;
|
|
429
444
|
}
|
|
445
|
+
case 'tls_client_auth':
|
|
446
|
+
case 'self_signed_tls_client_auth':
|
|
430
447
|
case 'none': {
|
|
431
|
-
assertNoClientSecret(
|
|
432
|
-
assertNoClientPrivateKey(
|
|
448
|
+
assertNoClientSecret(client.token_endpoint_auth_method, client.client_secret);
|
|
449
|
+
assertNoClientPrivateKey(client.token_endpoint_auth_method, clientPrivateKey);
|
|
433
450
|
body.set('client_id', client.client_id);
|
|
434
451
|
break;
|
|
435
452
|
}
|
|
@@ -531,13 +548,29 @@ async function publicJwk(key) {
|
|
|
531
548
|
jwkCache.set(key, jwk);
|
|
532
549
|
return jwk;
|
|
533
550
|
}
|
|
551
|
+
function validateEndpoint(value, endpoint, options) {
|
|
552
|
+
if (typeof value !== 'string') {
|
|
553
|
+
if (options?.[experimental_useMtlsAlias]) {
|
|
554
|
+
throw new TypeError(`"as.mtls_endpoint_aliases.${endpoint}" must be a string`);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
throw new TypeError(`"as.${endpoint}" must be a string`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return new URL(value);
|
|
561
|
+
}
|
|
562
|
+
function resolveEndpoint(as, endpoint, options) {
|
|
563
|
+
if (options?.[experimental_useMtlsAlias] &&
|
|
564
|
+
as.mtls_endpoint_aliases &&
|
|
565
|
+
endpoint in as.mtls_endpoint_aliases) {
|
|
566
|
+
return validateEndpoint(as.mtls_endpoint_aliases[endpoint], endpoint, options);
|
|
567
|
+
}
|
|
568
|
+
return validateEndpoint(as[endpoint], endpoint);
|
|
569
|
+
}
|
|
534
570
|
export async function pushedAuthorizationRequest(as, client, parameters, options) {
|
|
535
571
|
assertAs(as);
|
|
536
572
|
assertClient(client);
|
|
537
|
-
|
|
538
|
-
throw new TypeError('"as.pushed_authorization_request_endpoint" must be a string');
|
|
539
|
-
}
|
|
540
|
-
const url = new URL(as.pushed_authorization_request_endpoint);
|
|
573
|
+
const url = resolveEndpoint(as, 'pushed_authorization_request_endpoint', options);
|
|
541
574
|
const body = new URLSearchParams(parameters);
|
|
542
575
|
body.set('client_id', client.client_id);
|
|
543
576
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -585,7 +618,7 @@ function wwwAuth(scheme, params) {
|
|
|
585
618
|
};
|
|
586
619
|
}
|
|
587
620
|
export function parseWwwAuthenticateChallenges(response) {
|
|
588
|
-
if (!(response
|
|
621
|
+
if (!looseInstanceOf(response, Response)) {
|
|
589
622
|
throw new TypeError('"response" must be an instance of Response');
|
|
590
623
|
}
|
|
591
624
|
if (!response.headers.has('www-authenticate')) {
|
|
@@ -615,7 +648,7 @@ export function parseWwwAuthenticateChallenges(response) {
|
|
|
615
648
|
export async function processPushedAuthorizationResponse(as, client, response) {
|
|
616
649
|
assertAs(as);
|
|
617
650
|
assertClient(client);
|
|
618
|
-
if (!(response
|
|
651
|
+
if (!looseInstanceOf(response, Response)) {
|
|
619
652
|
throw new TypeError('"response" must be an instance of Response');
|
|
620
653
|
}
|
|
621
654
|
if (response.status !== 201) {
|
|
@@ -659,22 +692,18 @@ export async function protectedResourceRequest(accessToken, method, url, headers
|
|
|
659
692
|
await dpopProofJwt(headers, options.DPoP, url, 'GET', getClockSkew({ [clockSkew]: options?.[clockSkew] }), accessToken);
|
|
660
693
|
headers.set('authorization', `DPoP ${accessToken}`);
|
|
661
694
|
}
|
|
662
|
-
|
|
695
|
+
return (options?.[experimental_customFetch] || fetch)(url.href, {
|
|
663
696
|
body,
|
|
664
|
-
headers,
|
|
697
|
+
headers: Object.fromEntries(headers.entries()),
|
|
665
698
|
method,
|
|
666
699
|
redirect: 'manual',
|
|
667
700
|
signal: options?.signal ? signal(options.signal) : null,
|
|
668
|
-
});
|
|
669
|
-
return fetch(request).then(processDpopNonce);
|
|
701
|
+
}).then(processDpopNonce);
|
|
670
702
|
}
|
|
671
703
|
export async function userInfoRequest(as, client, accessToken, options) {
|
|
672
704
|
assertAs(as);
|
|
673
705
|
assertClient(client);
|
|
674
|
-
|
|
675
|
-
throw new TypeError('"as.userinfo_endpoint" must be a string');
|
|
676
|
-
}
|
|
677
|
-
const url = new URL(as.userinfo_endpoint);
|
|
706
|
+
const url = resolveEndpoint(as, 'userinfo_endpoint', options);
|
|
678
707
|
const headers = prepareHeaders(options?.headers);
|
|
679
708
|
if (client.userinfo_signed_response_alg) {
|
|
680
709
|
headers.set('accept', 'application/jwt');
|
|
@@ -778,7 +807,7 @@ function getContentType(response) {
|
|
|
778
807
|
export async function processUserInfoResponse(as, client, expectedSubject, response) {
|
|
779
808
|
assertAs(as);
|
|
780
809
|
assertClient(client);
|
|
781
|
-
if (!(response
|
|
810
|
+
if (!looseInstanceOf(response, Response)) {
|
|
782
811
|
throw new TypeError('"response" must be an instance of Response');
|
|
783
812
|
}
|
|
784
813
|
if (response.status !== 200) {
|
|
@@ -826,20 +855,16 @@ export async function processUserInfoResponse(as, client, expectedSubject, respo
|
|
|
826
855
|
async function authenticatedRequest(as, client, method, url, body, headers, options) {
|
|
827
856
|
await clientAuthentication(as, client, body, headers, options?.clientPrivateKey);
|
|
828
857
|
headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
|
829
|
-
|
|
858
|
+
return (options?.[experimental_customFetch] || fetch)(url.href, {
|
|
830
859
|
body,
|
|
831
|
-
headers,
|
|
860
|
+
headers: Object.fromEntries(headers.entries()),
|
|
832
861
|
method,
|
|
833
862
|
redirect: 'manual',
|
|
834
863
|
signal: options?.signal ? signal(options.signal) : null,
|
|
835
|
-
});
|
|
836
|
-
return fetch(request).then(processDpopNonce);
|
|
864
|
+
}).then(processDpopNonce);
|
|
837
865
|
}
|
|
838
866
|
async function tokenEndpointRequest(as, client, grantType, parameters, options) {
|
|
839
|
-
|
|
840
|
-
throw new TypeError('"as.token_endpoint" must be a string');
|
|
841
|
-
}
|
|
842
|
-
const url = new URL(as.token_endpoint);
|
|
867
|
+
const url = resolveEndpoint(as, 'token_endpoint', options);
|
|
843
868
|
parameters.set('grant_type', grantType);
|
|
844
869
|
const headers = prepareHeaders(options?.headers);
|
|
845
870
|
headers.set('accept', 'application/json');
|
|
@@ -872,7 +897,7 @@ export function getValidatedIdTokenClaims(ref) {
|
|
|
872
897
|
async function processGenericAccessTokenResponse(as, client, response, ignoreIdToken = false, ignoreRefreshToken = false) {
|
|
873
898
|
assertAs(as);
|
|
874
899
|
assertClient(client);
|
|
875
|
-
if (!(response
|
|
900
|
+
if (!looseInstanceOf(response, Response)) {
|
|
876
901
|
throw new TypeError('"response" must be an instance of Response');
|
|
877
902
|
}
|
|
878
903
|
if (response.status !== 200) {
|
|
@@ -994,17 +1019,20 @@ export async function authorizationCodeGrantRequest(as, client, callbackParamete
|
|
|
994
1019
|
parameters.set('code', code);
|
|
995
1020
|
return tokenEndpointRequest(as, client, 'authorization_code', parameters, options);
|
|
996
1021
|
}
|
|
997
|
-
const
|
|
1022
|
+
const idTokenClaimNames = {
|
|
998
1023
|
aud: 'audience',
|
|
999
1024
|
exp: 'expiration time',
|
|
1000
1025
|
iat: 'issued at',
|
|
1001
1026
|
iss: 'issuer',
|
|
1002
1027
|
sub: 'subject',
|
|
1028
|
+
nonce: 'nonce',
|
|
1029
|
+
s_hash: 'state hash',
|
|
1030
|
+
c_hash: 'code hash',
|
|
1003
1031
|
};
|
|
1004
1032
|
function validatePresence(required, result) {
|
|
1005
1033
|
for (const claim of required) {
|
|
1006
1034
|
if (result.claims[claim] === undefined) {
|
|
1007
|
-
throw new OPE(`JWT "${claim}" (${
|
|
1035
|
+
throw new OPE(`JWT "${claim}" (${idTokenClaimNames[claim]}) claim missing`);
|
|
1008
1036
|
}
|
|
1009
1037
|
}
|
|
1010
1038
|
return result;
|
|
@@ -1092,10 +1120,7 @@ export async function revocationRequest(as, client, token, options) {
|
|
|
1092
1120
|
if (!validateString(token)) {
|
|
1093
1121
|
throw new TypeError('"token" must be a non-empty string');
|
|
1094
1122
|
}
|
|
1095
|
-
|
|
1096
|
-
throw new TypeError('"as.revocation_endpoint" must be a string');
|
|
1097
|
-
}
|
|
1098
|
-
const url = new URL(as.revocation_endpoint);
|
|
1123
|
+
const url = resolveEndpoint(as, 'revocation_endpoint', options);
|
|
1099
1124
|
const body = new URLSearchParams(options?.additionalParameters);
|
|
1100
1125
|
body.set('token', token);
|
|
1101
1126
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -1103,7 +1128,7 @@ export async function revocationRequest(as, client, token, options) {
|
|
|
1103
1128
|
return authenticatedRequest(as, client, 'POST', url, body, headers, options);
|
|
1104
1129
|
}
|
|
1105
1130
|
export async function processRevocationResponse(response) {
|
|
1106
|
-
if (!(response
|
|
1131
|
+
if (!looseInstanceOf(response, Response)) {
|
|
1107
1132
|
throw new TypeError('"response" must be an instance of Response');
|
|
1108
1133
|
}
|
|
1109
1134
|
if (response.status !== 200) {
|
|
@@ -1126,10 +1151,7 @@ export async function introspectionRequest(as, client, token, options) {
|
|
|
1126
1151
|
if (!validateString(token)) {
|
|
1127
1152
|
throw new TypeError('"token" must be a non-empty string');
|
|
1128
1153
|
}
|
|
1129
|
-
|
|
1130
|
-
throw new TypeError('"as.introspection_endpoint" must be a string');
|
|
1131
|
-
}
|
|
1132
|
-
const url = new URL(as.introspection_endpoint);
|
|
1154
|
+
const url = resolveEndpoint(as, 'introspection_endpoint', options);
|
|
1133
1155
|
const body = new URLSearchParams(options?.additionalParameters);
|
|
1134
1156
|
body.set('token', token);
|
|
1135
1157
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -1144,7 +1166,7 @@ export async function introspectionRequest(as, client, token, options) {
|
|
|
1144
1166
|
export async function processIntrospectionResponse(as, client, response) {
|
|
1145
1167
|
assertAs(as);
|
|
1146
1168
|
assertClient(client);
|
|
1147
|
-
if (!(response
|
|
1169
|
+
if (!looseInstanceOf(response, Response)) {
|
|
1148
1170
|
throw new TypeError('"response" must be an instance of Response');
|
|
1149
1171
|
}
|
|
1150
1172
|
if (response.status !== 200) {
|
|
@@ -1186,23 +1208,19 @@ export async function processIntrospectionResponse(as, client, response) {
|
|
|
1186
1208
|
}
|
|
1187
1209
|
async function jwksRequest(as, options) {
|
|
1188
1210
|
assertAs(as);
|
|
1189
|
-
|
|
1190
|
-
throw new TypeError('"as.jwks_uri" must be a string');
|
|
1191
|
-
}
|
|
1192
|
-
const url = new URL(as.jwks_uri);
|
|
1211
|
+
const url = resolveEndpoint(as, 'jwks_uri');
|
|
1193
1212
|
const headers = prepareHeaders(options?.headers);
|
|
1194
1213
|
headers.set('accept', 'application/json');
|
|
1195
1214
|
headers.append('accept', 'application/jwk-set+json');
|
|
1196
|
-
|
|
1197
|
-
headers,
|
|
1215
|
+
return (options?.[experimental_customFetch] || fetch)(url.href, {
|
|
1216
|
+
headers: Object.fromEntries(headers.entries()),
|
|
1198
1217
|
method: 'GET',
|
|
1199
1218
|
redirect: 'manual',
|
|
1200
1219
|
signal: options?.signal ? signal(options.signal) : null,
|
|
1201
|
-
});
|
|
1202
|
-
return fetch(request).then(processDpopNonce);
|
|
1220
|
+
}).then(processDpopNonce);
|
|
1203
1221
|
}
|
|
1204
1222
|
async function processJwksResponse(response) {
|
|
1205
|
-
if (!(response
|
|
1223
|
+
if (!looseInstanceOf(response, Response)) {
|
|
1206
1224
|
throw new TypeError('"response" must be an instance of Response');
|
|
1207
1225
|
}
|
|
1208
1226
|
if (response.status !== 200) {
|
|
@@ -1329,8 +1347,9 @@ async function validateJwt(jws, checkAlg, getKey, clockSkew, clockTolerance) {
|
|
|
1329
1347
|
throw new OPE('unexpected JWT "crit" header parameter');
|
|
1330
1348
|
}
|
|
1331
1349
|
const signature = b64u(encodedSignature);
|
|
1350
|
+
let key;
|
|
1332
1351
|
if (getKey !== noSignatureCheck) {
|
|
1333
|
-
|
|
1352
|
+
key = await getKey(header);
|
|
1334
1353
|
const input = `${protectedHeader}.${payload}`;
|
|
1335
1354
|
const verified = await crypto.subtle.verify(keyToSubtle(key), key, signature, buf(input));
|
|
1336
1355
|
if (!verified) {
|
|
@@ -1379,7 +1398,7 @@ async function validateJwt(jws, checkAlg, getKey, clockSkew, clockTolerance) {
|
|
|
1379
1398
|
throw new OPE('unexpected JWT "aud" (audience) claim type');
|
|
1380
1399
|
}
|
|
1381
1400
|
}
|
|
1382
|
-
return { header, claims, signature };
|
|
1401
|
+
return { header, claims, signature, key };
|
|
1383
1402
|
}
|
|
1384
1403
|
export async function validateJwtAuthResponse(as, client, parameters, expectedState, options) {
|
|
1385
1404
|
assertAs(as);
|
|
@@ -1409,6 +1428,137 @@ export async function validateJwtAuthResponse(as, client, parameters, expectedSt
|
|
|
1409
1428
|
}
|
|
1410
1429
|
return validateAuthResponse(as, client, result, expectedState);
|
|
1411
1430
|
}
|
|
1431
|
+
async function idTokenHash(alg, data, key) {
|
|
1432
|
+
let algorithm;
|
|
1433
|
+
switch (alg) {
|
|
1434
|
+
case 'RS256':
|
|
1435
|
+
case 'PS256':
|
|
1436
|
+
case 'ES256':
|
|
1437
|
+
algorithm = 'SHA-256';
|
|
1438
|
+
break;
|
|
1439
|
+
case 'RS384':
|
|
1440
|
+
case 'PS384':
|
|
1441
|
+
case 'ES384':
|
|
1442
|
+
algorithm = 'SHA-384';
|
|
1443
|
+
break;
|
|
1444
|
+
case 'RS512':
|
|
1445
|
+
case 'PS512':
|
|
1446
|
+
case 'ES512':
|
|
1447
|
+
algorithm = 'SHA-512';
|
|
1448
|
+
break;
|
|
1449
|
+
case 'EdDSA':
|
|
1450
|
+
if (key.algorithm.name === 'Ed25519') {
|
|
1451
|
+
algorithm = 'SHA-512';
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
throw new UnsupportedOperationError();
|
|
1455
|
+
default:
|
|
1456
|
+
throw new UnsupportedOperationError();
|
|
1457
|
+
}
|
|
1458
|
+
const digest = await crypto.subtle.digest(algorithm, buf(data));
|
|
1459
|
+
return b64u(digest.slice(0, digest.byteLength / 2));
|
|
1460
|
+
}
|
|
1461
|
+
async function idTokenHashMatches(data, actual, alg, key) {
|
|
1462
|
+
const expected = await idTokenHash(alg, data, key);
|
|
1463
|
+
return actual === expected;
|
|
1464
|
+
}
|
|
1465
|
+
export async function experimental_validateDetachedSignatureResponse(as, client, parameters, expectedNonce, expectedState, maxAge, options) {
|
|
1466
|
+
assertAs(as);
|
|
1467
|
+
assertClient(client);
|
|
1468
|
+
if (!(parameters instanceof URLSearchParams)) {
|
|
1469
|
+
throw new TypeError('"parameters" must be an instance of URLSearchParams');
|
|
1470
|
+
}
|
|
1471
|
+
parameters = new URLSearchParams(parameters);
|
|
1472
|
+
const id_token = getURLSearchParameter(parameters, 'id_token');
|
|
1473
|
+
parameters.delete('id_token');
|
|
1474
|
+
switch (expectedState) {
|
|
1475
|
+
case undefined:
|
|
1476
|
+
case expectNoState:
|
|
1477
|
+
break;
|
|
1478
|
+
default:
|
|
1479
|
+
if (!validateString(expectedState)) {
|
|
1480
|
+
throw new TypeError('"expectedState" must be a non-empty string');
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
const result = validateAuthResponse({
|
|
1484
|
+
...as,
|
|
1485
|
+
authorization_response_iss_parameter_supported: false,
|
|
1486
|
+
}, client, parameters, expectedState);
|
|
1487
|
+
if (isOAuth2Error(result)) {
|
|
1488
|
+
return result;
|
|
1489
|
+
}
|
|
1490
|
+
if (!id_token) {
|
|
1491
|
+
throw new OPE('"parameters" does not contain an ID Token');
|
|
1492
|
+
}
|
|
1493
|
+
const code = getURLSearchParameter(parameters, 'code');
|
|
1494
|
+
if (!code) {
|
|
1495
|
+
throw new OPE('"parameters" does not contain an Authorization Code');
|
|
1496
|
+
}
|
|
1497
|
+
if (typeof as.jwks_uri !== 'string') {
|
|
1498
|
+
throw new TypeError('"as.jwks_uri" must be a string');
|
|
1499
|
+
}
|
|
1500
|
+
const requiredClaims = [
|
|
1501
|
+
'aud',
|
|
1502
|
+
'exp',
|
|
1503
|
+
'iat',
|
|
1504
|
+
'iss',
|
|
1505
|
+
'sub',
|
|
1506
|
+
'nonce',
|
|
1507
|
+
'c_hash',
|
|
1508
|
+
];
|
|
1509
|
+
if (typeof expectedState === 'string') {
|
|
1510
|
+
requiredClaims.push('s_hash');
|
|
1511
|
+
}
|
|
1512
|
+
const { claims, header, key } = await validateJwt(id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported), getPublicSigKeyFromIssuerJwksUri.bind(undefined, as, options), getClockSkew(client), getClockTolerance(client))
|
|
1513
|
+
.then(validatePresence.bind(undefined, requiredClaims))
|
|
1514
|
+
.then(validateIssuer.bind(undefined, as.issuer))
|
|
1515
|
+
.then(validateAudience.bind(undefined, client.client_id));
|
|
1516
|
+
const clockSkew = getClockSkew(client);
|
|
1517
|
+
const now = epochTime() + clockSkew;
|
|
1518
|
+
if (claims.iat < now - 3600) {
|
|
1519
|
+
throw new OPE('unexpected JWT "iat" (issued at) claim value, it is too far in the past');
|
|
1520
|
+
}
|
|
1521
|
+
if (typeof claims.c_hash !== 'string' ||
|
|
1522
|
+
(await idTokenHashMatches(code, claims.c_hash, header.alg, key)) !== true) {
|
|
1523
|
+
throw new OPE('invalid ID Token "c_hash" (code hash) claim value');
|
|
1524
|
+
}
|
|
1525
|
+
if (claims.s_hash !== undefined && typeof expectedState !== 'string') {
|
|
1526
|
+
throw new OPE('could not verify ID Token "s_hash" (state hash) claim value');
|
|
1527
|
+
}
|
|
1528
|
+
if (typeof expectedState === 'string' &&
|
|
1529
|
+
(typeof claims.s_hash !== 'string' ||
|
|
1530
|
+
(await idTokenHashMatches(expectedState, claims.s_hash, header.alg, key)) !== true)) {
|
|
1531
|
+
throw new OPE('invalid ID Token "s_hash" (state hash) claim value');
|
|
1532
|
+
}
|
|
1533
|
+
if (client.require_auth_time !== undefined && typeof claims.auth_time !== 'number') {
|
|
1534
|
+
throw new OPE('unexpected ID Token "auth_time" (authentication time) claim value');
|
|
1535
|
+
}
|
|
1536
|
+
maxAge ?? (maxAge = client.default_max_age ?? skipAuthTimeCheck);
|
|
1537
|
+
if ((client.require_auth_time || maxAge !== skipAuthTimeCheck) &&
|
|
1538
|
+
claims.auth_time === undefined) {
|
|
1539
|
+
throw new OPE('ID Token "auth_time" (authentication time) claim missing');
|
|
1540
|
+
}
|
|
1541
|
+
if (maxAge !== skipAuthTimeCheck) {
|
|
1542
|
+
if (typeof maxAge !== 'number' || maxAge < 0) {
|
|
1543
|
+
throw new TypeError('"options.max_age" must be a non-negative number');
|
|
1544
|
+
}
|
|
1545
|
+
const now = epochTime() + getClockSkew(client);
|
|
1546
|
+
const tolerance = getClockTolerance(client);
|
|
1547
|
+
if (claims.auth_time + maxAge < now - tolerance) {
|
|
1548
|
+
throw new OPE('too much time has elapsed since the last End-User authentication');
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (!validateString(expectedNonce)) {
|
|
1552
|
+
throw new TypeError('"expectedNonce" must be a non-empty string');
|
|
1553
|
+
}
|
|
1554
|
+
if (claims.nonce !== expectedNonce) {
|
|
1555
|
+
throw new OPE('unexpected ID Token "nonce" claim value');
|
|
1556
|
+
}
|
|
1557
|
+
if (Array.isArray(claims.aud) && claims.aud.length !== 1 && claims.azp !== client.client_id) {
|
|
1558
|
+
throw new OPE('unexpected ID Token "azp" (authorized party) claim value');
|
|
1559
|
+
}
|
|
1560
|
+
return result;
|
|
1561
|
+
}
|
|
1412
1562
|
function checkSigningAlgorithm(client, issuer, header) {
|
|
1413
1563
|
if (client !== undefined) {
|
|
1414
1564
|
if (header.alg !== client) {
|
|
@@ -1525,10 +1675,7 @@ async function importJwk(alg, jwk) {
|
|
|
1525
1675
|
export async function deviceAuthorizationRequest(as, client, parameters, options) {
|
|
1526
1676
|
assertAs(as);
|
|
1527
1677
|
assertClient(client);
|
|
1528
|
-
|
|
1529
|
-
throw new TypeError('"as.device_authorization_endpoint" must be a string');
|
|
1530
|
-
}
|
|
1531
|
-
const url = new URL(as.device_authorization_endpoint);
|
|
1678
|
+
const url = resolveEndpoint(as, 'device_authorization_endpoint', options);
|
|
1532
1679
|
const body = new URLSearchParams(parameters);
|
|
1533
1680
|
body.set('client_id', client.client_id);
|
|
1534
1681
|
const headers = prepareHeaders(options?.headers);
|
|
@@ -1538,7 +1685,7 @@ export async function deviceAuthorizationRequest(as, client, parameters, options
|
|
|
1538
1685
|
export async function processDeviceAuthorizationResponse(as, client, response) {
|
|
1539
1686
|
assertAs(as);
|
|
1540
1687
|
assertClient(client);
|
|
1541
|
-
if (!(response
|
|
1688
|
+
if (!looseInstanceOf(response, Response)) {
|
|
1542
1689
|
throw new TypeError('"response" must be an instance of Response');
|
|
1543
1690
|
}
|
|
1544
1691
|
if (response.status !== 200) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oauth4webapi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "OAuth 2 / OpenID Connect for JavaScript Runtimes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"@types/node": "^20.10.8",
|
|
69
69
|
"@types/oidc-provider": "^8.4.3",
|
|
70
70
|
"@types/qunit": "^2.19.9",
|
|
71
|
+
"archiver": "^6.0.1",
|
|
71
72
|
"ava": "^5.3.1",
|
|
72
73
|
"chrome-launcher": "^1.1.0",
|
|
73
74
|
"edge-runtime": "^2.5.7",
|
|
@@ -80,6 +81,7 @@
|
|
|
80
81
|
"puppeteer-core": "^21.7.0",
|
|
81
82
|
"qunit": "^2.20.0",
|
|
82
83
|
"raw-body": "^2.5.2",
|
|
84
|
+
"selfsigned": "^2.4.1",
|
|
83
85
|
"timekeeper": "^2.3.1",
|
|
84
86
|
"tsx": "^4.7.0",
|
|
85
87
|
"typedoc": "^0.25.7",
|