@protobase/server 0.68.5 → 0.69.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/dist/better-auth/auth-settings.d.ts +155 -28
- package/dist/better-auth/auth-settings.js +3 -2
- package/dist/better-auth/connections/connection-account-hooks.js +62 -0
- package/dist/better-auth/connections/connection-events.d.ts +11 -0
- package/dist/better-auth/connections/connection-events.js +16 -0
- package/dist/better-auth/connections/connection-oauth-provider.js +129 -0
- package/dist/better-auth/connections/connections-plugin.js +98 -0
- package/dist/better-auth/connections/declared-connections.d.ts +21 -0
- package/dist/better-auth/connections/declared-connections.js +54 -0
- package/dist/better-auth/connections/revoke-token.js +34 -0
- package/dist/better-auth/create-auth.d.ts +162 -17
- package/dist/better-auth/create-auth.js +59 -5
- package/dist/better-auth/refresh-refusals.js +43 -0
- package/dist/better-auth/staff-checks.js +1 -0
- package/dist/create-admin.js +11 -1
- package/dist/forms/rate-limit.js +33 -11
- package/dist/forms/routes.js +1 -1
- package/dist/functions/verify-signature.js +4 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/locale/en.js +3 -0
- package/dist/locale/nl.js +3 -0
- package/dist/mcp/mcp-tools.d.ts +2 -1
- package/dist/mcp/mcp-tools.js +4 -2
- package/dist/notifications/history-routes.js +1 -1
- package/dist/package.js +1 -1
- package/dist/personal-mail/linked-accounts.js +17 -13
- package/dist/types.d.ts +2 -0
- package/dist/webhooks/attempt.js +2 -2
- package/dist/webhooks/table/webhook-table.js +1 -1
- package/dist/workflows/connections/connection-accounts.js +18 -0
- package/dist/workflows/connections/connection-consent.js +49 -0
- package/dist/workflows/connections/connection-provider.js +10 -0
- package/dist/workflows/connections/connection-tokens.js +54 -0
- package/dist/workflows/connections/connection-triggers.js +37 -0
- package/dist/workflows/connections/connection-wiring.js +73 -0
- package/dist/workflows/connections/enabler-authority.js +35 -0
- package/dist/workflows/connections/enabler-mailbox.js +15 -0
- package/dist/workflows/connections/single-flight.js +17 -0
- package/dist/workflows/hooks/hook-auth.js +40 -0
- package/dist/workflows/hooks/hook-event.js +51 -0
- package/dist/workflows/hooks/hook-paths.d.ts +16 -0
- package/dist/workflows/hooks/hook-paths.js +32 -0
- package/dist/workflows/hooks/hook-route.js +99 -0
- package/dist/workflows/hooks/hook-runs.js +32 -0
- package/dist/workflows/hooks/hook-secrets.js +42 -0
- package/dist/workflows/hooks/hook-store.js +35 -0
- package/dist/workflows/hooks/hooks-sql.js +20 -0
- package/dist/workflows/hooks/issue-body.js +21 -0
- package/dist/workflows/hooks/issue-route.js +23 -0
- package/dist/workflows/hooks/issue-webhook.js +46 -0
- package/dist/workflows/hooks/read-body.js +32 -0
- package/dist/workflows/may-update-workflow.js +26 -0
- package/dist/workflows/mcp/webhook-tools.d.ts +9 -0
- package/dist/workflows/mcp/webhook-tools.js +103 -0
- package/dist/workflows/mcp/workflow-instructions.js +1 -1
- package/dist/workflows/mcp/workflow-read-tools.d.ts +3 -0
- package/dist/workflows/plugin-records.js +1 -1
- package/dist/workflows/plugin-services.js +1 -1
- package/dist/workflows/ready/ready-start.js +1 -1
- package/dist/workflows/run-files.js +7 -1
- package/dist/workflows/save-hook.js +7 -2
- package/dist/workflows/schema-sql.js +17 -3
- package/dist/workflows/schema.js +3 -1
- package/dist/workflows/start-workflow.d.ts +3 -1
- package/dist/workflows/start-workflow.js +10 -4
- package/dist/workflows/triggers/delete-hook.js +15 -0
- package/dist/workflows/triggers/plugin-call.js +45 -0
- package/dist/workflows/triggers/poll-job.js +120 -0
- package/dist/workflows/triggers/poll-result.js +32 -0
- package/dist/workflows/triggers/poll-schedule.js +32 -0
- package/dist/workflows/triggers/trigger-claim.js +81 -0
- package/dist/workflows/triggers/trigger-jobs.js +53 -7
- package/dist/workflows/triggers/trigger-start.js +112 -0
- package/dist/workflows/triggers/trigger-sync.js +15 -10
- package/dist/workflows/workflow-engine.js +57 -9
- package/dist/workflows/workflow-types.d.ts +11 -4
- package/dist/workflows/workflow-types.js +21 -15
- package/dist/write-shared.js +1 -0
- package/package.json +6 -6
- package/dist/better-auth/end-replaced-session.d.ts +0 -17
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { connectionProviderId } from "../../workflows/connections/connection-provider.js";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
//#region src/better-auth/connections/declared-connections.ts
|
|
4
|
+
const clientOf = (value) => typeof value?.clientId === "string" && value.clientId !== "" && typeof value.clientSecret === "string" && value.clientSecret !== "" ? {
|
|
5
|
+
clientId: value.clientId,
|
|
6
|
+
clientSecret: value.clientSecret
|
|
7
|
+
} : void 0;
|
|
8
|
+
const literalHost = (url) => {
|
|
9
|
+
const host = new URL(url).hostname.replace(/^\[|\]$/g, "");
|
|
10
|
+
return host === "localhost" || isIP(host) !== 0;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The connections of an app's workflow plugins, each with its provider: the sign-in provider it names, whose client
|
|
14
|
+
* revokes its tokens, or one of its own, whose client `clients` gives by `<plugin>:<connection>`. Startup stops on a
|
|
15
|
+
* client no `oauth` connection declares, and on an endpoint by address unless `allowPrivateAddresses`.
|
|
16
|
+
*/
|
|
17
|
+
const resolveConnections = (declared, clients, socialProviders, allowPrivateAddresses) => {
|
|
18
|
+
const keys = new Set(declared.filter(({ declaration }) => "oauth" in declaration).map(({ plugin, name }) => `${plugin}:${name}`));
|
|
19
|
+
const unknown = Object.keys(clients).find((key) => !keys.has(key));
|
|
20
|
+
if (unknown !== void 0) throw new Error(`auth.connections: "${unknown}" is no workflow plugin's connection that signs in through a provider of its own`);
|
|
21
|
+
return declared.map((connection) => {
|
|
22
|
+
const { plugin, name, declaration } = connection;
|
|
23
|
+
const key = `${plugin}:${name}`;
|
|
24
|
+
const providerId = connectionProviderId(plugin, name, declaration);
|
|
25
|
+
const urls = "signIn" in declaration ? [declaration.revocation_url] : [
|
|
26
|
+
declaration.oauth.authorization_url,
|
|
27
|
+
declaration.oauth.token_url,
|
|
28
|
+
declaration.oauth.user_info_url,
|
|
29
|
+
declaration.oauth.revocation_url
|
|
30
|
+
];
|
|
31
|
+
if (!allowPrivateAddresses && urls.some((url) => url !== void 0 && literalHost(url))) throw new Error(`Workflow plugin "${plugin}": the connection "${name}" names a host by its address; give its provider's name, or allow private addresses for development (workflows.allowPrivateAddresses)`);
|
|
32
|
+
if ("signIn" in declaration) {
|
|
33
|
+
const client = clientOf(socialProviders[declaration.signIn]);
|
|
34
|
+
return {
|
|
35
|
+
...connection,
|
|
36
|
+
key,
|
|
37
|
+
providerId,
|
|
38
|
+
...client && { client },
|
|
39
|
+
...declaration.revocation_url && { revocationUrl: declaration.revocation_url }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const client = clientOf(clients[key]);
|
|
43
|
+
if (Object.hasOwn(clients, key) && !client) throw new Error(`auth.connections: "${key}" needs its clientId and clientSecret`);
|
|
44
|
+
return {
|
|
45
|
+
...connection,
|
|
46
|
+
key,
|
|
47
|
+
providerId,
|
|
48
|
+
...client && { client },
|
|
49
|
+
...declaration.oauth.revocation_url && { revocationUrl: declaration.oauth.revocation_url }
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { resolveConnections };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AddressRefusedError, pinnedRequest } from "../../webhooks/table/pinned-request.js";
|
|
2
|
+
//#region src/better-auth/connections/revoke-token.ts
|
|
3
|
+
/** The longest a provider gets to revoke a token: disconnecting does not wait on it any longer. */
|
|
4
|
+
const revokeTimeoutMs = 1e4;
|
|
5
|
+
const unreachable = (error) => error instanceof AddressRefusedError || error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError" || typeof error.code === "string");
|
|
6
|
+
/**
|
|
7
|
+
* Revokes a token at its provider (RFC 7009), through the guarded request, which refuses a private address unless the
|
|
8
|
+
* app allows them: whether the provider took it. A provider out of reach or refusing is false, so disconnecting still
|
|
9
|
+
* deletes the token; nothing of the request or its answer is logged or returned, as both carry the token.
|
|
10
|
+
*/
|
|
11
|
+
const revokeToken = async ({ url, client, token, hint, allowPrivateAddresses }) => {
|
|
12
|
+
const init = {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
15
|
+
body: new URLSearchParams({
|
|
16
|
+
token,
|
|
17
|
+
token_type_hint: hint,
|
|
18
|
+
...client && {
|
|
19
|
+
client_id: client.clientId,
|
|
20
|
+
client_secret: client.clientSecret
|
|
21
|
+
}
|
|
22
|
+
}).toString(),
|
|
23
|
+
signal: AbortSignal.timeout(revokeTimeoutMs)
|
|
24
|
+
};
|
|
25
|
+
const response = await pinnedRequest(url, init, { allowPrivateAddresses }).catch((error) => {
|
|
26
|
+
if (unreachable(error)) return;
|
|
27
|
+
throw error;
|
|
28
|
+
});
|
|
29
|
+
if (!response) return false;
|
|
30
|
+
await response.body?.cancel();
|
|
31
|
+
return response.ok;
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { revokeToken };
|
|
@@ -5,12 +5,13 @@ import { StaffSignIn } from "./staff-log.js";
|
|
|
5
5
|
import { Person } from "./organizations/people-endpoint.js";
|
|
6
6
|
import { RoleDefinitions, RolesInput } from "./organizations/role-definitions.js";
|
|
7
7
|
import { ConnectedApp } from "./mcp/connected-apps.js";
|
|
8
|
-
import { ReplacingContext } from "./end-replaced-session.js";
|
|
9
8
|
import { VersionedSecret } from "./secret-versions.js";
|
|
10
9
|
import { UserDeletionListener } from "./user-deletion.js";
|
|
11
10
|
import { OrganizationRole } from "./organizations/store.js";
|
|
12
11
|
import { OrganizationsOptions, ResolvedOrganizations } from "./organizations/options.js";
|
|
13
12
|
import { PersonalMailProvider } from "../personal-mail/personal-mail-provider.js";
|
|
13
|
+
import { ConnectionClient, DeclaredConnection, ResolvedConnection } from "./connections/declared-connections.js";
|
|
14
|
+
import { ConnectionListener } from "./connections/connection-events.js";
|
|
14
15
|
import { McpAuthOptions } from "./mcp/mcp-plugins.js";
|
|
15
16
|
import { OperatorProvider } from "./operator-provider.js";
|
|
16
17
|
import { PlatformSignIn } from "./platform-sign-in.js";
|
|
@@ -117,8 +118,22 @@ export type CreateAuthOptions = {
|
|
|
117
118
|
* `socialProviders` must have: Gmail through `google`. Needs `encryptOAuthTokens`. Default none.
|
|
118
119
|
*/
|
|
119
120
|
personalMail?: PersonalMailProviderId[];
|
|
120
|
-
/** Encrypt the OAuth access and refresh tokens Better Auth stores for social sign-ins, with `secret`; `personalMail`
|
|
121
|
+
/** Encrypt the OAuth access and refresh tokens Better Auth stores for social sign-ins, with `secret`; `personalMail` and workflow connections need it. Default off. */
|
|
121
122
|
encryptOAuthTokens?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The OAuth client of each workflow plugin's connection that signs in through a provider of its own, by
|
|
125
|
+
* `<plugin>:<connection>`, such as `{ 'acme:account': { clientId, clientSecret } }`; its redirect URI is
|
|
126
|
+
* `{baseURL}/api/auth/callback/workflow-<plugin>-<connection>`. A connection through one of `socialProviders` needs none.
|
|
127
|
+
*/
|
|
128
|
+
connections?: Record<string, ConnectionClient>;
|
|
129
|
+
/**
|
|
130
|
+
* The connections the app's workflow plugins declare, which people connect accounts through (`createAdmin` passes
|
|
131
|
+
* them from `workflows.plugins`), and whether their providers may be at private addresses, for development and tests.
|
|
132
|
+
*/
|
|
133
|
+
workflowConnections?: {
|
|
134
|
+
declared: DeclaredConnection[];
|
|
135
|
+
allowPrivateAddresses?: boolean;
|
|
136
|
+
};
|
|
122
137
|
/** Runs after a user is created, by any route: `createUser`, an admin, or a social sign-up. Default none. */
|
|
123
138
|
onUserCreated?: UserCreatedHook;
|
|
124
139
|
/**
|
|
@@ -299,6 +314,7 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
299
314
|
account: {
|
|
300
315
|
encryptOAuthTokens?: true | undefined;
|
|
301
316
|
accountLinking: {
|
|
317
|
+
trustedProviders?: string[] | undefined;
|
|
302
318
|
allowDifferentEmails: true;
|
|
303
319
|
};
|
|
304
320
|
fields: {
|
|
@@ -590,17 +606,7 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
590
606
|
userId: string;
|
|
591
607
|
expiresAt: Date;
|
|
592
608
|
token: string;
|
|
593
|
-
ipAddress
|
|
594
|
-
/**
|
|
595
|
-
* What the sign-in page leads with: `password`, the email address and a password, with emailing a code below it; or
|
|
596
|
-
* `emailCode`, the address and "Email me a sign-in code", with the password behind "Use a password", for apps whose
|
|
597
|
-
* people mostly never set one. Only while the policy leaves both on; default `password`.
|
|
598
|
-
*/
|
|
599
|
-
/**
|
|
600
|
-
* What the sign-in page leads with: `password`, the email address and a password, with emailing a code below it; or
|
|
601
|
-
* `emailCode`, the address and "Email me a sign-in code", with the password behind "Use a password", for apps whose
|
|
602
|
-
* people mostly never set one. Only while the policy leaves both on; default `password`.
|
|
603
|
-
*/ : string | null | undefined;
|
|
609
|
+
ipAddress?: string | null | undefined;
|
|
604
610
|
userAgent?: string | null | undefined;
|
|
605
611
|
};
|
|
606
612
|
};
|
|
@@ -3419,6 +3425,102 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
3419
3425
|
INVALID_OAUTH_CONFIGURATION: import("better-auth").RawError<"INVALID_OAUTH_CONFIGURATION">;
|
|
3420
3426
|
TOKEN_URL_NOT_FOUND: import("better-auth").RawError<"TOKEN_URL_NOT_FOUND">;
|
|
3421
3427
|
};
|
|
3428
|
+
} | {
|
|
3429
|
+
id: "protobase-connection-providers";
|
|
3430
|
+
init: (ctx: import("better-auth").AuthContext) => {
|
|
3431
|
+
context: {
|
|
3432
|
+
socialProviders: import("better-auth").OAuthProvider<object, Partial<import("better-auth").ProviderOptions<object>>>[];
|
|
3433
|
+
};
|
|
3434
|
+
};
|
|
3435
|
+
} | {
|
|
3436
|
+
id: "protobase-connections";
|
|
3437
|
+
endpoints: {
|
|
3438
|
+
listConnections: import("better-call").StrictEndpoint<string, {
|
|
3439
|
+
method: "GET";
|
|
3440
|
+
use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
|
|
3441
|
+
session: {
|
|
3442
|
+
session: Record<string, any> & {
|
|
3443
|
+
id: string;
|
|
3444
|
+
createdAt: Date;
|
|
3445
|
+
updatedAt: Date;
|
|
3446
|
+
userId: string;
|
|
3447
|
+
expiresAt: Date;
|
|
3448
|
+
token: string;
|
|
3449
|
+
ipAddress?: string | null | undefined;
|
|
3450
|
+
userAgent?: string | null | undefined;
|
|
3451
|
+
};
|
|
3452
|
+
user: Record<string, any> & {
|
|
3453
|
+
id: string;
|
|
3454
|
+
createdAt: Date;
|
|
3455
|
+
updatedAt: Date;
|
|
3456
|
+
email: string;
|
|
3457
|
+
emailVerified: boolean;
|
|
3458
|
+
name: string;
|
|
3459
|
+
image?: string | null | undefined;
|
|
3460
|
+
};
|
|
3461
|
+
};
|
|
3462
|
+
}>>[];
|
|
3463
|
+
}, {
|
|
3464
|
+
connections: {
|
|
3465
|
+
key: string;
|
|
3466
|
+
label: string;
|
|
3467
|
+
provider: string;
|
|
3468
|
+
scopes: string[];
|
|
3469
|
+
link_params: Record<string, string>;
|
|
3470
|
+
accounts: {
|
|
3471
|
+
id: string;
|
|
3472
|
+
scopes: string[];
|
|
3473
|
+
created_at: Date;
|
|
3474
|
+
needs_sign_in: boolean;
|
|
3475
|
+
}[];
|
|
3476
|
+
}[];
|
|
3477
|
+
}>;
|
|
3478
|
+
disconnectConnection: import("better-call").StrictEndpoint<string, {
|
|
3479
|
+
method: "POST";
|
|
3480
|
+
use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
|
|
3481
|
+
session: {
|
|
3482
|
+
session: Record<string, any> & {
|
|
3483
|
+
id: string;
|
|
3484
|
+
createdAt: Date;
|
|
3485
|
+
updatedAt: Date;
|
|
3486
|
+
userId: string;
|
|
3487
|
+
expiresAt: Date;
|
|
3488
|
+
token: string;
|
|
3489
|
+
ipAddress?: string | null | undefined;
|
|
3490
|
+
userAgent?: string | null | undefined;
|
|
3491
|
+
};
|
|
3492
|
+
user: Record<string, any> & {
|
|
3493
|
+
id: string;
|
|
3494
|
+
createdAt: Date;
|
|
3495
|
+
updatedAt: Date;
|
|
3496
|
+
email: string;
|
|
3497
|
+
emailVerified: boolean;
|
|
3498
|
+
name: string;
|
|
3499
|
+
image?: string | null | undefined;
|
|
3500
|
+
};
|
|
3501
|
+
};
|
|
3502
|
+
}>>[];
|
|
3503
|
+
body: import("zod").ZodObject<{
|
|
3504
|
+
accountId: import("zod").ZodString;
|
|
3505
|
+
}, import("zod/v4/core").$strip>;
|
|
3506
|
+
}, {
|
|
3507
|
+
disconnected: boolean;
|
|
3508
|
+
revoked: boolean;
|
|
3509
|
+
}>;
|
|
3510
|
+
};
|
|
3511
|
+
hooks: {
|
|
3512
|
+
before: {
|
|
3513
|
+
matcher: (context: import("better-auth").HookEndpointContext) => boolean;
|
|
3514
|
+
handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
|
|
3515
|
+
}[];
|
|
3516
|
+
};
|
|
3517
|
+
} | {
|
|
3518
|
+
id: "protobase-refresh-refusals";
|
|
3519
|
+
init: (ctx: import("better-auth").AuthContext) => {
|
|
3520
|
+
context: {
|
|
3521
|
+
socialProviders: import("better-auth").OAuthProvider<object, Partial<import("better-auth").ProviderOptions<object>>>[];
|
|
3522
|
+
};
|
|
3523
|
+
};
|
|
3422
3524
|
} | {
|
|
3423
3525
|
id: "sso";
|
|
3424
3526
|
version: string;
|
|
@@ -8978,10 +9080,40 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
8978
9080
|
};
|
|
8979
9081
|
};
|
|
8980
9082
|
session: {
|
|
8981
|
-
create
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
9083
|
+
create?: {
|
|
9084
|
+
before?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
9085
|
+
data: {
|
|
9086
|
+
id?: string | undefined;
|
|
9087
|
+
createdAt?: Date | undefined;
|
|
9088
|
+
updatedAt?: Date | undefined;
|
|
9089
|
+
userId?: string | undefined;
|
|
9090
|
+
expiresAt?: Date | undefined;
|
|
9091
|
+
token?: string | undefined;
|
|
9092
|
+
ipAddress?: string | null | undefined;
|
|
9093
|
+
userAgent?: string | null | undefined;
|
|
9094
|
+
} & Record<string, any>;
|
|
9095
|
+
}>;
|
|
9096
|
+
after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
9097
|
+
};
|
|
9098
|
+
update?: {
|
|
9099
|
+
before?: (session: Partial<import("better-auth").Session> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
|
|
9100
|
+
data: {
|
|
9101
|
+
[x: string]: any;
|
|
9102
|
+
id?: string | undefined;
|
|
9103
|
+
createdAt?: Date | undefined;
|
|
9104
|
+
updatedAt?: Date | undefined;
|
|
9105
|
+
userId?: string | undefined;
|
|
9106
|
+
expiresAt?: Date | undefined;
|
|
9107
|
+
token?: string | undefined;
|
|
9108
|
+
ipAddress?: string | null | undefined;
|
|
9109
|
+
userAgent?: string | null | undefined;
|
|
9110
|
+
};
|
|
9111
|
+
}>;
|
|
9112
|
+
after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
9113
|
+
};
|
|
9114
|
+
delete?: {
|
|
9115
|
+
before?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
|
|
9116
|
+
after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
|
|
8985
9117
|
};
|
|
8986
9118
|
};
|
|
8987
9119
|
account: {
|
|
@@ -9201,6 +9333,19 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
|
|
|
9201
9333
|
personalMail: PersonalMailProvider[];
|
|
9202
9334
|
/** Mail from `userId`'s own mailbox, through the mail provider they linked (see `personalMailer`). */
|
|
9203
9335
|
personalMailer: (userId: string) => Mailer;
|
|
9336
|
+
/** What workflow connections can sign in with: whether tokens are stored encrypted, the sign-in providers, the connections with a client. */
|
|
9337
|
+
connectionSupport: {
|
|
9338
|
+
encrypted: boolean;
|
|
9339
|
+
signInProviders: string[];
|
|
9340
|
+
clients: string[];
|
|
9341
|
+
};
|
|
9342
|
+
/** The workflow connections people connect accounts through, and the listener (`set`) the workflows give for their links and disconnects. */
|
|
9343
|
+
connections: {
|
|
9344
|
+
declared: ResolvedConnection[];
|
|
9345
|
+
events: {
|
|
9346
|
+
set: (next: ConnectionListener) => undefined;
|
|
9347
|
+
};
|
|
9348
|
+
};
|
|
9204
9349
|
/** The OAuth provider for MCP clients, when on: the endpoint's resource (its URL) and path, and the scope its tokens carry. */
|
|
9205
9350
|
mcp: {
|
|
9206
9351
|
resource: string;
|
|
@@ -5,10 +5,16 @@ import { defaultSignInPolicy } from "./sign-in-policy.js";
|
|
|
5
5
|
import { roleDefinitions } from "./organizations/role-definitions.js";
|
|
6
6
|
import { signInPolicyPlugin } from "./policy-plugin.js";
|
|
7
7
|
import { goTrueSessionsPlugin } from "../gotrue/sessions-plugin.js";
|
|
8
|
+
import { refreshRefusalsPlugin } from "./refresh-refusals.js";
|
|
8
9
|
import { betterAuthLinkedAccounts } from "../personal-mail/linked-accounts.js";
|
|
9
10
|
import { resolvePersonalMail } from "../personal-mail/personal-mail-providers.js";
|
|
10
11
|
import { personalMailer } from "../personal-mail/personal-mailer.js";
|
|
11
12
|
import { mailLinkHooks } from "../personal-mail/mail-link-hooks.js";
|
|
13
|
+
import { bothAccountHooks, connectionAccountHooks } from "./connections/connection-account-hooks.js";
|
|
14
|
+
import { connectionEvents } from "./connections/connection-events.js";
|
|
15
|
+
import { connectionProvidersPlugin } from "./connections/connection-oauth-provider.js";
|
|
16
|
+
import { connectionsPlugin } from "./connections/connections-plugin.js";
|
|
17
|
+
import { resolveConnections } from "./connections/declared-connections.js";
|
|
12
18
|
import { endReplacedSession } from "./end-replaced-session.js";
|
|
13
19
|
import { authBasePath } from "./auth-base-path.js";
|
|
14
20
|
import { authLogger } from "./auth-logger.js";
|
|
@@ -69,6 +75,20 @@ const withEndedReplaced = (hooks) => ({
|
|
|
69
75
|
after: endReplacedSession
|
|
70
76
|
}
|
|
71
77
|
});
|
|
78
|
+
const refusingConnectionSessions = (hooks, refuse) => {
|
|
79
|
+
if (!refuse) return hooks;
|
|
80
|
+
const before = hooks.create?.before;
|
|
81
|
+
return {
|
|
82
|
+
...hooks,
|
|
83
|
+
create: {
|
|
84
|
+
...hooks.create,
|
|
85
|
+
before: async (session, context) => {
|
|
86
|
+
await refuse(session, context);
|
|
87
|
+
return before?.(session, context);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
};
|
|
72
92
|
const platformOperator = () => readOperatorSettings(globalThis.process?.env ?? {});
|
|
73
93
|
const platformSignInSettings = () => readPlatformSignIn(globalThis.process?.env ?? {});
|
|
74
94
|
const platformOrigins = () => readOrigins(globalThis.process?.env ?? {});
|
|
@@ -101,7 +121,9 @@ const organizationPaths = ["/organization/set-active", "/organization/accept-inv
|
|
|
101
121
|
* The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
|
|
102
122
|
* sign-in provider already resolved.
|
|
103
123
|
*/
|
|
104
|
-
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy, mobile, deletion, lockedAccounts = [], ...plugins }) => {
|
|
124
|
+
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, mcp, context = noContext, signInPolicy = defaultSignInPolicy, mobile, deletion, lockedAccounts = [], connections, ...plugins }) => {
|
|
125
|
+
const connectionHooks = connections && connections.resolved.length > 0 ? connectionAccountHooks(connections.resolved, connections.events) : void 0;
|
|
126
|
+
const ownProviders = (connections?.resolved ?? []).filter(({ declaration }) => "oauth" in declaration).map(({ providerId }) => providerId);
|
|
105
127
|
const secure = options.baseURL.startsWith("https://");
|
|
106
128
|
const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
|
|
107
129
|
const hosts = passkeyHosts([options.baseURL, ...origins]);
|
|
@@ -201,7 +223,10 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
201
223
|
verification: coreNames.verification,
|
|
202
224
|
account: {
|
|
203
225
|
...coreNames.account,
|
|
204
|
-
accountLinking: {
|
|
226
|
+
accountLinking: {
|
|
227
|
+
allowDifferentEmails: true,
|
|
228
|
+
...ownProviders.length > 0 && { trustedProviders: ownProviders }
|
|
229
|
+
},
|
|
205
230
|
...options.encryptOAuthTokens && { encryptOAuthTokens: true }
|
|
206
231
|
},
|
|
207
232
|
plugins: [
|
|
@@ -238,6 +263,12 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
238
263
|
defaultPolicy: signInPolicy
|
|
239
264
|
})] : [],
|
|
240
265
|
...signIn ? [genericOAuth({ config: [platformSignInProvider(signIn)] })] : [],
|
|
266
|
+
...connections && connections.resolved.length > 0 ? [connectionProvidersPlugin(connections.resolved, { allowPrivateAddresses: connections.allowPrivateAddresses }), connectionsPlugin({
|
|
267
|
+
connections: connections.resolved,
|
|
268
|
+
events: connections.events,
|
|
269
|
+
allowPrivateAddresses: connections.allowPrivateAddresses
|
|
270
|
+
})] : [],
|
|
271
|
+
refreshRefusalsPlugin(),
|
|
241
272
|
...saml.length > 0 ? [loaded(plugins.samlPlugin, "sso.saml")(saml, options.baseURL), samlGuardPlugin(samlIds)] : [],
|
|
242
273
|
signInPolicyPlugin(policy),
|
|
243
274
|
passwordChecksPlugin(),
|
|
@@ -320,8 +351,8 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
320
351
|
},
|
|
321
352
|
update: { before: async (user) => ({ data: confirmedOnUpdate(user, /* @__PURE__ */ new Date()) }) }
|
|
322
353
|
},
|
|
323
|
-
session: withEndedReplaced(organizations ? sessionOrganizationHooks(context) : {}),
|
|
324
|
-
account: mailLinkHooks(resolvePersonalMail(options.personalMail ?? [], Object.keys(options.socialProviders ?? {}), options.encryptOAuthTokens === true))
|
|
354
|
+
session: refusingConnectionSessions(withEndedReplaced(organizations ? sessionOrganizationHooks(context) : {}), connectionHooks?.session.create?.before),
|
|
355
|
+
account: ((mail) => connectionHooks ? bothAccountHooks(mail, connectionHooks.account) : mail)(mailLinkHooks(resolvePersonalMail(options.personalMail ?? [], Object.keys(options.socialProviders ?? {}), options.encryptOAuthTokens === true)))
|
|
325
356
|
},
|
|
326
357
|
rateLimit: {
|
|
327
358
|
enabled: true,
|
|
@@ -408,6 +439,13 @@ const createAuth = (options, plugins = {}) => {
|
|
|
408
439
|
const platformSignIn = platformSignInFor(signInSettings && resolvePlatformSignIn(signInSettings), socialProviders);
|
|
409
440
|
const saml = resolveSamlProviders(options.sso?.saml ?? [], [...socialProviders, ...platformSignIn ? [platformSignIn.provider] : []]);
|
|
410
441
|
const personalMail = resolvePersonalMail(options.personalMail ?? [], socialProviders, options.encryptOAuthTokens === true);
|
|
442
|
+
if (Object.keys(options.connections ?? {}).length > 0 && options.encryptOAuthTokens !== true) throw new Error("auth.connections: the connected accounts' tokens would be stored in plain text; turn on auth.encryptOAuthTokens");
|
|
443
|
+
const allowPrivateAddresses = options.workflowConnections?.allowPrivateAddresses === true;
|
|
444
|
+
const resolvedConnections = resolveConnections(options.workflowConnections?.declared ?? [], options.connections ?? {}, options.socialProviders ?? {}, allowPrivateAddresses);
|
|
445
|
+
const clash = resolvedConnections.find(({ declaration, providerId }) => "oauth" in declaration && socialProviders.includes(providerId));
|
|
446
|
+
if (clash) throw new Error(`Workflow plugin "${clash.plugin}": its connection "${clash.name}" signs in as "${clash.providerId}", which is a sign-in provider's id too`);
|
|
447
|
+
const connectionClients = resolvedConnections.filter(({ declaration, client }) => "oauth" in declaration && client).map(({ key }) => key);
|
|
448
|
+
const events = connectionEvents();
|
|
411
449
|
const origins = resolveOrigins(options.baseURL, options.origins ?? platformOrigins());
|
|
412
450
|
const passkeyDomain = resolvePasskeyDomain(options.passkeyDomain ?? platformPasskeyDomain(), [options.baseURL, ...origins]);
|
|
413
451
|
const organizations = options.organizations && resolveOrganizations(options.organizations, definitions);
|
|
@@ -436,7 +474,12 @@ const createAuth = (options, plugins = {}) => {
|
|
|
436
474
|
signInPolicy,
|
|
437
475
|
...organizations && { organizations },
|
|
438
476
|
...mcp && { mcp },
|
|
439
|
-
context
|
|
477
|
+
context,
|
|
478
|
+
connections: {
|
|
479
|
+
resolved: resolvedConnections,
|
|
480
|
+
events,
|
|
481
|
+
allowPrivateAddresses
|
|
482
|
+
}
|
|
440
483
|
});
|
|
441
484
|
assertNoEndpointKeyClashes(authOptions.plugins);
|
|
442
485
|
const auth = betterAuth(authOptions);
|
|
@@ -503,6 +546,17 @@ const createAuth = (options, plugins = {}) => {
|
|
|
503
546
|
personalMail,
|
|
504
547
|
/** Mail from `userId`'s own mailbox, through the mail provider they linked (see `personalMailer`). */
|
|
505
548
|
personalMailer: (userId) => personalMailer(betterAuthLinkedAccounts(auth), personalMail, userId),
|
|
549
|
+
/** What workflow connections can sign in with: whether tokens are stored encrypted, the sign-in providers, the connections with a client. */
|
|
550
|
+
connectionSupport: {
|
|
551
|
+
encrypted: options.encryptOAuthTokens === true,
|
|
552
|
+
signInProviders: socialProviders,
|
|
553
|
+
clients: connectionClients
|
|
554
|
+
},
|
|
555
|
+
/** The workflow connections people connect accounts through, and the listener (`set`) the workflows give for their links and disconnects. */
|
|
556
|
+
connections: {
|
|
557
|
+
declared: resolvedConnections,
|
|
558
|
+
events: { set: events.set }
|
|
559
|
+
},
|
|
506
560
|
/** The OAuth provider for MCP clients, when on: the endpoint's resource (its URL) and path, and the scope its tokens carry. */
|
|
507
561
|
mcp: mcp && {
|
|
508
562
|
resource: mcp.resource,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
//#region src/better-auth/refresh-refusals.ts
|
|
3
|
+
/**
|
|
4
|
+
* The token endpoint's error (RFC 6749, section 5.2) that refuses the refresh token itself, as for a revoked grant or
|
|
5
|
+
* an expired refresh token: the link no longer signs in, and only connecting it again helps.
|
|
6
|
+
*/
|
|
7
|
+
const refusal = "invalid_grant";
|
|
8
|
+
/** Whether a provider's refresh threw for a refused grant: Better Auth's own fetch's error, or a connection provider's. */
|
|
9
|
+
const refreshRefused = (error) => typeof error === "object" && error !== null && error.error === refusal;
|
|
10
|
+
const watched = new AsyncLocalStorage();
|
|
11
|
+
/** Runs `work` and tells whether a provider refused a refresh token in it, whether `work` resolved or threw. */
|
|
12
|
+
const watchRefusals = async (work) => {
|
|
13
|
+
const outcome = { refused: false };
|
|
14
|
+
return {
|
|
15
|
+
...await watched.run(outcome, () => work().then((value) => ({ value }), (error) => ({ error }))),
|
|
16
|
+
refused: outcome.refused
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
const marking = (provider) => {
|
|
20
|
+
const refresh = provider.refreshAccessToken;
|
|
21
|
+
if (!refresh) return provider;
|
|
22
|
+
return {
|
|
23
|
+
...provider,
|
|
24
|
+
refreshAccessToken: async (...args) => {
|
|
25
|
+
try {
|
|
26
|
+
return await refresh(...args);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (refreshRefused(error)) {
|
|
29
|
+
const outcome = watched.getStore();
|
|
30
|
+
if (outcome) outcome.refused = true;
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
/** Has every provider's refresh mark a refused grant for `watchRefusals`; after the plugins that add providers. */
|
|
38
|
+
const refreshRefusalsPlugin = () => ({
|
|
39
|
+
id: "protobase-refresh-refusals",
|
|
40
|
+
init: (ctx) => ({ context: { socialProviders: ctx.socialProviders.map(marking) } })
|
|
41
|
+
});
|
|
42
|
+
//#endregion
|
|
43
|
+
export { refreshRefusalsPlugin, refreshRefused, watchRefusals };
|
package/dist/create-admin.js
CHANGED
|
@@ -59,6 +59,8 @@ import { toApiFunction } from "./functions/define-function.js";
|
|
|
59
59
|
import { createWebhooks } from "./webhooks/webhooks.js";
|
|
60
60
|
import { webhookTable } from "./webhooks/table/webhook-table.js";
|
|
61
61
|
import { subscriptionUrlCheck, subscriptionWriteCheck } from "./webhooks/table/validate-hook.js";
|
|
62
|
+
import { hooksBase } from "./workflows/hooks/hook-paths.js";
|
|
63
|
+
import { issueWebhook } from "./workflows/hooks/issue-webhook.js";
|
|
62
64
|
import { workflowEngine } from "./workflows/workflow-engine.js";
|
|
63
65
|
import { workflowTypesRoute } from "./workflows/workflow-types-route.js";
|
|
64
66
|
import { callsService } from "./calls/calls-service.js";
|
|
@@ -387,7 +389,13 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
|
|
|
387
389
|
sql: options.mcp !== false && options.mcp?.sql === true,
|
|
388
390
|
writes,
|
|
389
391
|
commented,
|
|
390
|
-
...engine && { workflows:
|
|
392
|
+
...engine && { workflows: {
|
|
393
|
+
...engine.types,
|
|
394
|
+
issueWebhook: (session, id, secret) => issueWebhook(engine.issue, session, id, {
|
|
395
|
+
...secret !== void 0 && { secret },
|
|
396
|
+
origin: new URL(auth.mcp.resource).origin
|
|
397
|
+
})
|
|
398
|
+
} },
|
|
391
399
|
own: options.mcp !== false && options.mcp?.tools || []
|
|
392
400
|
});
|
|
393
401
|
const instructions = engine ? `${mcpInstructions}\n${workflowInstructions}` : mcpInstructions;
|
|
@@ -555,7 +563,9 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
|
|
|
555
563
|
}));
|
|
556
564
|
}
|
|
557
565
|
}
|
|
566
|
+
if (engine) app.route(hooksBase(systemPath), engine.routes.hooks());
|
|
558
567
|
if (appModels.some((model) => model.live) || deps.calls) app.route(systemPath || "/", guarded((live) => void live.route("/", liveRoutes(feed, (session) => visibleNotices(deps, session))), withAnonymous(authenticate, options.roles)));
|
|
568
|
+
if (engine) app.route(`${systemPath}/workflows`, guarded((routes) => void routes.route("/", engine.routes.issue())));
|
|
559
569
|
app.route(basePath, api);
|
|
560
570
|
app.route(`${deps.systemPath}/meta`, metaApp);
|
|
561
571
|
app.route(systemPath || "/", webManifestRoute(appName, identity, icons));
|
package/dist/forms/rate-limit.js
CHANGED
|
@@ -8,32 +8,54 @@ const windowMs = 6e4;
|
|
|
8
8
|
* sent). In development and tests a request without one is `127.0.0.1`.
|
|
9
9
|
*/
|
|
10
10
|
const clientAddress = (headers) => getIP(headers, {}) ?? "unknown";
|
|
11
|
+
/** The most keys a limit counts at once: a new key past it forgets the one whose window began first. */
|
|
12
|
+
const rateLimitKeys = 1e4;
|
|
11
13
|
/**
|
|
12
14
|
* At most `perMinute` takes per key in a fixed window of a minute, kept in this process. Keys whose window has passed are
|
|
13
|
-
* dropped as new ones come,
|
|
15
|
+
* dropped as new ones come, and at most `maxKeys` are kept, the oldest window dropped first, so memory stays bounded
|
|
16
|
+
* however many clients come within a minute.
|
|
14
17
|
*/
|
|
15
|
-
const createRateLimit = ({ now = Date.now } = {}) => {
|
|
18
|
+
const createRateLimit = ({ now = Date.now, maxKeys = rateLimitKeys } = {}) => {
|
|
16
19
|
const windows = /* @__PURE__ */ new Map();
|
|
17
20
|
const prune = (at) => {
|
|
18
|
-
for (const [key, window] of windows)
|
|
21
|
+
for (const [key, window] of windows) {
|
|
22
|
+
if (at - window.start < windowMs) return;
|
|
23
|
+
windows.delete(key);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const open = (key, at) => {
|
|
27
|
+
prune(at);
|
|
28
|
+
windows.delete(key);
|
|
29
|
+
if (windows.size >= maxKeys) windows.delete(windows.keys().next().value);
|
|
30
|
+
windows.set(key, {
|
|
31
|
+
start: at,
|
|
32
|
+
count: 1
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
const wait = (key, perMinute, at) => {
|
|
36
|
+
const current = windows.get(key);
|
|
37
|
+
if (!current || at - current.start >= windowMs || current.count < perMinute) return 0;
|
|
38
|
+
return Math.ceil((current.start + windowMs - at) / 1e3);
|
|
19
39
|
};
|
|
20
40
|
/** Counts a take for `key`: `0` when allowed, else the seconds until its window lets it through again. */
|
|
21
41
|
const take = (key, perMinute) => {
|
|
22
42
|
const at = now();
|
|
23
43
|
const current = windows.get(key);
|
|
24
44
|
if (!current || at - current.start >= windowMs) {
|
|
25
|
-
|
|
26
|
-
windows.set(key, {
|
|
27
|
-
start: at,
|
|
28
|
-
count: 1
|
|
29
|
-
});
|
|
45
|
+
open(key, at);
|
|
30
46
|
return 0;
|
|
31
47
|
}
|
|
32
|
-
|
|
48
|
+
const seconds = wait(key, perMinute, at);
|
|
49
|
+
if (seconds > 0) return seconds;
|
|
33
50
|
current.count += 1;
|
|
34
51
|
return 0;
|
|
35
52
|
};
|
|
36
|
-
|
|
53
|
+
/** Whether `key` has spent its `perMinute` takes, without taking one: the seconds to wait, or `0`. */
|
|
54
|
+
const spent = (key, perMinute) => wait(key, perMinute, now());
|
|
55
|
+
return {
|
|
56
|
+
take,
|
|
57
|
+
spent
|
|
58
|
+
};
|
|
37
59
|
};
|
|
38
60
|
//#endregion
|
|
39
|
-
export { clientAddress, createRateLimit };
|
|
61
|
+
export { clientAddress, createRateLimit, rateLimitKeys };
|
package/dist/forms/routes.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { HttpProblem, badRequest, notFound } from "../problem.js";
|
|
2
2
|
import { runWrite } from "../write-pipeline.js";
|
|
3
3
|
import { anonymousSession } from "../anonymous.js";
|
|
4
|
-
import { pruneView } from "../view-access.js";
|
|
5
4
|
import { clientAddress } from "./rate-limit.js";
|
|
5
|
+
import { pruneView } from "../view-access.js";
|
|
6
6
|
import { Hono } from "hono";
|
|
7
7
|
import { restrictModel } from "@protobase/schema";
|
|
8
8
|
//#region src/forms/routes.ts
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HttpProblem } from "../problem.js";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
3
|
//#region src/functions/verify-signature.ts
|
|
3
4
|
const invalid = (detail) => new HttpProblem(401, "invalid-signature", "Unauthorized", detail);
|
|
4
5
|
const decodeHex = (text) => /^(?:[0-9a-f]{2})+$/i.test(text) ? new Uint8Array(text.match(/../g).map((pair) => parseInt(pair, 16))) : void 0;
|
|
@@ -7,8 +8,9 @@ const verifyHmac = async (key, hash, signature, content) => {
|
|
|
7
8
|
const cryptoKey = await crypto.subtle.importKey("raw", key, {
|
|
8
9
|
name: "HMAC",
|
|
9
10
|
hash
|
|
10
|
-
}, false, ["
|
|
11
|
-
|
|
11
|
+
}, false, ["sign"]);
|
|
12
|
+
const expected = new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(content)));
|
|
13
|
+
return signature.length === expected.length && timingSafeEqual(signature, expected);
|
|
12
14
|
};
|
|
13
15
|
/**
|
|
14
16
|
* The request's body as text, once its HMAC signature checks out against `secret`, as GitHub, Shopify and many others
|