@better-auth/core 1.7.0-beta.1 → 1.7.0-beta.2
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/dist/api/index.mjs +29 -3
- package/dist/context/global.mjs +1 -1
- package/dist/db/adapter/factory.mjs +1 -1
- package/dist/db/adapter/get-id-field.mjs +1 -1
- package/dist/instrumentation/api.mjs +12 -0
- package/dist/instrumentation/noop.mjs +42 -0
- package/dist/instrumentation/pure.index.d.mts +7 -0
- package/dist/instrumentation/pure.index.mjs +7 -0
- package/dist/instrumentation/tracer.mjs +6 -3
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +2 -2
- package/dist/oauth2/utils.d.mts +10 -1
- package/dist/oauth2/utils.mjs +13 -1
- package/dist/social-providers/apple.d.mts +11 -2
- package/dist/social-providers/apple.mjs +7 -1
- package/dist/social-providers/atlassian.mjs +1 -1
- package/dist/social-providers/cognito.d.mts +1 -1
- package/dist/social-providers/cognito.mjs +3 -2
- package/dist/social-providers/facebook.d.mts +1 -1
- package/dist/social-providers/facebook.mjs +7 -0
- package/dist/social-providers/figma.mjs +1 -1
- package/dist/social-providers/google.d.mts +1 -1
- package/dist/social-providers/google.mjs +3 -2
- package/dist/social-providers/microsoft-entra-id.d.mts +2 -2
- package/dist/social-providers/microsoft-entra-id.mjs +6 -1
- package/dist/social-providers/paybin.mjs +1 -1
- package/dist/social-providers/paypal.mjs +1 -1
- package/dist/social-providers/salesforce.mjs +1 -1
- package/dist/utils/async.d.mts +22 -0
- package/dist/utils/async.mjs +32 -0
- package/dist/utils/host.d.mts +147 -0
- package/dist/utils/host.mjs +291 -0
- package/dist/utils/is-api-error.d.mts +6 -0
- package/dist/utils/is-api-error.mjs +8 -0
- package/package.json +10 -1
- package/src/api/index.ts +39 -5
- package/src/db/adapter/get-id-field.ts +2 -2
- package/src/instrumentation/api.ts +17 -0
- package/src/instrumentation/noop.ts +74 -0
- package/src/instrumentation/pure.index.ts +31 -0
- package/src/instrumentation/tracer.ts +8 -3
- package/src/oauth2/index.ts +5 -1
- package/src/oauth2/utils.ts +13 -0
- package/src/social-providers/apple.ts +10 -2
- package/src/social-providers/cognito.ts +3 -2
- package/src/social-providers/facebook.ts +10 -1
- package/src/social-providers/google.ts +3 -2
- package/src/social-providers/microsoft-entra-id.ts +13 -3
- package/src/utils/async.ts +53 -0
- package/src/utils/host.ts +401 -0
- package/src/utils/is-api-error.ts +10 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Noop variant of `./instrumentation` for runtimes where the dynamic
|
|
3
|
+
* `import("@opentelemetry/api")` in `./api` throws synchronously instead of
|
|
4
|
+
* rejecting its returned promise. Convex's V8 isolate is the reproducer: bare
|
|
5
|
+
* specifiers are rejected at resolve time in `get-convex/convex-backend`
|
|
6
|
+
* `crates/isolate/src/request_scope.rs`, so the `.catch()` in
|
|
7
|
+
* `getOpenTelemetryAPI` never runs and every `withSpan` call surfaces an
|
|
8
|
+
* uncaught error.
|
|
9
|
+
*
|
|
10
|
+
* Public surface must stay identical to `./index` (enforced by `pure.test.ts`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export * from "./attributes";
|
|
14
|
+
|
|
15
|
+
export function withSpan<T>(
|
|
16
|
+
name: string,
|
|
17
|
+
attributes: Record<string, string | number | boolean>,
|
|
18
|
+
fn: () => T,
|
|
19
|
+
): T;
|
|
20
|
+
export function withSpan<T>(
|
|
21
|
+
name: string,
|
|
22
|
+
attributes: Record<string, string | number | boolean>,
|
|
23
|
+
fn: () => Promise<T>,
|
|
24
|
+
): Promise<T>;
|
|
25
|
+
export function withSpan<T>(
|
|
26
|
+
_name: string,
|
|
27
|
+
_attributes: Record<string, string | number | boolean>,
|
|
28
|
+
fn: () => T | Promise<T>,
|
|
29
|
+
): T | Promise<T> {
|
|
30
|
+
return fn();
|
|
31
|
+
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { Span } from "@opentelemetry/api";
|
|
2
|
-
import {
|
|
2
|
+
import { getOpenTelemetryAPI } from "./api";
|
|
3
3
|
import { ATTR_HTTP_RESPONSE_STATUS_CODE } from "./attributes";
|
|
4
4
|
|
|
5
5
|
const INSTRUMENTATION_SCOPE = "better-auth";
|
|
6
6
|
const INSTRUMENTATION_VERSION = import.meta.env?.BETTER_AUTH_VERSION ?? "1.0.0";
|
|
7
7
|
|
|
8
|
-
const tracer = trace.getTracer(INSTRUMENTATION_SCOPE, INSTRUMENTATION_VERSION);
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
9
|
* Better-auth uses `throw ctx.redirect(url)` for flow control (e.g. OAuth
|
|
12
10
|
* callbacks). These are APIErrors with 3xx status codes and should not be
|
|
@@ -27,6 +25,7 @@ function isRedirectError(err: unknown): boolean {
|
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
function endSpanWithError(span: Span, err: unknown) {
|
|
28
|
+
const { SpanStatusCode } = getOpenTelemetryAPI();
|
|
30
29
|
if (isRedirectError(err)) {
|
|
31
30
|
span.setAttribute(
|
|
32
31
|
ATTR_HTTP_RESPONSE_STATUS_CODE,
|
|
@@ -66,6 +65,12 @@ export function withSpan<T>(
|
|
|
66
65
|
attributes: Record<string, string | number | boolean>,
|
|
67
66
|
fn: () => T | Promise<T>,
|
|
68
67
|
): T | Promise<T> {
|
|
68
|
+
const { trace } = getOpenTelemetryAPI();
|
|
69
|
+
const tracer = trace.getTracer(
|
|
70
|
+
INSTRUMENTATION_SCOPE,
|
|
71
|
+
INSTRUMENTATION_VERSION,
|
|
72
|
+
);
|
|
73
|
+
|
|
69
74
|
return tracer.startActiveSpan(name, { attributes }, (span) => {
|
|
70
75
|
try {
|
|
71
76
|
const result = fn();
|
package/src/oauth2/index.ts
CHANGED
|
@@ -25,7 +25,11 @@ export {
|
|
|
25
25
|
refreshAccessToken,
|
|
26
26
|
refreshAccessTokenRequest,
|
|
27
27
|
} from "./refresh-access-token";
|
|
28
|
-
export {
|
|
28
|
+
export {
|
|
29
|
+
generateCodeChallenge,
|
|
30
|
+
getOAuth2Tokens,
|
|
31
|
+
getPrimaryClientId,
|
|
32
|
+
} from "./utils";
|
|
29
33
|
export {
|
|
30
34
|
authorizationCodeRequest,
|
|
31
35
|
createAuthorizationCodeRequest,
|
package/src/oauth2/utils.ts
CHANGED
|
@@ -28,6 +28,19 @@ export function getOAuth2Tokens(data: Record<string, any>): OAuth2Tokens {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Return the provider's primary Client ID: the single string, or the entry at
|
|
33
|
+
* array index 0 for the cross-platform form used by ID token audience
|
|
34
|
+
* verification. Index 0 is the designated primary and pairs with
|
|
35
|
+
* `clientSecret` for the authorization code flow; later array entries are
|
|
36
|
+
* only used as additional accepted audiences. Returns `undefined` when the
|
|
37
|
+
* primary value is missing or an empty string.
|
|
38
|
+
*/
|
|
39
|
+
export function getPrimaryClientId(clientId: unknown): string | undefined {
|
|
40
|
+
const value = Array.isArray(clientId) ? clientId[0] : clientId;
|
|
41
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
export async function generateCodeChallenge(codeVerifier: string) {
|
|
32
45
|
const encoder = new TextEncoder();
|
|
33
46
|
const data = encoder.encode(codeVerifier);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
|
|
3
3
|
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
|
|
4
|
-
import {
|
|
4
|
+
import { logger } from "../env";
|
|
5
|
+
import { APIError, BetterAuthError } from "../error";
|
|
5
6
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
6
7
|
import {
|
|
7
8
|
createAuthorizationURL,
|
|
9
|
+
getPrimaryClientId,
|
|
8
10
|
refreshAccessToken,
|
|
9
11
|
validateAuthorizationCode,
|
|
10
12
|
} from "../oauth2";
|
|
@@ -70,7 +72,7 @@ export interface AppleNonConformUser {
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export interface AppleOptions extends ProviderOptions<AppleProfile> {
|
|
73
|
-
clientId: string;
|
|
75
|
+
clientId: string | string[];
|
|
74
76
|
appBundleIdentifier?: string | undefined;
|
|
75
77
|
audience?: (string | string[]) | undefined;
|
|
76
78
|
}
|
|
@@ -81,6 +83,12 @@ export const apple = (options: AppleOptions) => {
|
|
|
81
83
|
id: "apple",
|
|
82
84
|
name: "Apple",
|
|
83
85
|
async createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
86
|
+
if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
|
|
87
|
+
logger.error(
|
|
88
|
+
"Client ID and client secret are required for Apple. Make sure to provide them in the options.",
|
|
89
|
+
);
|
|
90
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
91
|
+
}
|
|
84
92
|
const _scope = options.disableDefaultScope ? [] : ["email", "name"];
|
|
85
93
|
if (options.scope) _scope.push(...options.scope);
|
|
86
94
|
if (scopes) _scope.push(...scopes);
|
|
@@ -5,6 +5,7 @@ import { APIError, BetterAuthError } from "../error";
|
|
|
5
5
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
6
6
|
import {
|
|
7
7
|
createAuthorizationURL,
|
|
8
|
+
getPrimaryClientId,
|
|
8
9
|
refreshAccessToken,
|
|
9
10
|
validateAuthorizationCode,
|
|
10
11
|
} from "../oauth2";
|
|
@@ -30,7 +31,7 @@ export interface CognitoProfile {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export interface CognitoOptions extends ProviderOptions<CognitoProfile> {
|
|
33
|
-
clientId: string;
|
|
34
|
+
clientId: string | string[];
|
|
34
35
|
/**
|
|
35
36
|
* The Cognito domain (e.g., "your-app.auth.us-east-1.amazoncognito.com")
|
|
36
37
|
*/
|
|
@@ -60,7 +61,7 @@ export const cognito = (options: CognitoOptions) => {
|
|
|
60
61
|
id: "cognito",
|
|
61
62
|
name: "Cognito",
|
|
62
63
|
async createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
63
|
-
if (!options.clientId) {
|
|
64
|
+
if (!getPrimaryClientId(options.clientId)) {
|
|
64
65
|
logger.error(
|
|
65
66
|
"ClientId is required for Amazon Cognito. Make sure to provide them in the options.",
|
|
66
67
|
);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { betterFetch } from "@better-fetch/fetch";
|
|
2
2
|
import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
3
|
+
import { logger } from "../env";
|
|
4
|
+
import { BetterAuthError } from "../error";
|
|
3
5
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
4
6
|
import {
|
|
5
7
|
createAuthorizationURL,
|
|
8
|
+
getPrimaryClientId,
|
|
6
9
|
refreshAccessToken,
|
|
7
10
|
validateAuthorizationCode,
|
|
8
11
|
} from "../oauth2";
|
|
@@ -22,7 +25,7 @@ export interface FacebookProfile {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export interface FacebookOptions extends ProviderOptions<FacebookProfile> {
|
|
25
|
-
clientId: string;
|
|
28
|
+
clientId: string | string[];
|
|
26
29
|
/**
|
|
27
30
|
* Extend list of fields to retrieve from the Facebook user profile.
|
|
28
31
|
*
|
|
@@ -41,6 +44,12 @@ export const facebook = (options: FacebookOptions) => {
|
|
|
41
44
|
id: "facebook",
|
|
42
45
|
name: "Facebook",
|
|
43
46
|
async createAuthorizationURL({ state, scopes, redirectURI, loginHint }) {
|
|
47
|
+
if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
|
|
48
|
+
logger.error(
|
|
49
|
+
"Client ID and client secret are required for Facebook. Make sure to provide them in the options.",
|
|
50
|
+
);
|
|
51
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
52
|
+
}
|
|
44
53
|
const _scopes = options.disableDefaultScope
|
|
45
54
|
? []
|
|
46
55
|
: ["email", "public_profile"];
|
|
@@ -5,6 +5,7 @@ import { APIError, BetterAuthError } from "../error";
|
|
|
5
5
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
6
6
|
import {
|
|
7
7
|
createAuthorizationURL,
|
|
8
|
+
getPrimaryClientId,
|
|
8
9
|
refreshAccessToken,
|
|
9
10
|
validateAuthorizationCode,
|
|
10
11
|
} from "../oauth2";
|
|
@@ -37,7 +38,7 @@ export interface GoogleProfile {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export interface GoogleOptions extends ProviderOptions<GoogleProfile> {
|
|
40
|
-
clientId: string;
|
|
41
|
+
clientId: string | string[];
|
|
41
42
|
/**
|
|
42
43
|
* The access type to use for the authorization code request
|
|
43
44
|
*/
|
|
@@ -64,7 +65,7 @@ export const google = (options: GoogleOptions) => {
|
|
|
64
65
|
loginHint,
|
|
65
66
|
display,
|
|
66
67
|
}) {
|
|
67
|
-
if (!options.clientId || !options.clientSecret) {
|
|
68
|
+
if (!getPrimaryClientId(options.clientId) || !options.clientSecret) {
|
|
68
69
|
logger.error(
|
|
69
70
|
"Client Id and Client Secret is required for Google. Make sure to provide them in the options.",
|
|
70
71
|
);
|
|
@@ -2,10 +2,11 @@ import { base64 } from "@better-auth/utils/base64";
|
|
|
2
2
|
import { betterFetch } from "@better-fetch/fetch";
|
|
3
3
|
import { decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";
|
|
4
4
|
import { logger } from "../env";
|
|
5
|
-
import { APIError } from "../error";
|
|
5
|
+
import { APIError, BetterAuthError } from "../error";
|
|
6
6
|
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
|
7
7
|
import {
|
|
8
8
|
createAuthorizationURL,
|
|
9
|
+
getPrimaryClientId,
|
|
9
10
|
refreshAccessToken,
|
|
10
11
|
validateAuthorizationCode,
|
|
11
12
|
} from "../oauth2";
|
|
@@ -116,7 +117,7 @@ export interface MicrosoftEntraIDProfile extends Record<string, any> {
|
|
|
116
117
|
|
|
117
118
|
export interface MicrosoftOptions
|
|
118
119
|
extends ProviderOptions<MicrosoftEntraIDProfile> {
|
|
119
|
-
clientId: string;
|
|
120
|
+
clientId: string | string[];
|
|
120
121
|
/**
|
|
121
122
|
* The tenant ID of the Microsoft account
|
|
122
123
|
* @default "common"
|
|
@@ -149,6 +150,15 @@ export const microsoft = (options: MicrosoftOptions) => {
|
|
|
149
150
|
id: "microsoft",
|
|
150
151
|
name: "Microsoft EntraID",
|
|
151
152
|
createAuthorizationURL(data) {
|
|
153
|
+
// Microsoft Entra supports public clients (SPA / native apps with
|
|
154
|
+
// PKCE only), so clientSecret is intentionally not required here.
|
|
155
|
+
// See https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
|
|
156
|
+
if (!getPrimaryClientId(options.clientId)) {
|
|
157
|
+
logger.error(
|
|
158
|
+
"Client Id is required for Microsoft Entra ID. Make sure to provide it in the options.",
|
|
159
|
+
);
|
|
160
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
161
|
+
}
|
|
152
162
|
const scopes = options.disableDefaultScope
|
|
153
163
|
? []
|
|
154
164
|
: ["openid", "profile", "email", "User.Read", "offline_access"];
|
|
@@ -190,7 +200,7 @@ export const microsoft = (options: MicrosoftOptions) => {
|
|
|
190
200
|
const publicKey = await getMicrosoftPublicKey(kid, tenant, authority);
|
|
191
201
|
const verifyOptions: {
|
|
192
202
|
algorithms: [string];
|
|
193
|
-
audience: string;
|
|
203
|
+
audience: string | string[];
|
|
194
204
|
maxTokenAge: string;
|
|
195
205
|
issuer?: string;
|
|
196
206
|
} = {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Awaitable } from "../types/helper";
|
|
2
|
+
|
|
3
|
+
export interface MapConcurrentOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Max in-flight mappers. Non-integer values are floored, then clamped
|
|
6
|
+
* to the range `[1, items.length]`. `NaN` falls back to 1.
|
|
7
|
+
*/
|
|
8
|
+
concurrency: number;
|
|
9
|
+
/**
|
|
10
|
+
* Rejects with `signal.reason` when aborted. In-flight mappers keep
|
|
11
|
+
* running but their results are not returned.
|
|
12
|
+
*/
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run an async mapper over items with bounded concurrency.
|
|
18
|
+
* Preserves input order in the result. Fails fast on the first rejection.
|
|
19
|
+
*/
|
|
20
|
+
export async function mapConcurrent<T, R>(
|
|
21
|
+
items: readonly T[],
|
|
22
|
+
fn: (item: T, index: number) => Awaitable<R>,
|
|
23
|
+
options: MapConcurrentOptions,
|
|
24
|
+
): Promise<R[]> {
|
|
25
|
+
const n = items.length;
|
|
26
|
+
if (n === 0) return [];
|
|
27
|
+
|
|
28
|
+
const { signal } = options;
|
|
29
|
+
if (signal?.aborted) throw signal.reason;
|
|
30
|
+
|
|
31
|
+
const raw = Math.floor(options.concurrency);
|
|
32
|
+
const width = Math.min(n, raw >= 1 ? raw : 1);
|
|
33
|
+
|
|
34
|
+
const results = new Array<R>(n);
|
|
35
|
+
let idx = 0;
|
|
36
|
+
let failed = false;
|
|
37
|
+
|
|
38
|
+
const worker = async (): Promise<void> => {
|
|
39
|
+
while (!failed && idx < n) {
|
|
40
|
+
if (signal?.aborted) throw signal.reason;
|
|
41
|
+
const i = idx++;
|
|
42
|
+
try {
|
|
43
|
+
results[i] = await fn(items[i] as T, i);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
failed = true;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
await Promise.all(Array.from({ length: width }, worker));
|
|
52
|
+
return results;
|
|
53
|
+
}
|