@zudojs/auth-oauth 1.2.4 → 1.2.6
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 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/oauthClient/http/index.d.ts +6 -0
- package/dist/oauthClient/http/index.js +6 -0
- package/dist/oauthClient/http/oauthHttp.timeout.d.ts +37 -0
- package/dist/oauthClient/http/oauthHttp.timeout.js +44 -0
- package/dist/oauthClient/index.d.ts +0 -1
- package/dist/oauthClient/index.js +0 -1
- package/dist/oauthClient/oauthAuthorize.core.d.ts +0 -1
- package/dist/oauthClient/oauthAuthorize.core.js +0 -1
- package/dist/oauthClient/oauthConfig.resolve.d.ts +0 -1
- package/dist/oauthClient/oauthConfig.resolve.js +0 -1
- package/dist/oauthClient/oauthHttp.core.d.ts +3 -2
- package/dist/oauthClient/oauthHttp.core.js +17 -6
- package/dist/oauthClient/oauthToken.core.d.ts +0 -1
- package/dist/oauthClient/oauthToken.core.js +0 -1
- package/dist/oauthClient/oauthUserInfo.core.d.ts +0 -1
- package/dist/oauthClient/oauthUserInfo.core.js +0 -1
- package/dist/oauthErrors/index.d.ts +0 -1
- package/dist/oauthErrors/index.js +0 -1
- package/dist/oauthErrors/oauthError.base.d.ts +0 -1
- package/dist/oauthErrors/oauthError.base.js +0 -1
- package/dist/oauthProviders/index.d.ts +0 -1
- package/dist/oauthProviders/index.js +0 -1
- package/dist/oauthProviders/oauthProvider.presets.d.ts +0 -1
- package/dist/oauthProviders/oauthProvider.presets.js +0 -1
- package/dist/oauthSecurity/index.d.ts +0 -1
- package/dist/oauthSecurity/index.js +0 -1
- package/dist/oauthSecurity/oauthJson.sanitize.d.ts +0 -1
- package/dist/oauthSecurity/oauthJson.sanitize.js +0 -1
- package/dist/oauthSecurity/oauthPkce.core.d.ts +0 -1
- package/dist/oauthSecurity/oauthPkce.core.js +0 -1
- package/dist/oauthSecurity/oauthState.core.d.ts +0 -1
- package/dist/oauthSecurity/oauthState.core.js +0 -1
- package/dist/oauthSecurity/oauthUrl.guard.d.ts +0 -1
- package/dist/oauthSecurity/oauthUrl.guard.js +0 -1
- package/dist/oauthTypes/index.d.ts +0 -1
- package/dist/oauthTypes/index.js +0 -1
- package/dist/oauthTypes/oauth.type.d.ts +0 -1
- package/dist/oauthTypes/oauth.type.js +0 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -189,8 +189,14 @@ Every one of these is covered by a test in `tests/`.
|
|
|
189
189
|
**Limit:** the check is on the literal host; DNS is not resolved, so DNS
|
|
190
190
|
rebinding is out of scope. Pair this with network egress controls if endpoint
|
|
191
191
|
URLs come from untrusted operators.
|
|
192
|
-
|
|
193
|
-
`
|
|
192
|
+
**Testing against a local fake:** the guard applies in tests too, so a
|
|
193
|
+
`custom` provider with `tokenUrl: "http://127.0.0.1:4000/token"` is refused.
|
|
194
|
+
Keep a public-looking `https` URL (`https://oauth.example.test/token`) and
|
|
195
|
+
route it to your fake with `config.fetch`, which receives the URL string and
|
|
196
|
+
`RequestInit` and can answer with any `Response`.
|
|
197
|
+
- **Bounded responses.** Every provider request carries a ref'd deadline of
|
|
198
|
+
`timeoutMs` (default 10s) — not `AbortSignal.timeout()`, whose unref'd timer
|
|
199
|
+
let a one-shot script exit with code 13 before the timeout fired — and the body is streamed and
|
|
194
200
|
abandoned the moment it passes `maxResponseBytes` (default 256 KiB); an
|
|
195
201
|
oversized `Content-Length` is refused before a byte is read.
|
|
196
202
|
- **Defensive parsing.** A token response must be a JSON _object_ with a
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request deadline that keeps the process alive until it fires.
|
|
3
|
+
*
|
|
4
|
+
* @module oauthClient/http/oauthHttp.timeout
|
|
5
|
+
*
|
|
6
|
+
* `AbortSignal.timeout()` schedules its timer *unref'd*: if nothing else
|
|
7
|
+
* holds the event loop — a CLI, a one-shot script, a test runner's last
|
|
8
|
+
* await — Node exits with code 13 ("unsettled top-level await") before the
|
|
9
|
+
* deadline ever fires, and the caller never sees the documented
|
|
10
|
+
* `OAuthNetworkError`. A plain `setTimeout` is ref'd, so the deadline is
|
|
11
|
+
* guaranteed to be observed; `clear()` releases it as soon as the request
|
|
12
|
+
* settles so a fast response does not hold the process open.
|
|
13
|
+
*/
|
|
14
|
+
/** A deadline bound to one provider request. */
|
|
15
|
+
export interface RequestDeadline {
|
|
16
|
+
/** Pass as `signal` to `fetch`. Aborts with a `TimeoutError`. */
|
|
17
|
+
readonly signal: AbortSignal;
|
|
18
|
+
/**
|
|
19
|
+
* Settle `promise` or reject with the timeout, whichever comes first.
|
|
20
|
+
*
|
|
21
|
+
* The global `fetch` rejects as soon as `signal` aborts, but a
|
|
22
|
+
* caller-supplied `config.fetch` (or the body stream of a hand-built
|
|
23
|
+
* `Response`) may ignore the signal; racing makes the deadline hold
|
|
24
|
+
* either way.
|
|
25
|
+
*/
|
|
26
|
+
race<T>(promise: Promise<T>): Promise<T>;
|
|
27
|
+
/** Release the timer. Idempotent; call once the request has settled. */
|
|
28
|
+
clear(): void;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a request deadline of `timeoutMs` milliseconds.
|
|
32
|
+
*
|
|
33
|
+
* The abort reason is a `DOMException` named `TimeoutError`, the same
|
|
34
|
+
* shape `AbortSignal.timeout()` produces, so `fetch` rejections are
|
|
35
|
+
* classified identically by the caller.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createRequestDeadline(timeoutMs: number): RequestDeadline;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request deadline that keeps the process alive until it fires.
|
|
3
|
+
*
|
|
4
|
+
* @module oauthClient/http/oauthHttp.timeout
|
|
5
|
+
*
|
|
6
|
+
* `AbortSignal.timeout()` schedules its timer *unref'd*: if nothing else
|
|
7
|
+
* holds the event loop — a CLI, a one-shot script, a test runner's last
|
|
8
|
+
* await — Node exits with code 13 ("unsettled top-level await") before the
|
|
9
|
+
* deadline ever fires, and the caller never sees the documented
|
|
10
|
+
* `OAuthNetworkError`. A plain `setTimeout` is ref'd, so the deadline is
|
|
11
|
+
* guaranteed to be observed; `clear()` releases it as soon as the request
|
|
12
|
+
* settles so a fast response does not hold the process open.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Create a request deadline of `timeoutMs` milliseconds.
|
|
16
|
+
*
|
|
17
|
+
* The abort reason is a `DOMException` named `TimeoutError`, the same
|
|
18
|
+
* shape `AbortSignal.timeout()` produces, so `fetch` rejections are
|
|
19
|
+
* classified identically by the caller.
|
|
20
|
+
*/
|
|
21
|
+
export function createRequestDeadline(timeoutMs) {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
controller.abort(new DOMException(`The operation was aborted due to timeout after ${timeoutMs}ms.`, "TimeoutError"));
|
|
25
|
+
}, timeoutMs);
|
|
26
|
+
const signal = controller.signal;
|
|
27
|
+
return {
|
|
28
|
+
signal,
|
|
29
|
+
race(promise) {
|
|
30
|
+
if (signal.aborted)
|
|
31
|
+
return Promise.reject(signal.reason);
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const onAbort = () => reject(signal.reason);
|
|
34
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
35
|
+
promise.then(resolve, reject).finally(() => {
|
|
36
|
+
signal.removeEventListener("abort", onAbort);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
clear() {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -7,4 +7,3 @@ export { DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_RESPONSE_BYTES, MIN_MAX
|
|
|
7
7
|
export { createAuthorizationUrl } from "./oauthAuthorize.core.js";
|
|
8
8
|
export { parseTokenResponse, exchangeCodeForToken, refreshAccessToken, } from "./oauthToken.core.js";
|
|
9
9
|
export { fetchUserInfo } from "./oauthUserInfo.core.js";
|
|
10
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -7,4 +7,3 @@ export { DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_RESPONSE_BYTES, MIN_MAX
|
|
|
7
7
|
export { createAuthorizationUrl } from "./oauthAuthorize.core.js";
|
|
8
8
|
export { parseTokenResponse, exchangeCodeForToken, refreshAccessToken, } from "./oauthToken.core.js";
|
|
9
9
|
export { fetchUserInfo } from "./oauthUserInfo.core.js";
|
|
10
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -31,4 +31,3 @@ import type { AuthorizationUrlOptions, AuthorizationUrlResult, OAuthConfig } fro
|
|
|
31
31
|
* @throws {OAuthEndpointNotAllowedError} If `authorizeUrl` fails the URL guard.
|
|
32
32
|
*/
|
|
33
33
|
export declare function createAuthorizationUrl(config: OAuthConfig, options: AuthorizationUrlOptions): AuthorizationUrlResult;
|
|
34
|
-
//# sourceMappingURL=oauthAuthorize.core.d.ts.map
|
|
@@ -67,4 +67,3 @@ export declare function resolveUserInfoUrl(resolved: ResolvedOAuthConfig): URL;
|
|
|
67
67
|
* @throws {OAuthRedirectUriError} If it is not allowlisted.
|
|
68
68
|
*/
|
|
69
69
|
export declare function assertRedirectUriAllowed(resolved: ResolvedOAuthConfig, redirectUri: string): string;
|
|
70
|
-
//# sourceMappingURL=oauthConfig.resolve.d.ts.map
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* An OAuth provider is an untrusted remote. Every request made here is
|
|
7
7
|
* bounded three ways:
|
|
8
8
|
*
|
|
9
|
-
* - **Time** — `
|
|
9
|
+
* - **Time** — a ref'd deadline of `config.timeoutMs` (default 10s) that
|
|
10
|
+
* aborts the request and the body read; see `createRequestDeadline` for
|
|
11
|
+
* why it is not `AbortSignal.timeout()`.
|
|
10
12
|
* - **Size** — the body is streamed and abandoned the moment it exceeds
|
|
11
13
|
* `config.maxResponseBytes` (default 256 KiB), and a `Content-Length` over
|
|
12
14
|
* the cap is refused before reading at all.
|
|
@@ -40,4 +42,3 @@ export declare function requestProviderJson(resolved: ResolvedOAuthConfig, reque
|
|
|
40
42
|
export declare function requestProviderValue(resolved: ResolvedOAuthConfig, request: ProviderRequest): Promise<unknown>;
|
|
41
43
|
/** Build the `Authorization: Basic` header for client authentication. */
|
|
42
44
|
export declare function basicAuthHeader(clientId: string, clientSecret: string): string;
|
|
43
|
-
//# sourceMappingURL=oauthHttp.core.d.ts.map
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* An OAuth provider is an untrusted remote. Every request made here is
|
|
7
7
|
* bounded three ways:
|
|
8
8
|
*
|
|
9
|
-
* - **Time** — `
|
|
9
|
+
* - **Time** — a ref'd deadline of `config.timeoutMs` (default 10s) that
|
|
10
|
+
* aborts the request and the body read; see `createRequestDeadline` for
|
|
11
|
+
* why it is not `AbortSignal.timeout()`.
|
|
10
12
|
* - **Size** — the body is streamed and abandoned the moment it exceeds
|
|
11
13
|
* `config.maxResponseBytes` (default 256 KiB), and a `Content-Length` over
|
|
12
14
|
* the cap is refused before reading at all.
|
|
@@ -15,6 +17,7 @@
|
|
|
15
17
|
*/
|
|
16
18
|
import { OAuthError, OAuthNetworkError, OAuthProviderError, OAuthResponseError, OAuthResponseTooLargeError, } from "../oauthErrors/index.js";
|
|
17
19
|
import { parseJsonObject, parseJsonValue } from "../oauthSecurity/index.js";
|
|
20
|
+
import { createRequestDeadline, } from "./http/index.js";
|
|
18
21
|
/** Provider `error` codes are echoed only if they look like OAuth error codes. */
|
|
19
22
|
const SAFE_ERROR_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
20
23
|
/** Read a response body, refusing to buffer more than `maxBytes`. */
|
|
@@ -85,15 +88,24 @@ export async function requestProviderJson(resolved, request) {
|
|
|
85
88
|
* `/user/emails`). Same time, size and redirect bounds.
|
|
86
89
|
*/
|
|
87
90
|
export async function requestProviderValue(resolved, request) {
|
|
91
|
+
const deadline = createRequestDeadline(resolved.timeoutMs);
|
|
92
|
+
try {
|
|
93
|
+
return await requestWithinDeadline(resolved, request, deadline);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
deadline.clear();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function requestWithinDeadline(resolved, request, deadline) {
|
|
88
100
|
let response;
|
|
89
101
|
try {
|
|
90
|
-
response = await resolved.fetchImpl(request.url.toString(), {
|
|
102
|
+
response = await deadline.race(resolved.fetchImpl(request.url.toString(), {
|
|
91
103
|
method: request.method,
|
|
92
104
|
headers: request.headers,
|
|
93
105
|
...(request.body !== undefined ? { body: request.body } : {}),
|
|
94
106
|
redirect: "manual",
|
|
95
|
-
signal:
|
|
96
|
-
});
|
|
107
|
+
signal: deadline.signal,
|
|
108
|
+
}));
|
|
97
109
|
}
|
|
98
110
|
catch (cause) {
|
|
99
111
|
throw toNetworkError(cause, request.label, resolved.timeoutMs);
|
|
@@ -103,7 +115,7 @@ export async function requestProviderValue(resolved, request) {
|
|
|
103
115
|
}
|
|
104
116
|
let text;
|
|
105
117
|
try {
|
|
106
|
-
text = await readCappedText(response, resolved.maxResponseBytes);
|
|
118
|
+
text = await deadline.race(readCappedText(response, resolved.maxResponseBytes));
|
|
107
119
|
}
|
|
108
120
|
catch (cause) {
|
|
109
121
|
// The timeout signal also aborts the body stream, and a transport can
|
|
@@ -151,4 +163,3 @@ export function basicAuthHeader(clientId, clientSecret) {
|
|
|
151
163
|
const encoded = Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`, "utf8").toString("base64");
|
|
152
164
|
return `Basic ${encoded}`;
|
|
153
165
|
}
|
|
154
|
-
//# sourceMappingURL=oauthHttp.core.js.map
|
|
@@ -47,4 +47,3 @@ export declare function exchangeCodeForToken(config: OAuthConfig, options: CodeE
|
|
|
47
47
|
* @throws {OAuthConfigurationError} If the provider has no refresh support.
|
|
48
48
|
*/
|
|
49
49
|
export declare function refreshAccessToken(config: OAuthConfig, refreshToken: string): Promise<OAuthTokenSet>;
|
|
50
|
-
//# sourceMappingURL=oauthToken.core.d.ts.map
|
|
@@ -24,4 +24,3 @@ import type { OAuthConfig, OAuthUserInfo } from "../oauthTypes/index.js";
|
|
|
24
24
|
* @throws {OAuthResponseError} If the payload has no usable user id.
|
|
25
25
|
*/
|
|
26
26
|
export declare function fetchUserInfo(config: OAuthConfig, accessToken: string): Promise<OAuthUserInfo>;
|
|
27
|
-
//# sourceMappingURL=oauthUserInfo.core.d.ts.map
|
|
@@ -4,4 +4,3 @@
|
|
|
4
4
|
* @module oauthErrors
|
|
5
5
|
*/
|
|
6
6
|
export { OAuthErrorCode, type OAuthErrorOptions, OAuthError, OAuthConfigurationError, OAuthEndpointNotAllowedError, OAuthRedirectUriError, OAuthStateMismatchError, OAuthProviderError, OAuthResponseError, OAuthResponseTooLargeError, OAuthNetworkError, } from "./oauthError.base.js";
|
|
7
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -4,4 +4,3 @@
|
|
|
4
4
|
* @module oauthErrors
|
|
5
5
|
*/
|
|
6
6
|
export { OAuthErrorCode, OAuthError, OAuthConfigurationError, OAuthEndpointNotAllowedError, OAuthRedirectUriError, OAuthStateMismatchError, OAuthProviderError, OAuthResponseError, OAuthResponseTooLargeError, OAuthNetworkError, } from "./oauthError.base.js";
|
|
7
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -50,4 +50,3 @@ export declare const PROVIDER_PRESETS: Readonly<Record<OAuthProvider, OAuthProvi
|
|
|
50
50
|
* @returns The normalised profile, or `undefined` if no stable id was found.
|
|
51
51
|
*/
|
|
52
52
|
export declare function normalizeUserInfo(provider: OAuthProvider, payload: Record<string, unknown>): OAuthUserInfo | undefined;
|
|
53
|
-
//# sourceMappingURL=oauthProvider.presets.d.ts.map
|
|
@@ -7,4 +7,3 @@ export { generateCodeVerifier, assertValidCodeVerifier, deriveCodeChallenge, } f
|
|
|
7
7
|
export { generateState, verifyState } from "./oauthState.core.js";
|
|
8
8
|
export { isBlockedFetchHost, assertSafeUrl, type UrlUse, } from "./oauthUrl.guard.js";
|
|
9
9
|
export { sanitizeJsonValue, parseJsonObject, parseJsonValue, } from "./oauthJson.sanitize.js";
|
|
10
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -7,4 +7,3 @@ export { generateCodeVerifier, assertValidCodeVerifier, deriveCodeChallenge, } f
|
|
|
7
7
|
export { generateState, verifyState } from "./oauthState.core.js";
|
|
8
8
|
export { isBlockedFetchHost, assertSafeUrl, } from "./oauthUrl.guard.js";
|
|
9
9
|
export { sanitizeJsonValue, parseJsonObject, parseJsonValue, } from "./oauthJson.sanitize.js";
|
|
10
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -35,4 +35,3 @@ export declare function parseJsonObject(text: string, label: string): Record<str
|
|
|
35
35
|
* @throws {OAuthResponseError} If the body is not valid JSON.
|
|
36
36
|
*/
|
|
37
37
|
export declare function parseJsonValue(text: string, label: string): unknown;
|
|
38
|
-
//# sourceMappingURL=oauthJson.sanitize.d.ts.map
|
|
@@ -4,4 +4,3 @@
|
|
|
4
4
|
* @module oauthTypes
|
|
5
5
|
*/
|
|
6
6
|
export { type OAuthProvider, type ClientAuthMethod, type FetchLike, type OAuthConfig, type AuthorizationUrlOptions, type AuthorizationUrlResult, type CodeExchangeOptions, type OAuthTokenSet, type OAuthUserInfo, } from "./oauth.type.js";
|
|
7
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/oauthTypes/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/auth-oauth",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "OAuth2 authorization-code client for the Zudojs framework — PKCE S256, mandatory state, SSRF-guarded endpoints, and provider presets for Google, GitHub, Microsoft, Apple and Discord.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"directory": "packages/auth-oauth"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@zudojs/errors": "1.
|
|
57
|
-
"@zudojs/security": "1.3.
|
|
56
|
+
"@zudojs/errors": "1.4.0",
|
|
57
|
+
"@zudojs/security": "1.3.4"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "tsc -p tsconfig.json",
|