@zudojs/auth-oauth 1.2.0 → 1.2.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 CHANGED
@@ -92,7 +92,9 @@ app.get("/auth/callback", async (req, res) => {
92
92
  const profile = await fetchUserInfo(config, tokens.accessToken);
93
93
 
94
94
  if (profile.email === undefined) {
95
- return res.status(400).send("This provider did not release an email address.");
95
+ return res
96
+ .status(400)
97
+ .send("This provider did not release an email address.");
96
98
  }
97
99
 
98
100
  // Now it is your application's turn: find or create the local account keyed
@@ -115,29 +117,29 @@ const fresh = await refreshAccessToken(config, storedRefreshToken);
115
117
 
116
118
  ## API
117
119
 
118
- | Export | What it does |
119
- | --- | --- |
120
- | `createAuthorizationUrl(config, options)` | Builds the authorize URL. Returns `{ url, state, codeVerifier, codeChallenge }`. |
121
- | `exchangeCodeForToken(config, options)` | Authorization-code grant. Returns a validated `OAuthTokenSet`. |
122
- | `refreshAccessToken(config, refreshToken)` | Refresh-token grant, where the provider supports one. |
123
- | `fetchUserInfo(config, accessToken)` | Bearer GET of the user-info endpoint, normalised to `OAuthUserInfo`. |
124
- | `generateState()` / `verifyState(expected, received)` | 256-bit state, timing-safe comparison. |
125
- | `generateCodeVerifier()` / `deriveCodeChallenge(verifier)` | PKCE primitives (`S256`). |
126
- | `parseTokenResponse(payload)` | Validate a token payload you obtained elsewhere. |
127
- | `normalizeUserInfo(provider, payload)` | Normalise a profile payload you obtained elsewhere. |
128
- | `assertSafeUrl(url, label, use)` / `isBlockedFetchHost(host)` | The URL and SSRF guards, exposed for your own checks. |
129
- | `PROVIDER_PRESETS` | Endpoint defaults per provider. |
120
+ | Export | What it does |
121
+ | ------------------------------------------------------------- | -------------------------------------------------------------------------------- |
122
+ | `createAuthorizationUrl(config, options)` | Builds the authorize URL. Returns `{ url, state, codeVerifier, codeChallenge }`. |
123
+ | `exchangeCodeForToken(config, options)` | Authorization-code grant. Returns a validated `OAuthTokenSet`. |
124
+ | `refreshAccessToken(config, refreshToken)` | Refresh-token grant, where the provider supports one. |
125
+ | `fetchUserInfo(config, accessToken)` | Bearer GET of the user-info endpoint, normalised to `OAuthUserInfo`. |
126
+ | `generateState()` / `verifyState(expected, received)` | 256-bit state, timing-safe comparison. |
127
+ | `generateCodeVerifier()` / `deriveCodeChallenge(verifier)` | PKCE primitives (`S256`). |
128
+ | `parseTokenResponse(payload)` | Validate a token payload you obtained elsewhere. |
129
+ | `normalizeUserInfo(provider, payload)` | Normalise a profile payload you obtained elsewhere. |
130
+ | `assertSafeUrl(url, label, use)` / `isBlockedFetchHost(host)` | The URL and SSRF guards, exposed for your own checks. |
131
+ | `PROVIDER_PRESETS` | Endpoint defaults per provider. |
130
132
 
131
133
  ## Providers
132
134
 
133
- | Provider | Endpoints | Client auth | Refresh | Notes |
134
- | --- | --- | --- | --- | --- |
135
- | `google` | preset | body | yes | `access_type=offline` + `prompt=consent` are sent so a refresh token is actually issued. |
136
- | `github` | preset | body | **no** | Classic OAuth App tokens do not expire and no refresh token is issued; `refreshAccessToken` throws rather than making a pointless request. |
137
- | `microsoft` | preset (`common` tenant) | body | yes | Override `authorizeUrl`/`tokenUrl` for a single-tenant app. |
138
- | `apple` | preset | body | yes | `clientSecret` is the ES256 JWT you mint from your private key — this package does not generate it. Apple has **no user-info endpoint**; the profile is in the `id_token`, so `fetchUserInfo` throws for `apple`. |
139
- | `discord` | preset | **basic** | yes | |
140
- | `custom` | you supply all three URLs | body | yes | |
135
+ | Provider | Endpoints | Client auth | Refresh | Notes |
136
+ | ----------- | ------------------------- | ----------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
137
+ | `google` | preset | body | yes | `access_type=offline` + `prompt=consent` are sent so a refresh token is actually issued. |
138
+ | `github` | preset | body | **no** | Classic OAuth App tokens do not expire and no refresh token is issued; `refreshAccessToken` throws rather than making a pointless request. |
139
+ | `microsoft` | preset (`common` tenant) | body | yes | Override `authorizeUrl`/`tokenUrl` for a single-tenant app. |
140
+ | `apple` | preset | body | yes | `clientSecret` is the ES256 JWT you mint from your private key — this package does not generate it. Apple has **no user-info endpoint**; the profile is in the `id_token`, so `fetchUserInfo` throws for `apple`. |
141
+ | `discord` | preset | **basic** | yes | |
142
+ | `custom` | you supply all three URLs | body | yes | |
141
143
 
142
144
  Any preset URL can be overridden on the config; every override goes through the
143
145
  same validation.
@@ -148,7 +150,7 @@ same validation.
148
150
 
149
151
  - **GitHub** omits `email` from `/user` whenever the address is private, which
150
152
  is the default. When the `user:email` scope was granted, this package asks
151
- `/user/emails` and uses the primary *verified* address. Without the scope, or
153
+ `/user/emails` and uses the primary _verified_ address. Without the scope, or
152
154
  without a verified primary, the profile simply comes back with no email.
153
155
  - **Discord** returns no email unless the `email` scope was granted.
154
156
 
@@ -173,7 +175,7 @@ Every one of these is covered by a test in `tests/`.
173
175
  arbitrary redirect target is never reflected.
174
176
  - **URL validation and SSRF guard.** Every URL must be `https` — `http` is
175
177
  tolerated only for `localhost` / `127.0.0.1` / `[::1]` and only on
176
- browser-facing URLs — and must not embed credentials. The *server-fetched*
178
+ browser-facing URLs — and must not embed credentials. The _server-fetched_
177
179
  endpoints (token, user-info) additionally may not point at a loopback,
178
180
  private, CGNAT, link-local, unique-local, multicast or reserved address, at
179
181
  `169.254.169.254` and friends, or at a `localhost` / `*.local` / `*.internal`
@@ -191,7 +193,7 @@ Every one of these is covered by a test in `tests/`.
191
193
  `AbortSignal.timeout(timeoutMs)` (default 10s) and the body is streamed and
192
194
  abandoned the moment it passes `maxResponseBytes` (default 256 KiB); an
193
195
  oversized `Content-Length` is refused before a byte is read.
194
- - **Defensive parsing.** A token response must be a JSON *object* with a
196
+ - **Defensive parsing.** A token response must be a JSON _object_ with a
195
197
  non-blank string `access_token`; `expires_in` must be a non-negative integer
196
198
  (or its decimal string); `refresh_token`, `id_token`, `scope` and `token_type`
197
199
  must be strings when present. `__proto__`, `constructor` and `prototype` are
@@ -214,30 +216,30 @@ Every failure is an `OAuthError` with a machine-readable `code`, a suggested
214
216
  class below is also a `BaseError`, and its codes equal the shared
215
217
  `ErrorCode.OAUTH_*` members.
216
218
 
217
- | Class | Code | Status | Exposed |
218
- | --- | --- | --- | --- |
219
- | `OAuthConfigurationError` | `OAUTH_CONFIGURATION_INVALID` | 500 | no |
220
- | `OAuthEndpointNotAllowedError` | `OAUTH_ENDPOINT_NOT_ALLOWED` | 500 | no |
221
- | `OAuthRedirectUriError` | `OAUTH_REDIRECT_URI_NOT_ALLOWED` | 400 | yes |
222
- | `OAuthStateMismatchError` | `OAUTH_STATE_MISMATCH` | 400 | yes |
223
- | `OAuthProviderError` | `OAUTH_PROVIDER_REJECTED` | 502 | yes |
224
- | `OAuthResponseError` | `OAUTH_PROVIDER_RESPONSE_INVALID` | 502 | yes |
225
- | `OAuthResponseTooLargeError` | `OAUTH_RESPONSE_TOO_LARGE` | 502 | yes |
226
- | `OAuthNetworkError` | `OAUTH_NETWORK` | 504 | yes |
219
+ | Class | Code | Status | Exposed |
220
+ | ------------------------------ | --------------------------------- | ------ | ------- |
221
+ | `OAuthConfigurationError` | `OAUTH_CONFIGURATION_INVALID` | 500 | no |
222
+ | `OAuthEndpointNotAllowedError` | `OAUTH_ENDPOINT_NOT_ALLOWED` | 500 | no |
223
+ | `OAuthRedirectUriError` | `OAUTH_REDIRECT_URI_NOT_ALLOWED` | 400 | yes |
224
+ | `OAuthStateMismatchError` | `OAUTH_STATE_MISMATCH` | 400 | yes |
225
+ | `OAuthProviderError` | `OAUTH_PROVIDER_REJECTED` | 502 | yes |
226
+ | `OAuthResponseError` | `OAUTH_PROVIDER_RESPONSE_INVALID` | 502 | yes |
227
+ | `OAuthResponseTooLargeError` | `OAUTH_RESPONSE_TOO_LARGE` | 502 | yes |
228
+ | `OAuthNetworkError` | `OAUTH_NETWORK` | 504 | yes |
227
229
 
228
230
  ## Configuration reference
229
231
 
230
- | Field | Required | Default |
231
- | --- | --- | --- |
232
- | `provider` | yes | — |
233
- | `clientId`, `clientSecret` | yes | — |
234
- | `allowedRedirectUris` | yes, non-empty | — |
235
- | `authorizeUrl`, `tokenUrl`, `userInfoUrl` | only for `custom` | the preset's |
236
- | `scopes` | no | the preset's |
237
- | `clientAuthMethod` | no | the preset's |
238
- | `timeoutMs` | no | `10000` (1 - 120000) |
239
- | `maxResponseBytes` | no | `262144` (1024 - 5242880) |
240
- | `fetch` | no | global `fetch` |
232
+ | Field | Required | Default |
233
+ | ----------------------------------------- | ----------------- | ------------------------- |
234
+ | `provider` | yes | — |
235
+ | `clientId`, `clientSecret` | yes | — |
236
+ | `allowedRedirectUris` | yes, non-empty | — |
237
+ | `authorizeUrl`, `tokenUrl`, `userInfoUrl` | only for `custom` | the preset's |
238
+ | `scopes` | no | the preset's |
239
+ | `clientAuthMethod` | no | the preset's |
240
+ | `timeoutMs` | no | `10000` (1 - 120000) |
241
+ | `maxResponseBytes` | no | `262144` (1024 - 5242880) |
242
+ | `fetch` | no | global `fetch` |
241
243
 
242
244
  Out-of-range values are rejected, not clamped.
243
245
 
@@ -28,7 +28,9 @@ function requireString(value, field) {
28
28
  function boundedInt(value, fallback, min, max, field) {
29
29
  if (value === undefined)
30
30
  return fallback;
31
- if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value)) {
31
+ if (typeof value !== "number" ||
32
+ !Number.isFinite(value) ||
33
+ !Number.isInteger(value)) {
32
34
  throw new OAuthConfigurationError(`${field} must be an integer.`);
33
35
  }
34
36
  if (value < min || value > max) {
@@ -50,7 +52,8 @@ export function assertScopes(scopes) {
50
52
  throw new OAuthConfigurationError("scopes must be an array of scope-tokens.");
51
53
  }
52
54
  for (const scope of scopes) {
53
- if (typeof scope !== "string" || !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
55
+ if (typeof scope !== "string" ||
56
+ !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
54
57
  throw new OAuthConfigurationError("Each scope must be a non-empty RFC 6749 scope-token.");
55
58
  }
56
59
  }
@@ -20,7 +20,9 @@ const SAFE_ERROR_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
20
20
  /** Read a response body, refusing to buffer more than `maxBytes`. */
21
21
  async function readCappedText(response, maxBytes) {
22
22
  const declared = response.headers.get("content-length");
23
- if (declared !== null && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
23
+ if (declared !== null &&
24
+ /^\d+$/.test(declared) &&
25
+ Number(declared) > maxBytes) {
24
26
  throw new OAuthResponseTooLargeError(maxBytes);
25
27
  }
26
28
  const body = response.body;
@@ -7,7 +7,7 @@ import { OAuthResponseError } from "../oauthErrors/index.js";
7
7
  import { normalizeUserInfo } from "../oauthProviders/index.js";
8
8
  import { assertSafeUrl } from "../oauthSecurity/index.js";
9
9
  import { resolveConfig, resolveUserInfoUrl, } from "./oauthConfig.resolve.js";
10
- import { requestProviderJson, requestProviderValue, } from "./oauthHttp.core.js";
10
+ import { requestProviderJson, requestProviderValue } from "./oauthHttp.core.js";
11
11
  /**
12
12
  * GitHub's `/user` omits `email` whenever the address is private — which is
13
13
  * the default for new accounts. When `user:email` was granted we ask
@@ -62,7 +62,13 @@ export const PROVIDER_PRESETS = {
62
62
  authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
63
63
  tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
64
64
  userInfoUrl: "https://graph.microsoft.com/v1.0/me",
65
- defaultScopes: ["openid", "email", "profile", "offline_access", "User.Read"],
65
+ defaultScopes: [
66
+ "openid",
67
+ "email",
68
+ "profile",
69
+ "offline_access",
70
+ "User.Read",
71
+ ],
66
72
  clientAuth: "body",
67
73
  supportsRefresh: true,
68
74
  },
@@ -59,7 +59,9 @@ export function sanitizeJsonValue(value) {
59
59
  */
60
60
  export function parseJsonObject(text, label) {
61
61
  const sanitized = parseJsonValue(text, label);
62
- if (sanitized === null || typeof sanitized !== "object" || Array.isArray(sanitized)) {
62
+ if (sanitized === null ||
63
+ typeof sanitized !== "object" ||
64
+ Array.isArray(sanitized)) {
63
65
  throw new OAuthResponseError(`${label} did not return a JSON object.`);
64
66
  }
65
67
  return sanitized;
@@ -115,9 +115,7 @@ function isNonPublicIpv6(raw) {
115
115
  */
116
116
  export function isBlockedFetchHost(hostname) {
117
117
  const host = hostname.toLowerCase();
118
- const unbracketed = host.startsWith("[") && host.endsWith("]")
119
- ? host.slice(1, -1)
120
- : host;
118
+ const unbracketed = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
121
119
  // A trailing dot marks a fully-qualified name (`localhost.`,
122
120
  // `metadata.google.internal.`). DNS resolves it to the same address as
123
121
  // the undotted form, but the WHATWG parser keeps the dot on domain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/auth-oauth",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
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.1.0",
57
- "@zudojs/security": "1.1.0"
56
+ "@zudojs/errors": "1.2.0",
57
+ "@zudojs/security": "1.2.0"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsc -p tsconfig.json",