@protobase/server 0.51.3 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/aggregate-groups.js +2 -2
  2. package/dist/aggregate-service.js +4 -4
  3. package/dist/aggregate-splits.js +1 -1
  4. package/dist/assistant/batch-approval.js +49 -51
  5. package/dist/assistant/batch-before.js +38 -0
  6. package/dist/assistant/batch-echo.js +36 -0
  7. package/dist/assistant/batch-write-args.js +14 -5
  8. package/dist/assistant/batch-write-tool.js +30 -11
  9. package/dist/assistant/budgeted-page.js +30 -0
  10. package/dist/assistant/comment-tools.d.ts +20 -0
  11. package/dist/assistant/comment-tools.js +120 -0
  12. package/dist/assistant/count-records-tool.d.ts +5 -0
  13. package/dist/assistant/count-records-tool.js +84 -0
  14. package/dist/assistant/query-result-budget.js +32 -0
  15. package/dist/assistant/query-tools.js +7 -7
  16. package/dist/assistant/record-before.js +2 -1
  17. package/dist/assistant/record-field-piece.js +54 -0
  18. package/dist/assistant/record-tool-args.js +6 -3
  19. package/dist/assistant/record-tools.d.ts +4 -1
  20. package/dist/assistant/record-tools.js +17 -6
  21. package/dist/assistant/records.d.ts +19 -0
  22. package/dist/assistant/records.js +20 -13
  23. package/dist/assistant/result-budget.js +72 -0
  24. package/dist/assistant/tools.d.ts +3 -0
  25. package/dist/assistant/write-echo.js +41 -0
  26. package/dist/assistant/write-messages.js +26 -6
  27. package/dist/assistant/write-prompts.js +37 -12
  28. package/dist/assistant/write-record-tools.js +15 -6
  29. package/dist/batch/run-batch.js +1 -1
  30. package/dist/better-auth/auth-settings.d.ts +1 -1
  31. package/dist/better-auth/status-route.js +2 -2
  32. package/dist/changes/capture-schema.js +1 -1
  33. package/dist/changes/resource-change.d.ts +4 -1
  34. package/dist/changes/resource-change.js +27 -3
  35. package/dist/comments/post-comment.js +1 -1
  36. package/dist/comments/read-comments.js +1 -1
  37. package/dist/comments/tool-comment-types.d.ts +28 -0
  38. package/dist/comments/tool-comments.js +30 -0
  39. package/dist/create-admin.d.ts +5 -2
  40. package/dist/create-admin.js +20 -10
  41. package/dist/deps.d.ts +1 -1
  42. package/dist/encryption/scan.js +1 -1
  43. package/dist/error-handler.js +3 -2
  44. package/dist/etag.js +1 -1
  45. package/dist/files/download.js +1 -1
  46. package/dist/files/file-cleanup.d.ts +1 -1
  47. package/dist/functions/records.d.ts +1 -1
  48. package/dist/functions/records.js +2 -2
  49. package/dist/history/read-history.js +2 -2
  50. package/dist/index.d.ts +9 -3
  51. package/dist/index.js +9 -4
  52. package/dist/jobs/run-job.d.ts +1 -1
  53. package/dist/list-photos.js +2 -2
  54. package/dist/list-service.js +6 -6
  55. package/dist/map-service.js +2 -2
  56. package/dist/mcp/mcp-context.d.ts +7 -4
  57. package/dist/mcp/mcp-context.js +6 -4
  58. package/dist/mcp/mcp-server.d.ts +3 -2
  59. package/dist/mcp/mcp-tools.d.ts +6 -1
  60. package/dist/mcp/mcp-tools.js +8 -4
  61. package/dist/package.js +1 -1
  62. package/dist/read-service.js +2 -2
  63. package/dist/realtime/broadcast-api.js +3 -3
  64. package/dist/realtime/broadcast-hub.js +15 -5
  65. package/dist/realtime/change-checker.js +1 -1
  66. package/dist/realtime/channel-access.js +1 -1
  67. package/dist/realtime/database-broadcasts.js +1 -1
  68. package/dist/realtime/presence-hub.js +74 -39
  69. package/dist/realtime/realtime-app.js +28 -4
  70. package/dist/realtime/realtime-routes.js +3 -5
  71. package/dist/realtime/realtime-socket.js +3 -1
  72. package/dist/realtime/relay/frame-cursor.js +38 -0
  73. package/dist/realtime/relay/presence-replica.js +57 -0
  74. package/dist/realtime/relay/realtime-options.d.ts +13 -0
  75. package/dist/realtime/relay/realtime-options.js +9 -0
  76. package/dist/realtime/relay/realtime-relay.js +104 -0
  77. package/dist/realtime/relay/relay-frame-sql.js +82 -0
  78. package/dist/realtime/relay/relay-presence-sql.js +71 -0
  79. package/dist/realtime/relay/relay-schema-sql.js +55 -0
  80. package/dist/realtime/relay/relay-schema.d.ts +7 -0
  81. package/dist/realtime/relay/relay-schema.js +18 -0
  82. package/dist/realtime/relay/relayed-hubs.js +61 -0
  83. package/dist/records.js +1 -1
  84. package/dist/related-labels.js +1 -1
  85. package/dist/rest/rest-context.js +1 -1
  86. package/dist/routes/batch.js +1 -1
  87. package/dist/routes/collection.js +2 -2
  88. package/dist/routes/record.js +3 -3
  89. package/dist/row-permissions.js +1 -1
  90. package/dist/sql-parts.js +1 -1
  91. package/dist/web-app/mcp-icons.d.ts +9 -0
  92. package/dist/web-app/mcp-icons.js +26 -0
  93. package/dist/webhooks/subscriber-view.js +27 -2
  94. package/dist/webhooks/webhooks.js +41 -12
  95. package/dist/write-pipeline.js +1 -1
  96. package/dist/write-shared.js +1 -1
  97. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -25,6 +25,8 @@ import { apiKeySettings, apiKeyVariables } from "./api-keys/api-key-settings.js"
