@protobase/server 0.33.4 → 0.34.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 (77) hide show
  1. package/dist/assistant/query-tools.js +1 -1
  2. package/dist/auth/jwt.d.ts +15 -1
  3. package/dist/auth/jwt.js +41 -5
  4. package/dist/better-auth/authenticator.d.ts +16 -1
  5. package/dist/better-auth/authenticator.js +41 -24
  6. package/dist/better-auth/create-auth.d.ts +3063 -364
  7. package/dist/better-auth/create-auth.js +36 -19
  8. package/dist/better-auth/mcp/connected-apps.d.ts +13 -0
  9. package/dist/better-auth/mcp/connected-apps.js +86 -0
  10. package/dist/better-auth/mcp/consent-check.js +38 -0
  11. package/dist/better-auth/mcp/consent-reference.js +16 -0
  12. package/dist/better-auth/mcp/discovery-routes.js +19 -0
  13. package/dist/better-auth/mcp/grant-reference.js +25 -0
  14. package/dist/better-auth/mcp/mcp-plugins.d.ts +13 -0
  15. package/dist/better-auth/mcp/mcp-plugins.js +107 -0
  16. package/dist/better-auth/mcp/mcp-token-claims.js +39 -0
  17. package/dist/better-auth/mcp/request-defaults.js +57 -0
  18. package/dist/better-auth/mcp/revoke-grants.js +75 -0
  19. package/dist/better-auth/mcp/setup-gate.js +38 -0
  20. package/dist/better-auth/mcp/signed-query-guard.js +29 -0
  21. package/dist/better-auth/origins.d.ts +7 -0
  22. package/dist/better-auth/origins.js +8 -1
  23. package/dist/better-auth/password-reset.js +4 -3
  24. package/dist/better-auth/policy-plugin.js +1 -24
  25. package/dist/better-auth/sign-in-enforcement.js +6 -1
  26. package/dist/better-auth/sign-in-setup.js +38 -0
  27. package/dist/better-auth/snake-case-names.js +102 -1
  28. package/dist/better-auth/staff-checks.js +9 -2
  29. package/dist/better-auth/staff-plugin.js +9 -2
  30. package/dist/better-auth/status-route.js +2 -1
  31. package/dist/better-auth/token-claims.js +19 -0
  32. package/dist/better-auth/users.d.ts +4 -1
  33. package/dist/better-auth/users.js +6 -1
  34. package/dist/check-roles.js +1 -1
  35. package/dist/create-admin.js +8 -0
  36. package/dist/deps.d.ts +1 -0
  37. package/dist/encryption/apply.d.ts +40 -0
  38. package/dist/encryption/apply.js +99 -0
  39. package/dist/encryption/binding.d.ts +2 -0
  40. package/dist/encryption/binding.js +53 -0
  41. package/dist/encryption/capture-off.js +12 -0
  42. package/dist/encryption/cascading-keys.d.ts +9 -0
  43. package/dist/encryption/cascading-keys.js +21 -0
  44. package/dist/encryption/envelope-open.d.ts +5 -0
  45. package/dist/encryption/envelope-open.js +57 -0
  46. package/dist/encryption/envelope-seal.js +38 -0
  47. package/dist/encryption/keyring.js +60 -0
  48. package/dist/encryption/mask-stored.js +16 -0
  49. package/dist/encryption/rebind-check.js +28 -0
  50. package/dist/encryption/remnants.js +53 -0
  51. package/dist/encryption/runtime.d.ts +1 -0
  52. package/dist/encryption/runtime.js +18 -0
  53. package/dist/encryption/scan.d.ts +4 -0
  54. package/dist/encryption/scan.js +61 -0
  55. package/dist/encryption/seal-values.js +30 -0
  56. package/dist/encryption/shared-columns.js +19 -0
  57. package/dist/encryption/status.d.ts +39 -0
  58. package/dist/encryption/status.js +92 -0
  59. package/dist/encryption/stored-value.js +16 -0
  60. package/dist/encryption/tools.d.ts +26 -0
  61. package/dist/encryption/tools.js +34 -0
  62. package/dist/forms/check-forms.js +1 -1
  63. package/dist/gotrue/user-routes.js +6 -0
  64. package/dist/index.d.ts +9 -5
  65. package/dist/index.js +3 -2
  66. package/dist/records.js +3 -3
  67. package/dist/request-access.js +9 -5
  68. package/dist/rest/encrypted-columns.js +37 -0
  69. package/dist/rest/table-routes.js +25 -13
  70. package/dist/reveal-service.js +17 -3
  71. package/dist/types.d.ts +5 -0
  72. package/dist/validate-body.js +4 -2
  73. package/dist/webhooks/subscriber-view.js +14 -1
  74. package/dist/write-pipeline.js +14 -2
  75. package/dist/write-shared.js +7 -3
  76. package/dist/write-update.js +12 -2
  77. package/package.json +11 -7
