@protobase/server 0.68.6 → 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.
Files changed (46) hide show
  1. package/dist/better-auth/auth-settings.d.ts +155 -28
  2. package/dist/better-auth/auth-settings.js +3 -2
  3. package/dist/better-auth/connections/connection-account-hooks.js +62 -0
  4. package/dist/better-auth/connections/connection-events.d.ts +11 -0
  5. package/dist/better-auth/connections/connection-events.js +16 -0
  6. package/dist/better-auth/connections/connection-oauth-provider.js +129 -0
  7. package/dist/better-auth/connections/connections-plugin.js +98 -0
  8. package/dist/better-auth/connections/declared-connections.d.ts +21 -0
  9. package/dist/better-auth/connections/declared-connections.js +54 -0
  10. package/dist/better-auth/connections/revoke-token.js +34 -0
  11. package/dist/better-auth/create-auth.d.ts +162 -17
  12. package/dist/better-auth/create-auth.js +59 -5
  13. package/dist/better-auth/refresh-refusals.js +43 -0
  14. package/dist/better-auth/staff-checks.js +1 -0
  15. package/dist/notifications/history-routes.js +1 -1
  16. package/dist/package.js +1 -1
  17. package/dist/personal-mail/linked-accounts.js +17 -13
  18. package/dist/types.d.ts +2 -0
  19. package/dist/webhooks/attempt.js +2 -2
  20. package/dist/webhooks/table/webhook-table.js +1 -1
  21. package/dist/workflows/connections/connection-accounts.js +18 -0
  22. package/dist/workflows/connections/connection-consent.js +49 -0
  23. package/dist/workflows/connections/connection-provider.js +10 -0
  24. package/dist/workflows/connections/connection-tokens.js +54 -0
  25. package/dist/workflows/connections/connection-triggers.js +37 -0
  26. package/dist/workflows/connections/connection-wiring.js +73 -0
  27. package/dist/workflows/connections/enabler-authority.js +35 -0
  28. package/dist/workflows/connections/enabler-mailbox.js +15 -0
  29. package/dist/workflows/connections/single-flight.js +17 -0
  30. package/dist/workflows/hooks/issue-webhook.js +3 -20
  31. package/dist/workflows/may-update-workflow.js +26 -0
  32. package/dist/workflows/mcp/webhook-tools.js +10 -3
  33. package/dist/workflows/save-hook.js +7 -2
  34. package/dist/workflows/schema-sql.js +3 -1
  35. package/dist/workflows/schema.js +1 -1
  36. package/dist/workflows/triggers/plugin-call.js +19 -9
  37. package/dist/workflows/triggers/poll-job.js +7 -7
  38. package/dist/workflows/triggers/trigger-jobs.js +23 -7
  39. package/dist/workflows/triggers/trigger-start.js +16 -5
  40. package/dist/workflows/triggers/trigger-sync.js +15 -10
  41. package/dist/workflows/workflow-engine.js +20 -5
  42. package/dist/workflows/workflow-types.d.ts +7 -3
  43. package/dist/workflows/workflow-types.js +12 -12
  44. package/dist/write-shared.js +1 -0
  45. package/package.json +6 -6
  46. package/dist/better-auth/end-replaced-session.d.ts +0 -17
@@ -5,12 +5,13 @@ import { StaffSignIn } from "./staff-log.js";
5
5
  import { Person } from "./organizations/people-endpoint.js";