25
25
  import { apiKeyCaller } from "./api-keys/resolve-caller.js";
26
26
  import { jwtAuthenticator, jwtVerifier } from "./auth/jwt.js";
27
27
  import { realtimeLimits } from "./realtime/realtime-limits.js";
28
+ import { relayed } from "./realtime/relay/realtime-options.js";
29
+ import { backgroundSettled, waitUntil } from "./functions/wait-until.js";
28
30
  import { objectKey } from "./storage/object-key.js";
29
31
  import { appNameOver, defaultAppName, shownAppName } from "./app-name.js";
30
32
  import { supabasePath } from "./supabase-paths.js";
@@ -36,20 +38,22 @@ import { readOperatorSettings } from "./better-auth/operator-provider.js";
36
38
  import { readPlatformSignIn } from "./better-auth/platform-sign-in.js";
37
39
  import { normalizeRoles } from "./better-auth/create-auth.js";
38
40
  import { mcpApprovals } from "./mcp/mcp-approval-store.js";
41
+ import { changesMigration, checkChanges, migrationCommand } from "./changes/capture-schema.js";
42
+ import { checkComments, commentsMigration } from "./comments/comments-sql.js";
39
43
  import { mcpToolContext } from "./mcp/mcp-context.js";
40
44
  import { mcpGate, sessionOf } from "./mcp/mcp-gate.js";
41
45
  import { mcpHandler } from "./mcp/mcp-handler.js";
42
46
  import { mcpRoutes } from "./mcp/mcp-routes.js";
43
47
  import { mcpServer } from "./mcp/mcp-server.js";
48
+ import { countRecordsTool } from "./assistant/count-records-tool.js";
44
49
  import { getRecordTool, listRecordsTool, listResourcesTool, readRecordTools } from "./assistant/record-tools.js";
45
50
  import { deleteRecordTool } from "./assistant/delete-record-tool.js";
46
51
  import { createRecordTool, updateRecordTool, writeRecordTools } from "./assistant/write-record-tools.js";
47
52
  import { batchWriteTool } from "./assistant/batch-write-tool.js";
53
+ import { addCommentTool, commentTools, listCommentsTool } from "./assistant/comment-tools.js";
48
54
  import { mcpTools } from "./mcp/mcp-tools.js";
49
55
  import { addAppRoleToOwners, addMember, createOrganization, listOrganizations, removeMember, setMemberRoles } from "./better-auth/organizations/host.js";
50
56
  import { createUser, deleteUser, hasUsers, listUsers, roleChoices, setUserBanned, setUserRole } from "./better-auth/users.js";
51
- import { changesMigration, checkChanges, migrationCommand } from "./changes/capture-schema.js";
52
- import { checkComments, commentsMigration } from "./comments/comments-sql.js";
53
57
  import { coalesce } from "./changes/coalesce.js";
54
58
  import { eventMatcher } from "./changes/match-events.js";
55
59
  import { parseColumns, readOutbox } from "./changes/outbox.js";
@@ -87,6 +91,7 @@ import { checkSourceNames, sourceOptions, sourcesFor, sourcesVariable } from "./
87
91
  import { renderVariant, thumbHash } from "./files/images.js";
88
92
  import { pngIcons } from "./web-app/app-icons.js";
89
93
  import { webManifestPath } from "./web-app/web-manifest-route.js";
94
+ import { mcpIcons } from "./web-app/mcp-icons.js";
90
95
  import { createAdmin } from "./create-admin.js";
91
96
  import { handleDroppedConnections } from "./dropped-connections.js";
92
97
  import { poolWaitMs, readDbConnections } from "./app-pool.js";
@@ -101,6 +106,7 @@ import { authStoreTables } from "./better-auth/auth-tables.js";
101
106
  import { checkResourcesOutsideAuthStore } from "./better-auth/store/resources-outside.js";
102
107
  import { checkComputed, computedMigration } from "./computed/computed-schema.js";
103
108
  import { checkWebhooksSchema, webhooksMigration } from "./webhooks/table/schema.js";
109
+ import { checkRealtimeRelay, realtimeRelayMigration } from "./realtime/relay/relay-schema.js";
104
110
  import { checkAi } from "./ai/check-ai.js";
105
111
  import { checkNotificationsSchema, notificationsMigration } from "./notifications/schema.js";
106
112
  import { sendNotification } from "./notifications/send-notification.js";
@@ -111,7 +117,6 @@ import { functionApp } from "./functions/function-app.js";
111
117
  import { readJson } from "./functions/read-json.js";
112
118
  import { verifySignature, verifyStandardWebhook } from "./functions/verify-signature.js";
113
119
  import { requireEnv } from "./functions/require-env.js";
114
- import { backgroundSettled, waitUntil } from "./functions/wait-until.js";
115
120
  import { storeFile } from "./files/store-file.js";
116
121
  import { createFileCleanup } from "./files/file-cleanup.js";
117
122
  import { createEncryptionTools } from "./encryption/tools.js";
@@ -123,4 +128,4 @@ import { jobsDrainApp } from "./jobs/drain-route.js";
123
128
  import { composeRelease, releaseManifest } from "./mobile-release/release.js";
124
129
  import { mobileReleaseApp } from "./mobile-release/release-app.js";
125
130
  import { mobileSigningKeyVariable, readSigningKey } from "./mobile-release/signing-key.js";