@@ -48,7 +48,7 @@ const readOnlyQueryTool = (db, options = {}) => defineTool({
48
48
  */
49
49
  const readWriteQueryTool = (db, options = {}) => defineTool({
50
50
  name: "propose_write_query",
51
- description: "Proposes one PostgreSQL INSERT, UPDATE or DELETE ending in a RETURNING clause. The user sees the statement and your summary and must approve it; only then it runs in a read-write transaction, and you get the returned rows as JSON, or the answer that it did not run. Use it only when the user asked for a change.",
51
+ description: "Proposes one PostgreSQL INSERT, UPDATE or DELETE ending in a RETURNING clause. The user sees the statement and your summary and must approve it; only then it runs in a read-write transaction, and you get the returned rows as JSON, or the answer that it did not run. Use it only when the user asked for a change. Never write a value that starts with pbenc: or a column that holds such values: those are encrypted fields, which only the app's own forms and API can write.",
52
52
  parameters: {
53
53
  type: "object",
54
54
  properties: {
@@ -1,4 +1,5 @@
1
1
  import { Authenticator, TenantValue } from "../types.js";
2
+ import { DpopReplayStore } from "better-auth/oauth2";
2
3
  import { JWTPayload, JWTVerifyGetKey } from "jose";
3
4
  //#region src/auth/jwt.d.ts
4
5
  export type JwtOptions = {
@@ -19,8 +20,21 @@ export type JwtOptions = {
19
20
  algorithms?: string[];
20
21
  /** A fixed tenant (or resolver) used instead of any claim, for single-tenant deployments. */
21
22
  tenant?: TenantValue | (() => Promise<TenantValue | undefined>);
23
+ /** The token type its header must name, such as `at+jwt` for an OAuth access token (RFC 9068). Default any. */
24
+ typ?: string;
25
+ /**
26
+ * Takes OAuth access tokens bound to a key (DPoP, RFC 9449) as well as bearer ones: the `DPoP` scheme with a proof in
27
+ * the `DPoP` header, made for `url` with the request's method. A bound token works only with a fresh proof from its
28
+ * key, each proof once (`replayStore`); a bearer token is refused with the `DPoP` scheme. Default bearer tokens only.
29
+ */
30
+ dpop?: {
31
+ url: string;
32
+ replayStore: () => Promise<DpopReplayStore>;
33
+ };
34
+ /** Keeps the OAuth client the token was issued to, its `client_id` and `scope`, in the session's `client`. */
35
+ oauthClient?: boolean;
22
36
  };
23
- export type JwtVerifyOptions = Pick<JwtOptions, 'jwksUrl' | 'keys' | 'issuer' | 'audience' | 'algorithms'>;
37
+ export type JwtVerifyOptions = Pick<JwtOptions, 'jwksUrl' | 'keys' | 'issuer' | 'audience' | 'algorithms' | 'typ'>;
24
38
  /** Verifies a JWT against a JWKS and returns its claims; a token that does not verify is a 401 problem. */
25
39
  export declare const jwtVerifier: (options: JwtVerifyOptions) => (token: string) => Promise<JWTPayload>;
26
40
  /** Verifies `Authorization: Bearer <jwt>` against a JWKS; `sub` is the user id. Without the header, `MissingCredentials`. */
package/dist/auth/jwt.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { MissingCredentials, unauthorized } from "../problem.js";
2
+ import { enforceDpopBinding, isDpopBindingError, parseAccessTokenAuthorization } from "better-auth/oauth2";
2
3
  import { createRemoteJWKSet, errors, jwtVerify } from "jose";
3
4
  //#region src/auth/jwt.ts
4
5
  const asymmetric = [
@@ -17,6 +18,20 @@ const rolesOf = (claim) => {
17
18
  if (typeof claim === "string") return claim.split(/[\s,]+/).filter(Boolean);
18
19
  return Array.isArray(claim) ? claim.filter((role) => typeof role === "string") : [];
19
20
  };
21
+ const authorizationOf = (header, dpop) => {
22
+ if (dpop) {
23
+ const authorization = parseAccessTokenAuthorization(header);
24
+ if (!authorization || authorization.scheme === "Unknown" || !authorization.token) throw unauthorized("Send an access token in the Authorization header");
25
+ return authorization;
26
+ }
27
+ const [scheme, token] = header.split(" ");
28
+ if (scheme?.toLowerCase() !== "bearer" || !token) throw unauthorized("Send a bearer token in the Authorization header");
29
+ return {
30
+ scheme: "Bearer",
31
+ token
32
+ };
33
+ };
34
+ const scopesOf = (claim) => typeof claim === "string" ? claim.split(" ").filter(Boolean) : [];
20
35
  /** Verifies a JWT against a JWKS and returns its claims; a token that does not verify is a 401 problem. */
21
36
  const jwtVerifier = (options) => {
22
37
  const keys = options.keys ?? (options.jwksUrl ? createRemoteJWKSet(new URL(options.jwksUrl)) : void 0);
@@ -25,7 +40,8 @@ const jwtVerifier = (options) => {
25
40
  return (await jwtVerify(token, keys, {
26
41
  algorithms: options.algorithms ?? asymmetric,
27
42
  ...options.issuer && { issuer: options.issuer },
28
- ...options.audience && { audience: options.audience }
43
+ ...options.audience && { audience: options.audience },
44
+ ...options.typ && { typ: options.typ }
29
45
  }).catch((error) => {
30
46
  if (error instanceof errors.JOSEError) throw unauthorized("The bearer token is invalid or expired");
31
47
  throw error;
@@ -39,14 +55,33 @@ const jwtSessions = (options) => {
39
55
  return async (request) => {
40
56
  const header = request.headers.get("authorization");
41
57
  if (header === null) throw new MissingCredentials("Send a bearer token in the Authorization header");
42
- const [scheme, token] = header.split(" ");
43
- if (scheme?.toLowerCase() !== "bearer" || !token) throw unauthorized("Send a bearer token in the Authorization header");
44
- const payload = await verify(token);
58
+ const authorization = authorizationOf(header, Boolean(options.dpop));
59
+ const payload = await verify(authorization.token);
60
+ if (options.dpop) {
61
+ const { url, replayStore } = options.dpop;
62
+ const proofJwt = request.headers.get("dpop") ?? void 0;
63
+ await enforceDpopBinding({
64
+ payload,
65
+ authorization,
66
+ proofJwt,
67
+ method: request.method,
68
+ url,
69
+ replayStore: await replayStore()
70
+ }).catch((error) => {
71
+ if (isDpopBindingError(error)) throw unauthorized(error.message);
72
+ throw error;
73
+ });
74
+ }
45
75
  const { sub, [tenantClaim]: tenant, [rolesClaim]: roles } = payload;
46
76
  const extraRoles = options.extraRolesClaim === void 0 ? [] : rolesOf(payload[options.extraRolesClaim]);
47
77
  if (!sub) throw unauthorized("The bearer token has no subject");
48
78
  const claimed = typeof tenant === "string" || typeof tenant === "number" ? tenant : void 0;
49
79
  const tenantValue = options.tenant === void 0 ? claimed : typeof options.tenant === "function" ? await options.tenant() : options.tenant;
80
+ const { client_id: clientId, scope } = payload;
81
+ const client = options.oauthClient && typeof clientId === "string" ? { client: {
82
+ id: clientId,
83
+ scopes: scopesOf(scope)
84
+ } } : {};
50
85
  return {
51
86
  payload,
52
87
  session: {
@@ -54,7 +89,8 @@ const jwtSessions = (options) => {
54
89
  id: sub,
55
90
  roles: [.../* @__PURE__ */ new Set([...rolesOf(roles), ...extraRoles])]
56
91
  },
57
- ...tenantValue !== void 0 && { tenant: tenantValue }
92
+ ...tenantValue !== void 0 && { tenant: tenantValue },
93
+ ...client
58
94
  }
59
95
  };
60
96
  };
@@ -19,6 +19,21 @@ export type BetterAuthAuthenticatorOptions = {
19
19
  cache?: JwksCacheOptions;
20
20
  /** Other Protobase apps whose users this API admits with their own tokens, so their admin apps can draw its resources (see `TrustedApp`). */
21
21
  apps?: TrustedApp[];
22
+ /** The audience the tokens must name; default `issuer`, the app's own tokens. */
23
+ audience?: string;
24
+ };
25
+ /**
26
+ * Verifies the app's tokens, and with `forAudience` the OAuth access tokens it issues for another audience, such as
27
+ * its MCP endpoint (`auth.mcp.resource`).
28
+ */
29
+ export type BetterAuthAuthenticator = Authenticator & {
30
+ /**
31
+ * An authenticator for the OAuth access tokens issued for `audience` alone: the app's keys, a `typ` of `at+jwt`, and
32
+ * proof of the key for a DPoP-bound token, checked against `audience` as the URL it guards (the endpoint must answer
33
+ * at exactly that URL). The app's own tokens and trusted apps' are refused; the session keeps the token's `client`.
34
+ * With `jwksUrl` instead of `auth`, a DPoP proof's single use holds in this process only.
35
+ */
36
+ forAudience: (audience: string) => Authenticator;
22
37
  };
23
38
  /** How the tokens of this Better Auth instance verify: its key set, and its issuer as both issuer and audience. */
24
39
  export declare const betterAuthTokens: (options: Pick<BetterAuthAuthenticatorOptions, "auth" | "jwksUrl" | "issuer" | "cache">) => JwtVerifyOptions & {
@@ -33,5 +48,5 @@ export declare const betterAuthTokens: (options: Pick<BetterAuthAuthenticatorOpt
33
48
  * a token works until it expires. With `apps`, the tokens of those apps are admitted too, with the roles each gives
34
49
  * its users here.
35
50
  */
36
- export declare const betterAuthAuthenticator: (options: BetterAuthAuthenticatorOptions) => Authenticator;
51
+ export declare const betterAuthAuthenticator: (options: BetterAuthAuthenticatorOptions) => BetterAuthAuthenticator;
37
52
  //#endregion
@@ -3,6 +3,7 @@ import { noGlobalRole } from "./organizations/role-definitions.js";
3
3
  import { cachedKeys } from "./jwks-cache.js";
4
4
  import { withTrustedApps } from "./app-tokens.js";
5
5
  import { checkTokenStanding, tokenClaims } from "./token-standing.js";
6
+ import { createDpopReplayStore, createInMemoryDpopReplayStore } from "better-auth/oauth2";
6
7
  //#region src/better-auth/authenticator.ts
7
8
  /** How the tokens of this Better Auth instance verify: its key set, and its issuer as both issuer and audience. */
8
9
  const betterAuthTokens = (options) => {
@@ -28,33 +29,49 @@ const betterAuthAuthenticator = (options) => {
28
29
  const tokens = betterAuthTokens(options);
29
30
  const { issuer } = tokens;
30
31
  const organizations = options.organizations ?? Boolean(options.auth?.organizations);
31
- const sessions = jwtSessions({
32
- ...tokens,
33
- rolesClaim: "role",
34
- ...organizations && {
35
- tenantClaim: "org",
36
- extraRolesClaim: "app_roles"
37
- },
38
- ...options.tenant !== void 0 && { tenant: options.tenant }
39
- });
40
32
  const { auth } = options;
41
- const verify = async (request) => {
42
- const { payload, session } = await sessions(request);
43
- if (auth) await checkTokenStanding(await auth.$context, tokenClaims(payload, session.user.roles, organizations ? session.tenant : void 0), organizations);
44
- return session;
45
- };
46
- return withTrustedApps(!organizations ? verify : async (request) => {
47
- const session = await verify(request);
48
- const user = {
49
- ...session.user,
50
- roles: session.user.roles.filter((role) => role !== noGlobalRole)
33
+ const sessionOf = (verifyOptions, standing) => {
34
+ const sessions = jwtSessions({
35
+ ...tokens,
36
+ rolesClaim: "role",
37
+ ...organizations && {
38
+ tenantClaim: "org",
39
+ extraRolesClaim: "app_roles"
40
+ },
41
+ ...options.tenant !== void 0 && { tenant: options.tenant },
42
+ ...verifyOptions
43
+ });
44
+ const verify = async (request) => {
45
+ const { payload, session } = await sessions(request);
46
+ if (auth && standing) await checkTokenStanding(await auth.$context, tokenClaims(payload, session.user.roles, organizations ? session.tenant : void 0), organizations);
47
+ return session;
51
48
  };
52
- return {
53
- ...session,
54
- user,
55
- ...session.tenant !== void 0 && { organization: { id: session.tenant } }
49
+ if (!organizations) return verify;
50
+ return async (request) => {
51
+ const session = await verify(request);
52
+ const user = {
53
+ ...session.user,
54
+ roles: session.user.roles.filter((role) => role !== noGlobalRole)
55
+ };
56
+ return {
57
+ ...session,
58
+ user,
59
+ ...session.tenant !== void 0 && { organization: { id: session.tenant } }
60
+ };
56
61
  };
57
- }, issuer, options.apps ?? []);
62
+ };
63
+ let replayStore;
64
+ const dpopReplayStore = () => replayStore ??= options.auth ? options.auth.$context.then(({ internalAdapter }) => createDpopReplayStore(internalAdapter)) : Promise.resolve(createInMemoryDpopReplayStore());
65
+ const own = sessionOf({ audience: options.audience ?? issuer }, true);
66
+ return Object.assign(withTrustedApps(own, issuer, options.apps ?? []), { forAudience: (audience) => sessionOf({
67
+ audience,
68
+ typ: "at+jwt",
69
+ dpop: {
70
+ url: audience,
71
+ replayStore: dpopReplayStore
72
+ },
73
+ oauthClient: true
74
+ }, false) });
58
75
  };
59
76
  //#endregion
60
77
  export { betterAuthAuthenticator, betterAuthTokens };