@udibo/oauth2 0.8.0 → 0.9.1
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 +8 -3
- package/esm/adapters/hono/bff/bff.js +2 -2
- package/esm/client/_http.js +2 -1
- package/esm/client/base.d.ts +6 -0
- package/esm/client/base.js +2 -1
- package/esm/client/check.d.ts +3 -1
- package/esm/client/check.js +2 -1
- package/esm/identity/external/_token-exchange.js +2 -1
- package/esm/identity/external/apple.js +2 -1
- package/esm/identity/external/github.js +2 -1
- package/esm/identity/external/oauth2.js +2 -1
- package/esm/identity/external/oidc.js +2 -1
- package/esm/identity/hibp.js +3 -2
- package/esm/server/authorization-server.d.ts +11 -1
- package/esm/server/authorization-server.js +4 -2
- package/esm/server/introspection-token-reader.d.ts +4 -1
- package/esm/server/introspection-token-reader.js +4 -3
- package/esm/server/jwks-token-reader.d.ts +4 -1
- package/esm/server/jwks-token-reader.js +4 -3
- package/esm/utils/_default-fetch.d.ts +19 -0
- package/esm/utils/_default-fetch.js +114 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@ Udibo's identity service, or host an authorization server for your own app. The
|
|
|
5
5
|
package includes clients, authorization and resource servers, Hono middleware,
|
|
6
6
|
React bindings, and optional login flows over your own database.
|
|
7
7
|
|
|
8
|
-
**Udibo's managed identity service is in private beta.**
|
|
9
|
-
[
|
|
10
|
-
without a hosted account.
|
|
8
|
+
**Udibo's managed identity service is in private beta.** See
|
|
9
|
+
[udibo.com](https://www.udibo.com/) for access. The self-hosted examples run
|
|
10
|
+
locally without a hosted account.
|
|
11
11
|
|
|
12
12
|
## Choose your path
|
|
13
13
|
|
|
@@ -76,6 +76,11 @@ a browser app with a backend, start with the BFF guide.
|
|
|
76
76
|
authorization server. The framework-independent core accepts standard `Request`
|
|
77
77
|
and `Response` objects; Hono adapters add routing and middleware.
|
|
78
78
|
|
|
79
|
+
For app-owned login, `CaptchaProvider` and `verifyCaptcha` let your route check
|
|
80
|
+
a client-issued bot-challenge token before calling `IdentityService`. The
|
|
81
|
+
package does not include a challenge provider; see the
|
|
82
|
+
[CAPTCHA route example](docs/guides/add-login.md#captcha-at-the-route-boundary).
|
|
83
|
+
|
|
79
84
|
## Examples
|
|
80
85
|
|
|
81
86
|
From a checkout of this repository:
|
|
@@ -51,6 +51,7 @@ import { Hono } from "hono";
|
|
|
51
51
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
52
52
|
import { assertMaxBodyBytes, DEFAULT_MAX_BODY_BYTES, readBoundedBody, } from "../../../utils/_body.js";
|
|
53
53
|
import { InvalidRequestError, InvalidTokenError, isOAuth2Error, TemporarilyUnavailableError, } from "../../../errors.js";
|
|
54
|
+
import { defaultFetch } from "../../../utils/_default-fetch.js";
|
|
54
55
|
import { timingSafeMatchIndex } from "../../../utils/_timing-safe.js";
|
|
55
56
|
import { sha256Hash } from "../../../server/utils/hash.js";
|
|
56
57
|
import { OAUTH2_CONTEXT_KEY } from "../_common.js";
|
|
@@ -1197,8 +1198,7 @@ export class HonoBff {
|
|
|
1197
1198
|
proxy(target, options = {}) {
|
|
1198
1199
|
const base = new URL(target);
|
|
1199
1200
|
const allowedHeaders = resolveForwardHeaders(options.forwardHeaders, this.#csrf?.headerName);
|
|
1200
|
-
const fetchImpl = options.fetch ??
|
|
1201
|
-
((input, init) => fetch(input, init));
|
|
1201
|
+
const fetchImpl = options.fetch ?? defaultFetch;
|
|
1202
1202
|
const retryOn401 = options.retryOn401 ?? true;
|
|
1203
1203
|
const mountPrefix = (options.stripPrefix ?? "").endsWith("/")
|
|
1204
1204
|
? options.stripPrefix.slice(0, -1)
|
package/esm/client/_http.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { AccessDeniedError, AuthorizationPendingError, ExpiredTokenError, InsufficientScopeError, InvalidClientError, InvalidGrantError, InvalidRequestError, InvalidScopeError, InvalidTokenError, OAuth2Error as OAuth2ErrorClass, ServerError, SlowDownError, TemporarilyUnavailableError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError, } from "../errors.js";
|
|
10
|
+
import { packageDeadlineSignal } from "../utils/_default-fetch.js";
|
|
10
11
|
import { sanitizeProviderText } from "../utils/text.js";
|
|
11
12
|
/** Response bytes accepted from an endpoint before the call is refused. */
|
|
12
13
|
export const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
@@ -61,7 +62,7 @@ export async function sendGuarded(fetchImpl, url, init, what, options = {}) {
|
|
|
61
62
|
res = await fetchImpl(url, {
|
|
62
63
|
...init,
|
|
63
64
|
redirect: redirect === "refuse" ? "manual" : "follow",
|
|
64
|
-
signal:
|
|
65
|
+
signal: packageDeadlineSignal(deadline, init.signal),
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
68
|
catch (error) {
|
package/esm/client/base.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export interface BaseOptions {
|
|
|
38
38
|
* an in-memory server. Preferred over stubbing `globalThis.fetch`,
|
|
39
39
|
* which would also intercept unrelated outbound requests the app may
|
|
40
40
|
* make.
|
|
41
|
+
*
|
|
42
|
+
* On Deno the default stops reusing a pooled HTTP/2 connection to a host
|
|
43
|
+
* once one of the client's own calls to it (discovery, token, revocation,
|
|
44
|
+
* userinfo, the session probe) misses its deadline; what the client's
|
|
45
|
+
* `fetch` method returns is the global `fetch`'s response, unaltered. An
|
|
46
|
+
* injected `fetch` manages its own connections.
|
|
41
47
|
*/
|
|
42
48
|
fetch?: typeof fetch;
|
|
43
49
|
}
|
package/esm/client/base.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
|
+
import { defaultFetch } from "../utils/_default-fetch.js";
|
|
14
15
|
import { EventBus, } from "./events.js";
|
|
15
16
|
/**
|
|
16
17
|
* The headers a wrapped `fetch` should send for `(input, init)`: the ones
|
|
@@ -88,6 +89,6 @@ export class OAuth2ClientBase {
|
|
|
88
89
|
* polyfilled global if the runtime installs one after construction.
|
|
89
90
|
*/
|
|
90
91
|
fetchImpl(input, init) {
|
|
91
|
-
return (this.#customFetch ??
|
|
92
|
+
return (this.#customFetch ?? defaultFetch)(input, init);
|
|
92
93
|
}
|
|
93
94
|
}
|
package/esm/client/check.d.ts
CHANGED
|
@@ -62,7 +62,9 @@ export interface CheckPermissionsOptions {
|
|
|
62
62
|
* in tests. Defaults to the global `fetch`. Whichever is used, the call runs
|
|
63
63
|
* under this package's deadline and response-size cap, and a redirect is
|
|
64
64
|
* refused rather than followed so the bearer token never reaches the host
|
|
65
|
-
* it names.
|
|
65
|
+
* it names. On Deno the default stops reusing a pooled HTTP/2 connection to
|
|
66
|
+
* a host once a check request to it misses its deadline; an injected `fetch`
|
|
67
|
+
* manages its own connections.
|
|
66
68
|
*/
|
|
67
69
|
fetch?: typeof fetch;
|
|
68
70
|
}
|
package/esm/client/check.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* @module
|
|
28
28
|
*/
|
|
29
29
|
import { ServerError, TemporarilyUnavailableError } from "../errors.js";
|
|
30
|
+
import { defaultFetch } from "../utils/_default-fetch.js";
|
|
30
31
|
import { receiveJson, sendGuarded } from "./_http.js";
|
|
31
32
|
const CHECK_ENDPOINT = "check endpoint";
|
|
32
33
|
/**
|
|
@@ -74,7 +75,7 @@ const CHECK_ENDPOINT = "check endpoint";
|
|
|
74
75
|
* ```
|
|
75
76
|
*/
|
|
76
77
|
export async function checkPermissions(options) {
|
|
77
|
-
const response = await sendGuarded(options.fetch ??
|
|
78
|
+
const response = await sendGuarded(options.fetch ?? defaultFetch, options.endpoint, {
|
|
78
79
|
method: "POST",
|
|
79
80
|
headers: {
|
|
80
81
|
authorization: `Bearer ${options.accessToken}`,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
+
import { packageDeadlineSignal } from "../../utils/_default-fetch.js";
|
|
11
12
|
import { describeError, sanitizeProviderText } from "./_shared.js";
|
|
12
13
|
import { ExternalAuthError } from "./errors.js";
|
|
13
14
|
/** Response bytes accepted from a token endpoint before the exchange is refused. */
|
|
@@ -33,7 +34,7 @@ export async function runTokenExchange(input) {
|
|
|
33
34
|
},
|
|
34
35
|
body: input.body,
|
|
35
36
|
redirect: "manual",
|
|
36
|
-
signal: AbortSignal.timeout(input.timeoutMs ?? TOKEN_EXCHANGE_TIMEOUT_MS),
|
|
37
|
+
signal: packageDeadlineSignal(AbortSignal.timeout(input.timeoutMs ?? TOKEN_EXCHANGE_TIMEOUT_MS)),
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
40
|
catch (error) {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { toArrayBuffer } from "../../utils/_buffer.js";
|
|
19
19
|
import { base64urlDecode } from "../../utils/crypto.js";
|
|
20
|
+
import { defaultFetch } from "../../utils/_default-fetch.js";
|
|
20
21
|
import { assertIdTokenClaims, describeError } from "./_shared.js";
|
|
21
22
|
import { runTokenExchange } from "./_token-exchange.js";
|
|
22
23
|
import { createAppleClientSecretFactory, } from "./apple-client-secret.js";
|
|
@@ -60,7 +61,7 @@ const SIGNATURE_NOT_VERIFIED = `Apple id_token signature did not verify against
|
|
|
60
61
|
*/
|
|
61
62
|
export function appleProvider(options) {
|
|
62
63
|
const id = "apple";
|
|
63
|
-
const fetchImpl = (input, init) => (options.fetch ??
|
|
64
|
+
const fetchImpl = (input, init) => (options.fetch ?? defaultFetch)(input, init);
|
|
64
65
|
const clientSecretFactory = options.clientSecret ??
|
|
65
66
|
createAppleClientSecretFactory({
|
|
66
67
|
providerId: id,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
|
+
import { defaultFetch } from "../../utils/_default-fetch.js";
|
|
7
8
|
import { describeError } from "./_shared.js";
|
|
8
9
|
import { runTokenExchange } from "./_token-exchange.js";
|
|
9
10
|
import { ExternalAuthError } from "./errors.js";
|
|
@@ -36,7 +37,7 @@ const GITHUB_EMAILS_URL = "https://api.github.com/user/emails";
|
|
|
36
37
|
*/
|
|
37
38
|
export function githubProvider(options) {
|
|
38
39
|
const id = "github";
|
|
39
|
-
const fetchImpl = (input, init) => (options.fetch ??
|
|
40
|
+
const fetchImpl = (input, init) => (options.fetch ?? defaultFetch)(input, init);
|
|
40
41
|
async function exchangeCode(input) {
|
|
41
42
|
const { value } = await runTokenExchange({
|
|
42
43
|
provider: id,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
|
+
import { defaultFetch } from "../../utils/_default-fetch.js";
|
|
10
11
|
import { generateCodeChallenge } from "../../utils/pkce.js";
|
|
11
12
|
import { describeError, readErrorBody } from "./_shared.js";
|
|
12
13
|
import { runTokenExchange } from "./_token-exchange.js";
|
|
@@ -39,7 +40,7 @@ import { ExternalAuthError } from "./errors.js";
|
|
|
39
40
|
*/
|
|
40
41
|
export function oauth2Provider(options) {
|
|
41
42
|
const id = options.id;
|
|
42
|
-
const fetchImpl = (input, init) => (options.fetch ??
|
|
43
|
+
const fetchImpl = (input, init) => (options.fetch ?? defaultFetch)(input, init);
|
|
43
44
|
const scopeSeparator = options.scopeSeparator ?? " ";
|
|
44
45
|
async function exchangeCode(input) {
|
|
45
46
|
const body = new URLSearchParams({
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { DirectClient } from "../../client/direct-client.js";
|
|
8
8
|
import { MemoryAuthRequestStorage } from "../../client/storage.js";
|
|
9
9
|
import { isOAuth2Error } from "../../errors.js";
|
|
10
|
+
import { defaultFetch } from "../../utils/_default-fetch.js";
|
|
10
11
|
import { generateCodeChallenge } from "../../utils/pkce.js";
|
|
11
12
|
import { assertIdTokenClaims, describeError, stripTrailingSlash, } from "./_shared.js";
|
|
12
13
|
import { ExternalAuthError } from "./errors.js";
|
|
@@ -53,7 +54,7 @@ export function oidcProvider(options) {
|
|
|
53
54
|
const defaultScopes = options.scopes ?? ["openid", "email", "profile"];
|
|
54
55
|
const fetchImpl = (input, init) => {
|
|
55
56
|
assertSecureIssuer(id, input instanceof Request ? input.url : String(input));
|
|
56
|
-
return (options.fetch ??
|
|
57
|
+
return (options.fetch ?? defaultFetch)(input, {
|
|
57
58
|
...init,
|
|
58
59
|
redirect: "error",
|
|
59
60
|
});
|
package/esm/identity/hibp.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* @module
|
|
19
19
|
*/
|
|
20
20
|
import { encodeHex } from "../deps/jsr.io/@std/encoding/1.0.11/hex.js";
|
|
21
|
+
import { defaultFetch, packageDeadlineSignal, } from "../utils/_default-fetch.js";
|
|
21
22
|
import { dispatchIdentityEvent } from "./events.js";
|
|
22
23
|
const DEFAULT_BASE_URL = "https://api.pwnedpasswords.com/range/";
|
|
23
24
|
const VALIDATOR_NAME = "breached_password";
|
|
@@ -44,7 +45,7 @@ export function breachedPasswordValidator(options = {}) {
|
|
|
44
45
|
const threshold = options.threshold ?? 1;
|
|
45
46
|
const failOpen = options.failOpen ?? true;
|
|
46
47
|
const timeoutMs = options.timeoutMs ?? 3000;
|
|
47
|
-
const fetchImpl = options.fetch ??
|
|
48
|
+
const fetchImpl = options.fetch ?? defaultFetch;
|
|
48
49
|
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
49
50
|
const onEvent = options.onEvent;
|
|
50
51
|
return async (password) => {
|
|
@@ -54,7 +55,7 @@ export function breachedPasswordValidator(options = {}) {
|
|
|
54
55
|
const suffix = hash.slice(5);
|
|
55
56
|
const response = await fetchImpl(`${baseUrl}${prefix}`, {
|
|
56
57
|
headers: { "Add-Padding": "true" },
|
|
57
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
58
|
+
signal: packageDeadlineSignal(AbortSignal.timeout(timeoutMs)),
|
|
58
59
|
});
|
|
59
60
|
if (!response.ok) {
|
|
60
61
|
await response.body?.cancel();
|
|
@@ -63,9 +63,19 @@ export interface EndSessionContext<Client extends ClientInterface> {
|
|
|
63
63
|
}
|
|
64
64
|
/** What an app's {@link EndSessionFn} may ask the server to do afterwards. */
|
|
65
65
|
export interface EndSessionResult {
|
|
66
|
+
/**
|
|
67
|
+
* The app refused this logout (for example, the verified `id_token_hint`
|
|
68
|
+
* names a different person than the current session). Ignore the caller's
|
|
69
|
+
* redirect and all `headers`, including any `Set-Cookie`, so the response
|
|
70
|
+
* neither sends the browser to the RP nor clears the current session.
|
|
71
|
+
* Answer at `fallback` if supplied, otherwise 204. Omit or set `false` to
|
|
72
|
+
* preserve the normal logout behavior.
|
|
73
|
+
*/
|
|
74
|
+
refused?: boolean;
|
|
66
75
|
/**
|
|
67
76
|
* Headers to merge into the response — the session-clearing `Set-Cookie`,
|
|
68
|
-
* typically. The server owns `Location`; anything else is yours.
|
|
77
|
+
* typically. The server owns `Location`; anything else is yours. Ignored
|
|
78
|
+
* when `refused` is true.
|
|
69
79
|
*/
|
|
70
80
|
headers?: HeadersInit;
|
|
71
81
|
/**
|
|
@@ -580,8 +580,10 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
580
580
|
subject: params.subject,
|
|
581
581
|
logoutHint: params.logoutHint,
|
|
582
582
|
}) ?? {};
|
|
583
|
-
const headers = new Headers(result.headers);
|
|
584
|
-
const authorized =
|
|
583
|
+
const headers = new Headers(result.refused ? undefined : result.headers);
|
|
584
|
+
const authorized = result.refused
|
|
585
|
+
? undefined
|
|
586
|
+
: this.#postLogoutRedirect(client, params);
|
|
585
587
|
const target = authorized ?? result.fallback;
|
|
586
588
|
if (!target)
|
|
587
589
|
return new Response(null, { status: 204, headers });
|
|
@@ -70,7 +70,10 @@ export interface IntrospectionTokenReaderOptions<Client extends ClientInterface,
|
|
|
70
70
|
/**
|
|
71
71
|
* `fetch` implementation used to call the introspection endpoint. Defaults
|
|
72
72
|
* to the global `fetch`. Inject it to route through a custom client (mTLS,
|
|
73
|
-
* timeouts, retries) or to stub the network in tests.
|
|
73
|
+
* timeouts, retries) or to stub the network in tests. On Deno the default
|
|
74
|
+
* stops reusing a pooled HTTP/2 connection to a host once a request to it
|
|
75
|
+
* misses {@linkcode fetchTimeoutMs}; an injected `fetch` manages its own
|
|
76
|
+
* connections.
|
|
74
77
|
*/
|
|
75
78
|
fetch?: typeof fetch;
|
|
76
79
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { ServerError, TemporarilyUnavailableError } from "../errors.js";
|
|
13
13
|
import { BasicScope } from "../models/scope.js";
|
|
14
14
|
import { encodeBasicAuth } from "../utils/basic-auth.js";
|
|
15
|
+
import { defaultFetch, packageDeadlineSignal, } from "../utils/_default-fetch.js";
|
|
15
16
|
/**
|
|
16
17
|
* Validates tokens by calling an OAuth2 introspection endpoint (RFC 7662).
|
|
17
18
|
*
|
|
@@ -127,9 +128,9 @@ export class IntrospectionTokenReader {
|
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
130
|
async #introspect(accessToken) {
|
|
130
|
-
const fetchImpl = this.customFetch ??
|
|
131
|
+
const fetchImpl = this.customFetch ?? defaultFetch;
|
|
131
132
|
const controller = new AbortController();
|
|
132
|
-
const deadline = setTimeout(() => controller.abort(), this.fetchTimeoutMs);
|
|
133
|
+
const deadline = setTimeout(() => controller.abort(new DOMException("token introspection timed out", "TimeoutError")), this.fetchTimeoutMs);
|
|
133
134
|
try {
|
|
134
135
|
let response;
|
|
135
136
|
try {
|
|
@@ -140,7 +141,7 @@ export class IntrospectionTokenReader {
|
|
|
140
141
|
"Authorization": encodeBasicAuth(this.clientId, this.clientSecret),
|
|
141
142
|
},
|
|
142
143
|
body: new URLSearchParams({ token: accessToken }),
|
|
143
|
-
signal: controller.signal,
|
|
144
|
+
signal: packageDeadlineSignal(controller.signal),
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
catch (cause) {
|
|
@@ -167,7 +167,10 @@ export interface JwksTokenReaderOptions<Client extends ClientInterface, User, S
|
|
|
167
167
|
/**
|
|
168
168
|
* `fetch` implementation used for JWKS and discovery requests. Defaults to
|
|
169
169
|
* the global `fetch`. Inject it to route through a custom client (mTLS,
|
|
170
|
-
* timeouts, retries) or to stub the network in tests.
|
|
170
|
+
* timeouts, retries) or to stub the network in tests. On Deno the default
|
|
171
|
+
* stops reusing a pooled HTTP/2 connection to a host once a request to it
|
|
172
|
+
* misses {@linkcode fetchTimeoutMs}; an injected `fetch` manages its own
|
|
173
|
+
* connections.
|
|
171
174
|
*/
|
|
172
175
|
fetch?: typeof fetch;
|
|
173
176
|
}
|
|
@@ -16,6 +16,7 @@ import { ServerError, TemporarilyUnavailableError } from "../errors.js";
|
|
|
16
16
|
import { BasicScope } from "../models/scope.js";
|
|
17
17
|
import { toArrayBuffer } from "../utils/_buffer.js";
|
|
18
18
|
import { base64urlDecode } from "../utils/crypto.js";
|
|
19
|
+
import { defaultFetch, packageDeadlineSignal, } from "../utils/_default-fetch.js";
|
|
19
20
|
const ALGORITHMS = {
|
|
20
21
|
ES256: {
|
|
21
22
|
kty: "EC",
|
|
@@ -472,15 +473,15 @@ export class JwksTokenReader {
|
|
|
472
473
|
throw transientError ?? lastError ?? new ServerError("discovery failed");
|
|
473
474
|
}
|
|
474
475
|
async #fetchJson(url, label) {
|
|
475
|
-
const fetchImpl = this.#customFetch ??
|
|
476
|
+
const fetchImpl = this.#customFetch ?? defaultFetch;
|
|
476
477
|
const controller = new AbortController();
|
|
477
|
-
const deadline = setTimeout(() => controller.abort(), this.#fetchTimeoutMs);
|
|
478
|
+
const deadline = setTimeout(() => controller.abort(new DOMException(`${label} request timed out`, "TimeoutError")), this.#fetchTimeoutMs);
|
|
478
479
|
try {
|
|
479
480
|
let response;
|
|
480
481
|
try {
|
|
481
482
|
response = await fetchImpl(url, {
|
|
482
483
|
headers: { "Accept": "application/json" },
|
|
483
|
-
signal: controller.signal,
|
|
484
|
+
signal: packageDeadlineSignal(controller.signal),
|
|
484
485
|
});
|
|
485
486
|
}
|
|
486
487
|
catch (cause) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Not part of the public API. The `fetch` the package sends with when its
|
|
3
|
+
* caller injects none: the global `fetch`, resolved at call time, except that
|
|
4
|
+
* on Deno, once a request to an `https` origin misses a deadline the package
|
|
5
|
+
* set (see {@link packageDeadlineSignal}) before its response has fully
|
|
6
|
+
* arrived, later requests to that origin each use a connection of their own
|
|
7
|
+
* instead of the pooled HTTP/2 connection, which may be stalled.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The signal to send a request with when the package bounds it by its own
|
|
13
|
+
* `deadline`; it also aborts when the caller's `signal` does. Only a request
|
|
14
|
+
* whose signal came from here can move its origin off the pooled connection,
|
|
15
|
+
* and only when `deadline` is what aborted it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function packageDeadlineSignal(deadline: AbortSignal, signal?: AbortSignal | null): AbortSignal;
|
|
18
|
+
/** The package's default `fetch`; see the module documentation. */
|
|
19
|
+
export declare const defaultFetch: typeof fetch;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Not part of the public API. The `fetch` the package sends with when its
|
|
3
|
+
* caller injects none: the global `fetch`, resolved at call time, except that
|
|
4
|
+
* on Deno, once a request to an `https` origin misses a deadline the package
|
|
5
|
+
* set (see {@link packageDeadlineSignal}) before its response has fully
|
|
6
|
+
* arrived, later requests to that origin each use a connection of their own
|
|
7
|
+
* instead of the pooled HTTP/2 connection, which may be stalled.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
const NULL_BODY_STATUSES = new Set([204, 205, 304]);
|
|
12
|
+
const abandonedOrigins = new Set();
|
|
13
|
+
const packageDeadlines = new WeakMap();
|
|
14
|
+
/**
|
|
15
|
+
* The signal to send a request with when the package bounds it by its own
|
|
16
|
+
* `deadline`; it also aborts when the caller's `signal` does. Only a request
|
|
17
|
+
* whose signal came from here can move its origin off the pooled connection,
|
|
18
|
+
* and only when `deadline` is what aborted it.
|
|
19
|
+
*/
|
|
20
|
+
export function packageDeadlineSignal(deadline, signal) {
|
|
21
|
+
const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
22
|
+
packageDeadlines.set(combined, deadline);
|
|
23
|
+
return combined;
|
|
24
|
+
}
|
|
25
|
+
/** The package's default `fetch`; see the module documentation. */
|
|
26
|
+
export const defaultFetch = async (input, init) => {
|
|
27
|
+
const createHttpClient = denoCreateHttpClient();
|
|
28
|
+
const origin = createHttpClient ? httpsOrigin(input) : undefined;
|
|
29
|
+
if (!createHttpClient || origin === undefined) {
|
|
30
|
+
return await globalThis.fetch(input, init);
|
|
31
|
+
}
|
|
32
|
+
if (abandonedOrigins.has(origin)) {
|
|
33
|
+
return await fetchOnOwnConnection(createHttpClient, input, init);
|
|
34
|
+
}
|
|
35
|
+
const signal = init?.signal;
|
|
36
|
+
const deadline = signal ? packageDeadlines.get(signal) : undefined;
|
|
37
|
+
if (!signal || !deadline)
|
|
38
|
+
return await globalThis.fetch(input, init);
|
|
39
|
+
const abandonIfDeadlineMissed = () => {
|
|
40
|
+
if (deadline.aborted && signal.reason === deadline.reason) {
|
|
41
|
+
abandonedOrigins.add(origin);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await globalThis.fetch(input, init);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
abandonIfDeadlineMissed();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
return watchBody(response, abandonIfDeadlineMissed);
|
|
53
|
+
};
|
|
54
|
+
function denoCreateHttpClient() {
|
|
55
|
+
const deno = globalThis.Deno;
|
|
56
|
+
const create = deno?.createHttpClient;
|
|
57
|
+
return typeof create === "function"
|
|
58
|
+
? (options) => create.call(deno, options)
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
function httpsOrigin(input) {
|
|
62
|
+
try {
|
|
63
|
+
const url = new URL(input instanceof Request ? input.url : input);
|
|
64
|
+
return url.protocol === "https:" ? url.origin : undefined;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function fetchOnOwnConnection(createHttpClient, input, init) {
|
|
71
|
+
let client;
|
|
72
|
+
try {
|
|
73
|
+
client = createHttpClient({});
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return await globalThis.fetch(input, init);
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return await globalThis.fetch(input, { ...init, client });
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
client.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function watchBody(response, onError) {
|
|
86
|
+
const { body: source, status } = response;
|
|
87
|
+
if (!source || status < 200 || status > 599 || NULL_BODY_STATUSES.has(status)) {
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
const reader = source.getReader();
|
|
91
|
+
const body = new ReadableStream({
|
|
92
|
+
async pull(controller) {
|
|
93
|
+
let chunk;
|
|
94
|
+
try {
|
|
95
|
+
chunk = await reader.read();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
onError();
|
|
99
|
+
controller.error(error);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (chunk.done)
|
|
103
|
+
controller.close();
|
|
104
|
+
else
|
|
105
|
+
controller.enqueue(chunk.value);
|
|
106
|
+
},
|
|
107
|
+
cancel: (reason) => reader.cancel(reason),
|
|
108
|
+
});
|
|
109
|
+
return new Response(body, {
|
|
110
|
+
status,
|
|
111
|
+
statusText: response.statusText,
|
|
112
|
+
headers: response.headers,
|
|
113
|
+
});
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@udibo/oauth2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "A flexible OAuth2 toolkit: framework-agnostic authorization & resource servers, a typed client, Hono adapters (including a backend-for-frontend), and React bindings.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|