6
6
  import { RoleDefinitions } 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, StoredMember, StoredOrganization } 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 { 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 "../index.js";
16
17
  import { CreateAuthOptions, UserCreatedHook } from "./create-auth.js";
@@ -48,7 +49,7 @@ export type AuthHooks = {
48
49
  * `auth` in `protobase.config.ts`: the sign-in settings as data. The store is the app's own database, in `schema`;
49
50
  * the hooks and a mailer of the project's own are found in auth/hooks.ts and auth/mailer.ts.
50
51
  */
51
- export type AuthSettings = Omit<CreateAuthOptions, 'database' | 'secret' | 'generateUserId' | 'onUserCreated' | 'mailer' | 'organizations' | 'mcp'> & {
52
+ export type AuthSettings = Omit<CreateAuthOptions, 'database' | 'secret' | 'generateUserId' | 'onUserCreated' | 'mailer' | 'organizations' | 'mcp' | 'workflowConnections'> & {
52
53
  /** Signs cookies and encrypts the JWT private keys, such as `PROTOBASE_SECRET`. Generate with `openssl rand -base64 32`. */
53
54
  secret?: string;
54
55
  /** The schema of the auth store in the app's database; default `auth`. */
@@ -70,9 +71,11 @@ export type AppAuthInput = {
70
71
  hooks?: AuthHooks;
71
72
  /** A mailer of the project's own, in place of the default. */
72
73
  mailer?: Mailer;
74
+ /** The connections the app's workflow plugins declare, which people connect accounts through. */
75
+ workflowConnections?: CreateAuthOptions['workflowConnections'];
73
76
  };
74
77
  /** createAuth's options for the `auth` settings, the store in the app's own database. */
75
- export declare const appAuthOptions: (settings: AuthSettings, { pool, db, hooks, mailer }: AppAuthInput) => CreateAuthOptions;
78
+ export declare const appAuthOptions: (settings: AuthSettings, { pool, db, hooks, mailer, workflowConnections }: AppAuthInput) => CreateAuthOptions;
76
79
  /** The app's Better Auth from its `auth` settings, built at serve time with only the plugins they turn on. */
77
80
  export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput) => Promise<import("better-auth").Auth<{
78
81
  user: {
@@ -166,6 +169,7 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
166
169
  account: {
167
170
  encryptOAuthTokens?: true | undefined;
168
171
  accountLinking: {
172
+ trustedProviders?: string[] | undefined;
169
173
  allowDifferentEmails: true;
170
174
  };
171
175
  fields: {
@@ -3276,6 +3280,102 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
3276
3280
  INVALID_OAUTH_CONFIGURATION: import("better-auth").RawError<"INVALID_OAUTH_CONFIGURATION">;
3277
3281
  TOKEN_URL_NOT_FOUND: import("better-auth").RawError<"TOKEN_URL_NOT_FOUND">;
3278
3282
  };
3283
+ } | {
3284
+ id: "protobase-connection-providers";
3285
+ init: (ctx: import("better-auth").AuthContext) => {
3286
+ context: {
3287
+ socialProviders: import("better-auth").OAuthProvider<object, Partial<import("better-auth").ProviderOptions<object>>>[];
3288
+ };
3289
+ };
3290
+ } | {
3291
+ id: "protobase-connections";
3292
+ endpoints: {
3293
+ listConnections: import("better-call").StrictEndpoint<string, {
3294
+ method: "GET";
3295
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
3296
+ session: {
3297
+ session: Record<string, any> & {
3298
+ id: string;
3299
+ createdAt: Date;
3300
+ updatedAt: Date;
3301
+ userId: string;
3302
+ expiresAt: Date;
3303
+ token: string;
3304
+ ipAddress?: string | null | undefined;
3305
+ userAgent?: string | null | undefined;
3306
+ };
3307
+ user: Record<string, any> & {
3308
+ id: string;
3309
+ createdAt: Date;
3310
+ updatedAt: Date;
3311
+ email: string;
3312
+ emailVerified: boolean;
3313
+ name: string;
3314
+ image?: string | null | undefined;
3315
+ };
3316
+ };
3317
+ }>>[];
3318
+ }, {
3319
+ connections: {
3320
+ key: string;
3321
+ label: string;
3322
+ provider: string;
3323
+ scopes: string[];
3324
+ link_params: Record<string, string>;
3325
+ accounts: {
3326
+ id: string;
3327
+ scopes: string[];
3328
+ created_at: Date;
3329
+ needs_sign_in: boolean;
3330
+ }[];
3331
+ }[];
3332
+ }>;
3333
+ disconnectConnection: import("better-call").StrictEndpoint<string, {
3334
+ method: "POST";
3335
+ use: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<{
3336
+ session: {
3337
+ session: Record<string, any> & {
3338
+ id: string;
3339
+ createdAt: Date;
3340
+ updatedAt: Date;
3341
+ userId: string;
3342
+ expiresAt: Date;
3343
+ token: string;
3344
+ ipAddress?: string | null | undefined;
3345
+ userAgent?: string | null | undefined;
3346
+ };
3347
+ user: Record<string, any> & {
3348
+ id: string;
3349
+ createdAt: Date;
3350
+ updatedAt: Date;
3351
+ email: string;
3352
+ emailVerified: boolean;
3353
+ name: string;
3354
+ image?: string | null | undefined;
3355
+ };
3356
+ };
3357
+ }>>[];
3358
+ body: import("zod").ZodObject<{
3359
+ accountId: import("zod").ZodString;
3360
+ }, import("zod/v4/core").$strip>;
3361
+ }, {
3362
+ disconnected: boolean;
3363
+ revoked: boolean;
3364
+ }>;
3365
+ };
3366
+ hooks: {
3367
+ before: {
3368
+ matcher: (context: import("better-auth").HookEndpointContext) => boolean;
3369
+ handler: import("better-call").Middleware<import("better-call").MiddlewareOptions, (inputContext: import("better-call").MiddlewareInputContext<import("better-call").MiddlewareOptions>) => Promise<void>>;
3370
+ }[];
3371
+ };
3372
+ } | {
3373
+ id: "protobase-refresh-refusals";
3374
+ init: (ctx: import("better-auth").AuthContext) => {
3375
+ context: {
3376
+ socialProviders: import("better-auth").OAuthProvider<object, Partial<import("better-auth").ProviderOptions<object>>>[];
3377
+ };
3378
+ };
3279
3379
  } | {
3280
3380
  id: "sso";
3281
3381
  version: string;
@@ -4407,9 +4507,7 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
4407
4507
  email: string;
4408
4508
  emailVerified: boolean;
4409
4509
  name: string;
4410
- image?
4411
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */
4412
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */ : string | null | undefined;
4510
+ image?: string | null | undefined;
4413
4511
  } & Record<string, any>;
4414
4512
  };
4415
4513
  }>>[];
@@ -4511,9 +4609,7 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
4511
4609
  email: string;
4512
4610
  emailVerified: boolean;
4513
4611
  name: string;
4514
- image?
4515
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */
4516
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */ : string | null | undefined;
4612
+ image?: string | null | undefined;
4517
4613
  } & Record<string, any>;
4518
4614
  };
