@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.
Files changed (52) hide show
  1. package/dist/create-admin.js +11 -1
  2. package/dist/forms/rate-limit.js +33 -11
  3. package/dist/forms/routes.js +1 -1
  4. package/dist/functions/verify-signature.js +4 -2
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +3 -2
  7. package/dist/locale/en.js +3 -0
  8. package/dist/locale/nl.js +3 -0
  9. package/dist/mcp/mcp-tools.d.ts +2 -1
  10. package/dist/mcp/mcp-tools.js +4 -2
  11. package/dist/package.js +1 -1
  12. package/dist/user-tables/definitions-resources.js +1 -1
  13. package/dist/user-tables/definitions-views.js +1 -1
  14. package/dist/user-tables/schema-sql.js +6 -1
  15. package/dist/user-tables/schema.js +6 -4
  16. package/dist/workflows/hooks/hook-auth.js +40 -0
  17. package/dist/workflows/hooks/hook-event.js +51 -0
  18. package/dist/workflows/hooks/hook-paths.d.ts +16 -0
  19. package/dist/workflows/hooks/hook-paths.js +32 -0
  20. package/dist/workflows/hooks/hook-route.js +99 -0
  21. package/dist/workflows/hooks/hook-runs.js +32 -0
  22. package/dist/workflows/hooks/hook-secrets.js +42 -0
  23. package/dist/workflows/hooks/hook-store.js +35 -0
  24. package/dist/workflows/hooks/hooks-sql.js +20 -0
  25. package/dist/workflows/hooks/issue-body.js +21 -0
  26. package/dist/workflows/hooks/issue-route.js +23 -0
  27. package/dist/workflows/hooks/issue-webhook.js +63 -0
  28. package/dist/workflows/hooks/read-body.js +32 -0
  29. package/dist/workflows/mcp/webhook-tools.d.ts +9 -0
  30. package/dist/workflows/mcp/webhook-tools.js +96 -0
  31. package/dist/workflows/mcp/workflow-instructions.js +1 -1
  32. package/dist/workflows/mcp/workflow-read-tools.d.ts +3 -0
  33. package/dist/workflows/plugin-records.js +1 -1
  34. package/dist/workflows/plugin-services.js +1 -1
  35. package/dist/workflows/ready/ready-start.js +1 -1
  36. package/dist/workflows/run-files.js +7 -1
  37. package/dist/workflows/schema-sql.js +15 -3
  38. package/dist/workflows/schema.js +3 -1
  39. package/dist/workflows/start-workflow.d.ts +3 -1
  40. package/dist/workflows/start-workflow.js +10 -4
  41. package/dist/workflows/triggers/delete-hook.js +15 -0
  42. package/dist/workflows/triggers/plugin-call.js +35 -0
  43. package/dist/workflows/triggers/poll-job.js +120 -0
  44. package/dist/workflows/triggers/poll-result.js +32 -0
  45. package/dist/workflows/triggers/poll-schedule.js +32 -0
  46. package/dist/workflows/triggers/trigger-claim.js +81 -0
  47. package/dist/workflows/triggers/trigger-jobs.js +37 -7
  48. package/dist/workflows/triggers/trigger-start.js +101 -0
  49. package/dist/workflows/workflow-engine.js +38 -5
  50. package/dist/workflows/workflow-types.d.ts +4 -1
  51. package/dist/workflows/workflow-types.js +9 -3
  52. package/package.json +6 -6
@@ -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: engine.types },
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));
@@ -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, so memory follows the clients of the last minute.
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) if (at - window.start >= windowMs) windows.delete(key);
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
- prune(at);
26
- windows.set(key, {
27
- start: at,
28
- count: 1
29
- });
45
+ open(key, at);
30
46
  return 0;
31
47
  }
32
- if (current.count >= perMinute) return Math.ceil((current.start + windowMs - at) / 1e3);
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
- return { take };
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 };
@@ -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, ["verify"]);
11
- return crypto.subtle.verify("HMAC", cryptoKey, signature, new TextEncoder().encode(content));
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": {
@@ -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` and `run_workflow` too.
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;
@@ -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
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.68.4";
2
+ var version = "0.68.6";
3
3
  //#endregion
4
4
  export { version };
@@ -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),
@@ -82,7 +82,7 @@ const fieldsView = view(fieldsResourceName).names({
82
82
  r.type,
83
83
  r.position
84
84
  ],
85
- sort: [[r.position, "asc"]]
85
+ sort: [[r.table_id, "asc"], [r.position, "asc"]]
86
86
  })).layout((r) => [l.section("Field", [
87
87
  r.table_id,
88
88
  r.label,
@@ -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 };