@protobase/server 0.33.3 → 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.
- package/dist/assistant/query-tools.js +1 -1
- package/dist/auth/jwt.d.ts +15 -1
- package/dist/auth/jwt.js +41 -5
- package/dist/better-auth/authenticator.d.ts +16 -1
- package/dist/better-auth/authenticator.js +41 -24
- package/dist/better-auth/create-auth.d.ts +3063 -364
- package/dist/better-auth/create-auth.js +36 -19
- package/dist/better-auth/mcp/connected-apps.d.ts +13 -0
- package/dist/better-auth/mcp/connected-apps.js +86 -0
- package/dist/better-auth/mcp/consent-check.js +38 -0
- package/dist/better-auth/mcp/consent-reference.js +16 -0
- package/dist/better-auth/mcp/discovery-routes.js +19 -0
- package/dist/better-auth/mcp/grant-reference.js +25 -0
- package/dist/better-auth/mcp/mcp-plugins.d.ts +13 -0
- package/dist/better-auth/mcp/mcp-plugins.js +107 -0
- package/dist/better-auth/mcp/mcp-token-claims.js +39 -0
- package/dist/better-auth/mcp/request-defaults.js +57 -0
- package/dist/better-auth/mcp/revoke-grants.js +75 -0
- package/dist/better-auth/mcp/setup-gate.js +38 -0
- package/dist/better-auth/mcp/signed-query-guard.js +29 -0
- package/dist/better-auth/origins.d.ts +7 -0
- package/dist/better-auth/origins.js +8 -1
- package/dist/better-auth/password-reset.js +4 -3
- package/dist/better-auth/policy-plugin.js +1 -24
- package/dist/better-auth/session-token.js +26 -0
- package/dist/better-auth/sign-in-enforcement.js +6 -1
- package/dist/better-auth/sign-in-setup.js +38 -0
- package/dist/better-auth/snake-case-names.js +102 -1
- package/dist/better-auth/staff-checks.js +9 -2
- package/dist/better-auth/staff-plugin.js +9 -2
- package/dist/better-auth/status-route.js +2 -1
- package/dist/better-auth/token-claims.js +19 -0
- package/dist/better-auth/users.d.ts +4 -1
- package/dist/better-auth/users.js +6 -1
- package/dist/check-roles.js +1 -1
- package/dist/create-admin.js +22 -2
- package/dist/deps.d.ts +1 -0
- package/dist/encryption/apply.d.ts +40 -0
- package/dist/encryption/apply.js +99 -0
- package/dist/encryption/binding.d.ts +2 -0
- package/dist/encryption/binding.js +53 -0
- package/dist/encryption/capture-off.js +12 -0
- package/dist/encryption/cascading-keys.d.ts +9 -0
- package/dist/encryption/cascading-keys.js +21 -0
- package/dist/encryption/envelope-open.d.ts +5 -0
- package/dist/encryption/envelope-open.js +57 -0
- package/dist/encryption/envelope-seal.js +38 -0
- package/dist/encryption/keyring.js +60 -0
- package/dist/encryption/mask-stored.js +16 -0
- package/dist/encryption/rebind-check.js +28 -0
- package/dist/encryption/remnants.js +53 -0
- package/dist/encryption/runtime.d.ts +1 -0
- package/dist/encryption/runtime.js +18 -0
- package/dist/encryption/scan.d.ts +4 -0
- package/dist/encryption/scan.js +61 -0
- package/dist/encryption/seal-values.js +30 -0
- package/dist/encryption/shared-columns.js +19 -0
- package/dist/encryption/status.d.ts +39 -0
- package/dist/encryption/status.js +92 -0
- package/dist/encryption/stored-value.js +16 -0
- package/dist/encryption/tools.d.ts +26 -0
- package/dist/encryption/tools.js +34 -0
- package/dist/forms/check-forms.js +1 -1
- package/dist/functions/accept-socket.js +32 -0
- package/dist/functions/define-function.d.ts +7 -0
- package/dist/functions/function-socket.d.ts +18 -0
- package/dist/functions/function-socket.js +81 -0
- package/dist/functions/routes.js +33 -8
- package/dist/functions/same-caller.js +9 -0
- package/dist/functions/socket-recheck.js +33 -0
- package/dist/functions/upgrade-caller.js +25 -0
- package/dist/gotrue/user-routes.js +6 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +3 -2
- package/dist/records.js +3 -3
- package/dist/request-access.js +9 -5
- package/dist/rest/encrypted-columns.js +37 -0
- package/dist/rest/table-routes.js +25 -13
- package/dist/reveal-service.js +17 -3
- package/dist/types.d.ts +5 -0
- package/dist/validate-body.js +4 -2
- package/dist/webhooks/attempt.js +5 -1
- package/dist/webhooks/subscriber-view.js +14 -1
- package/dist/write-pipeline.js +14 -2
- package/dist/write-shared.js +7 -3
- package/dist/write-update.js +12 -2
- package/package.json +11 -7
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { APIError, createAuthMiddleware } from "better-auth/api";
|
|
2
|
+
//#region src/better-auth/mcp/signed-query-guard.ts
|
|
3
|
+
const signedQueryPaths = [
|
|
4
|
+
"/oauth2/continue",
|
|
5
|
+
"/oauth2/consent",
|
|
6
|
+
"/oauth2/authorize"
|
|
7
|
+
];
|
|
8
|
+
/** Whether a request to `path` may carry a signed authorization query. */
|
|
9
|
+
const isSignedQueryAllowed = (path) => path !== void 0 && signedQueryPaths.includes(path);
|
|
10
|
+
const hasSignedQuery = (body) => typeof body === "object" && body !== null && "oauth_query" in body;
|
|
11
|
+
/**
|
|
12
|
+
* Refuses a signed authorization query on any other request (`400 OAUTH_QUERY_REFUSED`), so a sign-in by password,
|
|
13
|
+
* emailed code, passkey, two-factor or provider never resumes an authorization on its own: the person goes on from
|
|
14
|
+
* the connect page, which shows what they allow. Runs before the provider's own hook.
|
|
15
|
+
*/
|
|
16
|
+
const mcpSignedQueryGuard = () => ({
|
|
17
|
+
id: "protobase-mcp-signed-query-guard",
|
|
18
|
+
hooks: { before: [{
|
|
19
|
+
matcher: (context) => hasSignedQuery(context.body) && !isSignedQueryAllowed(context.path),
|
|
20
|
+
handler: createAuthMiddleware(async () => {
|
|
21
|
+
throw new APIError("BAD_REQUEST", {
|
|
22
|
+
code: "OAUTH_QUERY_REFUSED",
|
|
23
|
+
message: "Sign in first, then continue from the connect page."
|
|
24
|
+
});
|
|
25
|
+
})
|
|
26
|
+
}] }
|
|
27
|
+
});
|
|
28
|
+
//#endregion
|
|
29
|
+
export { isSignedQueryAllowed, mcpSignedQueryGuard };
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
//#region src/better-auth/origins.d.ts
|
|
2
2
|
/** Where browsers read which origins may use the app's passkeys (WebAuthn Related Origin Requests). */
|
|
3
3
|
export declare const webauthnPath = "/.well-known/webauthn";
|
|
4
|
+
/**
|
|
5
|
+
* Where MCP clients discover how to sign in: the OAuth authorization server's metadata, and the protected resource's
|
|
6
|
+
* (also below it, at the resource's path). Every bundle sends them to the API, which answers them with MCP on.
|
|
7
|
+
*/
|
|
8
|
+
export declare const oauthWellKnownPaths: string[];
|
|
9
|
+
/** Whether `path` is one of `oauthWellKnownPaths` or below one. */
|
|
10
|
+
export declare const isOAuthWellKnownPath: (path: string) => boolean;
|
|
4
11
|
//#endregion
|
|
@@ -44,5 +44,12 @@ const multiHostBaseURL = (baseURL, origins) => {
|
|
|
44
44
|
};
|
|
45
45
|
/** Where browsers read which origins may use the app's passkeys (WebAuthn Related Origin Requests). */
|
|
46
46
|
const webauthnPath = "/.well-known/webauthn";
|
|
47
|
+
/**
|
|
48
|
+
* Where MCP clients discover how to sign in: the OAuth authorization server's metadata, and the protected resource's
|
|
49
|
+
* (also below it, at the resource's path). Every bundle sends them to the API, which answers them with MCP on.
|
|
50
|
+
*/
|
|
51
|
+
const oauthWellKnownPaths = ["/.well-known/oauth-authorization-server", "/.well-known/oauth-protected-resource"];
|
|
52
|
+
/** Whether `path` is one of `oauthWellKnownPaths` or below one. */
|
|
53
|
+
const isOAuthWellKnownPath = (path) => oauthWellKnownPaths.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
|
47
54
|
//#endregion
|
|
48
|
-
export { appOrigins, multiHostBaseURL, originsVariable, readList, readOrigins, resolveOrigins, webauthnPath };
|
|
55
|
+
export { appOrigins, isOAuthWellKnownPath, multiHostBaseURL, oauthWellKnownPaths, originsVariable, readList, readOrigins, resolveOrigins, webauthnPath };
|
|
@@ -5,15 +5,16 @@ const resetLinkLifetimeSeconds = 3600;
|
|
|
5
5
|
const resetLinkLifetime = "1 hour";
|
|
6
6
|
/**
|
|
7
7
|
* Better Auth's own password reset, sending the link with `mail`. Resetting ends the user's sessions, so a reset
|
|
8
|
-
* after a leaked password also signs out whoever used it.
|
|
8
|
+
* after a leaked password also signs out whoever used it, and `onPasswordReset` disconnects the user's MCP clients.
|
|
9
9
|
*/
|
|
10
|
-
const passwordResetOptions = (mail) => ({
|
|
10
|
+
const passwordResetOptions = (mail, { onPasswordReset } = {}) => ({
|
|
11
11
|
resetPasswordTokenExpiresIn: resetLinkLifetimeSeconds,
|
|
12
12
|
revokeSessionsOnPasswordReset: true,
|
|
13
13
|
sendResetPassword: ({ user, url }) => sendAuthEmail(mail, "resetPasswordEmail", user.email, {
|
|
14
14
|
url,
|
|
15
15
|
lifetime: resetLinkLifetime
|
|
16
|
-
}, url)
|
|
16
|
+
}, url),
|
|
17
|
+
...onPasswordReset && { onPasswordReset }
|
|
17
18
|
});
|
|
18
19
|
//#endregion
|
|
19
20
|
export { passwordResetOptions, resetLinkLifetimeSeconds };
|
|
@@ -1,35 +1,12 @@
|
|
|
1
1
|
import { effectiveSignInPolicy, missingRequiredMethods, parseSignInPolicy, signInPolicyProblem } from "./sign-in-policy.js";
|
|
2
2
|
import { readSignInPolicy, saveSignInPolicy, signInPolicySchema } from "./policy-store.js";
|
|
3
3
|
import { isPoliced, lastPasskeyRequired, policyRefusal, setupRequired } from "./sign-in-enforcement.js";
|
|
4
|
+
import { accountMethods } from "./sign-in-setup.js";
|
|
4
5
|
import { adminOnly } from "./admin-only.js";
|
|
5
6
|
import { isStaffSession } from "./staff-checks.js";
|
|
6
7
|
import * as z$1 from "zod";
|
|
7
8
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
8
9
|
//#region src/better-auth/policy-plugin.ts
|
|
9
|
-
const accountMethods = async (context, user, socialProviders) => {
|
|
10
|
-
const accounts = await context.internalAdapter.findAccounts(user.id);
|
|
11
|
-
const passkeys = await context.adapter.count({
|
|
12
|
-
model: "passkey",
|
|
13
|
-
where: [{
|
|
14
|
-
field: "userId",
|
|
15
|
-
value: user.id
|
|
16
|
-
}]
|
|
17
|
-
});
|
|
18
|
-
const app = await context.adapter.findOne({
|
|
19
|
-
model: "twoFactor",
|
|
20
|
-
where: [{
|
|
21
|
-
field: "userId",
|
|
22
|
-
value: user.id
|
|
23
|
-
}]
|
|
24
|
-
});
|
|
25
|
-
return {
|
|
26
|
-
password: accounts.some((account) => account.providerId === "credential"),
|
|
27
|
-
passkeys,
|
|
28
|
-
twoFactor: Boolean(user.twoFactorEnabled),
|
|
29
|
-
authenticatorApp: Boolean(user.twoFactorEnabled) && app !== null && app.verified !== false,
|
|
30
|
-
socialSignIn: accounts.some((account) => socialProviders.includes(account.providerId))
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
10
|
const refuse = (status, refusal) => new APIError(status, refusal);
|
|
34
11
|
const currentPolicy = async (context, { mail, defaultPolicy }) => effectiveSignInPolicy((await readSignInPolicy(context.adapter, defaultPolicy)).policy, { mail });
|
|
35
12
|
const providersUnder = (policy, { socialProviders, platformSignIn }) => [...socialProviders, ...platformSignIn && policy.platformSignIn === "allowed" ? [platformSignIn.provider] : []];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { forbidden, unauthorized } from "../problem.js";
|
|
2
|
+
import { APIError } from "better-auth/api";
|
|
3
|
+
//#region src/better-auth/session-token.ts
|
|
4
|
+
const messageOf = (body) => {
|
|
5
|
+
const message = body?.message;
|
|
6
|
+
return typeof message === "string" ? message : "Signing in is refused";
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The bearer token of the session a request's cookie carries, minted as `GET /api/auth/token` would, with the same
|
|
10
|
+
* plugin hooks (the sign-in policy's hold-back included) but without its rate limit: a forged cookie fails its
|
|
11
|
+
* signature before any store read. No session is a 401 problem, a refusal a 403 with Better Auth's message; any other
|
|
12
|
+
* error stays as it is.
|
|
13
|
+
*/
|
|
14
|
+
const betterAuthSessionToken = (auth) => async (request) => {
|
|
15
|
+
try {
|
|
16
|
+
const { token } = await auth.api.getToken({ headers: request.headers });
|
|
17
|
+
return token;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (!(error instanceof APIError)) throw error;
|
|
20
|
+
if (error.statusCode === 401) throw unauthorized("Sign in first");
|
|
21
|
+
if (error.statusCode === 403) throw forbidden("sign-in-refused", messageOf(error.body));
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { betterAuthSessionToken };
|
|
@@ -65,9 +65,14 @@ const setupRequired = {
|
|
|
65
65
|
code: "SIGN_IN_SETUP_REQUIRED",
|
|
66
66
|
message: "Set up what this app requires for signing in first."
|
|
67
67
|
};
|
|
68
|
+
/** The answer to someone whose address is not verified who asks to allow an app (an MCP client) to act as them. */
|
|
69
|
+
const addressNotVerified = {
|
|
70
|
+
code: "ADDRESS_NOT_VERIFIED",
|
|
71
|
+
message: "Confirm your address first: sign out, then sign in with a code emailed to you."
|
|
72
|
+
};
|
|
68
73
|
const lastPasskeyRequired = {
|
|
69
74
|
code: "PASSKEY_REQUIRED",
|
|
70
75
|
message: "Passkeys are required for every account, so the last one cannot be removed."
|
|
71
76
|
};
|
|
72
77
|
//#endregion
|
|
73
|
-
export { isPoliced, lastPasskeyRequired, policyRefusal, setupRequired, staffSignInPath };
|
|
78
|
+
export { addressNotVerified, isPoliced, lastPasskeyRequired, policyRefusal, setupRequired, staffSignInPath };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { effectiveSignInPolicy, missingRequiredMethods } from "./sign-in-policy.js";
|
|
2
|
+
import { readSignInPolicy } from "./policy-store.js";
|
|
3
|
+
//#region src/better-auth/sign-in-setup.ts
|
|
4
|
+
/** What `user`'s account can sign in with and has set up; `socialProviders` are the provider ids that count as social sign-in. */
|
|
5
|
+
const accountMethods = async (context, user, socialProviders) => {
|
|
6
|
+
const accounts = await context.internalAdapter.findAccounts(user.id);
|
|
7
|
+
const passkeys = await context.adapter.count({
|
|
8
|
+
model: "passkey",
|
|
9
|
+
where: [{
|
|
10
|
+
field: "userId",
|
|
11
|
+
value: user.id
|
|
12
|
+
}]
|
|
13
|
+
});
|
|
14
|
+
const app = await context.adapter.findOne({
|
|
15
|
+
model: "twoFactor",
|
|
16
|
+
where: [{
|
|
17
|
+
field: "userId",
|
|
18
|
+
value: user.id
|
|
19
|
+
}]
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
password: accounts.some((account) => account.providerId === "credential"),
|
|
23
|
+
passkeys,
|
|
24
|
+
twoFactor: Boolean(user.twoFactorEnabled),
|
|
25
|
+
authenticatorApp: Boolean(user.twoFactorEnabled) && app !== null && app.verified !== false,
|
|
26
|
+
socialSignIn: accounts.some((account) => socialProviders.includes(account.providerId))
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The methods the sign-in policy requires that `user` has not set up yet; empty when nothing is missing. `defaultPolicy`
|
|
31
|
+
* applies until an admin saves one.
|
|
32
|
+
*/
|
|
33
|
+
const missingSetup = async (context, user, { mail, defaultPolicy }) => {
|
|
34
|
+
const policy = effectiveSignInPolicy((await readSignInPolicy(context.adapter, defaultPolicy)).policy, { mail });
|
|
35
|
+
return missingRequiredMethods(policy, await accountMethods(context, user, []));
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
38
|
+
export { accountMethods, missingSetup };
|
|
@@ -61,7 +61,108 @@ const pluginNames = {
|
|
|
61
61
|
deviceType: "device_type",
|
|
62
62
|
backedUp: "backed_up",
|
|
63
63
|
createdAt: "created_at"
|
|
64
|
-
} } }
|
|
64
|
+
} } },
|
|
65
|
+
mcp: {
|
|
66
|
+
oauthClient: {
|
|
67
|
+
modelName: "oauth_client",
|
|
68
|
+
fields: {
|
|
69
|
+
clientId: "client_id",
|
|
70
|
+
clientSecret: "client_secret",
|
|
71
|
+
clientDiscoveryId: "client_discovery_id",
|
|
72
|
+
skipConsent: "skip_consent",
|
|
73
|
+
enableEndSession: "enable_end_session",
|
|
74
|
+
subjectType: "subject_type",
|
|
75
|
+
clientCredentialsScopes: "client_credentials_scopes",
|
|
76
|
+
userId: "user_id",
|
|
77
|
+
createdAt: "created_at",
|
|
78
|
+
updatedAt: "updated_at",
|
|
79
|
+
softwareId: "software_id",
|
|
80
|
+
softwareVersion: "software_version",
|
|
81
|
+
softwareStatement: "software_statement",
|
|
82
|
+
redirectUris: "redirect_uris",
|
|
83
|
+
postLogoutRedirectUris: "post_logout_redirect_uris",
|
|
84
|
+
backchannelLogoutUri: "backchannel_logout_uri",
|
|
85
|
+
backchannelLogoutSessionRequired: "backchannel_logout_session_required",
|
|
86
|
+
tokenEndpointAuthMethod: "token_endpoint_auth_method",
|
|
87
|
+
applicationType: "application_type",
|
|
88
|
+
jwksUri: "jwks_uri",
|
|
89
|
+
grantTypes: "grant_types",
|
|
90
|
+
responseTypes: "response_types",
|
|
91
|
+
requirePKCE: "require_pkce",
|
|
92
|
+
dpopBoundAccessTokens: "dpop_bound_access_tokens",
|
|
93
|
+
referenceId: "reference_id"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
oauthResource: {
|
|
97
|
+
modelName: "oauth_resource",
|
|
98
|
+
fields: {
|
|
99
|
+
accessTokenTtl: "access_token_ttl",
|
|
100
|
+
refreshTokenTtl: "refresh_token_ttl",
|
|
101
|
+
signingAlgorithm: "signing_algorithm",
|
|
102
|
+
signingKeyId: "signing_key_id",
|
|
103
|
+
allowedScopes: "allowed_scopes",
|
|
104
|
+
customClaims: "custom_claims",
|
|
105
|
+
dpopBoundAccessTokensRequired: "dpop_bound_access_tokens_required",
|
|
106
|
+
createdAt: "created_at",
|
|
107
|
+
updatedAt: "updated_at",
|
|
108
|
+
policyVersion: "policy_version"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
oauthClientResource: {
|
|
112
|
+
modelName: "oauth_client_resource",
|
|
113
|
+
fields: {
|
|
114
|
+
clientId: "client_id",
|
|
115
|
+
resourceId: "resource_id",
|
|
116
|
+
createdAt: "created_at"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
oauthRefreshToken: {
|
|
120
|
+
modelName: "oauth_refresh_token",
|
|
121
|
+
fields: {
|
|
122
|
+
clientId: "client_id",
|
|
123
|
+
sessionId: "session_id",
|
|
124
|
+
userId: "user_id",
|
|
125
|
+
referenceId: "reference_id",
|
|
126
|
+
authorizationCodeId: "authorization_code_id",
|
|
127
|
+
requestedUserInfoClaims: "requested_user_info_claims",
|
|
128
|
+
expiresAt: "expires_at",
|
|
129
|
+
createdAt: "created_at",
|
|
130
|
+
rotatedAt: "rotated_at",
|
|
131
|
+
rotationReplayResponse: "rotation_replay_response",
|
|
132
|
+
rotationReplayExpiresAt: "rotation_replay_expires_at",
|
|
133
|
+
authTime: "auth_time"
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
oauthAccessToken: {
|
|
137
|
+
modelName: "oauth_access_token",
|
|
138
|
+
fields: {
|
|
139
|
+
clientId: "client_id",
|
|
140
|
+
sessionId: "session_id",
|
|
141
|
+
userId: "user_id",
|
|
142
|
+
referenceId: "reference_id",
|
|
143
|
+
authorizationCodeId: "authorization_code_id",
|
|
144
|
+
requestedUserInfoClaims: "requested_user_info_claims",
|
|
145
|
+
refreshId: "refresh_id",
|
|
146
|
+
expiresAt: "expires_at",
|
|
147
|
+
createdAt: "created_at"
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
oauthConsent: {
|
|
151
|
+
modelName: "oauth_consent",
|
|
152
|
+
fields: {
|
|
153
|
+
clientId: "client_id",
|
|
154
|
+
userId: "user_id",
|
|
155
|
+
referenceId: "reference_id",
|
|
156
|
+
requestedUserInfoClaims: "requested_user_info_claims",
|
|
157
|
+
createdAt: "created_at",
|
|
158
|
+
updatedAt: "updated_at"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
oauthClientAssertion: {
|
|
162
|
+
modelName: "oauth_client_assertion",
|
|
163
|
+
fields: { expiresAt: "expires_at" }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
65
166
|
};
|
|
66
167
|
//#endregion
|
|
67
168
|
export { coreNames, pluginNames };
|
|
@@ -55,7 +55,8 @@ const accountChanges = [
|
|
|
55
55
|
"/invitation/accept",
|
|
56
56
|
"/organization/leave",
|
|
57
57
|
"/organization/transfer-ownership",
|
|
58
|
-
"/organization/delete"
|
|
58
|
+
"/organization/delete",
|
|
59
|
+
"/account/connected-apps/disconnect"
|
|
59
60
|
];
|
|
60
61
|
const staffCannotChange = {
|
|
61
62
|
code: "STAFF_CANNOT_CHANGE_SIGN_IN",
|
|
@@ -63,7 +64,13 @@ const staffCannotChange = {
|
|
|
63
64
|
};
|
|
64
65
|
/** Whether a staff session's request to `path` is refused: changes to the person's sign-in, and saving the sign-in policy. */
|
|
65
66
|
const isRefusedForStaff = (path, method) => accountChanges.includes(path) || path === "/policy/sign-in" && method === "POST";
|
|
67
|
+
const staffCannotConnect = {
|
|
68
|
+
code: "STAFF_CANNOT_CONNECT",
|
|
69
|
+
message: "Staff cannot allow apps to act for a person."
|
|
70
|
+
};
|
|
71
|
+
/** Whether `path` is one of the OAuth provider's, through which an outside app (an MCP client) is allowed to act as someone. */
|
|
72
|
+
const isOAuthPath = (path) => path.startsWith("/oauth2/");
|
|
66
73
|
/** Whether a Better Auth session is one staff started as someone: the admin plugin's `impersonatedBy` names the staff member. */
|
|
67
74
|
const isStaffSession = (session) => Boolean(session.impersonatedBy);
|
|
68
75
|
//#endregion
|
|
69
|
-
export { isRefusedForStaff, isStaffSession, isStrongSignIn, permissionMissing, reasonProblem, signInNotStrong, signInStale, staffCannotChange, staffClaimsRefusal, staffLabel };
|
|
76
|
+
export { isOAuthPath, isRefusedForStaff, isStaffSession, isStrongSignIn, permissionMissing, reasonProblem, signInNotStrong, signInStale, staffCannotChange, staffCannotConnect, staffClaimsRefusal, staffLabel };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { adminOnly } from "./admin-only.js";
|
|
2
|
-
import { isRefusedForStaff, isStaffSession, staffCannotChange } from "./staff-checks.js";
|
|
2
|
+
import { isOAuthPath, isRefusedForStaff, isStaffSession, staffCannotChange, staffCannotConnect } 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";
|
|
@@ -7,7 +7,8 @@ import { deleteSessionCookie } from "better-auth/cookies";
|
|
|
7
7
|
//#region src/better-auth/staff-plugin.ts
|
|
8
8
|
/**
|
|
9
9
|
* Staff of the operator sign in as a person of this app, for support (see `staffSignInEndpoints`). A staff session
|
|
10
|
-
* shows who started it and why, can be stopped, and cannot change the person's sign-in or the sign-in policy
|
|
10
|
+
* shows who started it and why, can be stopped, and cannot change the person's sign-in or the sign-in policy, or allow
|
|
11
|
+
* an outside app to act as the person. Every
|
|
11
12
|
* staff sign-in is logged for the app's admins. The plugin is there without an operator provider too, so its table
|
|
12
13
|
* exists from the first auth migration.
|
|
13
14
|
*/
|
|
@@ -56,6 +57,12 @@ const staffSignInPlugin = ({ operator, mail, defaultPolicy }) => ({
|
|
|
56
57
|
const session = await getSessionFromCtx(ctx);
|
|
57
58
|
if (session && isStaffSession(session.session)) throw new APIError("FORBIDDEN", staffCannotChange);
|
|
58
59
|
})
|
|
60
|
+
}, {
|
|
61
|
+
matcher: (context) => context.path !== void 0 && isOAuthPath(context.path),
|
|
62
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
63
|
+
const session = await getSessionFromCtx(ctx);
|
|
64
|
+
if (session && isStaffSession(session.session)) throw new APIError("FORBIDDEN", staffCannotConnect);
|
|
65
|
+
})
|
|
59
66
|
}] }
|
|
60
67
|
});
|
|
61
68
|
//#endregion
|
|
@@ -14,7 +14,7 @@ import { Hono } from "hono";
|
|
|
14
14
|
* operator provider's name, when staff can sign in as people; `passkeyDomains`, when people have passkeys for more than
|
|
15
15
|
* one of the app's hosts, the RP IDs a passkey sign-in can ask for, the passkey domain first; with organizations, who may create them and the roles
|
|
16
16
|
* with their labels, `membership` for those a member can hold, and the roles each one's holders may also give; and
|
|
17
|
-
* `anonymous` when the project's anonymous role reads a resource, so the admin app opens for visitors before anyone signs in; and
|
|
17
|
+
* `anonymous` when the project's anonymous role reads a resource, so the admin app opens for visitors before anyone signs in; `mcp` when MCP clients can connect, so the account page lists the connected ones; and
|
|
18
18
|
* `plugins`, those the app loads before sign-in (see `PluginOption`); and `locale`, the language `options.locale` puts
|
|
19
19
|
* the screens in, so the sign-in screens and public forms, which come before `/meta`, are in it too.
|
|
20
20
|
*/
|
|
@@ -44,6 +44,7 @@ const statusRoute = (auth, { anonymous = false, plugins = [], locale } = {}) =>
|
|
|
44
44
|
...staffSignIn && { staffSignIn },
|
|
45
45
|
...passkeyDomains.length > 1 && { passkeyDomains },
|
|
46
46
|
...anonymous && { anonymous },
|
|
47
|
+
...auth.mcp && { mcp: true },
|
|
47
48
|
...plugins.length > 0 && { plugins },
|
|
48
49
|
...locale && { locale },
|
|
49
50
|
...auth.organizations && { organizations: {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { narrowClaims } from "./roles/active-role.js";
|
|
2
|
+
import { organizationClaims } from "./organizations/session-organization.js";
|
|
3
|
+
//#region src/better-auth/token-claims.ts
|
|
4
|
+
/**
|
|
5
|
+
* The claims of a token for `user`: the address and the role claims narrowed to one role; with organizations also the
|
|
6
|
+
* claims of `organizationId` while they may work in it (see `organizationClaims`).
|
|
7
|
+
*/
|
|
8
|
+
const tokenClaims = async (user, { wanted, organizationId, organizations }) => {
|
|
9
|
+
const claims = {
|
|
10
|
+
role: user.role,
|
|
11
|
+
...organizations && await organizationClaims(organizations, user, organizationId)
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
email: user.email,
|
|
15
|
+
...narrowClaims(claims, wanted, Boolean(organizations))
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { tokenClaims };
|
|
@@ -52,7 +52,10 @@ export declare const setUserRole: (auth: AdminAuth, input: {
|
|
|
52
52
|
email: string;
|
|
53
53
|
role: string;
|
|
54
54
|
}>;
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Disables or re-enables sign-in. Banning ends the user's sessions and, with MCP on, disconnects their MCP clients and
|
|
57
|
+
* forgets them, so re-enabling brings none back: each has to be allowed again. The last active admin cannot be banned.
|
|
58
|
+
*/
|
|
56
59
|
export declare const setUserBanned: (auth: AdminAuth, input: {
|
|
57
60
|
email: string;
|
|
58
61
|
banned: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseRoles } from "./parse-roles.js";
|
|
2
2
|
import { isActiveAdmin } from "./last-admin.js";
|
|
3
|
+
import { revokeMcpGrants } from "./mcp/revoke-grants.js";
|
|
3
4
|
import { ownOrganizationsWithoutMembers } from "./organizations/host.js";
|
|
4
5
|
//#region src/better-auth/users.ts
|
|
5
6
|
/** The roles this auth instance allows, `admin` first. */
|
|
@@ -98,7 +99,10 @@ const setUserRole = async (auth, input) => {
|
|
|
98
99
|
role: wanted.join(",")
|
|
99
100
|
};
|
|
100
101
|
};
|
|
101
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Disables or re-enables sign-in. Banning ends the user's sessions and, with MCP on, disconnects their MCP clients and
|
|
104
|
+
* forgets them, so re-enabling brings none back: each has to be allowed again. The last active admin cannot be banned.
|
|
105
|
+
*/
|
|
102
106
|
const setUserBanned = async (auth, input) => {
|
|
103
107
|
const user = await findUser(auth, input.email);
|
|
104
108
|
const { internalAdapter } = await auth.$context;
|
|
@@ -117,6 +121,7 @@ const setUserBanned = async (auth, input) => {
|
|
|
117
121
|
banExpires: null
|
|
118
122
|
});
|
|
119
123
|
await internalAdapter.deleteUserSessions(user.id);
|
|
124
|
+
if (auth.mcp) await revokeMcpGrants((await auth.$context).adapter, user.id);
|
|
120
125
|
};
|
|
121
126
|
//#endregion
|
|
122
127
|
export { createUser, deleteUser, hasUsers, listUsers, parseRoles, roleChoices, setUserBanned, setUserRole };
|
package/dist/check-roles.js
CHANGED
|
@@ -18,7 +18,7 @@ const checkRoleLimits = (registry, limits) => {
|
|
|
18
18
|
...model,
|
|
19
19
|
fields: Object.fromEntries(Object.entries(model.fields).map(([field, value]) => [field, {
|
|
20
20
|
...value,
|
|
21
|
-
filterable:
|
|
21
|
+
filterable: !value.encrypted
|
|
22
22
|
}]))
|
|
23
23
|
};
|
|
24
24
|
const checked = checkFilter(trusted, filter);
|
package/dist/create-admin.js
CHANGED
|
@@ -22,9 +22,11 @@ import { readAppName } from "./app-name.js";
|
|
|
22
22
|
import { realtimePath, restPath, storagePath, supabaseFunctionsPath, supabasePath } from "./supabase-paths.js";
|
|
23
23
|
import { goTruePath } from "./gotrue/keys.js";
|
|
24
24
|
import { authBasePath } from "./better-auth/auth-base-path.js";
|
|
25
|
+
import { mcpDiscovery } from "./better-auth/mcp/discovery-routes.js";
|
|
25
26
|
import { withoutSessionTokens } from "./better-auth/without-session-tokens.js";
|
|
26
27
|
import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
|
|
27
28
|
import { statusRoute } from "./better-auth/status-route.js";
|
|
29
|
+
import { betterAuthSessionToken } from "./better-auth/session-token.js";
|
|
28
30
|
import { goTrueTokens } from "./gotrue/access-tokens.js";
|
|
29
31
|
import { goTrueRoutes } from "./gotrue/routes.js";
|
|
30
32
|
import { changesSchema } from "./changes/events-options.js";
|
|
@@ -56,8 +58,10 @@ import { createFilesRuntime } from "./files/runtime.js";
|
|
|
56
58
|
import { fileRoutes } from "./files/routes.js";
|
|
57
59
|
import { formRoutes } from "./forms/routes.js";
|
|
58
60
|
import { createFormsRuntime } from "./forms/runtime.js";
|
|
61
|
+
import { createEncryptionRuntime } from "./encryption/runtime.js";
|
|
59
62
|
import { functionRoutes } from "./functions/routes.js";
|
|
60
63
|
import { supabaseFunctionRoutes } from "./functions/supabase-routes.js";
|
|
64
|
+
import { upgradeAuthenticator } from "./functions/upgrade-caller.js";
|
|
61
65
|
import { localeSetting } from "./locale/locale-option.js";
|
|
62
66
|
import { mapSettings } from "./maps/map-settings.js";
|
|
63
67
|
import { pluginOptions, pluginsBeforeSignIn } from "./plugins/plugin-settings.js";
|
|
@@ -144,6 +148,10 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
144
148
|
});
|
|
145
149
|
const apiKeys = apiKeySettings(options.apiKeys, globalThis.process?.env ?? {});
|
|
146
150
|
const servesKeys = Boolean(apiKeys.publishable || apiKeys.secret);
|
|
151
|
+
const encryption = createEncryptionRuntime({
|
|
152
|
+
models: registry.entries.map((entry) => entry.model),
|
|
153
|
+
env: globalThis.process?.env ?? {}
|
|
154
|
+
});
|
|
147
155
|
const files = createFilesRuntime({
|
|
148
156
|
...filesOptions && { options: filesOptions },
|
|
149
157
|
env: globalThis.process?.env ?? {},
|
|
@@ -185,6 +193,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
185
193
|
plugins,
|
|
186
194
|
sources,
|
|
187
195
|
...files && { files },
|
|
196
|
+
...encryption && { encryption },
|
|
188
197
|
...appModels.some(isCaptured) && { actorSetting: `${changes}.actor` },
|
|
189
198
|
...withHistory && { history: {
|
|
190
199
|
schema: changes,
|
|
@@ -254,6 +263,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
254
263
|
registry,
|
|
255
264
|
db
|
|
256
265
|
}));
|
|
266
|
+
if (auth.mcp) mcpDiscovery(app, auth);
|
|
257
267
|
app.on(["GET", "POST"], `${authBasePath}/*`, async (c) => withoutSessionTokens(c.req.raw, await auth.handler(c.req.raw)));
|
|
258
268
|
app.route(goTruePath, goTrueRoutes(auth));
|
|
259
269
|
if (auth.origins.length > 1) app.get(webauthnPath, (c) => c.json({ origins: auth.origins }));
|
|
@@ -272,6 +282,14 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
272
282
|
report: options.onUnhandledError
|
|
273
283
|
}));
|
|
274
284
|
}
|
|
285
|
+
const report = options.onUnhandledError ?? ((error) => console.error(error));
|
|
286
|
+
const upgradeAuthenticate = upgradeAuthenticator({
|
|
287
|
+
authenticate,
|
|
288
|
+
...auth && {
|
|
289
|
+
origins: auth.origins,
|
|
290
|
+
sessionToken: betterAuthSessionToken(auth)
|
|
291
|
+
}
|
|
292
|
+
});
|
|
275
293
|
app.route("/", functionRoutes({
|
|
276
294
|
basePath: `${systemPath}/functions`,
|
|
277
295
|
functions: apiFunctions,
|
|
@@ -280,7 +298,10 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
280
298
|
db,
|
|
281
299
|
records: (session) => callerRecords(deps, session),
|
|
282
300
|
onError,
|
|
283
|
-
wake: () => wakeDispatch(deps)
|
|
301
|
+
wake: () => wakeDispatch(deps),
|
|
302
|
+
upgradeWebSocket,
|
|
303
|
+
upgradeAuthenticate,
|
|
304
|
+
report
|
|
284
305
|
}));
|
|
285
306
|
const verifyGoTrue = auth ? jwtVerifier(goTrueTokens({ auth })) : () => Promise.reject(unauthorized("This app signs no user tokens"));
|
|
286
307
|
app.route("/", supabaseFunctionRoutes({
|
|
@@ -296,7 +317,6 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
296
317
|
app.route("/", batchRoute(deps, authenticate, meta));
|
|
297
318
|
app.route("/", fileRoutes(deps, authenticate));
|
|
298
319
|
if (forms) app.route(`${systemPath}/forms`, formRoutes(deps, forms));
|
|
299
|
-
const report = options.onUnhandledError ?? ((error) => console.error(error));
|
|
300
320
|
const feed = tableFeed({
|
|
301
321
|
db,
|
|
302
322
|
schema: changes,
|
package/dist/deps.d.ts
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import "../registry.js";
|
|
2
|
+
import { CascadingKey } from "./cascading-keys.js";
|
|
3
|
+
import { UnreadableEntry } from "./status.js";
|
|
4
|
+
import { Db } from "@protobase/query";
|
|
5
|
+
//#region src/encryption/apply.d.ts
|
|
6
|
+
/** Values sealed under another table (as `.table()` wrote it) or column, before a rename: of one field, or every one of the resource. */
|
|
7
|
+
export type RebindOption = {
|
|
8
|
+
resource: string;
|
|
9
|
+
field?: string;
|
|
10
|
+
from: {
|
|
11
|
+
table?: string;
|
|
12
|
+
column?: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export type ApplyOptions = {
|
|
16
|
+
rebind?: RebindOption;
|
|
17
|
+
};
|
|
18
|
+
/** What `apply` did to one encrypted field. */
|
|
19
|
+
export type FieldApplied = {
|
|
20
|
+
resource: string;
|
|
21
|
+
field: string;
|
|
22
|
+
/** Plaintext values it encrypted. */
|
|
23
|
+
encrypted: number;
|
|
24
|
+
/** Values under a previous key it encrypted with the current one. */
|
|
25
|
+
reencrypted: number;
|
|
26
|
+
/** Values that opened only where they were before a rename, sealed again where they are now. */
|
|
27
|
+
rebound: number;
|
|
28
|
+
/** Values a write changed while it ran, which it left alone. */
|
|
29
|
+
skipped: number;
|
|
30
|
+
unreadable: UnreadableEntry[];
|
|
31
|
+
/** History rows and undispatched changes it removed the field's old values from. */
|
|
32
|
+
historyScrubbed: number;
|
|
33
|
+
outboxScrubbed: number;
|
|
34
|
+
};
|
|
35
|
+
export type ApplyResult = {
|
|
36
|
+
kid: string;
|
|
37
|
+
fields: FieldApplied[];
|
|
38
|
+
warnings: CascadingKey[];
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|