4519
4615
  }>>[];
@@ -4939,9 +5035,7 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
4939
5035
  email: string;
4940
5036
  emailVerified: boolean;
4941
5037
  name: string;
4942
- image?
4943
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */
4944
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */ : string | null | undefined;
5038
+ image?: string | null | undefined;
4945
5039
  } & Record<string, any>;
4946
5040
  };
4947
5041
  }>>[];
@@ -5773,9 +5867,7 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
5773
5867
  email: string;
5774
5868
  emailVerified: boolean;
5775
5869
  name: string;
5776
- image?
5777
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */
5778
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */ : string | null | undefined;
5870
+ image?: string | null | undefined;
5779
5871
  } & Record<string, any>;
5780
5872
  };
5781
5873
  }>>[];
@@ -5878,9 +5970,7 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
5878
5970
  email: string;
5879
5971
  emailVerified: boolean;
5880
5972
  name: string;
5881
- image?
5882
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */
5883
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */ : string | null | undefined;
5973
+ image?: string | null | undefined;
5884
5974
  } & Record<string, any>;
5885
5975
  };
5886
5976
  }>>[];
@@ -5981,9 +6071,7 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
5981
6071
  email: string;
5982
6072
  emailVerified: boolean;
5983
6073
  name: string;
5984
- image?
5985
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */
5986
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */ : string | null | undefined;
6074
+ image?: string | null | undefined;
5987
6075
  } & Record<string, any>;
5988
6076
  };
5989
6077
  }>>[];
@@ -6082,9 +6170,7 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
6082
6170
  email: string;
6083
6171
  emailVerified: boolean;
6084
6172
  name: string;
