@protobase/server 0.68.4 → 0.68.6
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 +11 -1
- package/dist/forms/rate-limit.js +33 -11
- package/dist/forms/routes.js +1 -1
- package/dist/functions/verify-signature.js +4 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -2
- package/dist/locale/en.js +3 -0
- package/dist/locale/nl.js +3 -0
- package/dist/mcp/mcp-tools.d.ts +2 -1
- package/dist/mcp/mcp-tools.js +4 -2
- package/dist/package.js +1 -1
- package/dist/user-tables/definitions-resources.js +1 -1
- package/dist/user-tables/definitions-views.js +1 -1
- package/dist/user-tables/schema-sql.js +6 -1
- package/dist/user-tables/schema.js +6 -4
- package/dist/workflows/hooks/hook-auth.js +40 -0
- package/dist/workflows/hooks/hook-event.js +51 -0
- package/dist/workflows/hooks/hook-paths.d.ts +16 -0
- package/dist/workflows/hooks/hook-paths.js +32 -0
- package/dist/workflows/hooks/hook-route.js +99 -0
- package/dist/workflows/hooks/hook-runs.js +32 -0
- package/dist/workflows/hooks/hook-secrets.js +42 -0
- package/dist/workflows/hooks/hook-store.js +35 -0
- package/dist/workflows/hooks/hooks-sql.js +20 -0
- package/dist/workflows/hooks/issue-body.js +21 -0
- package/dist/workflows/hooks/issue-route.js +23 -0
- package/dist/workflows/hooks/issue-webhook.js +63 -0
- package/dist/workflows/hooks/read-body.js +32 -0
- package/dist/workflows/mcp/webhook-tools.d.ts +9 -0
- package/dist/workflows/mcp/webhook-tools.js +96 -0
- package/dist/workflows/mcp/workflow-instructions.js +1 -1
- package/dist/workflows/mcp/workflow-read-tools.d.ts +3 -0
- package/dist/workflows/plugin-records.js +1 -1
- package/dist/workflows/plugin-services.js +1 -1
- package/dist/workflows/ready/ready-start.js +1 -1
- package/dist/workflows/run-files.js +7 -1
- package/dist/workflows/schema-sql.js +15 -3
- package/dist/workflows/schema.js +3 -1
- package/dist/workflows/start-workflow.d.ts +3 -1
- package/dist/workflows/start-workflow.js +10 -4
- package/dist/workflows/triggers/delete-hook.js +15 -0
- package/dist/workflows/triggers/plugin-call.js +35 -0
- package/dist/workflows/triggers/poll-job.js +120 -0
- package/dist/workflows/triggers/poll-result.js +32 -0
- package/dist/workflows/triggers/poll-schedule.js +32 -0
- package/dist/workflows/triggers/trigger-claim.js +81 -0
- package/dist/workflows/triggers/trigger-jobs.js +37 -7
- package/dist/workflows/triggers/trigger-start.js +101 -0
- package/dist/workflows/workflow-engine.js +38 -5
- package/dist/workflows/workflow-types.d.ts +4 -1
- package/dist/workflows/workflow-types.js +9 -3
- package/package.json +6 -6
package/dist/create-admin.js
CHANGED
|
@@ -59,6 +59,8 @@ import { toApiFunction } from "./functions/define-function.js";
|
|
|
59
59
|
import { createWebhooks } from "./webhooks/webhooks.js";
|
|
60
60
|
import { webhookTable } from "./webhooks/table/webhook-table.js";
|
|
61
61
|
import { subscriptionUrlCheck, subscriptionWriteCheck } from "./webhooks/table/validate-hook.js";
|
|
62
|
+
import { hooksBase } from "./workflows/hooks/hook-paths.js";
|
|
63
|
+
import { issueWebhook } from "./workflows/hooks/issue-webhook.js";
|
|
62
64
|
import { workflowEngine } from "./workflows/workflow-engine.js";
|
|
63
65
|
import { workflowTypesRoute } from "./workflows/workflow-types-route.js";
|
|
64
66
|
import { callsService } from "./calls/calls-service.js";
|
|
@@ -387,7 +389,13 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
|
|
|
387
389
|
sql: options.mcp !== false && options.mcp?.sql === true,
|
|
388
390
|
writes,
|
|
389
391
|
commented,
|
|
390
|
-
...engine && { workflows:
|
|
392
|
+
...engine && { workflows: {
|
|
393
|
+
...engine.types,
|
|
394
|
+
issueWebhook: (session, id, secret) => issueWebhook(engine.issue, session, id, {
|
|
395
|
+
...secret !== void 0 && { secret },
|
|
396
|
+
origin: new URL(auth.mcp.resource).origin
|
|
397
|
+
})
|
|
398
|
+
} },
|
|
391
399
|
own: options.mcp !== false && options.mcp?.tools || []
|
|
392
400
|
});
|
|
393
401
|
const instructions = engine ? `${mcpInstructions}\n${workflowInstructions}` : mcpInstructions;
|
|
@@ -555,7 +563,9 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
|
|
|
555
563
|
}));
|
|
556
564
|
}
|
|
557
565
|
}
|
|
566
|
+
if (engine) app.route(hooksBase(systemPath), engine.routes.hooks());
|
|
558
567
|
if (appModels.some((model) => model.live) || deps.calls) app.route(systemPath || "/", guarded((live) => void live.route("/", liveRoutes(feed, (session) => visibleNotices(deps, session))), withAnonymous(authenticate, options.roles)));
|
|
568
|
+
if (engine) app.route(`${systemPath}/workflows`, guarded((routes) => void routes.route("/", engine.routes.issue())));
|
|
559
569
|
app.route(basePath, api);
|
|
560
570
|
app.route(`${deps.systemPath}/meta`, metaApp);
|
|
561
571
|
app.route(systemPath || "/", webManifestRoute(appName, identity, icons));
|
package/dist/forms/rate-limit.js
CHANGED
|
@@ -8,32 +8,54 @@ const windowMs = 6e4;
|
|
|
8
8
|
* sent). In development and tests a request without one is `127.0.0.1`.
|
|
9
9
|
*/
|
|
10
10
|
const clientAddress = (headers) => getIP(headers, {}) ?? "unknown";
|
|
11
|
+
/** The most keys a limit counts at once: a new key past it forgets the one whose window began first. */
|
|
12
|
+
const rateLimitKeys = 1e4;
|
|
11
13
|
/**
|
|
12
14
|
* At most `perMinute` takes per key in a fixed window of a minute, kept in this process. Keys whose window has passed are
|
|
13
|
-
* dropped as new ones come,
|
|
15
|
+
* dropped as new ones come, and at most `maxKeys` are kept, the oldest window dropped first, so memory stays bounded
|
|
16
|
+
* however many clients come within a minute.
|
|
14
17
|
*/
|
|
15
|
-
const createRateLimit = ({ now = Date.now } = {}) => {
|
|
18
|
+
const createRateLimit = ({ now = Date.now, maxKeys = rateLimitKeys } = {}) => {
|
|
16
19
|
const windows = /* @__PURE__ */ new Map();
|
|
17
20
|
const prune = (at) => {
|
|
18
|
-
for (const [key, window] of windows)
|
|
21
|
+
for (const [key, window] of windows) {
|
|
22
|
+
if (at - window.start < windowMs) return;
|
|
23
|
+
windows.delete(key);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const open = (key, at) => {
|
|
27
|
+
prune(at);
|
|
28
|
+
windows.delete(key);
|
|
29
|
+
if (windows.size >= maxKeys) windows.delete(windows.keys().next().value);
|
|
30
|
+
windows.set(key, {
|
|
31
|
+
start: at,
|
|
32
|
+
count: 1
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
const wait = (key, perMinute, at) => {
|
|
36
|
+
const current = windows.get(key);
|
|
37
|
+
if (!current || at - current.start >= windowMs || current.count < perMinute) return 0;
|
|
38
|
+
return Math.ceil((current.start + windowMs - at) / 1e3);
|
|
19
39
|
};
|
|
20
40
|
/** Counts a take for `key`: `0` when allowed, else the seconds until its window lets it through again. */
|
|
21
41
|
const take = (key, perMinute) => {
|
|
22
42
|
const at = now();
|
|
23
43
|
const current = windows.get(key);
|
|
24
44
|
if (!current || at - current.start >= windowMs) {
|
|
25
|
-
|
|
26
|
-
windows.set(key, {
|
|
27
|
-
start: at,
|
|
28
|
-
count: 1
|
|
29
|
-
});
|
|
45
|
+
open(key, at);
|
|
30
46
|
return 0;
|
|
31
47
|
}
|
|
32
|
-
|
|
48
|
+
const seconds = wait(key, perMinute, at);
|
|
49
|
+
if (seconds > 0) return seconds;
|
|
33
50
|
current.count += 1;
|
|
34
51
|
return 0;
|
|
35
52
|
};
|
|
36
|
-
|
|
53
|
+
/** Whether `key` has spent its `perMinute` takes, without taking one: the seconds to wait, or `0`. */
|
|
54
|
+
const spent = (key, perMinute) => wait(key, perMinute, now());
|
|
55
|
+
return {
|
|
56
|
+
take,
|
|
57
|
+
spent
|
|
58
|
+
};
|
|
37
59
|
};
|
|
38
60
|
//#endregion
|
|
39
|
-
export { clientAddress, createRateLimit };
|
|
61
|
+
export { clientAddress, createRateLimit, rateLimitKeys };
|
package/dist/forms/routes.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { HttpProblem, badRequest, notFound } from "../problem.js";
|
|
2
2
|
import { runWrite } from "../write-pipeline.js";
|
|
3
3
|
import { anonymousSession } from "../anonymous.js";
|
|
4
|
-
import { pruneView } from "../view-access.js";
|
|
5
4
|
import { clientAddress } from "./rate-limit.js";
|
|
5
|
+
import { pruneView } from "../view-access.js";
|
|
6
6
|
import { Hono } from "hono";
|
|
7
7
|
import { restrictModel } from "@protobase/schema";
|
|
8
8
|
//#region src/forms/routes.ts
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HttpProblem } from "../problem.js";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
3
|
//#region src/functions/verify-signature.ts
|
|
3
4
|
const invalid = (detail) => new HttpProblem(401, "invalid-signature", "Unauthorized", detail);
|
|
4
5
|
const decodeHex = (text) => /^(?:[0-9a-f]{2})+$/i.test(text) ? new Uint8Array(text.match(/../g).map((pair) => parseInt(pair, 16))) : void 0;
|
|
@@ -7,8 +8,9 @@ const verifyHmac = async (key, hash, signature, content) => {
|
|
|
7
8
|
const cryptoKey = await crypto.subtle.importKey("raw", key, {
|
|
8
9
|
name: "HMAC",
|
|
9
10
|
hash
|
|
10
|
-
}, false, ["
|
|
11
|
-
|
|
11
|
+
}, false, ["sign"]);
|
|
12
|
+
const expected = new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(content)));
|
|
13
|
+
return signature.length === expected.length && timingSafeEqual(signature, expected);
|
|
12
14
|
};
|
|
13
15
|
/**
|
|
14
16
|
* The request's body as text, once its HMAC signature checks out against `secret`, as GitHub, Shopify and many others
|
package/dist/index.d.ts
CHANGED
|
@@ -104,6 +104,7 @@ import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
|
|
|
104
104
|
import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
|
|
105
105
|
import { checkWorkflowsSchema, workflowsMigration } from "./workflows/schema.js";
|
|
106
106
|
import { StartWorkflowOptions, startWorkflow } from "./workflows/start-workflow.js";
|
|
107
|
+
import { hookPath, redactHookKeys, webhookIssuePath, webhookParams } from "./workflows/hooks/hook-paths.js";
|
|
107
108
|
import { checkUserTablesSchema, userTablesMigration } from "./user-tables/schema.js";
|
|
108
109
|
import { ownedDefinitionsMigration } from "./user-tables/owned-definitions.js";
|
|
109
110
|
import { UserTables, userTableResources } from "./user-tables/user-table-resources.js";
|
|
@@ -196,4 +197,4 @@ import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/ru
|
|
|
196
197
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
197
198
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
198
199
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, normalizeRoles } from "./better-auth/create-auth.js";
|
|
199
|
-
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 BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type CallBackend, type CallMember, type Caller, type CallerOptions, type CallsAccess, type CallsOptions, type CapturedRow, type CascadingKey, type ChannelBroadcast, type ChannelSender, 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 LiveKitBackend, type LocalFilesOptions, type MailAttachment, type MailMessage, MailServerTimeoutError, 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 PdfComponent, type PdfOptions, type PdfTemplate, PersonalMailError, type PersonalMailErrorCode, type PersonalMailProviderId, 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 StartWorkflowOptions, type StatusOptions, 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 UserTables, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WorkflowCatalog, type WorkflowTypes, type WorkflowsOptions, 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, callsMigration, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkCalls, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkUserTablesSchema, checkViewQueries, checkWebhooksSchema, checkWorkflowsSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, definePdf, 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, onDoNotDisturb, ownedDefinitionsMigration, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readDefinitionsVersion, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, reloadingAdmin, 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, sqlSends, startErrorReporting, startJobs, startWorkflow, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, userTableResources, userTablesMigration, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, workflowInstructions, workflowReadTools, workflowTypes, workflowWriteTools, workflowsMigration, writeRecordTools };
|
|
200
|
+
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 BaseToolContext, type BatchOperation, type BatchResult, type BetterAuthAuthenticator, type BetterAuthAuthenticatorOptions, type CallBackend, type CallMember, type Caller, type CallerOptions, type CallsAccess, type CallsOptions, type CapturedRow, type CascadingKey, type ChannelBroadcast, type ChannelSender, 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 LiveKitBackend, type LocalFilesOptions, type MailAttachment, type MailMessage, MailServerTimeoutError, 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 PdfComponent, type PdfOptions, type PdfTemplate, PersonalMailError, type PersonalMailErrorCode, type PersonalMailProviderId, 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 StartWorkflowOptions, type StatusOptions, 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 UserTables, type VapidSettings, type ViewSource, type WebPushFields, type WebPushOptions, type WebhooksOptions, type WidgetOptions, type WorkflowCatalog, type WorkflowTypes, type WorkflowsOptions, 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, callsMigration, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkCalls, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkUserTablesSchema, checkViewQueries, checkWebhooksSchema, checkWorkflowsSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, definePdf, 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, hookPath, 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, onDoNotDisturb, ownedDefinitionsMigration, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readDefinitionsVersion, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, redactHookKeys, refusalOf, refused, relayed, reloadingAdmin, 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, sqlSends, startErrorReporting, startJobs, startWorkflow, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, userTableResources, userTablesMigration, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhookIssuePath, webhookParams, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, workflowInstructions, workflowReadTools, workflowTypes, workflowWriteTools, workflowsMigration, writeRecordTools };
|
package/dist/index.js
CHANGED
|
@@ -68,6 +68,8 @@ import { recordOf, resourceChange } from "./changes/resource-change.js";
|
|
|
68
68
|
import { defineFunction } from "./functions/define-function.js";
|
|
69
69
|
import { newWebhookSecret } from "./webhooks/standard-webhooks.js";
|
|
70
70
|
import { subscription } from "./webhooks/subscription.js";
|
|
71
|
+
import { verifySignature, verifyStandardWebhook } from "./functions/verify-signature.js";
|
|
72
|
+
import { hookPath, redactHookKeys, webhookIssuePath, webhookParams } from "./workflows/hooks/hook-paths.js";
|
|
71
73
|
import { startWorkflow } from "./workflows/start-workflow.js";
|
|
72
74
|
import { workflowTypes } from "./workflows/workflow-types.js";
|
|
73
75
|
import { historyIdPattern, historyLimits } from "./notifications/history.js";
|
|
@@ -135,7 +137,6 @@ import { checkQueryRunner, queryRunnerMigration } from "./assistant/query-runner
|
|
|
135
137
|
import { startErrorReporting } from "./error-reporting/error-reporter.js";
|
|
136
138
|
import { functionApp } from "./functions/function-app.js";
|
|
137
139
|
import { readJson } from "./functions/read-json.js";
|
|
138
|
-
import { verifySignature, verifyStandardWebhook } from "./functions/verify-signature.js";
|
|
139
140
|
import { requireEnv } from "./functions/require-env.js";
|
|
140
141
|
import { storeFile } from "./files/store-file.js";
|
|
141
142
|
import { createFileCleanup } from "./files/file-cleanup.js";
|
|
@@ -146,4 +147,4 @@ import { defineEmail } from "./mail/define-email.js";
|
|
|
146
147
|
import { definePdf } from "./pdf/define-pdf.js";
|
|
147
148
|
import { startJobs } from "./jobs/runtime.js";
|
|
148
149
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
149
|
-
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MailServerTimeoutError, MissingCredentials, ModelError, NotificationSendError, PersonalMailError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, callsMigration, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkCalls, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkUserTablesSchema, checkViewQueries, checkWebhooksSchema, checkWorkflowsSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, definePdf, 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, onDoNotDisturb, ownedDefinitionsMigration, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readDefinitionsVersion, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, refusalOf, refused, relayed, reloadingAdmin, 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, sqlSends, startErrorReporting, startJobs, startWorkflow, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, userTableResources, userTablesMigration, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, workflowInstructions, workflowReadTools, workflowTypes, workflowWriteTools, workflowsMigration, writeRecordTools };
|
|
150
|
+
export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MailServerTimeoutError, MissingCredentials, ModelError, NotificationSendError, PersonalMailError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, callsMigration, capSocketMessages, changesMigration, channelSendsMigration, checkAi, checkAuthSchema, checkCalls, checkChanges, checkChannelSends, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkStatusSchema, checkUserTablesSchema, checkViewQueries, checkWebhooksSchema, checkWorkflowsSchema, coalesce, commentTools, commentsMigration, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, definePdf, 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, hookPath, 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, onDoNotDisturb, ownedDefinitionsMigration, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readDefinitionsVersion, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, redactHookKeys, refusalOf, refused, relayed, reloadingAdmin, 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, sqlSends, startErrorReporting, startJobs, startWorkflow, statusMigration, statusNames, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, userTableResources, userTablesMigration, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhookIssuePath, webhookParams, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, workflowInstructions, workflowReadTools, workflowTypes, workflowWriteTools, workflowsMigration, writeRecordTools };
|
package/dist/locale/en.js
CHANGED
|
@@ -26,6 +26,9 @@ var en_default = {
|
|
|
26
26
|
"hidden": "hidden",
|
|
27
27
|
"yes": "Yes",
|
|
28
28
|
"no": "No",
|
|
29
|
+
"webhookTitle": "Issue a new webhook URL for {record}?",
|
|
30
|
+
"webhookBody": "Its old URL stops at once, and the new URL and secret are shown to this client once.",
|
|
31
|
+
"issue": "Issue",
|
|
29
32
|
"createNamedAction": "Create {resource} \"{title}\""
|
|
30
33
|
},
|
|
31
34
|
"mcp/approval": {
|
package/dist/locale/nl.js
CHANGED
|
@@ -26,6 +26,9 @@ var nl_default = {
|
|
|
26
26
|
"hidden": "verborgen",
|
|
27
27
|
"yes": "Ja",
|
|
28
28
|
"no": "Nee",
|
|
29
|
+
"webhookTitle": "Een nieuwe webhook-URL uitgeven voor {record}?",
|
|
30
|
+
"webhookBody": "De oude URL werkt meteen niet meer, en de nieuwe URL en het geheim worden eenmalig aan deze client getoond.",
|
|
31
|
+
"issue": "Uitgeven",
|
|
29
32
|
"createNamedAction": "{resource} \"{title}\" aanmaken"
|
|
30
33
|
},
|
|
31
34
|
"mcp/approval": {
|
package/dist/mcp/mcp-tools.d.ts
CHANGED
|
@@ -21,7 +21,8 @@ export type McpToolsInput = {
|
|
|
21
21
|
commented?: string[];
|
|
22
22
|
/**
|
|
23
23
|
* Offers the workflow tools, for an app with `workflows`: reading workflows, their types and runs, and checking
|
|
24
|
-
* definitions against the app's types; with writes on, `create_workflow`, `update_workflow
|
|
24
|
+
* definitions against the app's types; with writes on, `create_workflow`, `update_workflow`, `run_workflow`,
|
|
25
|
+
* `enable_workflow` and `new_webhook_url` too.
|
|
25
26
|
* Off by default.
|
|
26
27
|
*/
|
|
27
28
|
workflows?: WorkflowCatalog;
|
package/dist/mcp/mcp-tools.js
CHANGED
|
@@ -7,6 +7,7 @@ import { batchWriteTool } from "../assistant/batch-write-tool.js";
|
|
|
7
7
|
import { commentTools } from "../assistant/comment-tools.js";
|
|
8
8
|
import { workflowReadTools } from "../workflows/mcp/workflow-read-tools.js";
|
|
9
9
|
import { workflowWriteTools } from "../workflows/mcp/workflow-write-tools.js";
|
|
10
|
+
import { webhookTools } from "../workflows/mcp/webhook-tools.js";
|
|
10
11
|
//#region src/mcp/mcp-tools.ts
|
|
11
12
|
const writeTools = (input) => [...writeRecordTools(input), batchWriteTool(input)];
|
|
12
13
|
const builtInNames = (db) => new Set([
|
|
@@ -21,7 +22,8 @@ const builtInNames = (db) => new Set([
|
|
|
21
22
|
triggers: {}
|
|
22
23
|
})
|
|
23
24
|
}),
|
|
24
|
-
...workflowWriteTools()
|
|
25
|
+
...workflowWriteTools(),
|
|
26
|
+
...webhookTools({}, () => Promise.reject(/* @__PURE__ */ new Error("Not called")))
|
|
25
27
|
].map((tool) => tool.name));
|
|
26
28
|
/** The app's own tools; one whose name is taken twice, or by a built-in tool, stops the server at startup. */
|
|
27
29
|
const checkedOwnTools = (db, own) => {
|
|
@@ -43,7 +45,7 @@ const mcpTools = ({ db, sql, writes, commented = [], workflows, own = [] }) => {
|
|
|
43
45
|
const limits = writes === false ? false : writes === true || !writes.resources ? {} : { resources: writes.resources };
|
|
44
46
|
const write = limits === false ? [] : writeTools(limits);
|
|
45
47
|
const comments = commentTools({ commented }, limits);
|
|
46
|
-
const flows = workflows ? [...workflowReadTools(workflows), ...limits === false ? [] : workflowWriteTools(limits)] : [];
|
|
48
|
+
const flows = workflows ? [...workflowReadTools(workflows), ...limits === false ? [] : [...workflowWriteTools(limits), ...webhookTools(limits, workflows.issueWebhook)]] : [];
|
|
47
49
|
const query = sql ? [readOnlyQueryTool(db)] : [];
|
|
48
50
|
return (session) => [
|
|
49
51
|
...read,
|
package/dist/package.js
CHANGED
|
@@ -51,7 +51,7 @@ const definitionsResources = ({ roles, taken, organizations = false }) => {
|
|
|
51
51
|
...organization
|
|
52
52
|
}).primaryKey((r) => r.id).explicitGrants().access(accessOf(roles, organizations)).validate((table) => [...issue("name", nameIssue(table.name)), ...issue("title_field", table.title_field ? nameProblem(table.title_field, "field") : void 0)]);
|
|
53
53
|
const fields = resource(fieldsResourceName).table(`${definitionsSchema}.fields`).fields({
|
|
54
|
-
table_id: f.relation(tablesResourceName).access(createOnly).filterable(),
|
|
54
|
+
table_id: f.relation(tablesResourceName).access(createOnly).filterable().sortable(),
|
|
55
55
|
name: f.text().access(createOnly),
|
|
56
56
|
label: f.text().max(maxLabelLength),
|
|
57
57
|
type: f.enum(definitionFieldTypes).access(createOnly),
|
|
@@ -46,6 +46,11 @@ const fieldsSql = () => `create table ${definitionsSchema}.fields (
|
|
|
46
46
|
check ((type = 'decimal') = (precision is not null and scale is not null))
|
|
47
47
|
)`;
|
|
48
48
|
/**
|
|
49
|
+
* Serves a table's fields in position order, the order its own fields take: the admin's scan guard accepts a sort on
|
|
50
|
+
* `position` only from an index that reaches it with `table_id` pinned. It also reaches a fields table made before it.
|
|
51
|
+
*/
|
|
52
|
+
const fieldsPositionIndexSql = () => `create index if not exists fields_position_idx on ${definitionsSchema}.fields (table_id, position)`;
|
|
53
|
+
/**
|
|
49
54
|
* Brings a definitions table made before organizations to the shape of `tablesSql`: a name unique per organization and
|
|
50
55
|
* the served name.
|
|
51
56
|
*/
|
|
@@ -65,4 +70,4 @@ const schemasSql = () => [
|
|
|
65
70
|
`grant usage, create on schema ${userTablesSchema} to current_user`
|
|
66
71
|
];
|
|
67
72
|
//#endregion
|
|
68
|
-
export { fieldsOrganizationSql, fieldsSql, nameIndexesSql, organizationsSql, schemasSql, tablesSql, versionDefaultSql };
|
|
73
|
+
export { fieldsOrganizationSql, fieldsPositionIndexSql, fieldsSql, nameIndexesSql, organizationsSql, schemasSql, tablesSql, versionDefaultSql };
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { migrationCommand } from "../changes/capture-schema.js";
|
|
2
2
|
import { definitionsSchema, userTablesSchema } from "./names.js";
|
|
3
|
-
import { fieldsOrganizationSql, fieldsSql, nameIndexesSql, organizationsSql, schemasSql, tablesSql, versionDefaultSql } from "./schema-sql.js";
|
|
3
|
+
import { fieldsOrganizationSql, fieldsPositionIndexSql, fieldsSql, nameIndexesSql, organizationsSql, schemasSql, tablesSql, versionDefaultSql } from "./schema-sql.js";
|
|
4
4
|
import { sql } from "kysely";
|
|
5
5
|
//#region src/user-tables/schema.ts
|
|
6
|
-
const current = (found) => found.tables && found.versionDefault && found.organizations && found.fields && found.data && found.trgm;
|
|
6
|
+
const current = (found) => found.tables && found.versionDefault && found.organizations && found.fields && found.positionIndex && found.data && found.trgm;
|
|
7
7
|
const readUserTablesSchema = async (db) => {
|
|
8
8
|
const { rows } = await sql`select
|
|
9
9
|
to_regclass(${`${definitionsSchema}.tables`}) is not null as tables,
|
|
10
10
|
exists (select from information_schema.columns where table_schema = ${definitionsSchema} and table_name = 'tables' and column_name = 'version' and column_default is not null) as "versionDefault",
|
|
11
11
|
exists (select from information_schema.columns where table_schema = ${definitionsSchema} and table_name = 'tables' and column_name = 'table_name') as organizations,
|
|
12
12
|
to_regclass(${`${definitionsSchema}.fields`}) is not null as fields,
|
|
13
|
+
to_regclass(${`${definitionsSchema}.fields_position_idx`}) is not null as "positionIndex",
|
|
13
14
|
exists (select from pg_namespace where nspname = ${userTablesSchema}) as data,
|
|
14
15
|
exists (select from pg_extension where extname = 'pg_trgm') as trgm`.execute(db);
|
|
15
16
|
return rows[0];
|
|
@@ -27,14 +28,15 @@ const userTablesMigration = async (db) => {
|
|
|
27
28
|
return `${[
|
|
28
29
|
...schemasSql(),
|
|
29
30
|
...tables,
|
|
30
|
-
...fields
|
|
31
|
+
...fields,
|
|
32
|
+
fieldsPositionIndexSql()
|
|
31
33
|
].join(";\n\n")};`;
|
|
32
34
|
};
|
|
33
35
|
/** Refuses to go on while the schemas of user tables are missing; the project's migrations add them. */
|
|
34
36
|
const checkUserTablesSchema = async (db) => {
|
|
35
37
|
const found = await readUserTablesSchema(db);
|
|
36
38
|
if (current(found)) return;
|
|
37
|
-
throw new Error(`The ${definitionsSchema} or ${userTablesSchema} schema, the default of a definition's version, a definition's organization, or pg_trgm, is missing, and userTables keeps the tables users add there. Write it as a migration with \`${migrationCommand}\`, apply it with the project's migrations, then start again.`);
|
|
39
|
+
throw new Error(`The ${definitionsSchema} or ${userTablesSchema} schema, the default of a definition's version, a definition's organization, the position index of its fields, or pg_trgm, is missing, and userTables keeps the tables users add there. Write it as a migration with \`${migrationCommand}\`, apply it with the project's migrations, then start again.`);
|
|
38
40
|
};
|
|
39
41
|
//#endregion
|
|
40
42
|
export { checkUserTablesSchema, userTablesMigration };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { HttpProblem } from "../../problem.js";
|
|
2
|
+
import { verifySignature, verifyStandardWebhook } from "../../functions/verify-signature.js";
|
|
3
|
+
import { bearerMatches, usableSecret } from "./hook-secrets.js";
|
|
4
|
+
//#region src/workflows/hooks/hook-auth.ts
|
|
5
|
+
/** The hmac options of a trigger's `auth`, and the header its signature comes in, which the run never sees. */
|
|
6
|
+
const hmacOptions = (input) => input.auth ?? {};
|
|
7
|
+
const verified = (check) => check.then(() => true, (error) => {
|
|
8
|
+
if (error instanceof HttpProblem && error.status === 401) return false;
|
|
9
|
+
throw error;
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Whether the call proves it comes from the sender, by the trigger's mode: a Standard Webhooks signature within five
|
|
13
|
+
* minutes, an HMAC of the body in the configured header, or the bearer secret. A secret the mode cannot check with, as
|
|
14
|
+
* after the mode changed since the URL was issued, proves nothing. `signed` holds the bytes already read.
|
|
15
|
+
*/
|
|
16
|
+
const authenticated = async (signed, mode, hook) => {
|
|
17
|
+
if (mode === "bearer") {
|
|
18
|
+
const token = /^Bearer (.+)$/.exec(signed.headers.get("authorization") ?? "")?.[1];
|
|
19
|
+
return token !== void 0 && bearerMatches(token, hook.secret_hash);
|
|
20
|
+
}
|
|
21
|
+
if (!usableSecret(mode, hook.secret)) return false;
|
|
22
|
+
if (mode === "standard_webhooks") return verified(verifyStandardWebhook(signed, { secret: hook.secret }));
|
|
23
|
+
const { header, prefix, encoding, hash } = hmacOptions(hook.input);
|
|
24
|
+
if (!header) return false;
|
|
25
|
+
return verified(verifySignature(signed, {
|
|
26
|
+
secret: hook.secret,
|
|
27
|
+
header,
|
|
28
|
+
...prefix !== void 0 && { prefix },
|
|
29
|
+
...encoding && { encoding },
|
|
30
|
+
...hash && { hash }
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
33
|
+
/** The key a call starts its run once under: the signed `webhook-id`, or the configured idempotency header's value. */
|
|
34
|
+
const idempotencyKey = (request, mode, input) => {
|
|
35
|
+
if (mode === "standard_webhooks") return request.headers.get("webhook-id") ?? void 0;
|
|
36
|
+
const header = input.idempotency_header;
|
|
37
|
+
return typeof header === "string" ? request.headers.get(header) ?? void 0 : void 0;
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { authenticated, hmacOptions, idempotencyKey };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { limits } from "@protobase/workflows";
|
|
2
|
+
//#region src/workflows/hooks/hook-event.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether the call as its run sees it is over a run input's limit as JSON, which escaping makes larger than the body
|
|
5
|
+
* itself: its body is then kept as a file of the run instead.
|
|
6
|
+
*/
|
|
7
|
+
const overInputLimit = (event) => new TextEncoder().encode(JSON.stringify(event)).length > limits.inputBytes;
|
|
8
|
+
const dropped = (name, signatureHeader) => name === "authorization" || name === "proxy-authorization" || name === "cookie" || name.startsWith("webhook-") || name.startsWith("x-forwarded-") || name === "forwarded" || name === signatureHeader;
|
|
9
|
+
/** The call's headers without its credentials, lower-cased as `Headers` keeps them, so no case of a name gets through. */
|
|
10
|
+
const keptHeaders = (headers, signatureHeader) => {
|
|
11
|
+
const signature = signatureHeader?.toLowerCase();
|
|
12
|
+
return Object.fromEntries([...headers].filter(([name]) => !dropped(name, signature)));
|
|
13
|
+
};
|
|
14
|
+
const formFields = (params) => Object.fromEntries([...new Set(params.keys())].map((name) => {
|
|
15
|
+
const values = params.getAll(name);
|
|
16
|
+
return [name, values.length === 1 ? values[0] : values];
|
|
17
|
+
}));
|
|
18
|
+
const isJson = (type) => /^application\/([\w.+-]+\+)?json\b/i.test(type);
|
|
19
|
+
const parsedBody = (bytes, type) => {
|
|
20
|
+
const text = new TextDecoder().decode(bytes);
|
|
21
|
+
if (isJson(type)) try {
|
|
22
|
+
return JSON.parse(text);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
25
|
+
return text;
|
|
26
|
+
}
|
|
27
|
+
if (/^application\/x-www-form-urlencoded\b/i.test(type)) return formFields(new URLSearchParams(text));
|
|
28
|
+
return text;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The call as its run sees it: method, query, headers without credentials, the body, or `file` when the caller kept the
|
|
32
|
+
* body as a file of the run, as for a call over the run input's 1 MiB, and when it came. The address is left out: it holds the key.
|
|
33
|
+
*/
|
|
34
|
+
const hookEvent = (request, bytes, signatureHeader, file) => ({
|
|
35
|
+
method: request.method,
|
|
36
|
+
query: Object.fromEntries(new URL(request.url).searchParams),
|
|
37
|
+
headers: keptHeaders(request.headers, signatureHeader),
|
|
38
|
+
body: file ?? parsedBody(bytes, request.headers.get("content-type") ?? ""),
|
|
39
|
+
received_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
40
|
+
});
|
|
41
|
+
/** The same call for a plugin's `receive`: the headers without credentials, the bytes read, and no key in its address. */
|
|
42
|
+
const receivedRequest = (request, bytes, signatureHeader) => {
|
|
43
|
+
const url = new URL(request.url);
|
|
44
|
+
return new Request(`${url.origin}/?${url.searchParams}`, {
|
|
45
|
+
method: request.method,
|
|
46
|
+
headers: keptHeaders(request.headers, signatureHeader),
|
|
47
|
+
body: bytes
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { hookEvent, keptHeaders, overInputLimit, receivedRequest };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/workflows/hooks/hook-paths.d.ts
|
|
2
|
+
/** The names of the parameters in the webhook addresses. */
|
|
3
|
+
export declare const webhookParams: {
|
|
4
|
+
readonly workflow: "workflow";
|
|
5
|
+
readonly key: "key";
|
|
6
|
+
};
|
|
7
|
+
/** Where a workflow's webhook URL is issued, `POST <systemPath>/workflows/<id>/webhook`; the route's pattern without an id. */
|
|
8
|
+
export declare const webhookIssuePath: (systemPath: string, workflow?: string) => string;
|
|
9
|
+
/** The public address a webhook trigger is called at, `<systemPath>/workflows/hooks/<key>`; the route's pattern without a key. */
|
|
10
|
+
export declare const hookPath: (systemPath: string, key?: string) => string;
|
|
11
|
+
/**
|
|
12
|
+
* `text` with the key of every webhook address in it replaced by `[key]`, for a request log: the key opens the URL. A
|
|
13
|
+
* word of the line is read decoded as the router reads it, so an address written with escapes is redacted too.
|
|
14
|
+
*/
|
|
15
|
+
export declare const redactHookKeys: (text: string) => string;
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/workflows/hooks/hook-paths.ts
|
|
2
|
+
/** The names of the parameters in the webhook addresses. */
|
|
3
|
+
const webhookParams = {
|
|
4
|
+
workflow: "workflow",
|
|
5
|
+
key: "key"
|
|
6
|
+
};
|
|
7
|
+
/** Where a workflow's webhook URL is issued, `POST <systemPath>/workflows/<id>/webhook`; the route's pattern without an id. */
|
|
8
|
+
const webhookIssuePath = (systemPath, workflow = `:${webhookParams.workflow}`) => `${systemPath}/workflows/${workflow}/webhook`;
|
|
9
|
+
/** Where the webhook addresses live, the route the public address is mounted at. */
|
|
10
|
+
const hooksBase = (systemPath) => `${systemPath}/workflows/hooks`;
|
|
11
|
+
/** The public address a webhook trigger is called at, `<systemPath>/workflows/hooks/<key>`; the route's pattern without a key. */
|
|
12
|
+
const hookPath = (systemPath, key = `:${webhookParams.key}`) => `${hooksBase(systemPath)}/${key}`;
|
|
13
|
+
const hookAddress = /\/workflows\/hooks\/[^/?#]+/g;
|
|
14
|
+
const decodedEscapes = (part) => part.replace(/(?:%[0-9A-Fa-f]{2})+/g, (escapes) => {
|
|
15
|
+
try {
|
|
16
|
+
return decodeURI(escapes);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (!(error instanceof URIError)) throw error;
|
|
19
|
+
return escapes;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* `text` with the key of every webhook address in it replaced by `[key]`, for a request log: the key opens the URL. A
|
|
24
|
+
* word of the line is read decoded as the router reads it, so an address written with escapes is redacted too.
|
|
25
|
+
*/
|
|
26
|
+
const redactHookKeys = (text) => text.replace(/\S+/g, (part) => {
|
|
27
|
+
const decoded = decodedEscapes(part);
|
|
28
|
+
const redacted = decoded.replace(hookAddress, "/workflows/hooks/[key]");
|
|
29
|
+
return redacted === decoded ? part : redacted;
|
|
30
|
+
});
|
|
31
|
+
//#endregion
|
|
32
|
+
export { hookPath, hooksBase, redactHookKeys, webhookIssuePath, webhookParams };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { HttpProblem, badRequest, notFound, problemResponse, unauthorized } from "../../problem.js";
|
|
2
|
+
import { clientAddress, createRateLimit } from "../../forms/rate-limit.js";
|
|
3
|
+
import { callPlugin } from "../triggers/plugin-call.js";
|
|
4
|
+
import { itemsProblem } from "../triggers/poll-result.js";
|
|
5
|
+
import { removeRunFile, storeRunFile } from "../run-files.js";
|
|
6
|
+
import { hookMode } from "./hook-secrets.js";
|
|
7
|
+
import { authenticated, hmacOptions, idempotencyKey } from "./hook-auth.js";
|
|
8
|
+
import { hookEvent, overInputLimit, receivedRequest } from "./hook-event.js";
|
|
9
|
+
import { webhookParams } from "./hook-paths.js";
|
|
10
|
+
import { findHook } from "./hook-store.js";
|
|
11
|
+
import { startHookRuns } from "./hook-runs.js";
|
|
12
|
+
import { readBody } from "./read-body.js";
|
|
13
|
+
import { Hono } from "hono";
|
|
14
|
+
import { limits } from "@protobase/workflows";
|
|
15
|
+
import { webhookBodyCeiling, webhookBodyDefault } from "@protobase/workflows/plugin";
|
|
16
|
+
//#region src/workflows/hooks/hook-route.ts
|
|
17
|
+
/** Calls an address may have refused (401, 404 and, once a URL was found, 400 and 413) a minute before it waits. */
|
|
18
|
+
const refusalsPerMinute = 30;
|
|
19
|
+
/** Calls a minute that start runs, per trigger, when its input names no `per_minute`. */
|
|
20
|
+
const callsPerMinute = 60;
|
|
21
|
+
const tooMany = (seconds, detail) => new HttpProblem(429, "too-many-calls", "Too Many Requests", detail, {}, { "Retry-After": String(seconds) });
|
|
22
|
+
const bodyLimit = (input) => Math.min(typeof input.body_limit === "number" ? input.body_limit : webhookBodyDefault, webhookBodyCeiling);
|
|
23
|
+
/**
|
|
24
|
+
* The public address a webhook trigger is called at, `POST .../workflows/hooks/<key>`, with no session: only POST; an
|
|
25
|
+
* address that was refused 30 times this minute waits, before any lookup; the key must open a running webhook trigger
|
|
26
|
+
* of an enabled version (404 for anything else, alike); the body is read up to the trigger's limit (413); the call
|
|
27
|
+
* must prove it comes from the sender (401); then the trigger's own rate (429), spent only by calls that did; the run
|
|
28
|
+
* starts once per signed id (202 `{ run_id }`). Each refusal starts no run. Its errors are its own: reported without
|
|
29
|
+
* the request, answered with nothing of it.
|
|
30
|
+
*/
|
|
31
|
+
const hookRoute = (input) => {
|
|
32
|
+
const { db, schema } = input;
|
|
33
|
+
const refusals = createRateLimit();
|
|
34
|
+
const calls = createRateLimit();
|
|
35
|
+
const app = new Hono();
|
|
36
|
+
app.onError((error) => {
|
|
37
|
+
if (error instanceof HttpProblem) return problemResponse(error, "/workflows/hooks");
|
|
38
|
+
input.report(error);
|
|
39
|
+
return problemResponse(new HttpProblem(500, "internal-error", "Internal Server Error", "The call could not be taken; send it again"), "/workflows/hooks");
|
|
40
|
+
});
|
|
41
|
+
app.all(`/:${webhookParams.key}`, async (c) => {
|
|
42
|
+
if (c.req.method !== "POST") return c.body(null, 405, { Allow: "POST" });
|
|
43
|
+
const request = c.req.raw;
|
|
44
|
+
const address = clientAddress(request.headers);
|
|
45
|
+
const wait = refusals.spent(address, refusalsPerMinute);
|
|
46
|
+
if (wait > 0) throw tooMany(wait, "Too many refused calls from this address; wait a minute");
|
|
47
|
+
const refused = (problem) => {
|
|
48
|
+
refusals.take(address, refusalsPerMinute);
|
|
49
|
+
return problem;
|
|
50
|
+
};
|
|
51
|
+
const target = await findHook(db, schema, c.req.param(webhookParams.key), input.webhookTypes());
|
|
52
|
+
const type = target && input.webhooks(target.type);
|
|
53
|
+
if (!target || !type) throw refused(notFound("There is no webhook at this address"));
|
|
54
|
+
const bytes = await readBody(request, bodyLimit(target.input));
|
|
55
|
+
if (!bytes) throw refused(new HttpProblem(413, "body-too-large", "Payload Too Large", `The body is over ${bodyLimit(target.input)} bytes, the most this webhook takes`));
|
|
56
|
+
const mode = hookMode(target.input);
|
|
57
|
+
const signed = new Request(request.url, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: request.headers,
|
|
60
|
+
body: bytes
|
|
61
|
+
});
|
|
62
|
+
if (!mode || !await authenticated(signed, mode, target)) throw refused(unauthorized("The call does not carry this webhook's secret or signature"));
|
|
63
|
+
const perMinute = typeof target.input.per_minute === "number" ? target.input.per_minute : callsPerMinute;
|
|
64
|
+
const rate = calls.take(target.trigger_id, perMinute);
|
|
65
|
+
if (rate > 0) throw tooMany(rate, `This webhook takes ${perMinute} calls a minute`);
|
|
66
|
+
const key = idempotencyKey(request, mode, target.input);
|
|
67
|
+
if (key !== void 0 && key.length > 200) throw refused(badRequest("invalid-idempotency-key", `The call's id is over 200 characters`));
|
|
68
|
+
const runId = crypto.randomUUID();
|
|
69
|
+
const signatureHeader = mode === "hmac" ? hmacOptions(target.input).header : void 0;
|
|
70
|
+
const inline = hookEvent(request, bytes, signatureHeader);
|
|
71
|
+
const large = overInputLimit(inline);
|
|
72
|
+
if (large && !input.files()?.providers.private) throw refused(new HttpProblem(413, "body-too-large", "Payload Too Large", `The call is over a run input's ${limits.inputBytes} bytes as JSON, and this app keeps no files for larger ones`));
|
|
73
|
+
const file = large ? await storeRunFile(input.files(), runId, bytes, "body", request.headers.get("content-type") ?? "application/octet-stream") : void 0;
|
|
74
|
+
let started = false;
|
|
75
|
+
try {
|
|
76
|
+
const event = file ? hookEvent(request, bytes, signatureHeader, file) : inline;
|
|
77
|
+
const received = await callPlugin(() => type.receive({
|
|
78
|
+
request: receivedRequest(request, bytes, signatureHeader),
|
|
79
|
+
event,
|
|
80
|
+
idempotencyKey: key,
|
|
81
|
+
input: target.input
|
|
82
|
+
}), input.redact);
|
|
83
|
+
if (!("value" in received)) throw new Error(`The webhook trigger ${target.type} failed: ${"failed" in received ? received.failed : received.expired}`);
|
|
84
|
+
const { items, response } = received.value;
|
|
85
|
+
const problem = itemsProblem(items, file ? 1 : 100, false);
|
|
86
|
+
if (problem) throw new Error(`The webhook trigger ${target.type} ${problem}`);
|
|
87
|
+
const ids = await startHookRuns(db, schema, target, items, runId);
|
|
88
|
+
if (!ids) throw refused(notFound("There is no webhook at this address"));
|
|
89
|
+
started = ids[0] === runId;
|
|
90
|
+
if (response) return response;
|
|
91
|
+
return ids.length === 1 ? c.json({ run_id: ids[0] }, 202) : c.json({ run_ids: ids }, 202);
|
|
92
|
+
} finally {
|
|
93
|
+
if (file && !started) await removeRunFile(input.files(), file);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return app;
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
export { hookRoute };
|