@protobase/server 0.25.12 → 0.26.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 (55) hide show
  1. package/dist/admin-options.d.ts +7 -0
  2. package/dist/better-auth/app-tokens.js +2 -2
  3. package/dist/better-auth/authenticator.js +1 -1
  4. package/dist/better-auth/create-auth.d.ts +121 -2
  5. package/dist/better-auth/create-auth.js +70 -51
  6. package/dist/better-auth/email-confirmed.js +31 -0
  7. package/dist/better-auth/organizations/accept-invitation.js +1 -1
  8. package/dist/better-auth/organizations/app-roles.js +1 -1
  9. package/dist/better-auth/organizations/options.js +1 -1
  10. package/dist/better-auth/organizations/organization-plugin.js +1 -1
  11. package/dist/better-auth/organizations/session-organization.js +1 -1
  12. package/dist/better-auth/passkey-plugin.js +1 -1
  13. package/dist/better-auth/policy-plugin.js +27 -12
  14. package/dist/better-auth/roles/active-role.js +1 -1
  15. package/dist/better-auth/sign-up/sign-up-account.js +26 -0
  16. package/dist/better-auth/sign-up/sign-up-plugin.js +6 -14
  17. package/dist/better-auth/staff-plugin.js +1 -1
  18. package/dist/better-auth/staff-sign-in-endpoints.js +3 -3
  19. package/dist/better-auth/status-route.js +1 -1
  20. package/dist/better-auth/user-table.d.ts +1 -0
  21. package/dist/better-auth/user-table.js +1 -0
  22. package/dist/better-auth/users/user-endpoints.js +1 -1
  23. package/dist/create-admin.d.ts +10 -1
  24. package/dist/create-admin.js +26 -2
  25. package/dist/gotrue/access-tokens.js +15 -0
  26. package/dist/gotrue/auth-codes.js +33 -0
  27. package/dist/gotrue/bearer.js +22 -0
  28. package/dist/gotrue/deps.js +8 -0
  29. package/dist/gotrue/email-code-routes.js +121 -0
  30. package/dist/gotrue/errors.js +52 -0
  31. package/dist/gotrue/forward.js +45 -0
  32. package/dist/gotrue/keys.js +14 -0
  33. package/dist/gotrue/pending-sign-up.js +26 -0
  34. package/dist/gotrue/provider-routes.js +79 -0
  35. package/dist/gotrue/request-body.js +22 -0
  36. package/dist/gotrue/routes.js +32 -0
  37. package/dist/gotrue/session-json.js +34 -0
  38. package/dist/gotrue/sessions-plugin.js +83 -0
  39. package/dist/gotrue/token-route.js +42 -0
  40. package/dist/gotrue/user-json.js +46 -0
  41. package/dist/gotrue/user-routes.js +99 -0
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.js +7 -7
  44. package/dist/live/notices-sql.js +4 -2
  45. package/dist/realtime/change-checker.js +102 -0
  46. package/dist/realtime/change-filter.js +106 -0
  47. package/dist/realtime/change-hub.js +100 -0
  48. package/dist/realtime/change-payload.js +31 -0
  49. package/dist/realtime/phoenix-message.js +63 -0
  50. package/dist/realtime/postgres-changes.js +62 -0
  51. package/dist/realtime/realtime-app.js +31 -0
  52. package/dist/realtime/realtime-routes.js +47 -0
  53. package/dist/realtime/realtime-socket.js +135 -0
  54. package/dist/rest/caller-settings.js +27 -0
  55. package/package.json +10 -5
@@ -1,5 +1,5 @@
1
- import { isRefusedForStaff, isStaffSession, staffCannotChange } from "./staff-checks.js";
2
1
  import { adminOnly } from "./admin-only.js";
2
+ import { isRefusedForStaff, isStaffSession, staffCannotChange } from "./staff-checks.js";
3
3
  import { endStaffSignIn, listStaffSignIns, staffSignInOfSession, staffSignInSchema } from "./staff-log.js";
4
4
  import { staffSignInEndpoints } from "./staff-sign-in-endpoints.js";
5
5
  import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
@@ -1,9 +1,9 @@
1
- import { reasonProblem, staffClaimsRefusal, staffLabel } from "./staff-checks.js";
2
- import { operatorClient, randomValue } from "./operator-provider.js";
3
- import { sendAuthEmail } from "./auth-mail/send-auth-email.js";
4
1
  import { effectiveSignInPolicy } from "./sign-in-policy.js";
5
2
  import { readSignInPolicy } from "./policy-store.js";
6
3
  import { policyRefusal, staffSignInPath } from "./sign-in-enforcement.js";
4
+ import { reasonProblem, staffClaimsRefusal, staffLabel } from "./staff-checks.js";
5
+ import { operatorClient, randomValue } from "./operator-provider.js";
6
+ import { sendAuthEmail } from "./auth-mail/send-auth-email.js";
7
7
  import { logStaffSignIn } from "./staff-log.js";
8
8
  import * as z$1 from "zod";
9
9
  import { APIError, createAuthEndpoint } from "better-auth/api";