126
- export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, NotificationSendError, PushMessageError, addAppRoleToOwners, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, 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, listOrganizations, listRecordsTool, listResourcesTool, listUsers, localFileStore, longQueryTimeoutMs, mapSettings, mapStyleUrl, mapStyleVariable, maxDevicesPerUser, maxHistoryPage, maxMessageBytes, maxRecipients, maxTokenTtlSeconds, maxTtlSeconds, maxUnreadCount, maxWidgetPropsBytes, mcpApprovals, mcpGate, mcpHandler, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readSigningKey, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, recordOf, releaseManifest, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
131
+ export { ConnectionsBusy, DeviceStoreError, FatalJobError, HistoryStoreError, HttpProblem, MissingCredentials, ModelError, NotificationSendError, PushMessageError, addAppRoleToOwners, addCommentTool, addMember, apiKeyCaller, apiKeySettings, apiKeyVariables, appNameOver, aspectRatio, assistantProtocolRoutes, assistantRoles, assistantSettings, assistantVariables, authSchemaDrift, authSchemaMigration, authStoreTables, backgroundSettled, badRequest, batchWriteTool, beforeSignInPluginsVariable, betterAuthTokens, capSocketMessages, changesMigration, checkAi, checkAuthSchema, checkChanges, checkComments, checkComputed, checkJobsSchema, checkNotificationsSchema, checkQueryRunner, checkRealtimeRelay, checkResourcesOutsideAuthStore, checkSourceNames, checkViewQueries, checkWebhooksSchema, coalesce, commentTools, commentsMigration, composeRelease, computedMigration, configExports, consoleAuditQueue, conversationKey, countRecordsTool, createAdmin, createConversations, createEncryptionTools, createFileCleanup, createOrganization, createRecordTool, createUser, defaultAppName, defaultHistoryPage, defaultSignInPolicy, defineEmail, defineFunction, defineJob, defineTool, deleteRecordTool, deleteUser, 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, mcpRoutes, mcpServer, mcpToolContext, mcpTools, migrationCommand, mobileReleaseApp, mobileSigningKeyVariable, newWebhookSecret, normalizeRoles, notFound, notificationSenders, notificationsMigration, oauthWellKnownPaths, objectKey, parseColumns, parseRoles, passwordLength, pluginOptions, pluginsBeforeSignIn, pluginsFor, pluginsVariable, pngIcons, poolWaitMs, postgresDeviceStore, postgresHistoryStore, processor, pushMessage, queryRunnerMigration, readDbConnections, readJson, readOnlyQueryTool, readOperatorSettings, readOutbox, readPlatformSignIn, readRecordTools, readSigningKey, readVapidSettings, readWriteQueryTool, readableResources, realtimeLimits, realtimeRelayMigration, recordOf, relayed, releaseManifest, removeDevicesWithUsers, removeHistoryWithUsers, removeMember, renderVariant, requestApproval, requireEnv, resourceChange, roleChoices, runInBackground, runReadOnlyQuery, runReadWriteQuery, runTurn, runtimeUrl, runtimeVariable, s3FileStore, seesAssistant, seesRuntime, sendNotification, sessionOf, setMemberRoles, setUserBanned, setUserRole, shownAppName, signInPolicyMethods, socketMessageCapOf, sourceOptions, sourcesFor, sourcesVariable, startErrorReporting, startJobs, storageCleanupSchedule, storageProvider, storeFile, storeUsage, streamChatCompletion, subscription, supabasePath, thumbHash, unauthorized, updateRecordTool, urgencies, vapidPrivateKeyVariable, vapidPublicKeyVariable, vapidSubjectVariable, variantPath, verifySignature, verifyStandardWebhook, waitUntil, wantsLongQueries, webManifestPath, webPushSender, webauthnPath, webhooksMigration, widget, withCacheBreakpoints, withTrustedApps, writeRecordTools };
@@ -1,7 +1,7 @@
1
1
  import "../notifications/send-input.js";
2
2
  import "./job-types.js";
3
- import "pg-boss";
4
3
  import { Db } from "@protobase/query";
4
+ import "pg-boss";
5
5
  //#region src/jobs/run-job.d.ts
6
6
  /** Thrown from a job's `run`, it fails the job now: the attempts it has left are not used. */
