@protobase/server 0.57.6 → 0.58.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/create-admin.js +4 -0
- package/dist/encryption/decrypt-field-types.d.ts +25 -0
- package/dist/encryption/decrypt-field.js +63 -0
- package/dist/encryption/envelope-open.js +1 -1
- package/dist/functions/define-function.d.ts +9 -1
- package/dist/functions/routes.js +12 -1
- package/dist/index.d.ts +2 -1
- package/dist/jobs/job-types.d.ts +6 -0
- package/dist/jobs/run-job.js +5 -1
- package/dist/jobs/runtime.d.ts +6 -3
- package/dist/jobs/runtime.js +5 -2
- package/dist/package.js +1 -1
- package/dist/types.d.ts +14 -4
- package/package.json +4 -4
package/dist/create-admin.js
CHANGED
|
@@ -77,6 +77,7 @@ import { createFilesRuntime } from "./files/runtime.js";
|
|
|
77
77
|
import { fileRoutes } from "./files/routes.js";
|
|
78
78
|
import { formRoutes } from "./forms/routes.js";
|
|
79
79
|
import { createFormsRuntime } from "./forms/runtime.js";
|
|
80
|
+
import { fieldDecrypter } from "./encryption/decrypt-field.js";
|
|
80
81
|
import { createEncryptionRuntime } from "./encryption/runtime.js";
|
|
81
82
|
import { functionRoutes } from "./functions/routes.js";
|
|
82
83
|
import { supabaseFunctionRoutes } from "./functions/supabase-routes.js";
|
|
@@ -373,6 +374,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
373
374
|
sessionToken: betterAuthSessionToken(auth)
|
|
374
375
|
}
|
|
375
376
|
});
|
|
377
|
+
const decryptField = fieldDecrypter(deps);
|
|
376
378
|
app.route("/", functionRoutes({
|
|
377
379
|
basePath: `${systemPath}/functions`,
|
|
378
380
|
functions: apiFunctions,
|
|
@@ -380,6 +382,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
380
382
|
access: deps.accessOptions,
|
|
381
383
|
db,
|
|
382
384
|
records: (session) => callerRecords(deps, session),
|
|
385
|
+
decryptField,
|
|
383
386
|
onError,
|
|
384
387
|
wake: () => wakeDispatch(deps),
|
|
385
388
|
upgradeWebSocket,
|
|
@@ -469,6 +472,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
469
472
|
});
|
|
470
473
|
return Object.assign(app, {
|
|
471
474
|
startRealtimePrune: prune,
|
|
475
|
+
decryptField,
|
|
472
476
|
jobs: [
|
|
473
477
|
...webhooks.jobs,
|
|
474
478
|
...withHistory && days !== void 0 ? [pruneHistoryJob(changes, days)] : [],
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/encryption/decrypt-field-types.d.ts
|
|
2
|
+
/** One record's key: its value, or its parts in the order of the resource's primary key. */
|
|
3
|
+
export type RecordKey = string | number | readonly (string | number)[];
|
|
4
|
+
/** Reads one encrypted field of one record as its cleartext, or `null` when it is empty. */
|
|
5
|
+
export type DecryptField = (resource: string, key: RecordKey, field: string) => Promise<string | null>;
|
|
6
|
+
/**
|
|
7
|
+
* Who reads, as the audit names them: a job, or a function with its caller and the request's origin (`AuditOrigin`).
|
|
8
|
+
* Filled in by the runtime, never by app code.
|
|
9
|
+
*/
|
|
10
|
+
export type FieldReader = {
|
|
11
|
+
job: string;
|
|
12
|
+
} | {
|
|
13
|
+
function: string;
|
|
14
|
+
actor: {
|
|
15
|
+
id: string | number;
|
|
16
|
+
roles: string[];
|
|
17
|
+
};
|
|
18
|
+
origin: {
|
|
19
|
+
userAgent?: string;
|
|
20
|
+
forwardedFor?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/** The app's server-side read of encrypted fields, for the reader the runtime names. */
|
|
24
|
+
export type FieldDecrypter = (reader: FieldReader) => DecryptField;
|
|
25
|
+
//#endregion
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readTransaction } from "../transactions.js";
|
|
2
|
+
import { bindingOf } from "./binding.js";
|
|
3
|
+
import { fetchRecord } from "../records.js";
|
|
4
|
+
import { openText } from "./envelope-open.js";
|
|
5
|
+
import { encodeKey } from "@protobase/schema/internal";
|
|
6
|
+
//#region src/encryption/decrypt-field.ts
|
|
7
|
+
const eventOf = (reader, base) => {
|
|
8
|
+
if ("job" in reader) return {
|
|
9
|
+
...base,
|
|
10
|
+
reader: { job: reader.job },
|
|
11
|
+
origin: {}
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
...base,
|
|
15
|
+
actor: reader.actor,
|
|
16
|
+
reader: { function: reader.function },
|
|
17
|
+
origin: reader.origin
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The read `decryptField` of a job's and a protected function's context: privileged like their `db`, so no access rules
|
|
22
|
+
* or tenant scope, of a live (not soft-deleted) record. It opens the value as the reveal does, then publishes a
|
|
23
|
+
* `field.revealed` event naming the reader, and only returns the value once that is accepted: when publishing fails,
|
|
24
|
+
* so does the read.
|
|
25
|
+
*/
|
|
26
|
+
const fieldDecrypter = (deps) => (reader) => async (resource, key, field) => {
|
|
27
|
+
const entry = deps.registry.find(resource);
|
|
28
|
+
if (!entry) throw new Error(`Unknown resource "${resource}"`);
|
|
29
|
+
const { model } = entry;
|
|
30
|
+
const target = Object.hasOwn(model.fields, field) ? model.fields[field] : void 0;
|
|
31
|
+
if (!target?.encrypted) throw new Error(`"${resource}.${field}" is not an encrypted field`);
|
|
32
|
+
if (!deps.encryption) throw new Error(`"${resource}" has encrypted fields but no encryption runtime`);
|
|
33
|
+
const parts = Array.isArray(key) ? key : [key];
|
|
34
|
+
if (parts.length !== model.primaryKey.length) throw new Error(`A ${resource} key has ${model.primaryKey.length} parts, not ${parts.length}`);
|
|
35
|
+
const tenantField = model.tenant && !model.primaryKey.includes(model.tenant) ? [model.tenant] : [];
|
|
36
|
+
const fields = [
|
|
37
|
+
...model.primaryKey,
|
|
38
|
+
...tenantField,
|
|
39
|
+
field
|
|
40
|
+
].map((name) => model.fields[name]);
|
|
41
|
+
const found = await readTransaction(deps.db, deps.statementTimeoutMs, (trx) => fetchRecord(trx, entry, parts, void 0, {
|
|
42
|
+
deleted: "hide",
|
|
43
|
+
fields
|
|
44
|
+
}));
|
|
45
|
+
if (!found) throw new Error(`No ${resource} with key "${encodeKey(parts)}"`);
|
|
46
|
+
const stored = found.record[field] ?? null;
|
|
47
|
+
const value = stored === null ? null : openText(deps.encryption.keyring, bindingOf(model, target, found.record), String(stored), {
|
|
48
|
+
resource,
|
|
49
|
+
field
|
|
50
|
+
});
|
|
51
|
+
const tenant = model.tenant ? found.record[model.tenant] : void 0;
|
|
52
|
+
await deps.audit.publish(eventOf(reader, {
|
|
53
|
+
type: "field.revealed",
|
|
54
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
55
|
+
...tenant !== void 0 && tenant !== null && { tenant },
|
|
56
|
+
resource,
|
|
57
|
+
recordKey: encodeKey(parts),
|
|
58
|
+
field
|
|
59
|
+
}));
|
|
60
|
+
return value;
|
|
61
|
+
};
|
|
62
|
+
//#endregion
|
|
63
|
+
export { fieldDecrypter };
|
|
@@ -42,7 +42,7 @@ const decrypt = (aes, aad, bytes) => {
|
|
|
42
42
|
};
|
|
43
43
|
/**
|
|
44
44
|
* The plaintext of `stored`, sealed under `binding` with a key of `keyring`, or an `UnreadableValue` thrown with why it
|
|
45
|
-
* does not open. The only place, with the CLI's status and apply
|
|
45
|
+
* does not open. The only place, with the CLI's status and apply and the audited `decryptField`, that decrypts.
|
|
46
46
|
*/
|
|
47
47
|
const openText = (keyring, binding, stored, subject) => {
|
|
48
48
|
const parsed = parse(stored);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DecryptField } from "../encryption/decrypt-field-types.js";
|
|
1
2
|
import { Session } from "../types.js";
|
|
2
3
|
import { Caller } from "../api-keys/resolve-caller.js";
|
|
3
4
|
import { SocketEvents } from "./function-socket.js";
|
|
@@ -29,7 +30,14 @@ export type FunctionCaller = {
|
|
|
29
30
|
records: FunctionRecords;
|
|
30
31
|
};
|
|
31
32
|
/** What a function that needs a signed-in caller gets: the caller resolved before it runs. */
|
|
32
|
-
export type FunctionContext = PublicFunctionContext & FunctionCaller
|
|
33
|
+
export type FunctionContext = PublicFunctionContext & FunctionCaller & {
|
|
34
|
+
/**
|
|
35
|
+
* The cleartext of an `.encrypted()` field of one record, or `null` when it is empty: privileged like `db`, with no
|
|
36
|
+
* access rules or tenant scope. Each read is audited as a reveal, naming this function and its caller, and fails when
|
|
37
|
+
* the audit does. Not on a public function's context: a public function that needs a value enqueues a job.
|
|
38
|
+
*/
|
|
39
|
+
decryptField: DecryptField;
|
|
40
|
+
};
|
|
33
41
|
/** What a function gets at `/api/sb/functions/v1/<name>`, where supabase-js calls it: the caller its keys and token make. */
|
|
34
42
|
export type KeyFunctionContext = {
|
|
35
43
|
name: string;
|
package/dist/functions/routes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { forbidden, notFound } from "../problem.js";
|
|
2
2
|
import { namingTimeouts } from "../statement-timeout.js";
|
|
3
|
+
import { auditOrigin } from "../audit/origin.js";
|
|
3
4
|
import { invokeFunction } from "./define-function.js";
|
|
4
5
|
import { runInBackground } from "../background-work.js";
|
|
5
6
|
import { socketAcceptor } from "./accept-socket.js";
|
|
@@ -22,7 +23,7 @@ const isUpgrade = (request) => request.headers.get("upgrade")?.toLowerCase() ===
|
|
|
22
23
|
* A function may answer an upgrade with a socket, only once it returned. While it is open, its caller is checked again
|
|
23
24
|
* every minute: a protected function's always, a public one's once `caller()` resolved.
|
|
24
25
|
*/
|
|
25
|
-
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, onError, wake, upgradeWebSocket, upgradeAuthenticate = authenticate, report }) => {
|
|
26
|
+
const functionRoutes = ({ basePath, functions, authenticate, access, db, records, decryptField, onError, wake, upgradeWebSocket, upgradeAuthenticate = authenticate, report }) => {
|
|
26
27
|
const app = new Hono().basePath(basePath);
|
|
27
28
|
app.onError(onError);
|
|
28
29
|
const sessionOf = (request) => isUpgrade(request) ? upgradeAuthenticate(request) : authenticate(request);
|
|
@@ -79,11 +80,21 @@ const functionRoutes = ({ basePath, functions, authenticate, access, db, records
|
|
|
79
80
|
report,
|
|
80
81
|
recheck
|
|
81
82
|
});
|
|
83
|
+
const { user } = caller.session;
|
|
84
|
+
const reader = {
|
|
85
|
+
function: name,
|
|
86
|
+
actor: {
|
|
87
|
+
id: user.id,
|
|
88
|
+
roles: [...user.roles]
|
|
89
|
+
},
|
|
90
|
+
origin: auditOrigin(c.req.raw.headers)
|
|
91
|
+
};
|
|
82
92
|
const context = {
|
|
83
93
|
name,
|
|
84
94
|
db,
|
|
85
95
|
...caller,
|
|
86
96
|
caller: async () => caller,
|
|
97
|
+
decryptField: decryptField(reader),
|
|
87
98
|
acceptWebSocket: acceptor.accept
|
|
88
99
|
};
|
|
89
100
|
return acceptor.answer(name, await invokeFunction(name, fn, request, context));
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { AppIdentity } from "./app-identity.js";
|
|
|
3
3
|
import { AppIcons, PngIcon, pngIcons } from "./web-app/app-icons.js";
|
|
4
4
|
import { RealtimeOptions, relayed } from "./realtime/relay/realtime-options.js";
|
|
5
5
|
import { EventsOptions } from "./changes/events-options.js";
|
|
6
|
+
import { DecryptField, FieldDecrypter, FieldReader, RecordKey } from "./encryption/decrypt-field-types.js";
|
|
6
7
|
import { StandardSchema, readJson } from "./functions/read-json.js";
|
|
7
8
|
import { MailMessage, Mailer } from "./mail/smtp-mailer.js";
|
|
8
9
|
import { JsonObject, JsonValue } from "./notifications/json-value.js";
|
|
@@ -169,4 +170,4 @@ import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/ru
|
|
|
169
170
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
170
171
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
171
172
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, normalizeRoles } from "./better-auth/create-auth.js";
|
|
172
|
-
export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BatchOperation, type BatchResult, 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 CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, 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 FileProviderSettings, 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 HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, 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 SendNotification, 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 StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
|
173
|
+
export { type Admin, type AdminAuth, type AdminOptions, type AggregateParams, type ApiFunction, type ApiKeys, type ApiKeysOptions, type ApnsEnvironment, type AppIcons, type AppIdentity, type AppPoolConfig, type ApplyOptions, type ApplyResult, type ApprovalAnswer, type ApprovalRequest, type ArgumentsPayload, type AssistantBackend, type AssistantOptions, type AssistantSettings, type AssistantTool, type AuditEvent, type AuditQueue, type AuthHooks, type AuthSettings, type AuthenticateSettings, type Authenticator, type BatchOperation, type BatchResult, 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 CommentToolsInput, type CommentUsers, type Completion, type CompletionOptions, ConnectionsBusy, type Conversation, type ConversationRecord, type ConversationStore, type ConversationsOptions, type CorsOptions, type CorsOrigins, type CreateAdminInput, type CreateAuthOptions, type DecryptField, type Device, type DeviceColumn, type DeviceKind, type DeviceStore, DeviceStoreError, type DrainResult, type EmailComponent, type EmailOptions, type EmailTemplate, type EncryptionStatus, type EncryptionTools, type EncryptionToolsInput, type EnqueueJob, type EnqueueOptions, type ErrorReporter, type ErrorReportingOptions, type ErrorReportingSettings, type EventData, type EventsOptions, FatalJobError, type FieldApplied, type FieldDecrypter, type FieldReader, type FieldStatus, type FileCleanupInput, type FileLimits, type FileObject, type FileProvider, type FileProviderSettings, 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 HistoryColumn, type HistoryOptions, type HistoryPosition, type HistoryStore, HistoryStoreError, type HookContext, HttpProblem, type IssueTokenOptions, type JobContext, type JobDeclaration, type JobDefinition, type JobName, type JobOptions, type JobPayload, type JobRun, type JobsRuntime, type JsonObject, type JsonValue, type JwtOptions, type JwtVerifyOptions, type KeyFunctionContext, type LinkedAccount, type ListParams, type LocalFilesOptions, type MailMessage, type Mailer, type MapLayer, type MapOptions, type MapSettings, type McpApprovals, type McpGateInput, type McpHandlerInput, type McpRoutesInput, type McpServerInput, type McpToolsInput, type McpWrites, type MemberInput, type Membership, MissingCredentials, type MobileConfig, ModelError, type ModelSettings, type NewDevice, type NewNotification, type NewOrganization, type NewUser, type NotificationRecipients, NotificationSendError, type NotificationsOptions, type OperatorProvider, type OrganizationRole, type OrganizationsOptions, type PgModule, type PipelineHook, type PlatformSignIn, type PluginOption, type PluginRef, type PngIcon, type ProtectedFunctionOptions, type PublicFunctionContext, type PublicFunctionEnv, type PublicFunctionHandler, type PublicFunctionOptions, type PushAnswer, type PushMessage, PushMessageError, type PushSender, type QueryOptions, type QueryResult, type ReadableResources, type RealtimeOptions, type RealtimePrune, type ReasoningDetail, type ReasoningEffort, type RebindOption, type RecordGroups, type RecordKey, 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 SendNotification, 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 StoredNotification, type StoredObject, type StoredOrganization, type StoredUser, type StrayValues, type SubscriptionDefinition, type SubscriptionEvent, type SubscriptionOptions, type TenantValue, type ToolAnnotations, type ToolCall, type ToolComment, type ToolCommentPage, type ToolComments, type ToolContext, type ToolRecord, type ToolRecords, type ToolSpec, type TrustedApp, type TurnOptions, type UnreadableEntry, type Urgency, type User, type UserCreatedHook, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WriteEvent, type WriteOperation, type WriteRecordToolsInput, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, describeResourceTool, deviceIdParam, deviceStore, devicesPath, enqueueJob, errorReportingSettings, errorReportingVariable, eventMatcher, fileConversationStore, fileSize, fileType, forbidden, formatUri, functionApp, generateVapidKeys, getRecordTool, goTruePath, handleDroppedConnections, hasUsers, historyIdParam, historyIdPattern, historyLimits, historyPath, historyStore, honorSocketMessageCaps, imageSize, isBackgroundWork, isCaptured, isJobDefinition, isOAuthWellKnownPath, isSchedule, issueToken, jobDefinitions, jobsDrainApp, jobsSchema, jobsSchemaMigration, jwtAuthenticator, jwtVerifier, limitedFileStore, listCommentsTool, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpIcons, mcpInstructions, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, relayed, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
|
package/dist/jobs/job-types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DecryptField } from "../encryption/decrypt-field-types.js";
|
|
1
2
|
import { StandardSchema } from "../functions/read-json.js";
|
|
2
3
|
import { Mailer } from "../mail/smtp-mailer.js";
|
|
3
4
|
import { SendNotification } from "../notifications/send-input.js";
|
|
@@ -17,6 +18,11 @@ export type JobContext = {
|
|
|
17
18
|
};
|
|
18
19
|
/** Aborted when the job's timeout passes, or when the worker shuts down and its grace period ends. */
|
|
19
20
|
signal: AbortSignal;
|
|
21
|
+
/**
|
|
22
|
+
* The cleartext of an `.encrypted()` field of one record, or `null` when it is empty: privileged like `db`, with no
|
|
23
|
+
* access rules or tenant scope. Each read is audited as a reveal, naming this job, and fails when the audit does.
|
|
24
|
+
*/
|
|
25
|
+
decryptField: DecryptField;
|
|
20
26
|
/** Sends mail with the app's mailer: its auth/mailer.ts, else the SMTP server of `PROTOBASE_SMTP_URL` and `PROTOBASE_MAIL_FROM`; `undefined` when the app has no mail. */
|
|
21
27
|
mail?: Mailer;
|
|
22
28
|
/** Sends a notification as `sendNotification` does; `undefined` when the app has no `notifications`. */
|
package/dist/jobs/run-job.js
CHANGED
|
@@ -49,11 +49,14 @@ const namedTimeout = (definition, error) => {
|
|
|
49
49
|
if (!isStatementTimeout(error)) return error;
|
|
50
50
|
return new Error(statementTimeoutDetail(`Job "${definition.name}"`, { kind: definition.options.longQueries ? "long" : "database" }), { cause: error });
|
|
51
51
|
};
|
|
52
|
+
const noDecrypter = (name) => async () => {
|
|
53
|
+
throw new Error(`Job "${name}" cannot decrypt fields: startJobs was not given the app's decryptField`);
|
|
54
|
+
};
|
|
52
55
|
/**
|
|
53
56
|
* One try of one job: completed when `run` returns, failed (to be retried while attempts are left) when it throws, and
|
|
54
57
|
* failed for good on a `FatalJobError` or an invalid payload.
|
|
55
58
|
*/
|
|
56
|
-
const runJob = async (definition, job, db, { mail, sendNotification } = {}) => {
|
|
59
|
+
const runJob = async (definition, job, db, { mail, sendNotification, decryptField } = {}) => {
|
|
57
60
|
try {
|
|
58
61
|
const payload = await checkPayload(definition, job.data ?? void 0);
|
|
59
62
|
const context = {
|
|
@@ -66,6 +69,7 @@ const runJob = async (definition, job, db, { mail, sendNotification } = {}) => {
|
|
|
66
69
|
startedOn: job.startedOn
|
|
67
70
|
},
|
|
68
71
|
signal: job.signal,
|
|
72
|
+
decryptField: decryptField ? decryptField({ job: definition.name }) : noDecrypter(definition.name),
|
|
69
73
|
...mail && { mail },
|
|
70
74
|
...sendNotification && { sendNotification }
|
|
71
75
|
};
|
package/dist/jobs/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FieldDecrypter } from "../encryption/decrypt-field-types.js";
|
|
1
2
|
import { Mailer } from "../mail/smtp-mailer.js";
|
|
2
3
|
import { JobDefinition } from "./job-types.js";
|
|
3
4
|
import { JobsEnqueuer } from "./current.js";
|
|
@@ -21,6 +22,8 @@ export type StartJobsOptions = {
|
|
|
21
22
|
* `PROTOBASE_MAIL_FROM`, when set; `false` turns mail off whatever the environment says.
|
|
22
23
|
*/
|
|
23
24
|
mailer?: Mailer | false;
|
|
25
|
+
/** The app's read of encrypted fields (`createAdmin(...).decryptField`), which each job's context binds to the job. */
|
|
26
|
+
decryptField?: FieldDecrypter;
|
|
24
27
|
};
|
|
25
28
|
/** What a drain left: whether no job is due or running any more, how many are, and when the next one is due. */
|
|
26
29
|
export type DrainResult = {
|
|
@@ -31,8 +34,8 @@ export type DrainResult = {
|
|
|
31
34
|
export type JobsRuntime = JobsEnqueuer & {
|
|
32
35
|
/** Whether this process runs the jobs, or only enqueues them. */
|
|
33
36
|
readonly works: boolean;
|
|
34
|
-
/** Takes the project's jobs again, as `protobase dev` does when their files change
|
|
35
|
-
update(jobs: JobDefinition[]): Promise<void>;
|
|
37
|
+
/** Takes the project's jobs again, as `protobase dev` does when their files change, with the rebuilt app's `decryptField`. */
|
|
38
|
+
update(jobs: JobDefinition[], decryptField?: FieldDecrypter): Promise<void>;
|
|
36
39
|
/**
|
|
37
40
|
* Runs the jobs that are due until none is due or running, or `seconds` have passed: for a host that wakes a sleeping
|
|
38
41
|
* app to run its jobs. Needs this process to run jobs.
|
|
@@ -48,5 +51,5 @@ export type JobsRuntime = JobsEnqueuer & {
|
|
|
48
51
|
* that retries jobs whose worker died. It reads the jobs schema, never changes it: check it first with
|
|
49
52
|
* `checkJobsSchema`. Enqueueing through `defineJob(...).enqueue` reaches the runtime started here.
|
|
50
53
|
*/
|
|
51
|
-
export declare const startJobs: ({ db, jobs, work, pollSeconds, shutdownSeconds, onError, now, mailer }: StartJobsOptions) => Promise<JobsRuntime>;
|
|
54
|
+
export declare const startJobs: ({ db, jobs, work, pollSeconds, shutdownSeconds, onError, now, mailer, decryptField }: StartJobsOptions) => Promise<JobsRuntime>;
|
|
52
55
|
//#endregion
|
package/dist/jobs/runtime.js
CHANGED
|
@@ -30,8 +30,9 @@ const createBoss = (db, work) => new PgBoss({
|
|
|
30
30
|
* that retries jobs whose worker died. It reads the jobs schema, never changes it: check it first with
|
|
31
31
|
* `checkJobsSchema`. Enqueueing through `defineJob(...).enqueue` reaches the runtime started here.
|
|
32
32
|
*/
|
|
33
|
-
const startJobs = async ({ db, jobs, work = true, pollSeconds = 2, shutdownSeconds = 30, onError, now = () => /* @__PURE__ */ new Date(), mailer }) => {
|
|
33
|
+
const startJobs = async ({ db, jobs, work = true, pollSeconds = 2, shutdownSeconds = 30, onError, now = () => /* @__PURE__ */ new Date(), mailer, decryptField }) => {
|
|
34
34
|
const mail = mailer === void 0 ? platformMailer() : mailer || void 0;
|
|
35
|
+
let decrypter = decryptField;
|
|
35
36
|
const database = busyDatabase(jobsDatabase(db));
|
|
36
37
|
const boss = createBoss(database.db, work);
|
|
37
38
|
boss.on("error", onError);
|
|
@@ -60,6 +61,7 @@ const startJobs = async ({ db, jobs, work = true, pollSeconds = 2, shutdownSecon
|
|
|
60
61
|
if (current.options.schedule !== void 0 && job.singletonKey === "protobase.schedule") await schedules.following(current, job.startAfter);
|
|
61
62
|
return [await runJob(current, job, db, {
|
|
62
63
|
mail,
|
|
64
|
+
...decrypter && { decryptField: decrypter },
|
|
63
65
|
...definitions.has("notifications.send") && { sendNotification }
|
|
64
66
|
})];
|
|
65
67
|
}, { longQueries: current.options.longQueries });
|
|
@@ -69,7 +71,8 @@ const startJobs = async ({ db, jobs, work = true, pollSeconds = 2, shutdownSecon
|
|
|
69
71
|
concurrency
|
|
70
72
|
});
|
|
71
73
|
};
|
|
72
|
-
const update = async (next) => {
|
|
74
|
+
const update = async (next, nextDecrypter) => {
|
|
75
|
+
if (nextDecrypter) decrypter = nextDecrypter;
|
|
73
76
|
for (const definition of next) await ensureQueue(definition);
|
|
74
77
|
for (const name of definitions.keys()) {
|
|
75
78
|
if (next.some((definition) => definition.name === name)) continue;
|
package/dist/package.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FieldDecrypter } from "./encryption/decrypt-field-types.js";
|
|
1
2
|
import { JobDefinition } from "./jobs/job-types.js";
|
|
2
3
|
import { RealtimePrune } from "./realtime/realtime-prune.js";
|
|
3
4
|
import { Hono } from "hono";
|
|
@@ -95,11 +96,18 @@ export type FieldRevealedEvent = {
|
|
|
95
96
|
type: 'field.revealed';
|
|
96
97
|
/** ISO 8601, when the server answered. */
|
|
97
98
|
at: string;
|
|
98
|
-
|
|
99
|
+
/** The caller; absent when a job read the value (`reader.job`). */
|
|
100
|
+
actor?: {
|
|
99
101
|
id: string | number;
|
|
100
102
|
roles: string[];
|
|
101
103
|
};
|
|
102
|
-
/** The
|
|
104
|
+
/** The job or API function that read the value with `decryptField`; absent for a reveal. */
|
|
105
|
+
reader?: {
|
|
106
|
+
job: string;
|
|
107
|
+
} | {
|
|
108
|
+
function: string;
|
|
109
|
+
};
|
|
110
|
+
/** The caller's tenant (organization), or the record's for `decryptField`, when the resource has one. */
|
|
103
111
|
tenant?: TenantValue;
|
|
104
112
|
resource: string;
|
|
105
113
|
/** The record's key as its URL has it (composite keys joined with a comma). */
|
|
@@ -137,11 +145,13 @@ export type PageSource = {
|
|
|
137
145
|
toModel(): PageModel;
|
|
138
146
|
};
|
|
139
147
|
/**
|
|
140
|
-
* The admin API, the jobs it needs a worker to run (the delivery of resource events),
|
|
141
|
-
* tables a serving process starts once it listens and stops as it closes
|
|
148
|
+
* The admin API, the jobs it needs a worker to run (the delivery of resource events), the pruning of the realtime
|
|
149
|
+
* tables a serving process starts once it listens and stops as it closes, and the read of encrypted fields that
|
|
150
|
+
* `startJobs` gives the jobs' context.
|
|
142
151
|
*/
|
|
143
152
|
export type Admin = Hono<AdminEnv> & {
|
|
144
153
|
jobs: JobDefinition[];
|
|
145
154
|
startRealtimePrune: () => RealtimePrune;
|
|
155
|
+
decryptField: FieldDecrypter;
|
|
146
156
|
};
|
|
147
157
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"@better-auth/passkey": "^1.7.12",
|
|
42
42
|
"@better-auth/sso": "^1.7.12",
|
|
43
43
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
44
|
-
"@protobase/layout": "0.
|
|
45
|
-
"@protobase/query": "0.
|
|
46
|
-
"@protobase/schema": "0.
|
|
44
|
+
"@protobase/layout": "0.58.0",
|
|
45
|
+
"@protobase/query": "0.58.0",
|
|
46
|
+
"@protobase/schema": "0.58.0",
|
|
47
47
|
"@react-email/render": "^2.1.0",
|
|
48
48
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
49
49
|
"@sentry/core": "^11.7.0",
|