@@ -1,6 +1,6 @@
1
- import { signInPasskeyDomains } from "./passkey-domain.js";
2
1
  import { effectiveSignInPolicy } from "./sign-in-policy.js";
3
2
  import { readSignInPolicy } from "./policy-store.js";
3
+ import { signInPasskeyDomains } from "./passkey-domain.js";
4
4
  import { roleLabels } from "./roles/role-labels.js";
5
5
  import { hasUsers } from "./users.js";
6
6
  import { Hono } from "hono";
@@ -5,6 +5,7 @@ declare const userFields: {
5
5
  readonly name: "name";
6
6
  readonly email: "email";
7
7
  readonly email_verified: "emailVerified";
8
+ readonly email_confirmed_at: "emailConfirmedAt";
8
9
  readonly image: "image";
9
10
  readonly created_at: "createdAt";
10
11
  readonly updated_at: "updatedAt";
@@ -4,6 +4,7 @@ const userFields = {
4
4
  name: "name",
5
5
  email: "email",
6
6
  email_verified: "emailVerified",
7
+ email_confirmed_at: "emailConfirmedAt",
7
8
  image: "image",
8
9
  created_at: "createdAt",
9
10
  updated_at: "updatedAt",
@@ -1,6 +1,6 @@
1
+ import { adminOnly } from "../admin-only.js";
1
2
  import { invitationLink, invitationToken } from "../invitations/invitation-token.js";
2
3
  import { sendAuthEmail } from "../auth-mail/send-auth-email.js";
3
- import { adminOnly } from "../admin-only.js";
4
4
  import { createAppInvitation, deleteAppInvitation, listAppInvitations } from "./app-invitation-store.js";
5
5
  import { roleLabels } from "../roles/role-labels.js";
6
6
  import * as z$1 from "zod";
@@ -13,6 +13,8 @@ import { FunctionSource } from "./functions/define-function.js";
13
13
  import { AdminOptions } from "./admin-options.js";
14
14
  import { BottomBarSource, UserMenuSource } from "@protobase/schema";
15
15
  import { Db } from "@protobase/query";
16
+ import { JWTPayload } from "jose";
17
+ import { UpgradeWebSocket } from "hono/ws";
16
18
  //#region src/create-admin.d.ts
17
19
  export type CreateAdminInput = {
18
20
  resources: ResourceSource[];
@@ -58,7 +60,14 @@ export type CreateAdminInput = {
58
60
  formsDifficulty?: Difficulty;
59
61
  /** @internal The users comments name, mention and mail, which tests fake; from `auth` otherwise. */
60
62
  commentUsers?: CommentUsers;
63
+ /** @internal Verifies a user's token on `/realtime/v1` and gives its claims, which tests fake; `auth`'s tokens otherwise. */
64
+ verifyToken?: (token: string) => Promise<JWTPayload>;
65
+ /**
66
+ * The host's WebSocket upgrade, such as `@hono/node-server`'s, with which an app with keys and live resources serves
67
+ * supabase-js's Realtime at `/realtime/v1/websocket`; without it, the app serves none.
68
+ */
69
+ upgradeWebSocket?: UpgradeWebSocket;
61
70
  };
62
71
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
63
- export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers }: CreateAdminInput) => Admin;
72
+ export declare const createAdmin: ({ resources: appResources, views: appViews, pages, userMenu, bottomBars, db, authenticate, auth, functions, files: filesOptions, events, history: historyOptions, jobs, subscriptions, webhooks: webhooksOptions, bundlePlugins, fieldTypes, options, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }: CreateAdminInput) => Admin;
64
73
  //#endregion
@@ -1,5 +1,6 @@
1
1
  import { aiModel } from "./ai/ai-model.js";
2
2
  import { hasAiActions, hasAiFields } from "./ai/ai-fields.js";
3
+ import { unauthorized } from "./problem.js";
3
4
  import { fillHook } from "./ai/fill-hook.js";
4
5
  import { createErrorHandler } from "./error-handler.js";
5
6
  import { isCaptured } from "./changes/capture-sql.js";
@@ -10,10 +11,17 @@ import { builtInAssistant } from "./assistant/built-in-assistant.js";
10
11
  import { consoleAuditQueue } from "./audit/console-queue.js";
11
12
  import { webauthnPath } from "./better-auth/origins.js";
12
13
  import { apiCors, readCorsOrigins } from "./api-cors.js";
14
+ import { apiKeySettings } from "./api-keys/api-key-settings.js";
15
+ import { apiKeyCaller } from "./api-keys/resolve-caller.js";
16
+ import { jwtVerifier } from "./auth/jwt.js";
13
17
  import { readAppName } from "./app-name.js";
18
+ import { goTruePath } from "./gotrue/keys.js";
14
19
  import { authBasePath } from "./better-auth/auth-base-path.js";
20
+ import { betterAuthTokens } from "./better-auth/authenticator.js";
21
+ import { realtimeApp } from "./realtime/realtime-app.js";
15
22
  import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
16
23
  import { statusRoute } from "./better-auth/status-route.js";
24
+ import { goTrueRoutes } from "./gotrue/routes.js";
17
25
  import { changesSchema } from "./changes/events-options.js";
18
26
  import { appOrigin } from "./comments/app-origin.js";
19
27
  import { betterAuthActorNames } from "./history/actor-names.js";
@@ -66,7 +74,7 @@ const declaredRoles = (auth, roles) => {
66
74
  return names.length > 0 ? [...new Set(names)] : void 0;
67
75
  };
68
76
  /** The admin REST API as a Hono app. It uses only Web APIs, so it runs on Node, Workers and anywhere else Hono does. */
69
- const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers }) => {
77
+ const createAdmin = ({ resources: appResources, views: appViews = [], pages = [], userMenu, bottomBars = [], db, authenticate, auth, functions = {}, files: filesOptions, events, history: historyOptions, jobs = [], subscriptions = [], webhooks: webhooksOptions, bundlePlugins = [], fieldTypes = {}, options = {}, formsDifficulty, commentUsers: givenCommentUsers, verifyToken, upgradeWebSocket }) => {
70
78
  const basePath = options.basePath ?? "/api/v1";
71
79
  const appModels = appResources.map((resource) => resource.toModel());
72
80
  const withComments = appModels.some((model) => model.comments);
@@ -125,6 +133,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
125
133
  resources: registry.entries.map((entry) => entry.name),
126
134
  pages: pageModels.map((entry) => entry.name)
127
135
  });