7
7
  export declare class FatalJobError extends Error {
@@ -1,10 +1,10 @@
1
1
  import { badRequest } from "./problem.js";
2
2
  import { checkedSort } from "./filter-input.js";
3
3
  import { requestAccess } from "./request-access.js";
4
- import { readTransaction } from "./transactions.js";
4
+ import { readScope } from "./read-scope.js";
5
5
  import { exposeRow } from "./rows.js";
6
+ import { readTransaction } from "./transactions.js";
6
7
  import { presentFiles } from "./files/present.js";
7
- import { readScope } from "./read-scope.js";
8
8
  import { cardPhotosErrors } from "./view-card-photos.js";
9
9
  import { cardPhotoLimit } from "@protobase/schema/internal";
10
10
  import { checkFilter, createWhere } from "@protobase/schema";
@@ -1,14 +1,14 @@
1
1
  import { checkedFilter, checkedSort } from "./filter-input.js";
2
2
  import { requestAccess, requireOperation } from "./request-access.js";
3
+ import { readScope } from "./read-scope.js";
4
+ import { exposeRow, pickFields } from "./rows.js";
5
+ import { labelRequests, relatedLabels } from "./related-labels.js";
3
6
  import { readTransaction } from "./transactions.js";
7
+ import { deletedRows, listParams, pageSizeOf, parseParams, seekParams, selectedFields } from "./params.js";
4
8
  import { etagAlias } from "./sql-parts.js";
5
9
  import { etagOf } from "./etag.js";
6
- import { exposeRow, pickFields } from "./rows.js";
7
10
  import { presentFiles } from "./files/present.js";
8
- import { deletedRows, listParams, pageSizeOf, parseParams, seekParams, selectedFields } from "./params.js";
9
- import { readScope } from "./read-scope.js";
10
11
  import { rowPermissions } from "./row-permissions.js";
11
- import { labelRequests, relatedLabels } from "./related-labels.js";
12
12
  import { photosRequest, recordPhotos } from "./list-photos.js";
13
13
  import { countUpTo, estimateCount, listQuery, seekToPosition } from "@protobase/query";
14
14
  //#region src/list-service.ts
@@ -102,11 +102,11 @@ const listRecords = async (deps, entry, session, input, maxFilterLength, jump =
102
102
  next_page_token: result.page.nextCursor ?? "",
103
103
  ...position !== void 0 && { prev_page_token: result.page.prevCursor ?? "" },
104
104
  total_size_estimate: result.estimate,
105
- ...result.counted !== void 0 && result.counted <= exactCountLimit && { total_size: result.counted },
105
+ ...result.counted !== void 0 && result.counted <= 1e4 && { total_size: result.counted },
106
106
  ...result.labels && { labels: result.labels },
107
107
  ...included && { photos: included.photos }
108
108
  }
109
109
  };
110
110
  };
111
111
  //#endregion
112
- export { listRecords };
112
+ export { exactCountLimit, listRecords };
@@ -1,9 +1,9 @@
1
1
  import { checkedFilter } from "./filter-input.js";
2
2
  import { requestAccess, requireOperation } from "./request-access.js";
3
- import { readTransaction } from "./transactions.js";
4
- import { parseParams, selectedFields } from "./params.js";
5
3
  import { readScope } from "./read-scope.js";
6
4
  import { labelRequests } from "./related-labels.js";
5
+ import { readTransaction } from "./transactions.js";
6
+ import { parseParams, selectedFields } from "./params.js";
7
7
  import { photosRequest } from "./list-photos.js";
8
8
  import { listRecords } from "./list-service.js";
9
9
  import { z } from "zod";
@@ -5,9 +5,12 @@ import { Meta } from "../meta.js";
5
5
  import { Locale } from "@protobase/schema/internal";
6
6
  //#region src/mcp/mcp-context.d.ts
7
7
  /**
8
- * What tools get over MCP for `session`: its records and the resources it may read, under its access rules, and
9
- * `locale`, the language its approvals are asked in. Nothing shows beside a tool's result, and `mcpServer` gives each
10
- * call its own `approve`.
8
+ * What tools get over MCP for `session`: its records, their comments and the resources it may read, under its access
9
+ * rules, and `locale`, the language its approvals are asked in. `requestURL` is the MCP request's. Nothing shows beside
10
+ * a tool's result, and `mcpServer` gives each call its own `approve`.
11
11
  */
12
- export declare const mcpToolContext: (deps: Deps, meta: Pick<Meta, "forCaller">, session: Session, locale?: Locale) => Omit<ToolContext, "approve">;
12
+ export declare const mcpToolContext: (deps: Deps, meta: Pick<Meta, "forCaller">, session: Session, { locale, requestURL }: {
13
+ locale?: Locale;
14
+ requestURL: string;
15
+ }) => Omit<ToolContext, "approve">;
13
16
  //#endregion
@@ -1,15 +1,17 @@
1
1
  import { assistantRecords } from "../assistant/records.js";
2
2
  import { readableResources } from "../assistant/readable-resources.js";
3
+ import { toolComments } from "../comments/tool-comments.js";
3
4
  //#region src/mcp/mcp-context.ts
4
5
  /**
5
- * What tools get over MCP for `session`: its records and the resources it may read, under its access rules, and
6
- * `locale`, the language its approvals are asked in. Nothing shows beside a tool's result, and `mcpServer` gives each
7
- * call its own `approve`.
6
+ * What tools get over MCP for `session`: its records, their comments and the resources it may read, under its access
7
+ * rules, and `locale`, the language its approvals are asked in. `requestURL` is the MCP request's. Nothing shows beside
8
+ * a tool's result, and `mcpServer` gives each call its own `approve`.
8
9
  */
