@protobase/server 0.33.4 → 0.34.1
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 -363
- package/dist/better-auth/create-auth.js +39 -20
- 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/new-account-password.d.ts +6 -0
- package/dist/better-auth/new-account-password.js +6 -3
- 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/sign-in-enforcement.js +6 -1
- package/dist/better-auth/sign-in-setup.js +38 -0
- package/dist/better-auth/sign-up/sign-up-plugin.js +1 -1
- 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 +8 -0
- 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/gotrue/user-routes.js +6 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +4 -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/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,92 @@
|
|
|
1
|
+
import { bindingOf } from "./binding.js";
|
|
2
|
+
import { envelopePrefix } from "./envelope-seal.js";
|
|
3
|
+
import { columnId, tableId } from "../sql-parts.js";
|
|
4
|
+
import { UnreadableValue, openText } from "./envelope-open.js";
|
|
5
|
+
import { cascadingKeys } from "./cascading-keys.js";
|
|
6
|
+
import { historyRemnants, outboxRemnants } from "./remnants.js";
|
|
7
|
+
import { scanColumn } from "./scan.js";
|
|
8
|
+
import { sql } from "kysely";
|
|
9
|
+
//#region src/encryption/status.ts
|
|
10
|
+
const encryptedFields = (entry) => Object.values(entry.model.fields).filter((field) => field.encrypted);
|
|
11
|
+
/** Opens one scanned value: its plaintext and key id, or why it does not open. A value that is no envelope is plaintext. */
|
|
12
|
+
const examine = (keyring, entry, field, row, binding = bindingOf(entry.model, field, row.record)) => {
|
|
13
|
+
if (!row.stored.startsWith("pbenc:")) return { kind: "plaintext" };
|
|
14
|
+
try {
|
|
15
|
+
return {
|
|
16
|
+
kind: "open",
|
|
17
|
+
plaintext: openText(keyring, binding, row.stored, {
|
|
18
|
+
resource: entry.name,
|
|
19
|
+
field: field.name
|
|
20
|
+
}),
|
|
21
|
+
kid: row.stored.split(":")[2]
|
|
22
|
+
};
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!(error instanceof UnreadableValue)) throw error;
|
|
25
|
+
return {
|
|
26
|
+
kind: "unreadable",
|
|
27
|
+
entry: {
|
|
28
|
+
key: entry.model.primaryKey.map((name) => String(row.record[name])),
|
|
29
|
+
...error.kid && { kid: error.kid },
|
|
30
|
+
kind: error.kind
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const fieldStatus = async (db, entry, field, keyring, changesSchema) => {
|
|
36
|
+
const status = {
|
|
37
|
+
resource: entry.name,
|
|
38
|
+
field: field.name,
|
|
39
|
+
byKey: {},
|
|
40
|
+
plaintext: 0,
|
|
41
|
+
unreadable: [],
|
|
42
|
+
history: 0,
|
|
43
|
+
outbox: 0
|
|
44
|
+
};
|
|
45
|
+
await scanColumn(db, entry, field, async (rows) => {
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
const found = examine(keyring, entry, field, row);
|
|
48
|
+
if (found.kind === "plaintext") status.plaintext++;
|
|
49
|
+
else if (found.kind === "open") status.byKey[found.kid] = (status.byKey[found.kid] ?? 0) + 1;
|
|
50
|
+
else status.unreadable.push(found.entry);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
status.history = await historyRemnants(db, changesSchema, entry.model, field);
|
|
54
|
+
status.outbox = await outboxRemnants(db, changesSchema, entry.model, field);
|
|
55
|
+
return status;
|
|
56
|
+
};
|
|
57
|
+
const strayValues = async (db, entry) => {
|
|
58
|
+
const fields = Object.values(entry.model.fields).filter((field) => field.type === "text" && !field.encrypted);
|
|
59
|
+
const stray = [];
|
|
60
|
+
for (const field of fields) {
|
|
61
|
+
const { rows } = await sql`select count(*)::text as count from ${tableId(entry.model)} where ${columnId(field)} like ${`${envelopePrefix}%`}`.execute(db);
|
|
62
|
+
const count = Number(rows[0].count);
|
|
63
|
+
if (count > 0) stray.push({
|
|
64
|
+
resource: entry.name,
|
|
65
|
+
field: field.name,
|
|
66
|
+
count
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return stray;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Per encrypted field: the values by key, the plaintext ones, those that do not open, and the old plaintext left in the
|
|
73
|
+
* history and the outbox; envelopes in fields that are not encrypted; foreign keys that rewrite a key. It writes nothing.
|
|
74
|
+
*/
|
|
75
|
+
const encryptionStatus = async (db, entries, keyring, changesSchema) => {
|
|
76
|
+
const fields = [];
|
|
77
|
+
const stray = [];
|
|
78
|
+
const cascading = [];
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
for (const field of encryptedFields(entry)) fields.push(await fieldStatus(db, entry, field, keyring, changesSchema));
|
|
81
|
+
stray.push(...await strayValues(db, entry));
|
|
82
|
+
if (encryptedFields(entry).length > 0) cascading.push(...await cascadingKeys(db, entry.model));
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
kid: keyring.current.id,
|
|
86
|
+
fields,
|
|
87
|
+
stray,
|
|
88
|
+
cascading
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { encryptedFields, encryptionStatus, examine };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Sealed } from "./envelope-seal.js";
|
|
2
|
+
//#region src/encryption/stored-value.ts
|
|
3
|
+
/**
|
|
4
|
+
* The value a record write sends for `field`: JSON as text, and for an encrypted field only `null` or a value `sealValues`
|
|
5
|
+
* sealed, so no write path stores plaintext in it.
|
|
6
|
+
*/
|
|
7
|
+
const storedValue = (field, value) => {
|
|
8
|
+
if (field.encrypted) {
|
|
9
|
+
if (value === null) return null;
|
|
10
|
+
if (value instanceof Sealed) return Sealed.textOf(value);
|
|
11
|
+
throw new Error(`"${field.name}" is encrypted, and its value was not sealed`);
|
|
12
|
+
}
|
|
13
|
+
return field.type === "json" && value !== null ? JSON.stringify(value) : value;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { storedValue };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EventsOptions } from "../changes/events-options.js";
|
|
2
|
+
import { ResourceSource } from "../resource-source.js";
|
|
3
|
+
import { EncryptionStatus } from "./status.js";
|
|
4
|
+
import { ApplyOptions, ApplyResult } from "./apply.js";
|
|
5
|
+
import { Db } from "@protobase/query";
|
|
6
|
+
//#region src/encryption/tools.d.ts
|
|
7
|
+
export type EncryptionToolsInput = {
|
|
8
|
+
resources: ResourceSource[];
|
|
9
|
+
db: Db;
|
|
10
|
+
events?: EventsOptions;
|
|
11
|
+
env: Record<string, string | undefined>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* What `protobase encryption status` and `apply` run on a project: `undefined` when it has no encrypted fields. It fails
|
|
15
|
+
* on the same config and key problems as the server's startup. `kid` is the id of the current key.
|
|
16
|
+
*/
|
|
17
|
+
export declare const createEncryptionTools: ({ resources, db, events, env }: EncryptionToolsInput) => {
|
|
18
|
+
kid: string;
|
|
19
|
+
/** What every encrypted field's column holds, and what remains of its plaintext; it writes nothing. */
|
|
20
|
+
status: () => Promise<EncryptionStatus>;
|
|
21
|
+
/** Encrypts the plaintext, moves previous keys' values to the current one, rebinds renamed ones, and scrubs remnants. */
|
|
22
|
+
apply: (options?: ApplyOptions) => Promise<ApplyResult>;
|
|
23
|
+
} | undefined;
|
|
24
|
+
/** The tools of a project with encrypted fields. */
|
|
25
|
+
export type EncryptionTools = NonNullable<ReturnType<typeof createEncryptionTools>>;
|
|
26
|
+
//#endregion
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { changesSchema } from "../changes/events-options.js";
|
|
2
|
+
import { createEncryptionRuntime } from "./runtime.js";
|
|
3
|
+
import { buildRegistry } from "../registry.js";
|
|
4
|
+
import { encryptionStatus } from "./status.js";
|
|
5
|
+
import { applyEncryption } from "./apply.js";
|
|
6
|
+
import { rebindProblem } from "./rebind-check.js";
|
|
7
|
+
//#region src/encryption/tools.ts
|
|
8
|
+
/**
|
|
9
|
+
* What `protobase encryption status` and `apply` run on a project: `undefined` when it has no encrypted fields. It fails
|
|
10
|
+
* on the same config and key problems as the server's startup. `kid` is the id of the current key.
|
|
11
|
+
*/
|
|
12
|
+
const createEncryptionTools = ({ resources, db, events, env }) => {
|
|
13
|
+
const { entries } = buildRegistry(resources);
|
|
14
|
+
const runtime = createEncryptionRuntime({
|
|
15
|
+
models: entries.map((entry) => entry.model),
|
|
16
|
+
env
|
|
17
|
+
});
|
|
18
|
+
if (!runtime) return void 0;
|
|
19
|
+
const { keyring } = runtime;
|
|
20
|
+
const schema = changesSchema(events);
|
|
21
|
+
return {
|
|
22
|
+
kid: keyring.current.id,
|
|
23
|
+
/** What every encrypted field's column holds, and what remains of its plaintext; it writes nothing. */
|
|
24
|
+
status: () => encryptionStatus(db, entries, keyring, schema),
|
|
25
|
+
/** Encrypts the plaintext, moves previous keys' values to the current one, rebinds renamed ones, and scrubs remnants. */
|
|
26
|
+
apply: async (options = {}) => {
|
|
27
|
+
const problem = options.rebind && rebindProblem(entries, options.rebind);
|
|
28
|
+
if (problem) throw new Error(problem);
|
|
29
|
+
return applyEncryption(db, entries, keyring, schema, options);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { createEncryptionTools };
|
|
@@ -20,7 +20,7 @@ const fieldErrors = (registry, { form, view }, granted) => {
|
|
|
20
20
|
const field = model.fields[name];
|
|
21
21
|
if (!field) return [`field "${name}" does not exist on ${view.resource}`];
|
|
22
22
|
if (field.readOnly) return [`field "${name}" is read-only, so nobody fills it in`];
|
|
23
|
-
if (field.sensitive) return [`field "${name}" is sensitive, and a public form
|
|
23
|
+
if (field.sensitive && !field.encrypted) return [`field "${name}" is sensitive, and a public form takes only encrypted ones of them`];
|
|
24
24
|
if (field.type === "relation") return [`field "${name}" is a relation, whose records a visitor cannot read to pick one`];
|
|
25
25
|
if (field.type === "file") return [`field "${name}" is a file field, and a visitor cannot upload`];
|
|
26
26
|
if (!plainTypes.includes(field.type)) return [`field "${name}" is ${field.type}, which a form has no input for`];
|
|
@@ -2,6 +2,7 @@ import { effectiveSignInPolicy } from "../better-auth/sign-in-policy.js";
|
|
|
2
2
|
import { readSignInPolicy } from "../better-auth/policy-store.js";
|
|
3
3
|
import { goTrueUser, userMetadata } from "./user-json.js";
|
|
4
4
|
import { checkNewAccountPassword } from "../better-auth/new-account-password.js";
|
|
5
|
+
import { revokeMcpGrants } from "../better-auth/mcp/revoke-grants.js";
|
|
5
6
|
import { goTrueTokens } from "./access-tokens.js";
|
|
6
7
|
import { GoTrueError } from "./errors.js";
|
|
7
8
|
import { readBody } from "./request-body.js";
|
|
@@ -22,6 +23,9 @@ const logoutScopes = [
|
|
|
22
23
|
/**
|
|
23
24
|
* The signed-in user's own endpoints, with their access token: `GET /user`, `PUT /user` for a new password or metadata,
|
|
24
25
|
* and `POST /logout`. A token whose session has ended, by signing out or a ban, is GoTrue's `session_not_found`.
|
|
26
|
+
* With MCP on, signing out everywhere or the other sessions, and setting a password, disconnect the user's MCP clients,
|
|
27
|
+
* as the same steps do through Better Auth: GoTrue has no way to tell a password reset from a change, so every new
|
|
28
|
+
* password does.
|
|
25
29
|
*/
|
|
26
30
|
const userRoutes = ({ auth, context }) => {
|
|
27
31
|
const claimsOf = accessClaimsOf(goTrueTokens({ auth }));
|
|
@@ -61,6 +65,7 @@ const userRoutes = ({ auth, context }) => {
|
|
|
61
65
|
model: "session",
|
|
62
66
|
where
|
|
63
67
|
});
|
|
68
|
+
if (scope !== "local" && auth.mcp) await revokeMcpGrants(adapter, claims.sub);
|
|
64
69
|
};
|
|
65
70
|
return new Hono().get("/user", async (c) => c.json(await userJson(await signedIn(c.req.raw)))).put("/user", async (c) => {
|
|
66
71
|
const { email, password, data } = await readBody(c.req.raw, updateBody);
|
|
@@ -78,6 +83,7 @@ const userRoutes = ({ auth, context }) => {
|
|
|
78
83
|
accountId: user.id,
|
|
79
84
|
password: hash
|
|
80
85
|
});
|
|
86
|
+
if (auth.mcp) await revokeMcpGrants(ctx.adapter, user.id);
|
|
81
87
|
}
|
|
82
88
|
const merged = data && Object.fromEntries(Object.entries({
|
|
83
89
|
...userMetadata(user.metadata),
|
package/dist/index.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ import { MobileConfig } from "./mobile-config.js";
|
|
|
2
2
|
import { AppIdentity } from "./app-identity.js";
|
|
3
3
|
import { AppIcons, PngIcon, pngIcons } from "./web-app/app-icons.js";
|
|
4
4
|
import { StaffSignIn } from "./better-auth/staff-log.js";
|
|
5
|
+
import { RoleDefinition, RolesInput } from "./better-auth/organizations/role-definitions.js";
|
|
5
6
|
import { MailMessage, Mailer } from "./mail/smtp-mailer.js";
|
|
6
7
|
import { OperatorProvider, readOperatorSettings } from "./better-auth/operator-provider.js";
|
|
7
|
-
import { RoleDefinition, RolesInput } from "./better-auth/organizations/role-definitions.js";
|
|
8
8
|
import { OrganizationRole, StoredMember, StoredOrganization } from "./better-auth/organizations/store.js";
|
|
9
|
+
import { SignInPolicy, SignInRule, StaffAccessRule, defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
|
|
9
10
|
import { OrganizationsOptions } from "./better-auth/organizations/options.js";
|
|
10
11
|
import { PlatformSignIn, readPlatformSignIn } from "./better-auth/platform-sign-in.js";
|
|
11
|
-
import { SignInPolicy, SignInRule, StaffAccessRule, defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
|
|
12
12
|
import { EmailComponent, EmailOptions, EmailTemplate, RenderedEmail, defineEmail } from "./mail/define-email.js";
|
|
13
13
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, createAuth, normalizeRoles } from "./better-auth/create-auth.js";
|
|
14
14
|
import { EventsOptions } from "./changes/events-options.js";
|
|
@@ -54,11 +54,12 @@ import { consoleAuditQueue } from "./audit/console-queue.js";
|
|
|
54
54
|
import { JwtOptions, JwtVerifyOptions, jwtAuthenticator, jwtVerifier } from "./auth/jwt.js";
|
|
55
55
|
import { HttpProblem, MissingCredentials, badRequest, forbidden, notFound, unauthorized } from "./problem.js";
|
|
56
56
|
import { TrustedApp, withTrustedApps } from "./better-auth/app-tokens.js";
|
|
57
|
-
import { BetterAuthAuthenticatorOptions, betterAuthAuthenticator, betterAuthTokens } from "./better-auth/authenticator.js";
|
|
57
|
+
import { BetterAuthAuthenticator, BetterAuthAuthenticatorOptions, betterAuthAuthenticator, betterAuthTokens } from "./better-auth/authenticator.js";
|
|
58
58
|
import { parseRoles } from "./better-auth/parse-roles.js";
|
|
59
59
|
import { LinkedAccount, NewUser, StoredUser, createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
|
|
60
60
|
import { IssueTokenOptions, issueToken, maxTokenTtlSeconds } from "./better-auth/tokens.js";
|
|
61
|
-
import {
|
|
61
|
+
import { passwordLength } from "./better-auth/new-account-password.js";
|
|
62
|
+
import { isOAuthWellKnownPath, oauthWellKnownPaths, webauthnPath } from "./better-auth/origins.js";
|
|
62
63
|
import { goTruePath } from "./gotrue/keys.js";
|
|
63
64
|
import { supabasePath } from "./supabase-paths.js";
|
|
64
65
|
import { defaultAppName } from "./app-name.js";
|
|
@@ -97,6 +98,10 @@ import { S3Bucket, S3FilesOptions, S3ListPage, S3Object, s3FileStore, s3Files }
|
|
|
97
98
|
import { storageProvider } from "./files/storage-provider.js";
|
|
98
99
|
import { objectKey } from "./storage/object-key.js";
|
|
99
100
|
import { FileCleanupInput, createFileCleanup } from "./files/file-cleanup.js";
|
|
101
|
+
import { CascadingKey } from "./encryption/cascading-keys.js";
|
|
102
|
+
import { EncryptionStatus, FieldStatus, StrayValues, UnreadableEntry } from "./encryption/status.js";
|
|
103
|
+
import { ApplyOptions, ApplyResult, FieldApplied, RebindOption } from "./encryption/apply.js";
|
|
104
|
+
import { EncryptionTools, EncryptionToolsInput, createEncryptionTools } from "./encryption/tools.js";
|
|
100
105
|
import { FileObject, FileVariantObject } from "./files/present.js";
|
|
101
106
|
import { FileRef, formatUri, variantPath } from "./files/uri.js";
|
|
102
107
|
import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/processors.js";
|
|
@@ -106,4 +111,4 @@ import { isSchedule } from "./jobs/schedule.js";
|
|
|
106
111
|
import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
|
|
107
112
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
108
113
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
109
|
-
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type AppIcons, type AppIdentity, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentUsers, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type MemberInput, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SocketEvents, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, backgroundSettled, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, objectKey, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, waitUntil, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
|
114
|
+
export { type Admin, type AdminAuth, type AdminOptions, type ApiFunction, type ApiKeys, type ApiKeysOptions, type AppIcons, type AppIdentity, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type Authenticator, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type Caller, type CallerOptions, type CapturedRow, type CascadingKey, type ChatMessage, type ChatTextPart, type CheckAiInput, type CleanupEntry, type CleanupReport, type CleanupSchedule, type CoalescedChange, type CommentItem, type CommentUsers, type Completion, type CompletionOptions, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileRef, type FileStore, type FileVariantObject, type FilesOptions, type FunctionApp, type FunctionCaller, type FunctionContext, type FunctionEnv, type FunctionHandler, type FunctionOptions, type FunctionRecords, type FunctionSocket, type FunctionSource, type HistoryOptions, HttpProblem, type IssueTokenOptions, type JobContext, type JobDefinition, type JobOptions, type JobsRuntime, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type MemberInput, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewOrganization, type NewUser, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProjectConfig, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type QueryOptions, type QueryResult, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordKeyInput, type RecordPage, type RenderedEmail, type ResourceChange, type ResourceSource, type RoleDefinition, type RolesInput, type RunContext, type RuntimeOptions, type S3Bucket, type S3FilesOptions, type S3ListPage, type S3Object, type ScanGuardMode, type ScheduledDelete, type Session, type SessionOrganization, type Sharp, type SignInPolicy, type SignInRule, type SignatureOptions, type SocketEvents, type SourceOption, type SourceRef, type StaffAccessRule, type StaffSignIn, type StandardSchema, type StandardWebhookOptions, type StartJobsOptions, type StoreFileInput, type StoredMember, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolCall, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type User, type UserCreatedHook, type ViewSource, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, backgroundSettled, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, waitUntil, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { requestApproval } from "./assistant/approval.js";
|
|
|
17
17
|
import { withCacheBreakpoints } from "./assistant/cache-breakpoints.js";
|
|
18
18
|
import { runTurn } from "./assistant/turn.js";
|
|
19
19
|
import { consoleAuditQueue } from "./audit/console-queue.js";
|
|
20
|
-
import { webauthnPath } from "./better-auth/origins.js";
|
|
20
|
+
import { isOAuthWellKnownPath, oauthWellKnownPaths, webauthnPath } from "./better-auth/origins.js";
|
|
21
21
|
import { apiKeySettings, apiKeyVariables } from "./api-keys/api-key-settings.js";
|
|
22
22
|
import { apiKeyCaller } from "./api-keys/resolve-caller.js";
|
|
23
23
|
import { jwtAuthenticator, jwtVerifier } from "./auth/jwt.js";
|
|
@@ -27,6 +27,7 @@ import { supabasePath } from "./supabase-paths.js";
|
|
|
27
27
|
import { goTruePath } from "./gotrue/keys.js";
|
|
28
28
|
import { defaultSignInPolicy, signInPolicyMethods } from "./better-auth/sign-in-policy.js";
|
|
29
29
|
import { parseRoles } from "./better-auth/parse-roles.js";
|
|
30
|
+
import { passwordLength } from "./better-auth/new-account-password.js";
|
|
30
31
|
import { readOperatorSettings } from "./better-auth/operator-provider.js";
|
|
31
32
|
import { readPlatformSignIn } from "./better-auth/platform-sign-in.js";
|
|
32
33
|
import { createAuth, normalizeRoles } from "./better-auth/create-auth.js";
|
|
@@ -76,8 +77,9 @@ import { requireEnv } from "./functions/require-env.js";
|
|
|
76
77
|
import { backgroundSettled, waitUntil } from "./functions/wait-until.js";
|
|
77
78
|
import { storeFile } from "./files/store-file.js";
|
|
78
79
|
import { createFileCleanup } from "./files/file-cleanup.js";
|
|
80
|
+
import { createEncryptionTools } from "./encryption/tools.js";
|
|
79
81
|
import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/processors.js";
|
|
80
82
|
import { defineEmail } from "./mail/define-email.js";
|
|
81
83
|
import { startJobs } from "./jobs/runtime.js";
|
|
82
84
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
83
|
-
export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, backgroundSettled, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, objectKey, parseColumns, parseRoles, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, waitUntil, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
|
85
|
+
export { FatalJobError, HttpProblem, MissingCredentials, ModelError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaMigration, backgroundSettled, badRequest, beforeSignInPluginsVariable, betterAuthAuthenticator, betterAuthTokens, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createAuth, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createUser, defaultAppName, defaultSignInPolicy, defineConfig, defineEmail, defineFunction, defineJob, defineTool, deleteUser, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, goTruePath, handleDroppedConnections, hasUsers, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listOrganizations, listUsers, localFileStore, localFiles, mapSettings, mapStyleUrl, mapStyleVariable, maxTokenTtlSeconds, maxWidgetPropsBytes, mergeConfig, migrationCommand, newWebhookSecret, normalizeRoles, notFound, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, processor, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readWriteQueryTool, recordOf, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, s3Files, seesAssistant, seesRuntime, setMemberRoles, setUserBanned, setUserRole, signInPolicyMethods, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, variantPath, verifySignature, verifyStandardWebhook, waitUntil, webauthnPath, webhooks, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps };
|
package/dist/records.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { storedValue } from "./encryption/stored-value.js";
|
|
1
2
|
import { columnId, filterCondition, keyConditions, returningList, scopeConditions, tableId, whereClause } from "./sql-parts.js";
|
|
2
3
|
import { etagOf } from "./etag.js";
|
|
3
4
|
import { exposeRow, pickFields } from "./rows.js";
|
|
@@ -24,10 +25,9 @@ const fetchRecord = async (db, entry, key, tenant, options = {}) => {
|
|
|
24
25
|
const query = sql`select ${returningList(entry, shape.fields)} from ${tableId(entry.model)} as t${whereClause(conditions)} limit 1${lockClause(lock)}`;
|
|
25
26
|
return stored(entry, (await rowsOf(db, query))[0], shape);
|
|
26
27
|
};
|
|
27
|
-
const writeValue = (field, value) => field.type === "json" && value !== null ? JSON.stringify(value) : value;
|
|
28
28
|
const insertRecord = async (db, entry, values, shape = {}) => {
|
|
29
29
|
const fields = Object.keys(values).map((name) => entry.model.fields[name]);
|
|
30
|
-
const body = fields.length === 0 ? sql`default values` : sql`(${sql.join(fields.map(columnId))}) values (${sql.join(fields.map((field) =>
|
|
30
|
+
const body = fields.length === 0 ? sql`default values` : sql`(${sql.join(fields.map(columnId))}) values (${sql.join(fields.map((field) => storedValue(field, values[field.name])))})`;
|
|
31
31
|
const query = sql`insert into ${tableId(entry.model)} ${body} returning ${returningList(entry, shape.fields)}`;
|
|
32
32
|
return await stored(entry, (await rowsOf(db, query))[0], shape);
|
|
33
33
|
};
|
|
@@ -39,7 +39,7 @@ const etagBump = (entry) => {
|
|
|
39
39
|
const updateRecord = async (db, entry, key, tenant, values, shape = {}) => {
|
|
40
40
|
const assignments = [...Object.entries(values).map(([name, value]) => {
|
|
41
41
|
const field = entry.model.fields[name];
|
|
42
|
-
return sql`${columnId(field)} = ${
|
|
42
|
+
return sql`${columnId(field)} = ${storedValue(field, value)}`;
|
|
43
43
|
}), ...etagBump(entry)];
|
|
44
44
|
const conditions = [...scopeConditions(entry.model, tenant), ...keyConditions(entry.model, key)];
|
|
45
45
|
const query = sql`update ${tableId(entry.model)} as t set ${sql.join(assignments)}${whereClause(conditions)} returning ${returningList(entry, shape.fields)}`;
|
package/dist/request-access.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { maskEncrypted } from "./encryption/mask-stored.js";
|
|
1
2
|
import { forbidden, notFound, unauthorized } from "./problem.js";
|
|
2
3
|
import { checkedFilter } from "./filter-input.js";
|
|
3
4
|
import { checkOwnerId } from "./owner-id.js";
|
|
@@ -98,7 +99,7 @@ const trusted = (model) => ({
|
|
|
98
99
|
...model,
|
|
99
100
|
fields: Object.fromEntries(Object.entries(model.fields).map(([name, field]) => [name, {
|
|
100
101
|
...field,
|
|
101
|
-
filterable:
|
|
102
|
+
filterable: !field.encrypted
|
|
102
103
|
}]))
|
|
103
104
|
});
|
|
104
105
|
const asFilter = (access, expr, operation) => {
|
|
@@ -109,13 +110,16 @@ const asFilter = (access, expr, operation) => {
|
|
|
109
110
|
};
|
|
110
111
|
/**
|
|
111
112
|
* The record-level rule of an operation, run with the stored record (and the input of a write). Returns a verdict, or a
|
|
112
|
-
* filter that the caller checks in the database, so its semantics match reads exactly.
|
|
113
|
+
* filter that the caller checks in the database, so its semantics match reads exactly. The rule gets a set encrypted
|
|
114
|
+
* field of the stored record as `encrypted`, never its ciphertext; the input it gets as written.
|
|
113
115
|
*/
|
|
114
116
|
const recordDecision = async (access, operation, record, input) => {
|
|
115
117
|
const { entry, deps, full, session } = access;
|
|
116
118
|
const explicit = entry.source.accessRules[operation] ?? (operation === "list" ? entry.source.accessRules.read : void 0);
|
|
117
119
|
const fallback = deps.accessOptions.defaultAccess ? () => deps.accessOptions.defaultAccess === "allow" : deps.accessOptions.roles ? deps.accessOptions.roles.can(`${entry.name}.${operation === "list" ? "read" : operation}`) : () => true;
|
|
118
|
-
const
|
|
120
|
+
const rule = explicit ?? fallback;
|
|
121
|
+
const stored = record && maskEncrypted(full, record);
|
|
122
|
+
const result = await rule({
|
|
119
123
|
user: {
|
|
120
124
|
id: session.user.id,
|
|
121
125
|
roles: session.user.roles
|
|
@@ -123,8 +127,8 @@ const recordDecision = async (access, operation, record, input) => {
|
|
|
123
127
|
...organizationOf(session),
|
|
124
128
|
model: full,
|
|
125
129
|
operation,
|
|
126
|
-
record
|
|
127
|
-
},
|
|
130
|
+
record: stored
|
|
131
|
+
}, stored, input);
|
|
128
132
|
if (typeof result === "boolean") return result;
|
|
129
133
|
return asFilter(access, result, operation);
|
|
130
134
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { restError } from "./rest-error.js";
|
|
2
|
+
import { sameTable } from "../encryption/shared-columns.js";
|
|
3
|
+
//#region src/rest/encrypted-columns.ts
|
|
4
|
+
const guardedTable = (served, models) => ({
|
|
5
|
+
name: served.table.name,
|
|
6
|
+
models: models.filter((model) => sameTable(model, served))
|
|
7
|
+
});
|
|
8
|
+
const encryptedColumns = (table) => new Set(table.models.flatMap((model) => Object.values(model.fields).filter((field) => field.encrypted).map((field) => field.column)));
|
|
9
|
+
const bindingColumns = (table) => table.models.filter((model) => Object.values(model.fields).some((field) => field.encrypted)).flatMap((model) => [...model.primaryKey, ...model.tenant ? [model.tenant] : []].map((name) => model.fields[name].column));
|
|
10
|
+
const refusal = (table, column, what) => restError(400, "encrypted-column", `Column '${column}' of '${table.name}' is encrypted: it cannot be ${what} here`, null, "Write and reveal encrypted fields through /api/v1");
|
|
11
|
+
const filterColumns = (filter) => filter.kind === "condition" ? [filter.field.column] : filter.args.flatMap(filterColumns);
|
|
12
|
+
/** Refuses filters, a sort or a join on an encrypted column, whose ciphertext no comparison matches. */
|
|
13
|
+
const refuseEncryptedReads = (table, filters, order, joined = []) => {
|
|
14
|
+
const encrypted = encryptedColumns(table);
|
|
15
|
+
const read = [
|
|
16
|
+
...filters.flatMap(filterColumns),
|
|
17
|
+
...order.map((term) => term.field.column),
|
|
18
|
+
...joined
|
|
19
|
+
].find((column) => encrypted.has(column));
|
|
20
|
+
if (read !== void 0) throw refusal(table, read, "filtered, sorted or joined on");
|
|
21
|
+
};
|
|
22
|
+
/** Refuses a write of an encrypted column, or matching an upsert's rows on one. */
|
|
23
|
+
const refuseEncryptedWrite = (table, columns) => {
|
|
24
|
+
const encrypted = encryptedColumns(table);
|
|
25
|
+
const written = columns.find((column) => encrypted.has(column));
|
|
26
|
+
if (written !== void 0) throw refusal(table, written, "written or matched on");
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* On a table with encrypted fields, refuses a write that changes the key or tenant of a row, which its encrypted values
|
|
30
|
+
* are bound to: an update's `columns`, or an upsert's beyond the `kept` ones it matches rows on and so writes unchanged.
|
|
31
|
+
*/
|
|
32
|
+
const refuseRebinding = (table, columns, kept = []) => {
|
|
33
|
+
const rebound = bindingColumns(table).find((column) => columns.includes(column) && !kept.includes(column));
|
|
34
|
+
if (rebound !== void 0) throw restError(400, "encrypted-column", `Column '${rebound}' of '${table.name}' cannot change here: its encrypted fields are bound to it`, null, "Change it through /api/v1");
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
export { guardedTable, refuseEncryptedReads, refuseEncryptedWrite, refuseRebinding };
|
|
@@ -7,6 +7,7 @@ import { parseQuery } from "./parse-query.js";
|
|
|
7
7
|
import { embedsOf, resolveShape } from "./embed-shape.js";
|
|
8
8
|
import { readRows } from "./read-rows.js";
|
|
9
9
|
import { asCaller, queryParams, readBody } from "./rest-context.js";
|
|
10
|
+
import { guardedTable, refuseEncryptedReads, refuseEncryptedWrite, refuseRebinding } from "./encrypted-columns.js";
|
|
10
11
|
import { scanRefusal, uncoveredReads } from "./rest-scan.js";
|
|
11
12
|
import { deleteRows, insertRows, updateRows } from "./write-rows.js";
|
|
12
13
|
import { Hono } from "hono";
|
|
@@ -35,27 +36,32 @@ const shapeOf = async (deps, foreignKeys, entry, query) => {
|
|
|
35
36
|
embedded: query.embedded
|
|
36
37
|
}, entry.model.table.name, query.select);
|
|
37
38
|
};
|
|
39
|
+
const guarded = (deps, entry) => guardedTable(entry.model, deps.registry.entries.map((one) => one.model));
|
|
38
40
|
const targetOf = (entry) => ({
|
|
39
41
|
table: tableId(entry.model),
|
|
40
42
|
alias: entry.model.table.name
|
|
41
43
|
});
|
|
44
|
+
/** What a request reads of the table and of every embedded one: its filters, its sort and the columns it joins on. */
|
|
45
|
+
const readsOf = (deps, entry, filters, order, shape) => [{
|
|
46
|
+
entry,
|
|
47
|
+
filters,
|
|
48
|
+
order,
|
|
49
|
+
joined: []
|
|
50
|
+
}, ...embedsOf(shape).map((embed) => ({
|
|
51
|
+
entry: tableEntry(deps, embed.table),
|
|
52
|
+
filters: embed.params.filters,
|
|
53
|
+
order: embed.params.order,
|
|
54
|
+
joined: embed.relationship.join.map((pair) => pair.target)
|
|
55
|
+
}))];
|
|
42
56
|
/**
|
|
43
|
-
* Refuses filters and a sort no index serves, as the scan guard
|
|
44
|
-
* one, whose join columns need an index too; in `warn` mode, a
|
|
57
|
+
* Refuses filters, a sort or a join on an encrypted column; then filters and a sort no index serves, as the scan guard
|
|
58
|
+
* does on `/api/v1`, on the table and on every embedded one, whose join columns need an index too; in `warn` mode, a
|
|
59
|
+
* warning header instead.
|
|
45
60
|
*/
|
|
46
61
|
const scanCheck = async (deps, entry, filters, order, shape = []) => {
|
|
62
|
+
const reads = readsOf(deps, entry, filters, order, shape);
|
|
63
|
+
for (const read of reads) refuseEncryptedReads(guarded(deps, read.entry), read.filters, read.order, read.joined);
|
|
47
64
|
if (deps.guard.mode === "off") return {};
|
|
48
|
-
const reads = [{
|
|
49
|
-
entry,
|
|
50
|
-
filters,
|
|
51
|
-
order,
|
|
52
|
-
joined: []
|
|
53
|
-
}, ...embedsOf(shape).map((embed) => ({
|
|
54
|
-
entry: tableEntry(deps, embed.table),
|
|
55
|
-
filters: embed.params.filters,
|
|
56
|
-
order: embed.params.order,
|
|
57
|
-
joined: embed.relationship.join.map((pair) => pair.target)
|
|
58
|
-
}))];
|
|
59
65
|
for (const read of reads) {
|
|
60
66
|
const [first] = uncoveredReads(await deps.guard.indexesOf(read.entry), read.filters, read.order, read.joined);
|
|
61
67
|
if (!first) continue;
|
|
@@ -130,6 +136,9 @@ const insert = async (deps, foreignKeys, c) => {
|
|
|
130
136
|
if (columns.length === 0 && rows.length > 1) throw restError(400, "PGRST102", "Several rows without any column cannot be inserted at once");
|
|
131
137
|
if (rows.length === 0 && object) checkSingle(0);
|
|
132
138
|
const conflict = query.onConflict ?? entry.model.primaryKey.map((name) => entry.model.fields[name].column);
|
|
139
|
+
const table = guarded(deps, entry);
|
|
140
|
+
refuseEncryptedWrite(table, prefer.resolution ? [...columns, ...conflict] : columns);
|
|
141
|
+
if (prefer.resolution === "merge-duplicates") refuseRebinding(table, columns, conflict);
|
|
133
142
|
const written = rows.length === 0 ? {
|
|
134
143
|
total: 0,
|
|
135
144
|
...prefer.return === "representation" && { body: "[]" }
|
|
@@ -154,6 +163,9 @@ const update = async (deps, foreignKeys, c) => {
|
|
|
154
163
|
const object = wantsObject(c.req.raw);
|
|
155
164
|
const body = await readBody(c);
|
|
156
165
|
if (!isRecord(body)) throw restError(400, "PGRST102", "Empty or invalid json");
|
|
166
|
+
const table = guarded(deps, entry);
|
|
167
|
+
refuseEncryptedWrite(table, Object.keys(body));
|
|
168
|
+
refuseRebinding(table, Object.keys(body));
|
|
157
169
|
const shape = await shapeOf(deps, foreignKeys, entry, query);
|
|
158
170
|
const warning = await scanCheck(deps, entry, query.filters, [], shape);
|
|
159
171
|
const written = Object.keys(body).length === 0 ? {
|
package/dist/reveal-service.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { badRequest, notFound } from "./problem.js";
|
|
2
2
|
import { tenantScope } from "./tenant.js";
|
|
3
3
|
import { requestAccess, requireOperation } from "./request-access.js";
|
|
4
|
+
import { bindingOf } from "./encryption/binding.js";
|
|
4
5
|
import { fetchReadable } from "./fetch-readable.js";
|
|
5
6
|
import { readTransaction } from "./transactions.js";
|
|
7
|
+
import { openText } from "./encryption/envelope-open.js";
|
|
6
8
|
import { encodeKey } from "@protobase/schema/internal";
|
|
7
9
|
//#region src/reveal-service.ts
|
|
10
|
+
const revealedValue = (deps, model, field, record) => {
|
|
11
|
+
const stored = record[field.name] ?? null;
|
|
12
|
+
if (!field.encrypted || stored === null) return stored;
|
|
13
|
+
if (!deps.encryption) throw new Error(`"${model.name}" has encrypted fields but no encryption runtime`);
|
|
14
|
+
return openText(deps.encryption.keyring, bindingOf(model, field, record), String(stored), {
|
|
15
|
+
resource: model.name,
|
|
16
|
+
field: field.name
|
|
17
|
+
});
|
|
18
|
+
};
|
|
8
19
|
/**
|
|
9
20
|
* The value of one sensitive field of one record, for a caller who may read the record and the field: the same tenant,
|
|
10
21
|
* row filter and record-level rule as a get. The reveal is published to the audit queue before the value is returned;
|
|
@@ -17,9 +28,12 @@ const revealField = async (deps, entry, session, request) => {
|
|
|
17
28
|
const field = Object.hasOwn(access.readable.fields, name) ? access.readable.fields[name] : void 0;
|
|
18
29
|
if (!field) throw badRequest("invalid-parameter", `Unknown field "${name}"`);
|
|
19
30
|
if (!field.sensitive) throw badRequest("invalid-parameter", `"${name}" is not sensitive; it comes with the record`);
|
|
20
|
-
const
|
|
21
|
-
const
|
|
31
|
+
const { full } = access;
|
|
32
|
+
const tenantField = full.fields[name].encrypted && full.tenant && !full.primaryKey.includes(full.tenant) ? [full.tenant] : [];
|
|
33
|
+
const keyFields = [...full.primaryKey, ...tenantField].map((part) => full.fields[part]);
|
|
34
|
+
const found = await readTransaction(deps.db, deps.statementTimeoutMs, (trx) => fetchReadable(trx, access, request.key, "hide", { fields: [...keyFields, full.fields[name]] }));
|
|
22
35
|
if (!found) throw notFound(`No ${entry.name} with this key`);
|
|
36
|
+
const value = revealedValue(deps, access.full, full.fields[name], found.record);
|
|
23
37
|
const tenant = tenantScope(entry, session)?.tenantValue;
|
|
24
38
|
await deps.audit.publish({
|
|
25
39
|
type: "field.revealed",
|
|
@@ -36,7 +50,7 @@ const revealField = async (deps, entry, session, request) => {
|
|
|
36
50
|
});
|
|
37
51
|
return {
|
|
38
52
|
field: name,
|
|
39
|
-
value
|
|
53
|
+
value
|
|
40
54
|
};
|
|
41
55
|
};
|
|
42
56
|
//#endregion
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,11 @@ export type Session = {
|
|
|
19
19
|
tenant?: TenantValue;
|
|
20
20
|
organization?: SessionOrganization;
|
|
21
21
|
anonymous?: true;
|
|
22
|
+
/** The outside app acting as the user, such as an MCP client: its OAuth client id and the scopes its token carries. */
|
|
23
|
+
client?: {
|
|
24
|
+
id: string;
|
|
25
|
+
scopes: string[];
|
|
26
|
+
};
|
|
22
27
|
};
|
|
23
28
|
/**
|
|
24
29
|
* Resolves the caller of a request, or throws a 401 problem (see `unauthorized`). `userIdType` declares the type of
|
package/dist/validate-body.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { maskEncrypted } from "./encryption/mask-stored.js";
|
|
1
2
|
import { badRequest } from "./problem.js";
|
|
2
3
|
import { writableFields } from "./request-access.js";
|
|
3
4
|
//#region src/validate-body.ts
|
|
@@ -31,7 +32,8 @@ const requiredOnCreate = (field, schema) => !(field.default && "db" in field.def
|
|
|
31
32
|
* for a text field that may not be null counts as not given: a create fills in its default or reports it required,
|
|
32
33
|
* an update reports it required. Only the values written are checked, so a stored '' still reads.
|
|
33
34
|
* Returns only the values to write: on create the defaults are filled in, on update only the provided fields.
|
|
34
|
-
* `current` is the stored record an update merges into for `.validate()` rules
|
|
35
|
+
* `current` is the stored record an update merges into for `.validate()` rules, which get a set encrypted field of it as
|
|
36
|
+
* `encrypted`: they see an encrypted value as text only when this write gives it.
|
|
35
37
|
*/
|
|
36
38
|
const validateBody = (access, mode, body, current = {}) => {
|
|
37
39
|
if (!isObject(body)) throw badRequest("invalid-record", "The request body must be a JSON object");
|
|
@@ -85,7 +87,7 @@ const validateBody = (access, mode, body, current = {}) => {
|
|
|
85
87
|
}
|
|
86
88
|
if (errors.length > 0) throw fail(errors);
|
|
87
89
|
const merged = {
|
|
88
|
-
...current,
|
|
90
|
+
...maskEncrypted(full, current),
|
|
89
91
|
...values
|
|
90
92
|
};
|
|
91
93
|
const issues = entry.source.validators.flatMap((validate) => validate(merged) ?? []);
|
|
@@ -40,7 +40,20 @@ const subscriptionEvent = (change, event, { fields, thin = false } = {}) => ({
|
|
|
40
40
|
transaction: change.transaction
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
|
-
const
|
|
43
|
+
const withUnknownEncrypted = (model, record) => {
|
|
44
|
+
const fields = Object.values(model.fields).filter((field) => field.encrypted);
|
|
45
|
+
if (fields.length === 0) return record;
|
|
46
|
+
const unknown = { ...record };
|
|
47
|
+
for (const field of fields) Object.defineProperty(unknown, field.name, {
|
|
48
|
+
enumerable: true,
|
|
49
|
+
get: () => {
|
|
50
|
+
throw new Error(`a subscription's access check cannot tell whether the encrypted field "${field.name}" is set`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return unknown;
|
|
54
|
+
};
|
|
55
|
+
const mayRead = async (access, captured) => {
|
|
56
|
+
const record = withUnknownEncrypted(access.full, captured);
|
|
44
57
|
const { resolved, session, entry } = access;
|
|
45
58
|
if (!resolved.operations.read) return false;
|
|
46
59
|
if (resolved.rowFilter.read && evaluateFilter(resolved.rowFilter.read, record) !== true) return false;
|