136
+ const apiKeys = apiKeySettings(options.apiKeys, globalThis.process?.env ?? {});
128
137
  const files = createFilesRuntime({
129
138
  ...filesOptions && { options: filesOptions },
130
139
  env: globalThis.process?.env ?? {},
@@ -233,6 +242,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
233
242
  db
234
243
  }));
235
244
  app.on(["GET", "POST"], `${authBasePath}/*`, (c) => auth.handler(c.req.raw));
245
+ app.route(goTruePath, goTrueRoutes(auth));
236
246
  if (auth.origins.length > 1) app.get(webauthnPath, (c) => c.json({ origins: auth.origins }));
237
247
  }
238
248
  if (assistant?.kind === "built-in") {
@@ -261,12 +271,26 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
261
271
  app.route("/", fileRoutes(deps, authenticate));
262
272
  if (forms) app.route(`${systemPath}/forms`, formRoutes(deps, forms));
263
273
  if (appModels.some((model) => model.live)) {
274
+ const report = options.onUnhandledError ?? ((error) => console.error(error));
264
275
  const feed = tableFeed({
265
276
  db,
266
277
  schema: changes,
267
- report: options.onUnhandledError ?? ((error) => console.error(error))
278
+ report
268
279
  });
269
280
  app.route(systemPath || "/", guarded((live) => void live.route("/", liveRoutes(feed, (session) => visibleNotices(deps, session))), withAnonymous(authenticate, options.roles)));
281
+ if (upgradeWebSocket && (apiKeys.publishable || apiKeys.secret)) {
282
+ const verify = verifyToken ?? (auth ? jwtVerifier(betterAuthTokens({ auth })) : () => Promise.reject(unauthorized("This app signs no user tokens")));
283
+ app.route("/realtime/v1", realtimeApp({
284
+ deps,
285
+ feed,
286
+ upgradeWebSocket,
287
+ caller: apiKeyCaller({
288
+ keys: apiKeys,
289
+ verify
290
+ }),
291
+ report
292
+ }));
293
+ }
270
294
  }
271
295
  app.route(basePath, api);
272
296
  app.route(`${deps.systemPath}/meta`, metaApp);
@@ -0,0 +1,15 @@
1
+ import { goTrueAlgorithm, goTrueAudience, goTrueIssuer, goTrueKeys } from "./keys.js";
2
+ import { cachedKeys } from "../better-auth/jwks-cache.js";
3
+ //#region src/gotrue/access-tokens.ts
4
+ /**
5
+ * How the tokens of the GoTrue-compatible endpoints verify, read from the auth instance: for `jwtVerifier`, such as
6
+ * `apiKeyCaller({ keys, verify: jwtVerifier(goTrueTokens({ auth })) })`.
7
+ */
8
+ const goTrueTokens = ({ auth, cache }) => ({
9
+ keys: cachedKeys(async () => goTrueKeys(await auth.api.getJwks()), cache),
10
+ issuer: goTrueIssuer(auth.issuer),
11
+ audience: goTrueAudience,
12
+ algorithms: [goTrueAlgorithm]
13
+ });
14
+ //#endregion
15
+ export { goTrueTokens };
@@ -0,0 +1,33 @@
1
+ import { hashOf } from "../better-auth/sign-up/sign-up-code.js";
2
+ import { GoTrueError } from "./errors.js";
3
+ import { generateRandomString } from "better-auth/crypto";
4
+ //#region src/gotrue/auth-codes.ts
5
+ const codeSeconds = 300;
6
+ const identifierOf = async (code) => `gotrue-auth-code-${await hashOf(code)}`;
7
+ const base64url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
8
+ /** Whether `verifier` is the one `challenge` was made from: its SHA-256 in base64url for `s256`, itself for `plain`. */
9
+ const matchesChallenge = async (verifier, { challenge, method }) => method === "plain" ? verifier === challenge : base64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))) === challenge;
10
+ /** A new code that signs in `userId` for whoever holds the verifier of `challenge`. */
11
+ const issueAuthCode = async (context, userId, challenge) => {
12
+ const code = generateRandomString(32);
13
+ await context.internalAdapter.createVerificationValue({
14
+ identifier: await identifierOf(code),
15
+ value: JSON.stringify({
16
+ userId,
17
+ ...challenge
18
+ }),
19
+ expiresAt: new Date(Date.now() + codeSeconds * 1e3)
20
+ });
21
+ return code;
22
+ };
23
+ const invalidGrant = () => new GoTrueError(400, "flow_state_not_found", "invalid flow state, no valid flow state found");
24
+ /** Takes `code`, so it works once, and answers its user when `verifier` matches its challenge. */
25
+ const takeAuthCode = async (context, code, verifier) => {
26
+ const consumed = await context.internalAdapter.consumeVerificationValue(await identifierOf(code));
27
+ if (!consumed || new Date(consumed.expiresAt) < /* @__PURE__ */ new Date()) throw invalidGrant();
28
+ const { userId, ...challenge } = JSON.parse(consumed.value);
29
+ if (!await matchesChallenge(verifier, challenge)) throw new GoTrueError(400, "bad_code_verifier", "code challenge does not match previously saved code verifier");
30
+ return userId;
31
+ };
32
+ //#endregion
33
+ export { issueAuthCode, matchesChallenge, takeAuthCode };
@@ -0,0 +1,22 @@
1
+ import { GoTrueError } from "./errors.js";
2
+ import { errors, jwtVerify } from "jose";
3
+ //#region src/gotrue/bearer.ts
4
+ /**
5
+ * Reads the signed-in user's access token from `Authorization: Bearer`, verified with the keys, issuer and audience of `goTrueTokens`:
6
+ * its user and session. Without one, GoTrue's `no_authorization`; one that does not verify is `bad_jwt`.
7
+ */
8
+ const accessClaimsOf = ({ keys, ...expected }) => async (request) => {
9
+ const [scheme, token] = (request.headers.get("authorization") ?? "").split(" ");
10
+ if (scheme?.toLowerCase() !== "bearer" || !token) throw new GoTrueError(401, "no_authorization", "This endpoint requires a valid Bearer token");
11
+ const { payload } = await jwtVerify(token, keys, expected).catch((error) => {
12
+ if (error instanceof errors.JOSEError) throw new GoTrueError(403, "bad_jwt", "invalid JWT: unable to parse or verify signature, token is invalid or expired");
13
+ throw error;
14
+ });
15
+ if (typeof payload.sub !== "string" || typeof payload.session_id !== "string") throw new GoTrueError(403, "bad_jwt", "invalid JWT: it names no user and session");
16
+ return {
17
+ sub: payload.sub,
18
+ session_id: payload.session_id
19
+ };
20
+ };
21
+ //#endregion
22
+ export { accessClaimsOf };
@@ -0,0 +1,8 @@
1
+ //#region src/gotrue/deps.ts
2
+ const goTrueDeps = (auth) => ({
3
+ auth,
4
+ context: () => auth.$context,
5
+ sessionOf: (sessionToken) => auth.api.goTrueSession({ body: { sessionToken } })
6
+ });
7
+ //#endregion
8
+ export { goTrueDeps };
@@ -0,0 +1,121 @@
1
+ import { effectiveSignInPolicy } from "../better-auth/sign-in-policy.js";
2
+ import { readSignInPolicy } from "../better-auth/policy-store.js";
3
+ import { checkNewAccountPassword } from "../better-auth/new-account-password.js";
4
+ import { createSignedUpAccount } from "../better-auth/sign-up/sign-up-account.js";
5
+ import { consumeSignUpTicket } from "../better-auth/sign-up/sign-up-ticket.js";
6
+ import { GoTrueError } from "./errors.js";
7
+ import { callBetterAuth, signedInToken } from "./forward.js";
8
+ import { hasPendingSignUp, savePendingSignUp, takePendingSignUp } from "./pending-sign-up.js";
9
+ import { address, readBody } from "./request-body.js";
10
+ import { Hono } from "hono";
11
+ import * as z$1 from "zod";
12
+ //#region src/gotrue/email-code-routes.ts
13
+ const metadata = z$1.record(z$1.string(), z$1.unknown()).default({});
14
+ const signUpBody = z$1.object({
15
+ email: address,
16
+ password: z$1.string(),
17
+ data: metadata
18
+ });
19
+ const otpBody = z$1.object({
20
+ email: address,
21
+ create_user: z$1.boolean().default(true),
22
+ data: metadata
23
+ });
24
+ const verifyBody = z$1.object({
25
+ email: address.optional(),
26
+ token: z$1.string().trim().optional(),
27
+ token_hash: z$1.string().optional(),
28
+ type: z$1.string()
29
+ });
30
+ const emailBody = z$1.object({ email: address });
31
+ const resendBody = z$1.object({
32
+ email: address,
33
+ type: z$1.string()
34
+ });
35
+ const signUpDisabled = () => new GoTrueError(422, "signup_disabled", "Signups not allowed for this instance");
36
+ const nameOf = (email, data) => {
37
+ const given = data.name ?? data.full_name;
38
+ return typeof given === "string" && given.trim() ? given.trim() : email.split("@")[0];
39
+ };
40
+ const emailCodeRoutes = ({ auth, context, sessionOf }) => {
41
+ const checkMetadata = (data) => {
42
+ if (Object.keys(data).length > 0 && !auth.userMetadata) throw new GoTrueError(400, "validation_failed", "This app keeps no user metadata: leave data out");
43
+ };
44
+ const signUpPolicy = async () => effectiveSignInPolicy((await readSignInPolicy((await context()).adapter)).policy, { mail: true });
45
+ const sendSignUpCode = (request, email) => callBetterAuth(auth, request, "/sign-up/send-code", { email });
46
+ const sendSignInCode = (request, email) => callBetterAuth(auth, request, "/email-otp/send-verification-otp", {
47
+ email,
48
+ type: "sign-in"
49
+ });
50
+ const signInWithCode = async (request, email, code) => {
51
+ const { json } = await callBetterAuth(auth, request, "/sign-in/email-otp", {
52
+ email,
53
+ otp: code
54
+ });
55
+ return sessionOf(signedInToken(json));
56
+ };
57
+ const signUpWithCode = async (request, email, code) => {
58
+ const ctx = await context();
59
+ if (!auth.signUpRole || !await hasPendingSignUp(ctx, email)) throw new GoTrueError(403, "otp_expired", "Token has expired or is invalid");
60
+ const { json } = await callBetterAuth(auth, request, "/sign-up/verify-code", {
61
+ email,
62
+ code
63
+ });
64
+ await consumeSignUpTicket(ctx, String(json.ticket));
65
+ const pending = await takePendingSignUp(ctx, email);
66
+ if (!pending) throw new GoTrueError(403, "otp_expired", "Token has expired or is invalid");
67
+ const { passwordHash, metadata } = pending;
68
+ const user = await createSignedUpAccount(ctx, {
69
+ email,
70
+ name: nameOf(email, metadata),
71
+ role: auth.signUpRole,
72
+ ...passwordHash !== void 0 && { passwordHash },
73
+ ...Object.keys(metadata).length > 0 && { metadata }
74
+ });
75
+ return sessionOf((await ctx.internalAdapter.createSession(user.id)).token);
76
+ };
77
+ return new Hono().post("/signup", async (c) => {
78
+ const { email, password, data } = await readBody(c.req.raw, signUpBody);
79
+ if (!auth.signUp) throw signUpDisabled();
80
+ checkMetadata(data);
81
+ checkNewAccountPassword(await signUpPolicy(), password);
82
+ await sendSignUpCode(c.req.raw, email);
83
+ await savePendingSignUp(await context(), email, {
84
+ passwordHash: await (await context()).password.hash(password),
85
+ metadata: data
86
+ });
87
+ return c.json({});
88
+ }).post("/otp", async (c) => {
89
+ const { email, create_user, data } = await readBody(c.req.raw, otpBody);
90
+ if (!(create_user && auth.signUp && !await (await context()).internalAdapter.findUserByEmail(email))) {
91
+ await sendSignInCode(c.req.raw, email);
92
+ return c.json({});
93
+ }
94
+ checkMetadata(data);
95
+ checkNewAccountPassword(await signUpPolicy(), void 0);
96
+ await sendSignUpCode(c.req.raw, email);
97
+ await savePendingSignUp(await context(), email, { metadata: data });
98
+ return c.json({});
99
+ }).post("/verify", async (c) => {
100
+ const { email, token, token_hash, type } = await readBody(c.req.raw, verifyBody);
101
+ if (token_hash !== void 0 || email === void 0 || !token) throw new GoTrueError(400, "validation_failed", "Send the email address and the code mailed to it: links are not offered");
102
+ if (type === "signup") return c.json(await signUpWithCode(c.req.raw, email, token));
103
+ if (type === "recovery") return c.json(await signInWithCode(c.req.raw, email, token));
104
+ if (type === "email" || type === "magiclink") {
105
+ const known = await (await context()).internalAdapter.findUserByEmail(email);
106
+ return c.json(await (known ? signInWithCode(c.req.raw, email, token) : signUpWithCode(c.req.raw, email, token)));
107
+ }
108
+ throw new GoTrueError(400, "validation_failed", `Verify type ${type} is not offered: use email, signup or recovery`);
109
+ }).post("/recover", async (c) => {
110
+ const { email } = await readBody(c.req.raw, emailBody);
111
+ await sendSignInCode(c.req.raw, email);
112
+ return c.json({});
113
+ }).post("/resend", async (c) => {
114
+ const { email, type } = await readBody(c.req.raw, resendBody);
115
+ if (type !== "signup") throw new GoTrueError(400, "validation_failed", `Resend type ${type} is not offered: use signup`);
116
+ if (auth.signUp && await hasPendingSignUp(await context(), email)) await sendSignUpCode(c.req.raw, email);
117
+ return c.json({});
118
+ });
119
+ };
120
+ //#endregion
121
+ export { emailCodeRoutes };
@@ -0,0 +1,52 @@
1
+ import { APIError } from "better-auth/api";
2
+ //#region src/gotrue/errors.ts
3
+ /** A refusal as GoTrue words it: an HTTP status, one of its error codes, and a message for people. */
4
+ var GoTrueError = class extends Error {
5
+ status;
6
+ errorCode;
7
+ constructor(status, errorCode, message) {
8
+ super(message);
9
+ this.status = status;
10
+ this.errorCode = errorCode;
11
+ }
12
+ };
13
+ /** The JSON GoTrue answers a refusal with; supabase-js reads `error_code` and `msg` from it. */
14
+ const errorBody = (error) => ({
15
+ code: error.status,
16
+ error_code: error.errorCode,
17
+ msg: error.message
18
+ });
19
+ const invalidCode = [
20
+ 403,
21
+ "otp_expired",
22
+ "Token has expired or is invalid"
23
+ ];
24
+ const known = {
25
+ INVALID_EMAIL_OR_PASSWORD: [
26
+ 400,
27
+ "invalid_credentials",
28
+ "Invalid login credentials"
29
+ ],
30
+ INVALID_OTP: invalidCode,
31
+ OTP_EXPIRED: invalidCode,
32
+ TOO_MANY_ATTEMPTS: invalidCode,
33
+ SIGN_UP_CODE_INVALID: invalidCode,
34
+ SIGN_UP_TICKET_INVALID: invalidCode,
35
+ BANNED_USER: [403, "user_banned"],
36
+ NO_ADMIN_YET: [422, "signup_disabled"],
37
+ PASSWORD_LENGTH: [422, "weak_password"],
38
+ PASSWORD_REQUIRED: [422, "weak_password"]
39
+ };
40
+ /** A refusal of Better Auth, by its status and body, as GoTrue's: a known code becomes GoTrue's, any other its own in lower case. */
41
+ const fromBetterAuth = (status, body) => {
42
+ const code = typeof body?.code === "string" ? body.code : void 0;
43
+ const message = typeof body?.message === "string" ? body.message : "The request was refused";
44
+ if (status === 429) return new GoTrueError(429, "over_request_rate_limit", message);
45
+ const mapped = code === void 0 ? void 0 : known[code];
46
+ if (mapped) return new GoTrueError(mapped[0], mapped[1], mapped[2] ?? message);
47
+ return new GoTrueError(status, code?.toLowerCase() ?? (status >= 500 ? "unexpected_failure" : "bad_request"), message);
48
+ };
49
+ /** A Better Auth `APIError` as GoTrue's refusal; any other error as it is. */
50
+ const asGoTrueError = (error) => error instanceof APIError ? fromBetterAuth(error.statusCode, error.body) : error;
51
+ //#endregion
52
+ export { GoTrueError, asGoTrueError, errorBody, fromBetterAuth };
@@ -0,0 +1,45 @@
1
+ import { authBasePath } from "../better-auth/auth-base-path.js";
2
+ import { GoTrueError, fromBetterAuth } from "./errors.js";
3
+ //#region src/gotrue/forward.ts
4
+ const withheld = [
5
+ "cookie",
6
+ "authorization",
7
+ "origin",
8
+ "referer",
9
+ "content-length",
10
+ "content-type"
11
+ ];
12
+ const parsed = (text) => text ? JSON.parse(text) : void 0;
13
+ /**
14
+ * Posts `body` to Better Auth's endpoint at `path`, such as `/sign-in/email`, for the client's `request`: with its
15
+ * other headers, so the rate limits and the session made count the client's address and agent. Answers the endpoint's
16
+ * JSON and headers; a refusal is thrown as GoTrue's.
17
+ */
18
+ const callBetterAuth = async (auth, request, path, body) => {
19
+ const headers = new Headers(request.headers);
20
+ for (const name of withheld) headers.delete(name);
21
+ const origin = new URL(request.url).origin;
22
+ headers.set("content-type", "application/json");
23
+ headers.set("origin", origin);
24
+ const response = await auth.handler(new Request(`${origin}${authBasePath}${path}`, {
25
+ method: "POST",
26
+ headers,
27
+ body: JSON.stringify(body)
28
+ }));
29
+ const json = parsed(await response.text());
30
+ if (!response.ok) throw fromBetterAuth(response.status, json);
31
+ return {
32
+ json: json ?? {},
33
+ headers: response.headers
34
+ };
35
+ };
36
+ /**
37
+ * The session token a Better Auth sign-in answers. Someone with two-factor authentication gets none yet, only the
38
+ * second step, which is not offered here: GoTrue's `insufficient_aal`.
39
+ */
40
+ const signedInToken = (json) => {
41
+ if (typeof json.token !== "string") throw new GoTrueError(403, "insufficient_aal", "This account signs in with a second factor, which is not offered here");
42
+ return json.token;
43
+ };
44
+ //#endregion
45
+ export { callBetterAuth, signedInToken };
@@ -0,0 +1,14 @@
1
+ //#region src/gotrue/keys.ts
2
+ /** Where the GoTrue-compatible endpoints are served, below the app's origin. */
3
+ const goTruePath = "/auth/v1";
4
+ /** The key pair the JWT plugin keeps beside its EdDSA one for these tokens; it makes it on the first token. */
5
+ const goTrueKeyPair = { alg: "ES256" };
6
+ const goTrueAlgorithm = goTrueKeyPair.alg;
7
+ /** The audience of every such token: the database role a signed-in user's requests run as. */
8
+ const goTrueAudience = "authenticated";
9
+ /** The issuer of the tokens of the app at `baseURL`. */
10
+ const goTrueIssuer = (baseURL) => `${baseURL.replace(/\/+$/, "")}${goTruePath}`;
11
+ /** The keys of the JWT plugin's key set that sign these tokens. */
12
+ const goTrueKeys = (set) => ({ keys: set.keys.filter((key) => key.alg === goTrueAlgorithm) });
13
+ //#endregion
14
+ export { goTrueAlgorithm, goTrueAudience, goTrueIssuer, goTrueKeyPair, goTrueKeys, goTruePath };
@@ -0,0 +1,26 @@
1
+ //#region src/gotrue/pending-sign-up.ts
2
+ const pendingSeconds = 3600;
3
+ const identifierOf = (email) => `gotrue-sign-up-${email}`;
4
+ /** Keeps what `email` signs up with, replacing what an earlier sign-up with it gave. */
5
+ const savePendingSignUp = async (context, email, pending) => {
6
+ const identifier = identifierOf(email);
7
+ await context.internalAdapter.deleteVerificationByIdentifier(identifier);
8
+ await context.internalAdapter.createVerificationValue({
9
+ identifier,
10
+ value: JSON.stringify(pending),
11
+ expiresAt: new Date(Date.now() + pendingSeconds * 1e3)
12
+ });
13
+ };
14
+ /** Whether a sign-up with `email` waits for its code. */
15
+ const hasPendingSignUp = async (context, email) => {
16
+ const found = await context.internalAdapter.findVerificationValue(identifierOf(email));
17
+ return found !== null && new Date(found.expiresAt) > /* @__PURE__ */ new Date();
18
+ };
19
+ /** Takes what `email` signed up with, so it is used once; `undefined` when nothing waits, or it waited too long. */
20
+ const takePendingSignUp = async (context, email) => {
21
+ const consumed = await context.internalAdapter.consumeVerificationValue(identifierOf(email));
22
+ if (!consumed || new Date(consumed.expiresAt) < /* @__PURE__ */ new Date()) return void 0;
23
+ return JSON.parse(consumed.value);
24
+ };
25
+ //#endregion
26
+ export { hasPendingSignUp, savePendingSignUp, takePendingSignUp };
@@ -0,0 +1,79 @@
1
+ import { goTruePath } from "./keys.js";
2
+ import { GoTrueError } from "./errors.js";
3
+ import { callBetterAuth } from "./forward.js";
4
+ import { issueAuthCode } from "./auth-codes.js";
5
+ import { Hono } from "hono";
6
+ //#region src/gotrue/provider-routes.ts
7
+ const challengeOf = (query) => {
8
+ const challenge = query.code_challenge;
9
+ if (!challenge) return void 0;
10
+ const method = (query.code_challenge_method ?? "s256").toLowerCase();
11
+ if (method !== "s256" && method !== "plain") throw new GoTrueError(400, "validation_failed", "code_challenge_method must be s256 or plain");
12
+ return {
13
+ challenge,
14
+ method
15
+ };
16
+ };
17
+ const withParams = (url, params, where) => {
18
+ const target = new URL(url);
19
+ const values = new URLSearchParams(where === "hash" ? target.hash.slice(1) : target.search);
20
+ for (const [name, value] of Object.entries(params)) values.set(name, value);
21
+ target[where] = values.toString();
22
+ return target.toString();
23
+ };
24
+ const providerRoutes = ({ auth, context, sessionOf }) => {
25
+ const destination = async (requested, request) => {
26
+ const own = new URL(request.url).origin;
27
+ if (requested && URL.canParse(requested) && (await context()).isTrustedOrigin(requested)) return requested;
28
+ return own;
29
+ };
30
+ return new Hono().get("/authorize", async (c) => {
31
+ const query = c.req.query();
32
+ const provider = query.provider ?? "";
33
+ if (!auth.socialProviders.includes(provider)) throw new GoTrueError(400, "validation_failed", `Unsupported provider: ${provider || "none"} is not one of this app's`);
34
+ const challenge = challengeOf(query);
35
+ const callback = new URL(`${goTruePath}/callback`, c.req.url);
36
+ callback.searchParams.set("redirect_to", await destination(query.redirect_to, c.req.raw));
37
+ if (challenge) {
38
+ callback.searchParams.set("code_challenge", challenge.challenge);
39
+ callback.searchParams.set("code_challenge_method", challenge.method);
40
+ }
41
+ const { json, headers } = await callBetterAuth(auth, c.req.raw, "/sign-in/social", {
42
+ provider,
43
+ callbackURL: callback.toString(),
44
+ errorCallbackURL: callback.toString(),
45
+ disableRedirect: true,
46
+ ...query.scopes && { scopes: query.scopes.split(/[\s,]+/).filter(Boolean) }
47
+ });
48
+ const response = c.redirect(String(json.url), 302);
49
+ for (const cookie of headers.getSetCookie()) response.headers.append("set-cookie", cookie);
50
+ return response;
51
+ }).get("/callback", async (c) => {
52
+ const query = c.req.query();
53
+ const target = await destination(query.redirect_to, c.req.raw);
54
+ const challenge = challengeOf(query);
55
+ const where = challenge ? "search" : "hash";
56
+ const signedIn = !query.error && await auth.api.getSession({ headers: c.req.raw.headers });
57
+ if (!signedIn) {
58
+ const error = query.error ?? "access_denied";
59
+ return c.redirect(withParams(target, {
60
+ error,
61
+ error_code: error,
62
+ error_description: query.error_description ?? "The sign-in with the provider did not complete"
63
+ }, where), 302);
64
+ }
65
+ const ctx = await context();
66
+ if (challenge) return c.redirect(withParams(target, { code: await issueAuthCode(ctx, signedIn.user.id, challenge) }, "search"), 302);
67
+ const session = await sessionOf((await ctx.internalAdapter.createSession(signedIn.user.id)).token);
68
+ const fragment = {
69
+ access_token: session.access_token,
70
+ token_type: "bearer",
71
+ expires_in: String(session.expires_in),
72
+ expires_at: String(session.expires_at),
73
+ refresh_token: session.refresh_token
74
+ };
75
+ return c.redirect(withParams(target, fragment, "hash"), 302);
76
+ });
77
+ };
78
+ //#endregion
79
+ export { providerRoutes };
@@ -0,0 +1,22 @@
1
+ import { GoTrueError } from "./errors.js";
2
+ import * as z$1 from "zod";
3
+ //#region src/gotrue/request-body.ts
4
+ /** An address as GoTrue takes it: trimmed and in lower case. */
5
+ const address = z$1.string().trim().toLowerCase().pipe(z$1.email());
6
+ const parsedJson = (text) => {
7
+ try {
8
+ return text ? JSON.parse(text) : {};
9
+ } catch (error) {
10
+ if (error instanceof SyntaxError) throw new GoTrueError(400, "bad_json", "Could not parse request body as JSON");
11
+ throw error;
12
+ }
13
+ };
14
+ /** The request's JSON body as `schema` reads it; anything else is GoTrue's `validation_failed`, naming the field. */
15
+ const readBody = async (request, schema) => {
16
+ const result = schema.safeParse(parsedJson(await request.text()));
17
+ if (result.success) return result.data;
18
+ const [issue] = result.error.issues;
19
+ throw new GoTrueError(400, "validation_failed", issue ? `${issue.path.join(".") || "body"}: ${issue.message}` : "The request body is not valid");
20
+ };
21
+ //#endregion
22
+ export { address, readBody };
@@ -0,0 +1,32 @@
1
+ import { goTrueKeys } from "./keys.js";
2
+ import { goTrueDeps } from "./deps.js";
3
+ import { GoTrueError, asGoTrueError, errorBody } from "./errors.js";
4
+ import { emailCodeRoutes } from "./email-code-routes.js";
5
+ import { providerRoutes } from "./provider-routes.js";
6
+ import { tokenRoute } from "./token-route.js";
7
+ import { userRoutes } from "./user-routes.js";
8
+ import { Hono } from "hono";
9
+ //#region src/gotrue/routes.ts
10
+ /**
11
+ * GoTrue's endpoints over Better Auth, for `supabase.auth` and `@supabase/ssr`, to mount at `goTruePath`: sign-in by
12
+ * password, emailed code or a sign-in provider, sign-up with an emailed code, refresh, the user and sign-out, and the
13
+ * keys of the access tokens at `/.well-known/jwks.json`. Every step is Better Auth's own, with its rate limits and the
14
+ * app's sign-in policy; refusals are GoTrue's JSON.
15
+ */
16
+ const goTrueRoutes = (auth) => {
17
+ const deps = goTrueDeps(auth);
18
+ const app = new Hono();
19
+ app.onError((error, c) => {
20
+ const refusal = asGoTrueError(error);
21
+ if (!(refusal instanceof GoTrueError)) throw error;
22
+ return c.json(errorBody(refusal), refusal.status);
23
+ });
24
+ app.get("/.well-known/jwks.json", async (c) => c.json(goTrueKeys(await auth.api.getJwks())));
25
+ app.route("/", tokenRoute(deps));
26
+ app.route("/", emailCodeRoutes(deps));
27
+ app.route("/", userRoutes(deps));
28
+ app.route("/", providerRoutes(deps));
29
+ return app;
30
+ };
31
+ //#endregion
32
+ export { goTrueRoutes };