9
- const mcpToolContext = (deps, meta, session, locale) => ({
10
+ const mcpToolContext = (deps, meta, session, { locale, requestURL }) => ({
10
11
  session,
11
12
  ...locale && { locale },
12
13
  records: assistantRecords(deps, session),
14
+ comments: toolComments(deps, session, requestURL),
13
15
  resources: () => readableResources(meta, session),
14
16
  show: () => {}
15
17
  });
@@ -1,13 +1,14 @@
1
1
  import { AssistantTool, ToolContext } from "../assistant/tools.js";
2
2
  import { McpApprovals } from "./mcp-approval-store.js";
3
- import { McpServer } from "@modelcontextprotocol/server";
3
+ import { Icon, McpServer } from "@modelcontextprotocol/server";
4
4
  //#region src/mcp/mcp-server.d.ts
5
5
  export type McpServerInput = {
6
- /** The name, version and title the server tells clients when they connect. */
6
+ /** The name, version, title and icons the server tells clients when they connect, each icon an absolute URL. */
7
7
  info: {
8
8
  name: string;
9
9
  version: string;
10
10
  title?: string;
11
+ icons?: Icon[];
11
12
  };
12
13
  tools: AssistantTool[];
13
14
  /** What each tool gets as its context, for the caller of the request; `approve` comes from each call's own answers. */
@@ -13,6 +13,11 @@ export type McpToolsInput = {
13
13
  * `rolesWithoutApproval`.
14
14
  */
15
15
  writes: McpWrites;
16
+ /**
17
+ * The resources whose records take comments: offers `list_comments` on them, and `add_comment` with writes on, on
18
+ * those of them that `writes` lists when it lists some. None by default.
19
+ */
20
+ commented?: string[];
16
21
  };
17
- export declare const mcpTools: ({ db, sql, writes }: McpToolsInput) => (session: Session) => AssistantTool[];
22
+ export declare const mcpTools: ({ db, sql, writes, commented }: McpToolsInput) => (session: Session) => AssistantTool[];
18
23
  //#endregion
@@ -3,19 +3,23 @@ import { readOnlyQueryTool } from "../assistant/query-tools.js";
3
3
  import { readRecordTools } from "../assistant/record-tools.js";
4
4
  import { writeRecordTools } from "../assistant/write-record-tools.js";
5
5
  import { batchWriteTool } from "../assistant/batch-write-tool.js";
6
+ import { commentTools } from "../assistant/comment-tools.js";
6
7
  //#region src/mcp/mcp-tools.ts
7
8
  /**
8
- * The default MCP tools for a caller: the record tools, the write tools when `writes` is on, and read-only SQL when `sql`
9
- * is on and the caller sees the assistant.
9
+ * The default MCP tools for a caller: the record tools, the write tools when `writes` is on, the comment tools when a
10
+ * resource takes comments, and read-only SQL when `sql` is on and the caller sees the assistant.
10
11
  */
11
12
  const writeTools = (input) => [...writeRecordTools(input), batchWriteTool(input)];
12
- const mcpTools = ({ db, sql, writes }) => {
13
+ const mcpTools = ({ db, sql, writes, commented = [] }) => {
13
14
  const read = readRecordTools();
14
- const write = writes === false ? [] : writeTools(writes === true || !writes.resources ? {} : { resources: writes.resources });
15
+ const limits = writes === false ? false : writes === true || !writes.resources ? {} : { resources: writes.resources };
16
+ const write = limits === false ? [] : writeTools(limits);
17
+ const comments = commentTools({ commented }, limits);
15
18
  const query = sql ? [readOnlyQueryTool(db)] : [];
16
19
  return (session) => [
17
20
  ...read,
18
21
  ...write,
22
+ ...comments,
19
23
  ...seesAssistant(session.user.roles) ? query : []
20
24
  ];
21
25
  };
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.51.3";
2
+ var version = "0.52.1";
3
3
  //#endregion
4
4
  export { version };
@@ -1,10 +1,10 @@
1
1
  import { notFound } from "./problem.js";
2
2
  import { requestAccess, requireOperation, visibleFields, writableFields } from "./request-access.js";
3
- import { readTransaction } from "./transactions.js";
4
3
  import { pickFields } from "./rows.js";
4
+ import { readTransaction } from "./transactions.js";
5
+ import { deletedRows } from "./params.js";
5
6
  import { fetchReadable } from "./fetch-readable.js";
6
7
  import { presentFiles } from "./files/present.js";
7
- import { deletedRows } from "./params.js";
8
8
  import { rowPermissions } from "./row-permissions.js";
9
9
  //#region src/read-service.ts
10
10
  /**
@@ -17,7 +17,7 @@ const parsedJson = async (c) => {
17
17
  /**
18
18
  * `POST /api/broadcast` and `POST /api/broadcast/:topic/events/:event`, Supabase Realtime's broadcast API, which
19
19
  * supabase-js's `send()` on a channel that has not joined and `httpSend()` call: they send to the channels joined on a
20
- * topic in this process, as a broadcast sent on the socket, under the same topic rate. A request is admitted by the
20
+ * topic, as a broadcast sent on the socket, under the same topic rate. A request is admitted by the
21
21
  * app's key and the token in its bearer, as the socket is, and its body is at most a socket's frame. A private message
22
22
  * reaches private channels only, and only when the caller may send broadcasts on its topic by the policies on
23
23
  * `realtime.messages`: the batch drops one it may not, as Supabase does, and the single message is refused with 403.
@@ -47,7 +47,7 @@ const broadcastApi = ({ caller, broadcasts, access, report }) => {
47
47
  app.post("/", admitted, limited, async (c) => {
48
48
  if (typeOf(c) !== "application/json") throw broadcastProblem(415, "json-only", "Unsupported Media Type", "Send the messages as application/json");
49
49
  const publications = await allowed(c.var.caller, batchOf(await parsedJson(c)));
50
- if (!broadcasts.publish(publications)) throw tooFast();
50
+ if (!await broadcasts.publish(publications)) throw tooFast();
51
51
  return c.body(null, 202);
52
52
  });
53
53
  app.post("/:topic/events/:event", admitted, limited, async (c) => {
@@ -57,7 +57,7 @@ const broadcastApi = ({ caller, broadcasts, access, report }) => {
57
57
  if (type === "application/json" && (typeof payload !== "object" || payload === null || Array.isArray(payload))) throw broadcastProblem(422, "invalid-broadcast", "Unprocessable Content", "Payload must be a JSON object");
58
58
  const publication = publicationOf(c.req.param("topic"), c.req.param("event"), payload, c.req.query("private") === "true");
59
59
  if ((await allowed(c.var.caller, [publication])).length === 0) throw broadcastProblem(403, "unauthorized", "Forbidden", "Unauthorized");
60
- if (!broadcasts.publish([publication])) throw tooFast();
60
+ if (!await broadcasts.publish([publication])) throw tooFast();
61
61
  return c.body(null, 202);
62
62
  });
63
63
  return app;
@@ -2,8 +2,8 @@ import { fixedWindows } from "./fixed-window.js";
2
2
  import { realtimeLimits } from "./realtime-limits.js";
3
3
  //#region src/realtime/broadcast-hub.ts
4
4
  /**
5
- * The channels of a process by topic, which hear each other's broadcasts in-process: a cloud app runs as one process,
6
- * so channels of other processes are not reached. Private and public channels of a topic do not hear each other, as on
5
+ * The channels of a process by topic, which hear each other's broadcasts in-process; with several processes,
6
+ * `relayedHubs` carries them between them through this hub's `deliver`. Private and public channels of a topic do not hear each other, as on
7
7
  * Supabase, so a public channel cannot reach past the policies of a private one. A topic takes `realtimeLimits.topicBroadcastsPerSecond`, whoever sends
8
8
  * them; its window is kept here, so a topic left empty and joined again does not start a new one.
9
9
  */
@@ -12,6 +12,10 @@ const broadcastHub = ({ now = () => Date.now() } = {}) => {
12
12
  const privateTopics = /* @__PURE__ */ new Map();
13
13
  const topicsOf = (isPrivate) => isPrivate ? privateTopics : publicTopics;
14
14
  const rate = fixedWindows(realtimeLimits.topicBroadcastsPerSecond, 1e3, now);
15
+ /** Sends a broadcast to the topic's members, whatever the rate, but not to `from` unless it hears its own. */
16
+ const deliver = (publication, from) => {
17
+ for (const member of topicsOf(publication.private).get(publication.topic) ?? []) if (member !== from || member.self) member.receive(publication.event, publication.payload, publication.meta);
18
+ };
15
19
  return {
16
20
  /** Puts the member on the topic until the returned function takes it off. */
17
21
  join: (topic, member) => {
@@ -29,7 +33,12 @@ const broadcastHub = ({ now = () => Date.now() } = {}) => {
29
33
  */
30
34
  send: (topic, from, event, payload) => {
31
35
  if (!rate(topic)) return false;
32
- for (const member of topicsOf(from.private).get(topic) ?? []) if (member !== from || member.self) member.receive(event, payload);
36
+ deliver({
37
+ topic,
38
+ event,
39
+ payload,
40
+ private: from.private
41
+ }, from);
33
42
  return true;
34
43
  },
35
44
  /**
@@ -40,9 +49,10 @@ const broadcastHub = ({ now = () => Date.now() } = {}) => {
40
49
  const takes = /* @__PURE__ */ new Map();
41
50
  for (const { topic } of publications) takes.set(topic, (takes.get(topic) ?? 0) + 1);
42
51
  if (!rate.all(takes)) return false;
43
- for (const publication of publications) for (const member of topicsOf(publication.private).get(publication.topic) ?? []) member.receive(publication.event, publication.payload, publication.meta);
52
+ for (const publication of publications) deliver(publication);
44
53
  return true;
45
- }
54
+ },
55
+ deliver
46
56
  };
47
57
  };
48
58
  //#endregion
@@ -1,5 +1,5 @@
1
- import { quoteIdent, tableSql } from "../changes/capture-sql.js";
2
1
  import { readTransaction } from "../transactions.js";
2
+ import { quoteIdent, tableSql } from "../changes/capture-sql.js";
3
3
  import { callerSettings } from "../rest/caller-settings.js";
4
4
  import { filterSql } from "./change-filter.js";
5
5
  import { decodeKey, encodeKey } from "@protobase/schema/internal";
@@ -1,5 +1,5 @@
1
- import { isDatabaseError } from "../error-handler.js";
2
1
  import { writeTransaction } from "../transactions.js";
2
+ import { isDatabaseError } from "../error-handler.js";
3
3
  import { callerSettings } from "../rest/caller-settings.js";
4
4
  import { sql } from "kysely";
5
5
  import { randomUUID } from "node:crypto";
@@ -50,7 +50,7 @@ const databaseBroadcasts = ({ db, statementTimeoutMs, broadcasts, intervalMs = 1
50
50
  const rows = await takeMessages(db, statementTimeoutMs);
51
51
  for (const row of rows) {
52
52
  const publication = publicationOfRow(row);
53
- if (publication) broadcasts.publish([publication]);
53
+ if (publication) await broadcasts.publish([publication]);
54
54
  }
55
55
  if (rows.length < batchSize) return;
56
56
  }
@@ -4,58 +4,93 @@ const stateOf = (tracked) => {
4
4
  for (const [key, meta] of tracked) (state[key] ??= { metas: [] }).metas.push(meta);
5
5
  return state;
6
6
  };
7
+ const alone = {
8
+ state: () => [],
9
+ changed: () => {},
10
+ ready: async () => {}
11
+ };
7
12
  /**
8
13
  * The presences of a process by topic, in-process like broadcasts: each joined channel tracks at most one, under its
9
14
  * key, and channels that listen hear every join and leave on their topic, their own included, as Phoenix Presence
10
- * sends them. Tracking again replaces the presence: a leave of the old one and a join of the new. Refs count up, unique
11
- * in the process. Private and public channels of a topic do not see each other, as with broadcasts.
15
+ * sends them. Tracking again replaces the presence: a leave of the old one and a join of the new. Refs count up after
16
+ * `refPrefix`, unique in the process. Private and public channels of a topic do not see each other, as with broadcasts.
17
+ * With `remote`, a topic's state holds the other processes' presences too, each change goes to them, and `hear` tells
18
+ * the channels theirs.
12
19
  */
13
- const presenceHub = () => {
20
+ const presenceHub = ({ refPrefix = "", remote = alone } = {}) => {
14
21
  const publicTopics = /* @__PURE__ */ new Map();
15
22
  const privateTopics = /* @__PURE__ */ new Map();
16
23
  let lastRef = 0;
24
+ const topicsOf = (isPrivate) => isPrivate ? privateTopics : publicTopics;
17
25
  const tell = (members, diff) => {
18
26
  for (const member of members.keys()) if (member.listens) member.receive(diff);
19
27
  };
20
- return {
21
- /** Puts the member on the topic, with what it may do there until it leaves. */
22
- join: (topic, member) => {
23
- const topics = member.private ? privateTopics : publicTopics;
24
- const members = topics.get(topic) ?? /* @__PURE__ */ new Map();
25
- topics.set(topic, members.set(member, void 0));
26
- const untrack = () => {
27
- const old = members.get(member);
28
- if (!old) return;
29
- members.set(member, void 0);
30
- tell(members, {
31
- joins: {},
32
- leaves: stateOf([[member.key, old]])
33
- });
34
- };
35
- return {
36
- /** The topic's presences now, for `presence_state`. */
37
- state: () => stateOf([...members].flatMap(([{ key }, meta]) => meta ? [[key, meta]] : [])),
38
- track: (payload) => {
28
+ return {
29
+ /** Puts the member on the topic, with what it may do there until it leaves. */
30
+ join: (topic, member) => {
31
+ const topics = topicsOf(member.private);
32
+ const members = topics.get(topic) ?? /* @__PURE__ */ new Map();
33
+ topics.set(topic, members.set(member, void 0));
34
+ const untrack = () => {
39
35
  const old = members.get(member);
40
- const meta = {
41
- ...payload,
42
- phx_ref: String(++lastRef),
43
- ...old && { phx_ref_prev: old.phx_ref }
44
- };
45
- members.set(member, meta);
36
+ if (!old) return;
37
+ members.set(member, void 0);
46
38
  tell(members, {
47
- joins: stateOf([[member.key, meta]]),
48
- leaves: old ? stateOf([[member.key, old]]) : {}
39
+ joins: {},
40
+ leaves: stateOf([[member.key, old]])
41
+ });
42
+ remote.changed({
43
+ topic,
44
+ private: member.private,
45
+ key: member.key,
46
+ left: old
49
47
  });
50
- },
51
- untrack,
52
- leave: () => {
53
- untrack();
54
- members.delete(member);
55
- if (members.size === 0 && topics.get(topic) === members) topics.delete(topic);
56
- }
57
- };
58
- } };
48
+ };
49
+ return {
50
+ /** The topic's presences now, for `presence_state`. */
51
+ state: () => stateOf([...[...members].flatMap(([{ key }, meta]) => meta ? [[key, meta]] : []), ...remote.state(topic, member.private)]),
52
+ track: (payload) => {
53
+ const old = members.get(member);
54
+ const meta = {
55
+ ...payload,
56
+ phx_ref: `${refPrefix}${++lastRef}`,
57
+ ...old && { phx_ref_prev: old.phx_ref }
58
+ };
59
+ members.set(member, meta);
60
+ tell(members, {
61
+ joins: stateOf([[member.key, meta]]),
62
+ leaves: old ? stateOf([[member.key, old]]) : {}
63
+ });
64
+ remote.changed({
65
+ topic,
66
+ private: member.private,
67
+ key: member.key,
68
+ joined: meta,
69
+ ...old && { left: old }
70
+ });
71
+ },
72
+ untrack,
73
+ leave: () => {
74
+ untrack();
75
+ members.delete(member);
76
+ if (members.size === 0 && topics.get(topic) === members) topics.delete(topic);
77
+ }
78
+ };
79
+ },
80
+ /** Tells the topic's listening channels a diff of another process's presences. */
81
+ hear: (topic, isPrivate, diff) => {
82
+ const members = topicsOf(isPrivate).get(topic);
83
+ if (members) tell(members, diff);
84
+ },
85
+ /** Every presence this process's channels track. */
86
+ tracked: () => [true, false].flatMap((isPrivate) => [...topicsOf(isPrivate)].flatMap(([topic, members]) => [...members].flatMap(([{ key }, meta]) => meta ? [{
87
+ topic,
88
+ private: isPrivate,
89
+ key,
90
+ meta
91
+ }] : []))),
92
+ ready: remote.ready
93
+ };
59
94
  };
60
95
  //#endregion
61
96
  export { presenceHub };
@@ -3,15 +3,31 @@ import { changeChecker } from "./change-checker.js";
3
3
  import { changeHub } from "./change-hub.js";
4
4
  import { channelAccess } from "./channel-access.js";
5
5
  import { databaseBroadcasts } from "./database-broadcasts.js";
6
+ import { presenceHub } from "./presence-hub.js";
6
7
  import { realtimeRoutes } from "./realtime-routes.js";
8
+ import { relayed } from "./relay/realtime-options.js";
9
+ import { relayedHubs } from "./relay/relayed-hubs.js";
7
10
  //#region src/realtime/realtime-app.ts
11
+ const hubsOf = (deps, options, report) => {
12
+ if (relayed(options)) return relayedHubs({
13
+ db: deps.db,
14
+ statementTimeoutMs: deps.statementTimeoutMs,
15
+ report
16
+ });
17
+ return {
18
+ broadcasts: broadcastHub(),
19
+ presences: presenceHub(),
20
+ follow: () => () => {}
21
+ };
22
+ };
8
23
  /**
9
24
  * `/api/sb/realtime/v1`: the `postgres_changes` of the live resources, each checked under the subscriber's role, claims and
10
- * the tables' RLS policies before it is sent, and broadcasts and presence between the channels of the process, on a
25
+ * the tables' RLS policies before it is sent, and broadcasts and presence between the channels on a topic, of this
26
+ * process or, with `processes: 'many'`, of every process serving the app, on a
11
27
  * private channel as the policies on `realtime.messages` allow, and the broadcasts the database sends with
12
28
  * `realtime.send()`.
13
29
  */
14
- const realtimeApp = ({ deps, feed, upgradeWebSocket, caller, report }) => {
30
+ const realtimeApp = ({ deps, feed, upgradeWebSocket, caller, report, options }) => {
15
31
  const live = deps.registry.entries.filter((entry) => entry.model.live);
16
32
  const check = changeChecker({
17
33
  db: deps.db,
@@ -28,7 +44,7 @@ const realtimeApp = ({ deps, feed, upgradeWebSocket, caller, report }) => {
28
44
  db: deps.db,
29
45
  statementTimeoutMs: deps.statementTimeoutMs
30
46
  });
31
- const broadcasts = broadcastHub();
47
+ const { broadcasts, presences, follow } = hubsOf(deps, options, report);
32
48
  const database = databaseBroadcasts({
33
49
  db: deps.db,
34
50
  statementTimeoutMs: deps.statementTimeoutMs,
@@ -41,7 +57,15 @@ const realtimeApp = ({ deps, feed, upgradeWebSocket, caller, report }) => {
41
57
  live,
42
58
  hub,
43
59
  broadcasts,
44
- database,
60
+ presences,
61
+ database: { follow: () => {
62
+ const unfollowRelay = follow();
63
+ const unfollowDatabase = database.follow();
64
+ return () => {
65
+ unfollowDatabase();
66
+ unfollowRelay();
67
+ };
68
+ } },
45
69
  access,
46
70
  report
47
71
  });
@@ -2,7 +2,6 @@ import { HttpProblem } from "../problem.js";
2
2
  import { broadcastApi } from "./broadcast-api.js";
3
3
  import { bufferedOf } from "./buffered-amount.js";
4
4
  import { vsnOf } from "./phoenix-message.js";
5
- import { presenceHub } from "./presence-hub.js";
6
5
  import { realtimeSocket } from "./realtime-socket.js";
7
6
  import { Hono } from "hono";
8
7
  //#region src/realtime/realtime-routes.ts
@@ -12,15 +11,14 @@ const keyRequest = (apikey, token) => new Request("http://realtime.invalid/", {
12
11
  } });
13
12
  /**
14
13
  * `GET /api/sb/realtime/v1/websocket?apikey=…&vsn=…`: Supabase Realtime's socket, for `postgres_changes` of the live
15
- * resources, and broadcasts and presence between the sockets of this process (see `realtimeSocket`). A missing or wrong
14
+ * resources, and broadcasts and presence between the channels on a topic (see `realtimeSocket`). A missing or wrong
16
15
  * key is refused before the upgrade, with 401. A socket whose raw socket is not one of npm `ws`, such as Bun's own `ws`
17
16
  * shim's, is closed at once with 1011, since its unsent bytes cannot be counted (`realtimeLimits.maxUnsentBytes`).
18
17
  * `POST /api/sb/realtime/v1/api/broadcast` sends to the same channels without a socket (see `broadcastApi`), and so
19
18
  * does the database (see `databaseBroadcasts`).
20
19
  */
21
- const realtimeRoutes = ({ upgradeWebSocket, caller, live, hub, broadcasts, database, access, report }) => {
20
+ const realtimeRoutes = ({ upgradeWebSocket, caller, live, hub, broadcasts, presences, database, access, report }) => {
22
21
  const app = new Hono();
23
- const presences = presenceHub();
24
22
  let lastId = 0;
25
23
  const nextId = () => ++lastId;
26
24
  app.route("/api/broadcast", broadcastApi({
@@ -38,8 +36,8 @@ const realtimeRoutes = ({ upgradeWebSocket, caller, live, hub, broadcasts, datab
38
36
  let socket;
39
37
  let unfollow = () => {};
40
38
  const closed = () => {
41
- unfollow();
42
39
  socket?.closed();
40
+ unfollow();
43
41
  };
44
42
  return {
45
43
  onOpen: (_event, ws) => {
@@ -123,6 +123,8 @@ const realtimeSocket = ({ vsn, send, buffered, close, resolve, live, hub, broadc
123
123
  const refused = await gate.authorize(caller);
124
124
  if (!open) return;
125
125
  if (refused) return push(reply(message, "error", { reason: `Unauthorized: ${readRefusalText(message.topic, refused)}` }));
126
+ await presences.ready();
127
+ if (!open) return;
126
128
  const parsed = parseBindings(config.postgres_changes, live, nextId);
127
129
  if (typeof parsed === "string") return push(reply(message, "error", { reason: parsed }));
128
130
  const { topic } = message;
@@ -189,7 +191,7 @@ const realtimeSocket = ({ vsn, send, buffered, close, resolve, live, hub, broadc
189
191
  const allowed = await channel.gate.mayWrite(channel.caller, "broadcast");
190
192
  if (channels.get(message.topic) !== channel) return;
191
193
  if (!allowed) return push(reply(message, "error", { error: "unauthorized" }));
192
- if (!broadcasts.send(message.topic, channel.member, event, payload)) return push(reply(message, "error", { reason: `${tooFast} on this topic` }));
194
+ if (!await broadcasts.send(message.topic, channel.member, event, payload)) return push(reply(message, "error", { reason: `${tooFast} on this topic` }));
193
195
  if (channel.ack) push(reply(message, "ok", {}));
194
196
  };
195
197
  const updatePresence = async (message, channel) => {