6085
- image?
6086
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */
6087
- /** Turns on the OAuth provider for MCP clients: `true`, or the endpoint's `path` and the `connectPage`. */ : string | null | undefined;
6173
+ image?: string | null | undefined;
6088
6174
  } & Record<string, any>;
6089
6175
  };
6090
6176
  }>>[];
@@ -8849,10 +8935,40 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
8849
8935
  };
8850
8936
  };
8851
8937
  session: {
8852
- create: {
8853
- after: (session: {
8854
- token: string;
8855
- }, ctx: ReplacingContext | null) => Promise<void>;
8938
+ create?: {
8939
+ before?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
8940
+ data: {
8941
+ id?: string | undefined;
8942
+ createdAt?: Date | undefined;
8943
+ updatedAt?: Date | undefined;
8944
+ userId?: string | undefined;
8945
+ expiresAt?: Date | undefined;
8946
+ token?: string | undefined;
8947
+ ipAddress?: string | null | undefined;
8948
+ userAgent?: string | null | undefined;
8949
+ } & Record<string, any>;
8950
+ }>;
8951
+ after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
8952
+ };
8953
+ update?: {
8954
+ before?: (session: Partial<import("better-auth").Session> & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void | {
8955
+ data: {
8956
+ [x: string]: any;
8957
+ id?: string | undefined;
8958
+ createdAt?: Date | undefined;
8959
+ updatedAt?: Date | undefined;
8960
+ userId?: string | undefined;
8961
+ expiresAt?: Date | undefined;
8962
+ token?: string | undefined;
8963
+ ipAddress?: string | null | undefined;
8964
+ userAgent?: string | null | undefined;
8965
+ };
8966
+ }>;
8967
+ after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
8968
+ };
8969
+ delete?: {
8970
+ before?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<boolean | void>;
8971
+ after?: (session: import("better-auth").Session & Record<string, unknown>, context: import("better-auth").GenericEndpointContext | null) => Promise<void>;
8856
8972
  };
8857
8973
  };
8858
8974
  account: {
@@ -9031,6 +9147,17 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
9031
9147
  }[];
9032
9148
  personalMail: PersonalMailProvider[];
9033
9149
  personalMailer: (userId: string) => Mailer;
9150
+ connectionSupport: {
9151
+ encrypted: boolean;
9152
+ signInProviders: string[];
9153
+ clients: string[];
9154
+ };
9155
+ connections: {
9156
+ declared: ResolvedConnection[];
9157
+ events: {
9158
+ set: (next: ConnectionListener) => undefined;
9159
+ };
9160
+ };
9034
9161
  mcp: {
9035
9162
  resource: string;
9036
9163
  path: string;
@@ -14,7 +14,7 @@ const organizationOptions = (organizations, hooks, app) => {
14
14
  };
15
15
  };
16
16
  /** createAuth's options for the `auth` settings, the store in the app's own database. */
17
- const appAuthOptions = (settings, { pool, db, hooks = {}, mailer }) => {
17
+ const appAuthOptions = (settings, { pool, db, hooks = {}, mailer, workflowConnections }) => {
18
18
  const { schema = "auth", secret, generateUserId, mailer: mail, organizations, mcp, ...rest } = settings;
19
19
  if (!secret) throw new Error("the auth secret is not set: give it the app's secret, PROTOBASE_SECRET, made with `openssl rand -base64 32`");
20
20
  const app = { db };
@@ -34,7 +34,8 @@ const appAuthOptions = (settings, { pool, db, hooks = {}, mailer }) => {
34
34
  ...onUserCreated && { onUserCreated },
35
35
  ...mail === false ? { mailer: false } : mailer && { mailer },
36
36
  ...organizationsOptions && { organizations: organizationsOptions },
37
- ...mcp && { mcp }
37
+ ...mcp && { mcp },
38
+ ...workflowConnections && { workflowConnections }
38
39
  };
39
40
  };
40
41
  /** The app's Better Auth from its `auth` settings, built at serve time with only the plugins they turn on. */
@@ -0,0 +1,62 @@
1
+ import { APIError, getOAuthState } from "better-auth/api";
2
+ //#region src/better-auth/connections/connection-account-hooks.ts
3
+ const refused = () => new APIError("FORBIDDEN", {
4
+ code: "CONNECTION_NOT_SIGN_IN",
5
+ message: "This provider connects an account to a workflow; it does not sign in."
6
+ });
7
+ /**
8
+ * The account and session hooks of workflow connections. An account of a connection's provider written by a link
9
+ * resumes the triggers waiting on it. On the OAuth callback of a provider only connections use, an account written
10
+ * without a link, and any session, are refused: a connected account never signs anyone in. Refreshing a token writes
11
+ * the account off the callback, so it passes.
12
+ */
13
+ const connectionAccountHooks = (connections, events) => {
14
+ const providers = new Set(connections.map(({ providerId }) => providerId));
15
+ const own = new Set(connections.filter(({ declaration }) => "oauth" in declaration).map(({ providerId }) => providerId));
16
+ const onOwnCallback = (context) => context?.path === "/callback/:id" && own.has(context.params?.id ?? "");
17
+ const guard = async (_, context) => {
18
+ if (onOwnCallback(context) && !(await getOAuthState())?.link) throw refused();
19
+ };
20
+ const resume = async (account) => {
21
+ if (providers.has(account.providerId) && (await getOAuthState())?.link) await events.linked(account.id);
22
+ };
23
+ return {
24
+ account: {
25
+ create: {
26
+ before: guard,
27
+ after: resume
28
+ },
29
+ update: {
30
+ before: guard,
31
+ after: resume
32
+ }
33
+ },
34
+ session: { create: { before: async (_, context) => {
35
+ if (onOwnCallback(context)) throw refused();
36
+ } } }
37
+ };
38
+ };
39
+ /** Runs each hook of `first` and then of `second`; their `before` hooks change no data, as neither returns any. */
40
+ const bothAccountHooks = (first, second) => {
41
+ const join = (stage, operation) => {
42
+ const a = first[operation]?.[stage];
43
+ const b = second[operation]?.[stage];
44
+ if (!a || !b) return a ?? b;
45
+ return async (record, context) => {
46
+ await a(record, context);
47
+ return b(record, context);
48
+ };
49
+ };
50
+ return {
51
+ create: {
52
+ before: join("before", "create"),
53
+ after: join("after", "create")
54
+ },
55
+ update: {
56
+ before: join("before", "update"),
57
+ after: join("after", "update")
58
+ }
59
+ };
60
+ };
61
+ //#endregion
62
+ export { bothAccountHooks, connectionAccountHooks };
@@ -0,0 +1,11 @@
1
+ //#region src/better-auth/connections/connection-events.d.ts
2
+ /** What the workflows do when an account of a connection is linked or disconnected, and which accounts their triggers wait on. */
3
+ export type ConnectionListener = {
4
+ /** An account was linked again: the triggers waiting on its sign-in poll again. */
5
+ linked: (accountId: string) => Promise<void>;
6
+ /** An account was disconnected: the triggers on it wait for a new sign-in. */
7
+ disconnected: (accountId: string) => Promise<void>;
8
+ /** The accounts among `accountIds` that a trigger waits on to be signed in again. */
9
+ needsSignIn: (accountIds: string[]) => Promise<string[]>;
10
+ };
11
+ //#endregion
@@ -0,0 +1,16 @@
1
+ //#region src/better-auth/connections/connection-events.ts
2
+ /**
3
+ * The listener sign-in tells of connections, which the workflows set once they are made, after the auth instance;
4
+ * until then, and in an app without workflows, nothing listens.
5
+ */
6
+ const connectionEvents = () => {
7
+ let listener;
8
+ return {
9
+ set: (next) => void (listener = next),
10
+ linked: async (accountId) => listener?.linked(accountId),
11
+ disconnected: async (accountId) => listener?.disconnected(accountId),
12
+ needsSignIn: async (accountIds) => listener && accountIds.length > 0 ? listener.needsSignIn(accountIds) : []
13
+ };
14
+ };
15
+ //#endregion
16
+ export { connectionEvents };
@@ -0,0 +1,129 @@
1
+ import { pinnedRequest } from "../../webhooks/table/pinned-request.js";
2
+ import { readLimited } from "../../webhooks/table/read-limited.js";
3
+ import { authorizationCodeRequest, createAuthorizationURL, getOAuth2Tokens, refreshAccessTokenRequest } from "@better-auth/core/oauth2";
4
+ //#region src/better-auth/connections/connection-oauth-provider.ts
5
+ /** The most of a provider's token or user answer read: enough for any token response, and no more. */
6
+ const answerBytes = 65536;
7
+ const requestTimeoutMs = 1e4;
8
+ const guardedJson = async (url, init, { allowPrivateAddresses }) => {
9
+ const response = await pinnedRequest(url, {
10
+ ...init,
11
+ signal: AbortSignal.timeout(requestTimeoutMs)
12
+ }, { allowPrivateAddresses });
13
+ const bytes = await readLimited(response, answerBytes);
14
+ if (!response.ok) throw new ProviderAnswerError(response.status, oauthError(bytes));
15
+ return JSON.parse(new TextDecoder().decode(bytes));
16
+ };
17
+ /** A provider's answer that is not a success: its status, and its OAuth `error` code when it gives one, as `invalid_grant`. */
18
+ var ProviderAnswerError = class extends Error {
19
+ status;
20
+ error;
21
+ constructor(status, error) {
22
+ super(`The provider answered ${status}${error ? ` (${error})` : ""}`);
23
+ this.status = status;
24
+ this.error = error;
25
+ }
26
+ };
27
+ const oauthError = (bytes) => {
28
+ let body;
29
+ try {
30
+ body = JSON.parse(new TextDecoder().decode(bytes));
31
+ } catch (error) {
32
+ if (error instanceof SyntaxError) return;
33
+ throw error;
34
+ }
35
+ const code = typeof body === "object" && body !== null ? body.error : void 0;
36
+ return typeof code === "string" ? code : void 0;
37
+ };
38
+ const tokenRequest = async (url, request, guarded) => getOAuth2Tokens(await guardedJson(url, {
39
+ method: "POST",
40
+ headers: request.headers,
41
+ body: request.body.toString()
42
+ }, guarded));
43
+ const userOf = (raw) => {
44
+ const id = raw.sub ?? raw.id;
45
+ if (typeof id !== "string" && typeof id !== "number") return null;
46
+ const text = (value) => typeof value === "string" && value !== "" ? value : void 0;
47
+ const email = text(raw.email);
48
+ return {
49
+ user: {
50
+ id: String(id),
51
+ email,
52
+ name: text(raw.name) ?? email ?? String(id),
53
+ emailVerified: raw.email_verified === true
54
+ },
55
+ data: raw
56
+ };
57
+ };
58
+ /**
59
+ * A Better Auth provider for a connection with an OAuth provider of its own: it never signs up anyone, and sends its
60
+ * code exchange, refresh and user request only to the URLs the plugin declares, each through the guarded request.
61
+ */
62
+ const connectionOAuthProvider = (connection, endpoints, guarded) => {
63
+ const options = {
64
+ clientId: connection.client.clientId,
65
+ clientSecret: connection.client.clientSecret
66
+ };
67
+ const pkce = endpoints.pkce ?? true;
68
+ return {
69
+ id: connection.providerId,
70
+ name: connection.declaration.label,
71
+ disableSignUp: true,
72
+ disableImplicitSignUp: true,
73
+ options,
74
+ accountSubject: ({ profile }) => String(profile.sub ?? profile.id ?? ""),
75
+ createAuthorizationURL: (data) => createAuthorizationURL({
76
+ id: connection.providerId,
77
+ options,
78
+ authorizationEndpoint: endpoints.authorization_url,
79
+ state: data.state,
80
+ ...pkce && { codeVerifier: data.codeVerifier },
81
+ scopes: [...connection.declaration.scopes ?? [], ...data.scopes ?? []],
82
+ redirectURI: data.redirectURI,
83
+ additionalParams: { ...connection.declaration.link_params }
84
+ }),
85
+ validateAuthorizationCode: async (data) => {
86
+ const request = await authorizationCodeRequest({
87
+ code: data.code,
88
+ ...pkce && { codeVerifier: data.codeVerifier },
89
+ redirectURI: data.redirectURI,
90
+ options,
91
+ tokenEndpoint: endpoints.token_url,
92
+ authentication: "post"
93
+ });
94
+ return tokenRequest(endpoints.token_url, request, guarded);
95
+ },
96
+ refreshAccessToken: async (refreshToken) => {
97
+ const request = await refreshAccessTokenRequest({
98
+ refreshToken,
99
+ options,
100
+ tokenEndpoint: endpoints.token_url,
101
+ authentication: "post"
102
+ });
103
+ return tokenRequest(endpoints.token_url, request, guarded);
104
+ },
105
+ getUserInfo: async (tokens) => {
106
+ const raw = await guardedJson(endpoints.user_info_url, {
107
+ method: "GET",
108
+ headers: {
109
+ accept: "application/json",
110
+ authorization: `Bearer ${tokens.accessToken}`
111
+ }
112
+ }, guarded);
113
+ return userOf(raw);
114
+ }
115
+ };
116
+ };
117
+ /** Adds the connections' own OAuth providers to the instance's providers, so `/link-social` connects through them. */
118
+ const connectionProvidersPlugin = (connections, guarded) => {
119
+ const providers = connections.flatMap((connection) => "oauth" in connection.declaration && connection.client ? [connectionOAuthProvider({
120
+ ...connection,
121
+ client: connection.client
122
+ }, connection.declaration.oauth, guarded)] : []);
123
+ return {
124
+ id: "protobase-connection-providers",
125
+ init: (ctx) => ({ context: { socialProviders: [...providers, ...ctx.socialProviders] } })
126
+ };
127
+ };
128
+ //#endregion
129
+ export { ProviderAnswerError, connectionOAuthProvider, connectionProvidersPlugin };
@@ -0,0 +1,98 @@
1
+ import { storedScopes } from "../../personal-mail/linked-accounts.js";
2
+ import { holdsScopes } from "../../workflows/connections/connection-provider.js";
3
+ import { revokeToken } from "./revoke-token.js";
4
+ import * as z$1 from "zod";
5
+ import { decryptOAuthToken } from "better-auth/oauth2";
6
+ import { APIError, createAuthEndpoint, createAuthMiddleware, sessionMiddleware } from "better-auth/api";
7
+ //#region src/better-auth/connections/connections-plugin.ts
8
+ const connectionPaths = {
9
+ list: "/connections",
10
+ disconnect: "/connections/disconnect"
11
+ };
12
+ const notFound = () => new APIError("NOT_FOUND", {
13
+ code: "CONNECTION_NOT_FOUND",
14
+ message: "You have no connected account with this id."
15
+ });
16
+ /**
17
+ * The signed-in person's connections: `GET /connections` lists each connection the app's workflow plugins declare with
18
+ * the person's accounts that fit it, and whether a trigger waits for one to be signed in again, never a token.
19
+ * `POST /connections/disconnect` revokes an account's token at its provider, then deletes it whatever the provider
20
+ * answered, and stops the triggers on it; their own account only, and never the last account they sign in with. Connecting is
21
+ * Better Auth's `/link-social`; a sign-in (not a link) through a provider only connections use is refused.
22
+ */
23
+ const connectionsPlugin = ({ connections, events, allowPrivateAddresses }) => {
24
+ const own = new Set(connections.filter(({ declaration }) => "oauth" in declaration).map(({ providerId }) => providerId));
25
+ return {
26
+ id: "protobase-connections",
27
+ endpoints: {
28
+ listConnections: createAuthEndpoint(connectionPaths.list, {
29
+ method: "GET",
30
+ use: [sessionMiddleware]
31
+ }, async (ctx) => {
32
+ const accounts = await ctx.context.internalAdapter.findAccounts(ctx.context.session.user.id);
33
+ const fitting = (connection) => accounts.filter((account) => account.providerId === connection.providerId && holdsScopes(storedScopes(account.scope), connection.declaration));
34
+ const waiting = new Set(await events.needsSignIn(connections.flatMap((connection) => fitting(connection).map((account) => account.id))));
35
+ return ctx.json({ connections: connections.map((connection) => ({
36
+ key: connection.key,
37
+ label: connection.declaration.label,
38
+ provider: connection.providerId,
39
+ scopes: connection.declaration.scopes ?? [],
40
+ link_params: connection.declaration.link_params ?? {},
41
+ accounts: fitting(connection).map((account) => ({
42
+ id: account.id,
43
+ scopes: storedScopes(account.scope),
44
+ created_at: account.createdAt,
45
+ needs_sign_in: waiting.has(account.id)
46
+ }))
47
+ })) });
48
+ }),
49
+ disconnectConnection: createAuthEndpoint(connectionPaths.disconnect, {
50
+ method: "POST",
51
+ use: [sessionMiddleware],
52
+ body: z$1.object({ accountId: z$1.string() })
53
+ }, async (ctx) => {
54
+ const adapter = ctx.context.internalAdapter;
55
+ const accounts = await adapter.findAccounts(ctx.context.session.user.id);
56
+ const account = accounts.find((candidate) => candidate.id === ctx.body.accountId);
57
+ const connection = account && connections.find((candidate) => candidate.providerId === account.providerId);
58
+ if (!account || !connection) throw notFound();
59
+ const signIns = accounts.filter((candidate) => !own.has(candidate.providerId));
60
+ if (!own.has(account.providerId) && signIns.length === 1) throw new APIError("BAD_REQUEST", {
61
+ code: "FAILED_TO_UNLINK_LAST_ACCOUNT",
62
+ message: "This is your only way to sign in: add another before you disconnect it."
63
+ });
64
+ const stored = account.refreshToken ?? account.accessToken;
65
+ const token = stored ? await decryptOAuthToken(stored, ctx.context) : void 0;
66
+ let revoked = false;
67
+ try {
68
+ if (connection.revocationUrl && token) revoked = await revokeToken({
69
+ url: connection.revocationUrl,
70
+ client: connection.client,
71
+ token,
72
+ hint: account.refreshToken ? "refresh_token" : "access_token",
73
+ allowPrivateAddresses
74
+ });
75
+ } finally {
76
+ await adapter.deleteAccount(account.id);
77
+ await events.disconnected(account.id);
78
+ }
79
+ return ctx.json({
80
+ disconnected: true,
81
+ revoked
82
+ });
83
+ })
84
+ },
85
+ hooks: { before: [{
86
+ matcher: (context) => context.path === "/sign-in/social",
87
+ handler: createAuthMiddleware(async (ctx) => {
88
+ const provider = ctx.body?.provider;
89
+ if (typeof provider === "string" && own.has(provider)) throw new APIError("FORBIDDEN", {
90
+ code: "CONNECTION_NOT_SIGN_IN",
91
+ message: "This provider connects an account to a workflow; it does not sign in."
92
+ });
93
+ })
94
+ }] }
95
+ };
96
+ };
97
+ //#endregion
98
+ export { connectionPaths, connectionsPlugin };
@@ -0,0 +1,21 @@
1
+ import { ConnectionDeclaration } from "@protobase/workflows/plugin";
2
+ //#region src/better-auth/connections/declared-connections.d.ts
3
+ /** A connection a workflow plugin declares, as the app loaded it. */
4
+ export type DeclaredConnection = {
5
+ plugin: string;
6
+ name: string;
7
+ declaration: ConnectionDeclaration;
8
+ };
9
+ /** The OAuth client an app's owner made at a provider for a workflow connection. */
10
+ export type ConnectionClient = {
11
+ clientId: string;
12
+ clientSecret: string;
13
+ };
14
+ /** A connection as sign-in keeps it: its Better Auth provider, and the client and URL that revoke its tokens. */
15
+ export type ResolvedConnection = DeclaredConnection & {
16
+ key: string;
17
+ providerId: string;
18
+ client?: ConnectionClient;
19
+ revocationUrl?: string;
20
+ };
21
+ //#endregion