@db3.ai/app 0.1.0-beta.1 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -9
- package/agent-instructions.md +76 -0
- package/dist/ai/AIErrors.d.ts +7 -1
- package/dist/ai/AIErrors.js +8 -1
- package/dist/ai/AIFailover.d.ts +10 -0
- package/dist/ai/AIFailover.js +3 -2
- package/dist/ai/AIProviderAdmission.d.ts +101 -0
- package/dist/ai/AIProviderAdmission.js +336 -0
- package/dist/ai/AIRateLimiter.d.ts +11 -2
- package/dist/ai/AIRateLimiter.js +125 -10
- package/dist/ai/AIRequestTrackingError.d.ts +22 -0
- package/dist/ai/AIRequestTrackingError.js +33 -0
- package/dist/ai/Agent.d.ts +7 -7
- package/dist/ai/Agent.js +84 -56
- package/dist/ai/Ai.d.ts +11 -3
- package/dist/ai/Ai.js +94 -44
- package/dist/ai/OpenAIProviderError.js +1 -1
- package/dist/ai/ProviderSseObserver.d.ts +19 -0
- package/dist/ai/ProviderSseObserver.js +98 -0
- package/dist/ai/aiServiceTier.d.ts +5 -0
- package/dist/ai/aiServiceTier.js +14 -0
- package/dist/ai/chunkEmbeddingText.d.ts +14 -0
- package/dist/ai/chunkEmbeddingText.js +147 -0
- package/dist/ai/contracts/AI.d.ts +16 -0
- package/dist/ai/contracts/AIRequestErrorDiagnostics.d.ts +12 -0
- package/dist/ai/contracts/AIRequestErrorDiagnostics.js +1 -0
- package/dist/ai/contracts/AIServiceTier.d.ts +2 -0
- package/dist/ai/contracts/AIServiceTier.js +1 -0
- package/dist/ai/contracts/Agent.d.ts +3 -0
- package/dist/ai/contracts/EmbeddingTextChunk.d.ts +25 -0
- package/dist/ai/contracts/EmbeddingTextChunk.js +1 -0
- package/dist/ai/contracts/Pricing.d.ts +7 -1
- package/dist/ai/contracts/Providers.d.ts +2 -0
- package/dist/ai/contracts/index.d.ts +2 -0
- package/dist/ai/contracts/index.js +1 -0
- package/dist/ai/index.d.ts +8 -2
- package/dist/ai/index.js +5 -2
- package/dist/ai/{Models.d.ts → modelPricing.d.ts} +2 -2
- package/dist/ai/{Models.js → modelPricing.js} +38 -4
- package/dist/apps/AppInstallation.d.ts +21 -0
- package/dist/apps/AppInstallation.js +16 -0
- package/dist/apps/AppRouteError.d.ts +6 -0
- package/dist/apps/AppRouteError.js +10 -0
- package/dist/apps/AppService.d.ts +24 -0
- package/dist/apps/AppService.js +21 -0
- package/dist/apps/Apps.d.ts +59 -0
- package/dist/apps/Apps.js +566 -0
- package/dist/apps/appsPlugin.d.ts +3 -0
- package/dist/apps/appsPlugin.js +36 -0
- package/dist/apps/commands/disableApp.d.ts +2 -0
- package/dist/apps/commands/disableApp.js +5 -0
- package/dist/apps/commands/enableApp.d.ts +2 -0
- package/dist/apps/commands/enableApp.js +5 -0
- package/dist/apps/commands/index.d.ts +10 -0
- package/dist/apps/commands/index.js +27 -0
- package/dist/apps/commands/installApp.d.ts +2 -0
- package/dist/apps/commands/installApp.js +5 -0
- package/dist/apps/commands/listApps.d.ts +2 -0
- package/dist/apps/commands/listApps.js +5 -0
- package/dist/apps/commands/makeAppMigration.d.ts +2 -0
- package/dist/apps/commands/makeAppMigration.js +9 -0
- package/dist/apps/commands/rollbackApp.d.ts +2 -0
- package/dist/apps/commands/rollbackApp.js +5 -0
- package/dist/apps/commands/uninstallApp.d.ts +2 -0
- package/dist/apps/commands/uninstallApp.js +5 -0
- package/dist/apps/contracts/AppAction.d.ts +2 -0
- package/dist/apps/contracts/AppAction.js +1 -0
- package/dist/apps/contracts/AppDefinition.d.ts +45 -0
- package/dist/apps/contracts/AppDefinition.js +1 -0
- package/dist/apps/contracts/AppDescription.d.ts +32 -0
- package/dist/apps/contracts/AppDescription.js +1 -0
- package/dist/apps/contracts/AppManifest.d.ts +30 -0
- package/dist/apps/contracts/AppManifest.js +1 -0
- package/dist/apps/contracts/AppNavigation.d.ts +9 -0
- package/dist/apps/contracts/AppNavigation.js +1 -0
- package/dist/apps/contracts/AppNavigationBadge.d.ts +6 -0
- package/dist/apps/contracts/AppNavigationBadge.js +1 -0
- package/dist/apps/contracts/AppNavigationContext.d.ts +8 -0
- package/dist/apps/contracts/AppNavigationContext.js +1 -0
- package/dist/apps/contracts/AppNavigationResult.d.ts +12 -0
- package/dist/apps/contracts/AppNavigationResult.js +1 -0
- package/dist/apps/contracts/AppRoute.d.ts +19 -0
- package/dist/apps/contracts/AppRoute.js +1 -0
- package/dist/apps/contracts/DiscoveredApp.d.ts +10 -0
- package/dist/apps/contracts/DiscoveredApp.js +1 -0
- package/dist/apps/contracts/InstalledApps.d.ts +3 -0
- package/dist/apps/contracts/InstalledApps.js +1 -0
- package/dist/apps/contracts/index.d.ts +11 -0
- package/dist/apps/contracts/index.js +1 -0
- package/dist/apps/defineApp.d.ts +7 -0
- package/dist/apps/defineApp.js +8 -0
- package/dist/apps/discoverApps.d.ts +3 -0
- package/dist/apps/discoverApps.js +62 -0
- package/dist/apps/fastify.d.ts +1 -0
- package/dist/apps/fastify.js +1 -0
- package/dist/apps/index.d.ts +7 -0
- package/dist/apps/index.js +7 -0
- package/dist/apps/normalizeAppNavigation.d.ts +3 -0
- package/dist/apps/normalizeAppNavigation.js +30 -0
- package/dist/apps/registerAppRoutes.d.ts +6 -0
- package/dist/apps/registerAppRoutes.js +52 -0
- package/dist/apps/validateAppDefinitions.d.ts +3 -0
- package/dist/apps/validateAppDefinitions.js +65 -0
- package/dist/apps/vite.d.ts +1 -0
- package/dist/apps/vite.js +1 -0
- package/dist/apps/writeAppTypes.d.ts +2 -0
- package/dist/apps/writeAppTypes.js +13 -0
- package/dist/auth/Auth.d.ts +18 -0
- package/dist/auth/Auth.js +89 -13
- package/dist/auth/PasswordLoginAttempt.d.ts +50 -0
- package/dist/auth/PasswordLoginAttempt.js +60 -0
- package/dist/auth/PasswordSuspendedError.d.ts +10 -0
- package/dist/auth/PasswordSuspendedError.js +13 -0
- package/dist/auth/contracts/PasswordAuthProviderOptions.d.ts +26 -0
- package/dist/auth/contracts/PasswordAuthProviderOptions.js +1 -0
- package/dist/auth/contracts/PasswordSuspension.d.ts +7 -0
- package/dist/auth/contracts/PasswordSuspension.js +1 -0
- package/dist/auth/contracts/PasswordSuspensionOptions.d.ts +10 -0
- package/dist/auth/contracts/PasswordSuspensionOptions.js +1 -0
- package/dist/auth/contracts/index.d.ts +3 -0
- package/dist/auth/contracts/index.js +3 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/providers/PasswordAuthProvider.d.ts +19 -23
- package/dist/auth/providers/PasswordAuthProvider.js +96 -8
- package/dist/auth/providers/resolveAuthProviders.js +1 -0
- package/dist/cli/Cli.d.ts +2 -0
- package/dist/cli/Cli.js +8 -0
- package/dist/cli/commands/repl.js +2 -1
- package/dist/cli/contracts/CliCommand.d.ts +2 -0
- package/dist/cli/contracts/CliCommandInfo.d.ts +12 -0
- package/dist/cli/contracts/CliCommandInfo.js +1 -0
- package/dist/cli/contracts/index.d.ts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/listDb3Commands.d.ts +8 -0
- package/dist/cli/listDb3Commands.js +10 -0
- package/dist/cli/runDb3.d.ts +3 -6
- package/dist/cli/runDb3.js +5 -59
- package/dist/cli/withDb3Cli.d.ts +4 -0
- package/dist/cli/withDb3Cli.js +60 -0
- package/dist/db/ActiveQueryBuilder.d.ts +8 -0
- package/dist/db/ActiveQueryBuilder.js +11 -0
- package/dist/db/ActiveRecord.d.ts +6 -0
- package/dist/db/ActiveRecord.js +8 -0
- package/dist/db/connection.js +14 -2
- package/dist/db/fields/TimestampField.d.ts +1 -1
- package/dist/db/fields/TimestampField.js +7 -12
- package/dist/db/migrations/DatabaseMigrationManager.d.ts +3 -1
- package/dist/db/migrations/DatabaseMigrationManager.js +9 -3
- package/dist/db/migrations/contracts/DatabaseMigration.d.ts +7 -0
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +1 -1
- package/dist/db/migrations/{KnexMigrationRenderer.d.ts → renderKnexMigration.d.ts} +4 -3
- package/dist/db/migrations/{KnexMigrationRenderer.js → renderKnexMigration.js} +7 -6
- package/dist/flows/FlowCompiler.js +1 -1
- package/dist/flows/Flows.js +1 -1
- package/dist/flows/index.d.ts +1 -1
- package/dist/flows/index.js +1 -1
- package/dist/health/Health.d.ts +41 -0
- package/dist/health/Health.js +153 -0
- package/dist/health/contracts/Health.d.ts +36 -0
- package/dist/health/contracts/Health.js +1 -0
- package/dist/health/contracts/HealthHttp.d.ts +15 -0
- package/dist/health/contracts/HealthHttp.js +1 -0
- package/dist/health/contracts/index.d.ts +2 -0
- package/dist/health/contracts/index.js +1 -0
- package/dist/health/databaseHealthCheck.d.ts +15 -0
- package/dist/health/databaseHealthCheck.js +18 -0
- package/dist/health/fastify.d.ts +13 -0
- package/dist/health/fastify.js +44 -0
- package/dist/health/index.d.ts +3 -0
- package/dist/health/index.js +2 -0
- package/dist/in-app/InApp.d.ts +33 -0
- package/dist/in-app/InApp.js +191 -0
- package/dist/in-app/InAppError.d.ts +6 -0
- package/dist/in-app/InAppError.js +10 -0
- package/dist/in-app/InAppRecord.d.ts +36 -0
- package/dist/in-app/InAppRecord.js +24 -0
- package/dist/in-app/contracts/InAppInbox.d.ts +45 -0
- package/dist/in-app/contracts/InAppInbox.js +1 -0
- package/dist/in-app/contracts/InAppMessage.d.ts +22 -0
- package/dist/in-app/contracts/InAppMessage.js +1 -0
- package/dist/in-app/contracts/InAppScope.d.ts +17 -0
- package/dist/in-app/contracts/InAppScope.js +1 -0
- package/dist/in-app/contracts/index.d.ts +3 -0
- package/dist/in-app/contracts/index.js +3 -0
- package/dist/in-app/index.d.ts +4 -0
- package/dist/in-app/index.js +4 -0
- package/dist/in-app/validation.d.ts +21 -0
- package/dist/in-app/validation.js +50 -0
- package/dist/index.d.ts +2 -14
- package/dist/index.js +2 -5
- package/dist/logging/DevelopmentConsole.d.ts +42 -0
- package/dist/logging/DevelopmentConsole.js +162 -0
- package/dist/logging/Log.d.ts +3 -1
- package/dist/logging/Log.js +5 -2
- package/dist/logging/PrettyLogFormatter.d.ts +16 -0
- package/dist/logging/PrettyLogFormatter.js +151 -0
- package/dist/logging/acquireDevelopmentConsole.d.ts +6 -0
- package/dist/logging/acquireDevelopmentConsole.js +21 -0
- package/dist/logging/contracts/DevelopmentConsoleOptions.d.ts +16 -0
- package/dist/logging/contracts/DevelopmentConsoleOptions.js +1 -0
- package/dist/logging/contracts/EmailLogOptions.d.ts +8 -0
- package/dist/logging/contracts/EmailLogOptions.js +1 -0
- package/dist/logging/contracts/HttpExchangeMonitorOptions.d.ts +12 -0
- package/dist/logging/contracts/HttpExchangeMonitorOptions.js +1 -0
- package/dist/logging/contracts/LogTransportOptions.d.ts +19 -0
- package/dist/logging/contracts/LogTransportOptions.js +1 -0
- package/dist/logging/contracts/LoggingOptions.d.ts +12 -1
- package/dist/logging/contracts/index.d.ts +4 -0
- package/dist/logging/contracts/index.js +4 -0
- package/dist/logging/drivers/PinoLoggerDriver.d.ts +3 -1
- package/dist/logging/drivers/PinoLoggerDriver.js +75 -16
- package/dist/logging/formatLogEmail.d.ts +4 -0
- package/dist/logging/formatLogEmail.js +21 -0
- package/dist/logging/highlightQueueMessage.d.ts +11 -0
- package/dist/logging/highlightQueueMessage.js +36 -0
- package/dist/logging/httpBodyCapture.d.ts +24 -0
- package/dist/logging/httpBodyCapture.js +321 -0
- package/dist/logging/index.d.ts +2 -0
- package/dist/logging/index.js +2 -0
- package/dist/logging/redactLogEmail.d.ts +2 -0
- package/dist/logging/redactLogEmail.js +12 -0
- package/dist/logging/registerHttpExchangeMonitor.d.ts +10 -0
- package/dist/logging/registerHttpExchangeMonitor.js +43 -0
- package/dist/logging/transports/EmailLogTransport.d.ts +18 -0
- package/dist/logging/transports/EmailLogTransport.js +47 -0
- package/dist/mail/Mail.d.ts +2 -0
- package/dist/mail/Mail.js +3 -0
- package/dist/mail/MailDeliveryError.d.ts +9 -0
- package/dist/mail/MailDeliveryError.js +16 -0
- package/dist/mail/index.d.ts +1 -0
- package/dist/mail/index.js +1 -0
- package/dist/mail/transports/MailgunTransport.js +2 -0
- package/dist/mail/transports/ResendTransport.d.ts +3 -2
- package/dist/mail/transports/ResendTransport.js +22 -14
- package/dist/media/MediaManager.d.ts +4 -2
- package/dist/media/MediaManager.js +40 -8
- package/dist/network/BlockedUrlError.d.ts +16 -0
- package/dist/network/BlockedUrlError.js +19 -0
- package/dist/network/assertPublicUrl.d.ts +22 -0
- package/dist/network/assertPublicUrl.js +67 -0
- package/dist/network/contracts/GuardedFetch.d.ts +35 -0
- package/dist/network/contracts/GuardedFetch.js +1 -0
- package/dist/network/contracts/PublicUrlOptions.d.ts +17 -0
- package/dist/network/contracts/PublicUrlOptions.js +1 -0
- package/dist/network/contracts/index.d.ts +2 -0
- package/dist/network/contracts/index.js +2 -0
- package/dist/network/guardedFetch.d.ts +29 -0
- package/dist/network/guardedFetch.js +133 -0
- package/dist/network/index.d.ts +6 -0
- package/dist/network/index.js +6 -0
- package/dist/network/playwright/GuardedBrowserProxy.d.ts +32 -0
- package/dist/network/playwright/GuardedBrowserProxy.js +172 -0
- package/dist/network/playwright/contracts/GuardedBrowserContextOptions.d.ts +37 -0
- package/dist/network/playwright/contracts/GuardedBrowserContextOptions.js +1 -0
- package/dist/network/playwright/contracts/index.d.ts +1 -0
- package/dist/network/playwright/contracts/index.js +1 -0
- package/dist/network/playwright/index.d.ts +2 -0
- package/dist/network/playwright/index.js +2 -0
- package/dist/network/playwright/newGuardedBrowserContext.d.ts +21 -0
- package/dist/network/playwright/newGuardedBrowserContext.js +86 -0
- package/dist/network/privateNetworkAddresses.d.ts +44 -0
- package/dist/network/privateNetworkAddresses.js +157 -0
- package/dist/network/publicAddressLookup.d.ts +26 -0
- package/dist/network/publicAddressLookup.js +42 -0
- package/dist/notifications/Notifications.d.ts +27 -0
- package/dist/notifications/Notifications.js +99 -0
- package/dist/notifications/contracts/Notification.d.ts +56 -0
- package/dist/notifications/contracts/Notification.js +1 -0
- package/dist/notifications/contracts/NotificationDelivery.d.ts +11 -0
- package/dist/notifications/contracts/NotificationDelivery.js +1 -0
- package/dist/notifications/contracts/index.d.ts +2 -0
- package/dist/notifications/contracts/index.js +2 -0
- package/dist/notifications/index.d.ts +2 -0
- package/dist/notifications/index.js +2 -0
- package/dist/queue/JobRunAttempt.d.ts +16 -0
- package/dist/queue/JobRunAttempt.js +17 -0
- package/dist/queue/JobRunEvent.d.ts +15 -0
- package/dist/queue/JobRunEvent.js +16 -0
- package/dist/queue/JobRunStoppedError.d.ts +6 -0
- package/dist/queue/JobRunStoppedError.js +10 -0
- package/dist/queue/Queue.d.ts +19 -4
- package/dist/queue/Queue.js +122 -9
- package/dist/queue/QueueTerminalError.d.ts +9 -0
- package/dist/queue/QueueTerminalError.js +12 -0
- package/dist/queue/QueueWorker.d.ts +12 -5
- package/dist/queue/QueueWorker.js +94 -9
- package/dist/queue/QueueWorkerReporter.d.ts +17 -0
- package/dist/queue/QueueWorkerReporter.js +71 -0
- package/dist/queue/QueueableJob.d.ts +2 -2
- package/dist/queue/RunnableJob.d.ts +78 -0
- package/dist/queue/RunnableJob.js +284 -0
- package/dist/queue/console.d.ts +8 -0
- package/dist/queue/console.js +53 -19
- package/dist/queue/contracts/QueueDriver.d.ts +8 -0
- package/dist/queue/contracts/QueueJobRunner.d.ts +4 -0
- package/dist/queue/contracts/QueueJobRunner.js +1 -0
- package/dist/queue/contracts/QueuePayload.d.ts +1 -1
- package/dist/queue/contracts/QueueSelection.d.ts +11 -0
- package/dist/queue/contracts/QueueSelection.js +1 -0
- package/dist/queue/contracts/QueueService.d.ts +18 -4
- package/dist/queue/contracts/QueueWorkerLifecycle.d.ts +2 -2
- package/dist/queue/contracts/QueueWorkerSnapshot.d.ts +14 -0
- package/dist/queue/contracts/QueueWorkerSnapshot.js +1 -0
- package/dist/queue/contracts/QueueableJob.d.ts +7 -3
- package/dist/queue/contracts/QueueableJobContract.d.ts +12 -0
- package/dist/queue/contracts/QueueableJobContract.js +1 -0
- package/dist/queue/contracts/index.d.ts +4 -0
- package/dist/queue/contracts/index.js +2 -0
- package/dist/queue/drivers/DatabaseQueueDriver.d.ts +4 -0
- package/dist/queue/drivers/DatabaseQueueDriver.js +27 -3
- package/dist/queue/drivers/RedisQueueDriver.d.ts +7 -0
- package/dist/queue/drivers/RedisQueueDriver.js +37 -0
- package/dist/queue/index.d.ts +5 -0
- package/dist/queue/index.js +5 -0
- package/dist/queue/jobEnvelopeHasPrefix.d.ts +3 -0
- package/dist/queue/jobEnvelopeHasPrefix.js +4 -0
- package/dist/scheduler/ScheduledEvent.d.ts +8 -4
- package/dist/scheduler/ScheduledEvent.js +17 -3
- package/dist/scheduler/ScheduledJob.d.ts +1 -1
- package/dist/scheduler/Scheduler.d.ts +5 -0
- package/dist/scheduler/Scheduler.js +92 -28
- package/dist/scheduler/SchedulerCheckpointRecord.d.ts +16 -0
- package/dist/scheduler/SchedulerCheckpointRecord.js +53 -0
- package/dist/scheduler/SchedulerWorker.d.ts +3 -5
- package/dist/scheduler/SchedulerWorker.js +58 -14
- package/dist/scheduler/console.js +3 -1
- package/dist/scheduler/contracts/Schedule.d.ts +19 -0
- package/dist/scheduler/contracts/Schedule.js +1 -0
- package/dist/scheduler/contracts/Scheduler.d.ts +18 -6
- package/dist/scheduler/contracts/SchedulerCheckpoint.d.ts +23 -0
- package/dist/scheduler/contracts/SchedulerCheckpoint.js +1 -0
- package/dist/scheduler/contracts/SchedulerConsole.d.ts +5 -0
- package/dist/scheduler/contracts/SchedulerWorker.d.ts +6 -1
- package/dist/scheduler/contracts/index.d.ts +2 -0
- package/dist/scheduler/contracts/index.js +2 -0
- package/dist/scheduler/index.d.ts +1 -0
- package/dist/scheduler/index.js +1 -0
- package/dist/server/App.d.ts +36 -4
- package/dist/server/App.js +57 -7
- package/dist/server/contracts/HttpErrorHandlerOptions.d.ts +29 -0
- package/dist/server/contracts/HttpErrorHandlerOptions.js +1 -0
- package/dist/server/contracts/index.d.ts +1 -0
- package/dist/server/contracts/index.js +1 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- package/dist/server/publicServerErrorMessage.d.ts +11 -0
- package/dist/server/publicServerErrorMessage.js +16 -0
- package/dist/server/registerBrowserJsonFormatting.d.ts +16 -0
- package/dist/server/registerBrowserJsonFormatting.js +100 -0
- package/dist/server/registerHttpErrorHandler.d.ts +20 -0
- package/dist/server/registerHttpErrorHandler.js +93 -0
- package/dist/ssr/vite/contracts/ViteSsrOptions.d.ts +2 -0
- package/dist/ssr/vite/fastifyViteSsr.js +4 -3
- package/dist/storage/drivers/FlystorageDisk.js +2 -2
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +2 -2
- package/dist/websocket/WebSocketChannels.d.ts +18 -0
- package/dist/websocket/WebSocketChannels.js +129 -0
- package/dist/websocket/WebSocketConnection.d.ts +25 -0
- package/dist/websocket/WebSocketConnection.js +244 -0
- package/dist/websocket/WebSockets.d.ts +41 -0
- package/dist/websocket/WebSockets.js +194 -0
- package/dist/websocket/client/WebSocketClient.d.ts +21 -0
- package/dist/websocket/client/WebSocketClient.js +230 -0
- package/dist/websocket/client/index.d.ts +3 -0
- package/dist/websocket/client/index.js +1 -0
- package/dist/websocket/contracts/WebSocketChannel.d.ts +20 -0
- package/dist/websocket/contracts/WebSocketChannel.js +1 -0
- package/dist/websocket/contracts/WebSocketClient.d.ts +45 -0
- package/dist/websocket/contracts/WebSocketClient.js +1 -0
- package/dist/websocket/contracts/WebSocketEndpoint.d.ts +43 -0
- package/dist/websocket/contracts/WebSocketEndpoint.js +1 -0
- package/dist/websocket/contracts/WebSocketOptions.d.ts +48 -0
- package/dist/websocket/contracts/WebSocketOptions.js +1 -0
- package/dist/websocket/contracts/WebSocketPresence.d.ts +6 -0
- package/dist/websocket/contracts/WebSocketPresence.js +1 -0
- package/dist/websocket/contracts/WebSocketServiceOptions.d.ts +12 -0
- package/dist/websocket/contracts/WebSocketServiceOptions.js +1 -0
- package/dist/websocket/contracts/index.d.ts +6 -0
- package/dist/websocket/contracts/index.js +1 -0
- package/dist/websocket/defineChannel.d.ts +8 -0
- package/dist/websocket/defineChannel.js +13 -0
- package/dist/websocket/defineWebSocket.d.ts +14 -0
- package/dist/websocket/defineWebSocket.js +4 -0
- package/dist/websocket/fastify.d.ts +11 -0
- package/dist/websocket/fastify.js +38 -0
- package/dist/websocket/index.d.ts +5 -0
- package/dist/websocket/index.js +4 -0
- package/package.json +108 -7
- package/src/ai/AIErrors.ts +10 -0
- package/src/ai/AIFailover.ts +3 -2
- package/src/ai/AIProviderAdmission.ts +329 -0
- package/src/ai/AIRateLimiter.ts +134 -10
- package/src/ai/AIRequestTrackingError.ts +37 -0
- package/src/ai/Agent.ts +85 -71
- package/src/ai/Ai.ts +104 -48
- package/src/ai/OpenAIProviderError.ts +1 -1
- package/src/ai/ProviderSseObserver.ts +89 -0
- package/src/ai/README.md +216 -11
- package/src/ai/aiServiceTier.ts +14 -0
- package/src/ai/chunkEmbeddingText.ts +137 -0
- package/src/ai/contracts/AI.ts +17 -0
- package/src/ai/contracts/AIRequestErrorDiagnostics.ts +12 -0
- package/src/ai/contracts/AIServiceTier.ts +2 -0
- package/src/ai/contracts/Agent.ts +3 -0
- package/src/ai/contracts/EmbeddingTextChunk.ts +26 -0
- package/src/ai/contracts/Pricing.ts +7 -1
- package/src/ai/contracts/Providers.ts +2 -0
- package/src/ai/contracts/index.ts +2 -0
- package/src/ai/examples/prepareDocumentEmbeddings.ts +20 -0
- package/src/ai/index.ts +9 -2
- package/src/ai/{Models.ts → modelPricing.ts} +37 -4
- package/src/apps/AppInstallation.ts +25 -0
- package/src/apps/AppRouteError.ts +8 -0
- package/src/apps/AppService.ts +30 -0
- package/src/apps/Apps.ts +508 -0
- package/src/apps/README.md +354 -0
- package/src/apps/appsPlugin.ts +34 -0
- package/src/apps/commands/disableApp.ts +6 -0
- package/src/apps/commands/enableApp.ts +6 -0
- package/src/apps/commands/index.ts +29 -0
- package/src/apps/commands/installApp.ts +6 -0
- package/src/apps/commands/listApps.ts +6 -0
- package/src/apps/commands/makeAppMigration.ts +9 -0
- package/src/apps/commands/rollbackApp.ts +6 -0
- package/src/apps/commands/uninstallApp.ts +6 -0
- package/src/apps/contracts/AppAction.ts +2 -0
- package/src/apps/contracts/AppDefinition.ts +49 -0
- package/src/apps/contracts/AppDescription.ts +27 -0
- package/src/apps/contracts/AppManifest.ts +32 -0
- package/src/apps/contracts/AppNavigation.ts +10 -0
- package/src/apps/contracts/AppNavigationBadge.ts +6 -0
- package/src/apps/contracts/AppNavigationContext.ts +6 -0
- package/src/apps/contracts/AppNavigationResult.ts +9 -0
- package/src/apps/contracts/AppRoute.ts +18 -0
- package/src/apps/contracts/DiscoveredApp.ts +11 -0
- package/src/apps/contracts/InstalledApps.ts +2 -0
- package/src/apps/contracts/index.ts +11 -0
- package/src/apps/defineApp.ts +10 -0
- package/src/apps/discoverApps.ts +56 -0
- package/src/apps/fastify.ts +1 -0
- package/src/apps/index.ts +7 -0
- package/src/apps/normalizeAppNavigation.ts +29 -0
- package/src/apps/registerAppRoutes.ts +48 -0
- package/src/apps/validateAppDefinitions.ts +52 -0
- package/src/apps/vite.ts +1 -0
- package/src/apps/writeAppTypes.ts +14 -0
- package/src/auth/Auth.ts +89 -23
- package/src/auth/PasswordLoginAttempt.ts +72 -0
- package/src/auth/PasswordSuspendedError.ts +13 -0
- package/src/auth/README.md +94 -4
- package/src/auth/contracts/PasswordAuthProviderOptions.ts +30 -0
- package/src/auth/contracts/PasswordSuspension.ts +7 -0
- package/src/auth/contracts/PasswordSuspensionOptions.ts +10 -0
- package/src/auth/contracts/index.ts +3 -0
- package/src/auth/examples/runPasswordAuth.ts +6 -6
- package/src/auth/index.ts +2 -0
- package/src/auth/providers/PasswordAuthProvider.ts +96 -30
- package/src/auth/providers/resolveAuthProviders.ts +1 -0
- package/src/cli/Cli.ts +9 -0
- package/src/cli/README.md +19 -0
- package/src/cli/commands/repl.ts +2 -1
- package/src/cli/contracts/CliCommand.ts +2 -0
- package/src/cli/contracts/CliCommandInfo.ts +13 -0
- package/src/cli/contracts/index.ts +1 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/listDb3Commands.ts +12 -0
- package/src/cli/runDb3.ts +5 -51
- package/src/cli/withDb3Cli.ts +55 -0
- package/src/db/ActiveQueryBuilder.ts +12 -0
- package/src/db/ActiveRecord.ts +9 -0
- package/src/db/README.md +45 -1
- package/src/db/connection.ts +12 -1
- package/src/db/examples/FieldNote.ts +7 -13
- package/src/db/examples/KnowledgeNote.ts +7 -13
- package/src/db/examples/runNoteMigrations.ts +11 -11
- package/src/db/fields/TimestampField.ts +8 -17
- package/src/db/migrations/DatabaseMigrationManager.ts +12 -4
- package/src/db/migrations/contracts/DatabaseMigration.ts +9 -0
- package/src/db/migrations/index.ts +1 -1
- package/src/db/migrations/{KnexMigrationRenderer.ts → renderKnexMigration.ts} +8 -14
- package/src/flows/FlowCompiler.ts +1 -1
- package/src/flows/Flows.ts +1 -1
- package/src/flows/index.ts +1 -1
- package/src/health/Health.ts +163 -0
- package/src/health/README.md +46 -0
- package/src/health/contracts/Health.ts +47 -0
- package/src/health/contracts/HealthHttp.ts +19 -0
- package/src/health/contracts/index.ts +2 -0
- package/src/health/databaseHealthCheck.ts +21 -0
- package/src/health/fastify.ts +49 -0
- package/src/health/index.ts +3 -0
- package/src/in-app/InApp.ts +173 -0
- package/src/in-app/InAppError.ts +8 -0
- package/src/in-app/InAppRecord.ts +41 -0
- package/src/in-app/README.md +193 -0
- package/src/in-app/contracts/InAppInbox.ts +51 -0
- package/src/in-app/contracts/InAppMessage.ts +19 -0
- package/src/in-app/contracts/InAppScope.ts +12 -0
- package/src/in-app/contracts/index.ts +3 -0
- package/src/in-app/examples/sendReportReady.ts +14 -0
- package/src/in-app/index.ts +4 -0
- package/src/in-app/validation.ts +49 -0
- package/src/index.ts +2 -22
- package/src/logging/DevelopmentConsole.ts +154 -0
- package/src/logging/Log.ts +5 -2
- package/src/logging/PrettyLogFormatter.ts +165 -0
- package/src/logging/README.md +52 -109
- package/src/logging/acquireDevelopmentConsole.ts +19 -0
- package/src/logging/contracts/DevelopmentConsoleOptions.ts +13 -0
- package/src/logging/contracts/EmailLogOptions.ts +9 -0
- package/src/logging/contracts/HttpExchangeMonitorOptions.ts +13 -0
- package/src/logging/contracts/LogTransportOptions.ts +10 -0
- package/src/logging/contracts/LoggingOptions.ts +12 -1
- package/src/logging/contracts/index.ts +4 -0
- package/src/logging/development/README.md +88 -0
- package/src/logging/drivers/PinoLoggerDriver.ts +72 -20
- package/src/logging/examples/runDevelopmentLogs.ts +36 -0
- package/src/logging/formatLogEmail.ts +24 -0
- package/src/logging/highlightQueueMessage.ts +38 -0
- package/src/logging/httpBodyCapture.ts +393 -0
- package/src/logging/index.ts +2 -0
- package/src/logging/redactLogEmail.ts +12 -0
- package/src/logging/registerHttpExchangeMonitor.ts +45 -0
- package/src/logging/transports/EmailLogTransport.ts +46 -0
- package/src/mail/Mail.ts +5 -0
- package/src/mail/MailDeliveryError.ts +8 -0
- package/src/mail/README.md +11 -1
- package/src/mail/index.ts +1 -0
- package/src/mail/transports/MailgunTransport.ts +2 -0
- package/src/mail/transports/ResendTransport.ts +24 -22
- package/src/media/MediaManager.ts +31 -10
- package/src/media/README.md +4 -0
- package/src/media/examples/runPrivateFiles.ts +2 -2
- package/src/network/BlockedUrlError.ts +19 -0
- package/src/network/README.md +133 -0
- package/src/network/assertPublicUrl.ts +77 -0
- package/src/network/contracts/GuardedFetch.ts +40 -0
- package/src/network/contracts/PublicUrlOptions.ts +17 -0
- package/src/network/contracts/index.ts +2 -0
- package/src/network/examples/runGuardedFetch.ts +35 -0
- package/src/network/guardedFetch.ts +150 -0
- package/src/network/index.ts +6 -0
- package/src/network/playwright/GuardedBrowserProxy.ts +172 -0
- package/src/network/playwright/contracts/GuardedBrowserContextOptions.ts +43 -0
- package/src/network/playwright/contracts/index.ts +1 -0
- package/src/network/playwright/index.ts +2 -0
- package/src/network/playwright/newGuardedBrowserContext.ts +74 -0
- package/src/network/privateNetworkAddresses.ts +166 -0
- package/src/network/publicAddressLookup.ts +49 -0
- package/src/notifications/Notifications.ts +105 -0
- package/src/notifications/README.md +59 -0
- package/src/notifications/contracts/Notification.ts +65 -0
- package/src/notifications/contracts/NotificationDelivery.ts +11 -0
- package/src/notifications/contracts/index.ts +2 -0
- package/src/notifications/examples/ReportReadyNotification.ts +31 -0
- package/src/notifications/index.ts +2 -0
- package/src/queue/JobRunAttempt.ts +17 -0
- package/src/queue/JobRunEvent.ts +16 -0
- package/src/queue/JobRunStoppedError.ts +8 -0
- package/src/queue/Queue.ts +112 -11
- package/src/queue/QueueTerminalError.ts +12 -0
- package/src/queue/QueueWorker.ts +86 -7
- package/src/queue/QueueWorkerReporter.ts +59 -0
- package/src/queue/QueueableJob.ts +2 -2
- package/src/queue/README.md +142 -1
- package/src/queue/RunnableJob.ts +272 -0
- package/src/queue/console.ts +54 -21
- package/src/queue/contracts/QueueDriver.ts +9 -0
- package/src/queue/contracts/QueueJobRunner.ts +4 -0
- package/src/queue/contracts/QueuePayload.ts +1 -1
- package/src/queue/contracts/QueueSelection.ts +11 -0
- package/src/queue/contracts/QueueService.ts +19 -4
- package/src/queue/contracts/QueueWorkerLifecycle.ts +2 -2
- package/src/queue/contracts/QueueWorkerSnapshot.ts +15 -0
- package/src/queue/contracts/QueueableJob.ts +7 -3
- package/src/queue/contracts/QueueableJobContract.ts +13 -0
- package/src/queue/contracts/index.ts +6 -0
- package/src/queue/drivers/DatabaseQueueDriver.ts +25 -3
- package/src/queue/drivers/RedisQueueDriver.ts +36 -0
- package/src/queue/examples/runQueueSelection.ts +34 -0
- package/src/queue/index.ts +7 -0
- package/src/queue/jobEnvelopeHasPrefix.ts +6 -0
- package/src/scheduler/README.md +50 -9
- package/src/scheduler/ScheduledEvent.ts +20 -5
- package/src/scheduler/ScheduledJob.ts +1 -1
- package/src/scheduler/Scheduler.ts +73 -27
- package/src/scheduler/SchedulerCheckpointRecord.ts +51 -0
- package/src/scheduler/SchedulerWorker.ts +54 -19
- package/src/scheduler/console.ts +3 -1
- package/src/scheduler/contracts/Schedule.ts +22 -0
- package/src/scheduler/contracts/Scheduler.ts +21 -6
- package/src/scheduler/contracts/SchedulerCheckpoint.ts +23 -0
- package/src/scheduler/contracts/SchedulerConsole.ts +5 -0
- package/src/scheduler/contracts/SchedulerWorker.ts +6 -1
- package/src/scheduler/contracts/index.ts +2 -0
- package/src/scheduler/examples/runSchedulerWindow.ts +54 -0
- package/src/scheduler/index.ts +1 -0
- package/src/server/App.ts +73 -10
- package/src/server/README.md +95 -2
- package/src/server/contracts/HttpErrorHandlerOptions.ts +29 -0
- package/src/server/contracts/index.ts +1 -0
- package/src/server/examples/createFirstServer.ts +5 -3
- package/src/server/index.ts +4 -0
- package/src/server/publicServerErrorMessage.ts +17 -0
- package/src/server/registerBrowserJsonFormatting.ts +94 -0
- package/src/server/registerHttpErrorHandler.ts +96 -0
- package/src/ssr/README.md +6 -0
- package/src/ssr/vite/contracts/ViteSsrOptions.ts +2 -0
- package/src/ssr/vite/fastifyViteSsr.ts +4 -3
- package/src/storage/drivers/FlystorageDisk.ts +2 -2
- package/src/storage/index.ts +2 -2
- package/src/websocket/README.md +458 -0
- package/src/websocket/WebSocketChannels.ts +99 -0
- package/src/websocket/WebSocketConnection.ts +162 -0
- package/src/websocket/WebSockets.ts +157 -0
- package/src/websocket/client/WebSocketClient.ts +188 -0
- package/src/websocket/client/index.ts +3 -0
- package/src/websocket/contracts/WebSocketChannel.ts +23 -0
- package/src/websocket/contracts/WebSocketClient.ts +48 -0
- package/src/websocket/contracts/WebSocketEndpoint.ts +46 -0
- package/src/websocket/contracts/WebSocketOptions.ts +50 -0
- package/src/websocket/contracts/WebSocketPresence.ts +6 -0
- package/src/websocket/contracts/WebSocketServiceOptions.ts +12 -0
- package/src/websocket/contracts/index.ts +6 -0
- package/src/websocket/defineChannel.ts +14 -0
- package/src/websocket/defineWebSocket.ts +15 -0
- package/src/websocket/examples/board/LiveBoard.ts +97 -0
- package/src/websocket/examples/board/SummarizeBoardJob.ts +52 -0
- package/src/websocket/examples/board/boardSnapshot.ts +16 -0
- package/src/websocket/examples/board/createBoardClient.ts +43 -0
- package/src/websocket/examples/board/createBoardStore.ts +58 -0
- package/src/websocket/examples/board/registerBoardRoutes.ts +31 -0
- package/src/websocket/examples/createChannelSync.ts +66 -0
- package/src/websocket/examples/deploymentEndpoint.ts +13 -0
- package/src/websocket/examples/deploymentSnapshot.ts +36 -0
- package/src/websocket/examples/resourceChannels.ts +18 -0
- package/src/websocket/examples/userEndpoint.ts +27 -0
- package/src/websocket/fastify.ts +39 -0
- package/src/websocket/index.ts +5 -0
- package/templates/AGENTS.md +33 -2
- /package/dist/flows/{FlowValueValidator.d.ts → flowValueValidator.d.ts} +0 -0
- /package/dist/flows/{FlowValueValidator.js → flowValueValidator.js} +0 -0
- /package/dist/storage/{mime.d.ts → mimeTypeFromPath.d.ts} +0 -0
- /package/dist/storage/{mime.js → mimeTypeFromPath.js} +0 -0
- /package/dist/storage/{contents.d.ts → storageContentsToBuffer.d.ts} +0 -0
- /package/dist/storage/{contents.js → storageContentsToBuffer.js} +0 -0
- /package/src/flows/{FlowValueValidator.ts → flowValueValidator.ts} +0 -0
- /package/src/storage/{mime.ts → mimeTypeFromPath.ts} +0 -0
- /package/src/storage/{contents.ts → storageContentsToBuffer.ts} +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# WebSocket endpoints
|
|
2
|
+
|
|
3
|
+
`@db3.ai/app/websocket` provides `app.webSockets` and controller-style JSON
|
|
4
|
+
endpoints on the existing HTTP/S server. It uses framework Auth, with a fresh
|
|
5
|
+
RequestContext for each action and server send. No new database tables are needed.
|
|
6
|
+
It depends on Auth, Server and `ws`; the Fastify adapter is optional. The browser
|
|
7
|
+
subpath uses the native WebSocket API and imports no server runtime.
|
|
8
|
+
|
|
9
|
+
## Define and mount an endpoint
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import Fastify from 'fastify';
|
|
13
|
+
import { App } from '@db3.ai/app/server';
|
|
14
|
+
import { defineWebSocket } from '@db3.ai/app/websocket';
|
|
15
|
+
import { registerWebSockets } from '@db3.ai/app/websocket/fastify';
|
|
16
|
+
|
|
17
|
+
const application = new App(); // Use your existing configured application.
|
|
18
|
+
const server = Fastify();
|
|
19
|
+
const userEndpoint = defineWebSocket({
|
|
20
|
+
/** Runs after this connection authenticates. */
|
|
21
|
+
async open({ userId, send }) {
|
|
22
|
+
await send({ type: 'hello', userId });
|
|
23
|
+
},
|
|
24
|
+
/** Validates untrusted input and supplies the message action's inferred type. */
|
|
25
|
+
parse(data: unknown): { text: string } {
|
|
26
|
+
if (!data || typeof data !== 'object' || !('text' in data) || typeof data.text !== 'string') throw new Error('Expected text.');
|
|
27
|
+
return { text: data.text };
|
|
28
|
+
},
|
|
29
|
+
/** Handles one validated command at a time for this socket. */
|
|
30
|
+
async message({ send }, data) {
|
|
31
|
+
await send({ type: 'echo', text: data.text });
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
registerWebSockets(server, {
|
|
35
|
+
app: application,
|
|
36
|
+
origins: ['https://app.example.com'],
|
|
37
|
+
endpoints: { '/ws/me': userEndpoint },
|
|
38
|
+
});
|
|
39
|
+
server.addHook('onClose', async () => { await application.close(); });
|
|
40
|
+
await server.listen({ host: '127.0.0.1', port: 8001 });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Definitions can live in controller files and be imported into the endpoint map.
|
|
44
|
+
Paths are exact; file discovery, dynamic route parameters and HTTP middleware
|
|
45
|
+
execution are not part of this module. `request.url` exposes query parameters.
|
|
46
|
+
For a plain Node HTTP/S server use `application.webSockets.mount(server, options)`;
|
|
47
|
+
close the WebSocket service **before** awaiting HTTP server shutdown. The Fastify
|
|
48
|
+
adapter does this in `preClose`, and `App.close()` also closes the service before
|
|
49
|
+
clearing application context or closing the database.
|
|
50
|
+
|
|
51
|
+
The [user endpoint example](./examples/userEndpoint.ts) includes command validation
|
|
52
|
+
and private presence access. Tests execute it through the public exports.
|
|
53
|
+
|
|
54
|
+
## Browser client
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { WebSocketClient } from '@db3.ai/app/websocket/client';
|
|
58
|
+
|
|
59
|
+
const url = new URL('/ws/me', window.location.href);
|
|
60
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
61
|
+
const client = new WebSocketClient({
|
|
62
|
+
url: url.href,
|
|
63
|
+
/** Return the current bearer session from the application's existing Auth store. */
|
|
64
|
+
token: () => authStore.token,
|
|
65
|
+
/** Validate your application payload before assigning it to a store. */
|
|
66
|
+
onMessage(data) { handleMessage(data); },
|
|
67
|
+
onState(state) { connectionState.value = state; },
|
|
68
|
+
});
|
|
69
|
+
client.connect();
|
|
70
|
+
client.send({ text: 'Hello' }); // false until authenticated; nothing is queued.
|
|
71
|
+
// On logout/component ownership disposal:
|
|
72
|
+
client.close();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Own the client once per desired endpoint/SPA lifecycle. Imports are safe during
|
|
76
|
+
SSR; the native constructor is resolved only when connecting. Messages remain
|
|
77
|
+
`unknown` until the application validates them. There is no snapshot protocol,
|
|
78
|
+
replay, Pinia dependency or automatic store update. `open` may send ordinary
|
|
79
|
+
application data if desired. When a token changes, close and reconnect explicitly.
|
|
80
|
+
|
|
81
|
+
The client automatically retries transient closure (including 1001 during a
|
|
82
|
+
deployment) with jittered exponential backoff up to 30 seconds. Authentication,
|
|
83
|
+
authorization and protocol errors stop retries (`denied`); call `connect()` after
|
|
84
|
+
repairing credentials. No offline writes are queued or repeated. An HTTP upgrade
|
|
85
|
+
rejection appears as a generic network failure in browsers, so it follows the
|
|
86
|
+
transient retry policy; inspect server/proxy configuration if this persists.
|
|
87
|
+
|
|
88
|
+
## Authentication and authorization
|
|
89
|
+
|
|
90
|
+
The first frame is `{ "type": "authenticate", "token": "..." }`. The token is
|
|
91
|
+
the existing framework bearer session, held in memory for the connection and
|
|
92
|
+
never placed in URLs or subprotocols. Use **WSS/HTTPS in production**, and do not
|
|
93
|
+
log frames or credentials. Cookie applications supply `resolveToken(request)` at
|
|
94
|
+
mount time and use browser client `auth: 'cookie'`. The resolver reads the existing
|
|
95
|
+
HttpOnly session cookie on the server; it replaces (never falls back to) the frame
|
|
96
|
+
token. No readable cookie or ticket endpoint is needed. Configure exact allowed
|
|
97
|
+
origins; missing/foreign origins are rejected, including for public endpoints.
|
|
98
|
+
|
|
99
|
+
Use `defineWebSocket({ auth: 'public', ...actions })` and client `auth: 'public'`
|
|
100
|
+
for an explicitly anonymous endpoint. Its first frame is `{ "type": "connect" }`.
|
|
101
|
+
Both `context.user` and `context.userId` are null, even when the browser has a
|
|
102
|
+
session cookie. Omitted auth always requires authentication. Public endpoints
|
|
103
|
+
still have origin, heartbeat, payload and connection limits; applications must
|
|
104
|
+
add appropriate abuse controls before opening anonymous writes to the internet.
|
|
105
|
+
|
|
106
|
+
After authentication the server sends `{ "type": "ready" }`. Application frames
|
|
107
|
+
in both directions are `{ "type": "message", "data": ... }`. The library hides
|
|
108
|
+
these envelopes from controller and browser callbacks. Text JSON is supported;
|
|
109
|
+
binary frames and malformed messages close the connection. `send()` returning true
|
|
110
|
+
means accepted into the local transport, not that the browser processed it.
|
|
111
|
+
|
|
112
|
+
Every `open`/`message` action runs with the authenticated user in framework Auth.
|
|
113
|
+
`context.send()` and heartbeat checks also revalidate the session and optional
|
|
114
|
+
`authorize(context)` policy. Authorization must be a pure access decision and
|
|
115
|
+
must not send data. Apps own resource/tenant checks and mutation permissions,
|
|
116
|
+
including read-only impersonation. Fastify HTTP hooks are bypassed by upgrades;
|
|
117
|
+
do not assume existing HTTP billing or tenant middleware protects these actions.
|
|
118
|
+
|
|
119
|
+
Auth currently records session/account activity on authentication, so these fresh
|
|
120
|
+
checks perform database reads and writes. This deliberately prioritizes existing
|
|
121
|
+
Auth semantics over adding a second session cache in this first implementation.
|
|
122
|
+
Permission checks fail closed. Idle revocation is detected on the next heartbeat
|
|
123
|
+
(30 seconds by default), plus query time; a pending check is terminated if it
|
|
124
|
+
has not settled by the following heartbeat. Already-running actions may finish.
|
|
125
|
+
Use `context.signal` to cancel application work and `close({ id, userId, code })`
|
|
126
|
+
to dispose listeners. Close callbacks run without authenticated request context.
|
|
127
|
+
They must settle promptly; shutdown is bounded even if application work hangs.
|
|
128
|
+
|
|
129
|
+
## One connection, multiple authorized channels
|
|
130
|
+
|
|
131
|
+
Own one `WebSocketClient` per browser tab at app startup and share it across
|
|
132
|
+
features. Register private channel policies on one authenticated endpoint:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { defineChannel, defineWebSocket } from '@db3.ai/app/websocket';
|
|
136
|
+
|
|
137
|
+
const endpoint = defineWebSocket({
|
|
138
|
+
channels: [
|
|
139
|
+
defineChannel('website:{websiteId}:jobs', {
|
|
140
|
+
/** Application helper queries current organization and website membership. */
|
|
141
|
+
authorize: ({ userId, params }) => canAccessWebsite(userId, params.websiteId),
|
|
142
|
+
}),
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
// Mount endpoint at /ws through registerWebSockets using your existing app.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`canAccessWebsite` is your application's access helper, not a framework method.
|
|
149
|
+
The [resource channels example](./examples/resourceChannels.ts) accepts this
|
|
150
|
+
policy as a function and also demonstrates private account notifications.
|
|
151
|
+
Authentication alone never grants website access. Parameters match whole
|
|
152
|
+
colon-delimited segments. Unknown or ambiguously matched channels are denied.
|
|
153
|
+
Every subscription and delivery checks the current session, endpoint policy and
|
|
154
|
+
resource policy. Revoked membership removes the subscription before sending the
|
|
155
|
+
payload. Anonymous endpoints cannot subscribe to these private channels.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// After saving the new job state, in the same API process:
|
|
159
|
+
await application.webSockets.channel(`website:${websiteId}:jobs`)
|
|
160
|
+
.publish('completed', { jobId });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const realtime = new WebSocketClient({
|
|
165
|
+
url: websocketUrl, // Absolute ws(s) URL for /ws.
|
|
166
|
+
token: () => authStore.token,
|
|
167
|
+
/** Reload authorized HTTP state after subscribing, including reconnect. */
|
|
168
|
+
onSubscribed: channel => reloadChannelState(channel),
|
|
169
|
+
onChannelDenied: channel => showAccessDenied(channel),
|
|
170
|
+
});
|
|
171
|
+
realtime.connect();
|
|
172
|
+
|
|
173
|
+
const unsubscribe = realtime.channel(`website:${websiteId}:jobs`)
|
|
174
|
+
.on('completed', data => {
|
|
175
|
+
// Payloads are unknown: validate with your application's schema.
|
|
176
|
+
const event = jobCompletedSchema.parse(data);
|
|
177
|
+
refreshJob(event.jobId);
|
|
178
|
+
});
|
|
179
|
+
// Component disposal removes this listener only:
|
|
180
|
+
unsubscribe();
|
|
181
|
+
// App logout disposes the shared connection:
|
|
182
|
+
realtime.close();
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The first listener subscribes; removing the final listener unsubscribes. Each
|
|
186
|
+
registration owns its cleanup, including identical callbacks. Connections are
|
|
187
|
+
not shared across separate tabs/devices. A reconnect restores remaining
|
|
188
|
+
subscriptions and reruns access checks. `onSubscribed` is the place to reconcile
|
|
189
|
+
saved state; events have no history or replay guarantee. `onChannelDenied`
|
|
190
|
+
reports rejected subscriptions without closing unrelated channels. To retry a
|
|
191
|
+
denied subscription after permissions change, remove its listeners and add them
|
|
192
|
+
again, or reconnect. On logout, also dispose user-owned feature listeners before
|
|
193
|
+
reusing the client with another account. At most 128 channels may be subscribed
|
|
194
|
+
per connection. Channel and event names are limited to 256 characters.
|
|
195
|
+
|
|
196
|
+
Publishing returns accepted local sends, not browser delivery receipts. Configure authenticated HTTP publication below for separate job workers.
|
|
197
|
+
Public subscription policies are not provided by this API. Existing explicit public endpoints remain available.
|
|
198
|
+
|
|
199
|
+
## Push from separate workers
|
|
200
|
+
|
|
201
|
+
Use the existing API as an authenticated HTTP receiver. No broker service is
|
|
202
|
+
required. In the API's Fastify registration, add an explicit private route and a
|
|
203
|
+
dedicated random service secret (32–256 URL-safe characters):
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
registerWebSockets(server, {
|
|
207
|
+
app: application,
|
|
208
|
+
origins: ['https://app.example.com'],
|
|
209
|
+
endpoints: { '/ws': endpoint },
|
|
210
|
+
publish: { path: '/_internal/realtime/publish', token: process.env.REALTIME_SECRET! },
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Configure the worker App with the receiver's trusted URL and the same secret:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const application = new App({
|
|
218
|
+
webSockets: {
|
|
219
|
+
publish: {
|
|
220
|
+
url: 'http://api:8001/_internal/realtime/publish',
|
|
221
|
+
token: process.env.REALTIME_SECRET!,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
// Publish after the authoritative outer transaction commits:
|
|
226
|
+
await application.webSockets.channel(`website:${websiteId}:jobs`)
|
|
227
|
+
.publish('completed', { jobId });
|
|
228
|
+
await application.close();
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Mounted APIs deliver directly to local sockets, even if configured with the same
|
|
232
|
+
outbound publishing options as workers. The HTTP receiver also delivers locally
|
|
233
|
+
and never relays recursively. Every send still validates the browser's session
|
|
234
|
+
and channel access. Without outbound configuration, publication is process-local.
|
|
235
|
+
|
|
236
|
+
The Fastify adapter owns HTTP registration; a plain Node host must provide its
|
|
237
|
+
own authenticated receiver. No startup subscription or readiness dependency is
|
|
238
|
+
introduced. Keep the publishing route on a private listener/network, and use
|
|
239
|
+
HTTPS when traffic crosses an untrusted network. The service bearer secret never
|
|
240
|
+
belongs in browser code or URLs. The receiver rejects Origin-bearing browser
|
|
241
|
+
requests and invalid credentials before parsing the JSON body.
|
|
242
|
+
|
|
243
|
+
The receiver validates channel/event names and limits bodies to 64 KiB. Worker
|
|
244
|
+
requests have a five-second deadline (`timeoutMs` can override it), do not follow
|
|
245
|
+
redirects, and do not retry automatically. Non-success responses reject without
|
|
246
|
+
including their response body or credentials in the error. Publishing returns
|
|
247
|
+
accepted local sends, or zero after remote HTTP acceptance; it is not a browser
|
|
248
|
+
delivery receipt.
|
|
249
|
+
|
|
250
|
+
One configured URL reaches one API process. During blue/green replacement, route
|
|
251
|
+
worker requests to the active API and close old sockets so browsers reconnect and
|
|
252
|
+
reload authorized state. A load balancer alone does not broadcast to all replicas.
|
|
253
|
+
Multiple concurrent socket owners require explicit fan-out. Presence and
|
|
254
|
+
lower-level `channels.publish()` memberships remain process-local.
|
|
255
|
+
|
|
256
|
+
Events with no local subscribers are discarded immediately, with no per-user
|
|
257
|
+
authentication or resource queries. They are not retained for later subscribers.
|
|
258
|
+
Workers still make one authenticated HTTP request per publication, even with no
|
|
259
|
+
viewers; this is lightweight receiver work, not a zero-cost path. Subscription
|
|
260
|
+
registries remove empty channels and disconnect listeners when viewers leave.
|
|
261
|
+
|
|
262
|
+
There is no durable replay. A crash between commit and publication, an HTTP
|
|
263
|
+
failure, or a disconnect may lose a signal. Reload saved state after subscription
|
|
264
|
+
and reconnect, and provide explicit refresh. Retrying an ambiguous timeout can
|
|
265
|
+
duplicate events; prefer idempotent state invalidations. Guaranteed delivery
|
|
266
|
+
requires an outbox.
|
|
267
|
+
|
|
268
|
+
For inbox integration, set `inApp.onChanged` to publish a recipient invalidation.
|
|
269
|
+
InApp defers this callback until the surrounding transaction commits and reports
|
|
270
|
+
callback failures through `inApp.onDeliveryError` without undoing saved messages.
|
|
271
|
+
Generic channel publication is not transaction-aware: call it after the outer
|
|
272
|
+
transaction returns.
|
|
273
|
+
|
|
274
|
+
## Presence and local subscriptions
|
|
275
|
+
|
|
276
|
+
`application.webSockets.channels.join(name, context)` joins an accepted connection
|
|
277
|
+
from a trusted controller action and returns an idempotent leave function. It
|
|
278
|
+
automatically leaves on disconnect. `channels.publish(name, data)` fans out to
|
|
279
|
+
local members and returns the number of accepted sends, not delivery receipts.
|
|
280
|
+
Every send rechecks session and endpoint authorization. This lower-level API has no browser-controlled membership. For browser
|
|
281
|
+
subscriptions use the authorized channel definitions above.
|
|
282
|
+
For private notifications, choose `inbox:${context.userId}` on the server, commit
|
|
283
|
+
the in-app message, then publish an invalidation to that user's channel. The
|
|
284
|
+
browser reloads the authorized HTTP inbox, including after reconnect. InApp has no automatic WebSocket dependency. Configure its `onChanged` callback
|
|
285
|
+
to publish an invalidation after committed service writes.
|
|
286
|
+
|
|
287
|
+
The starter's Live rooms demo implements three endpoint policies: a public lobby,
|
|
288
|
+
signed-in members, and a studio requiring an explicit database grant. Its
|
|
289
|
+
application controller serializes each room's joins and edits, sends saved history
|
|
290
|
+
before live membership, and owns tab-level guest presence and cursor broadcasts.
|
|
291
|
+
The whiteboard stores at most 500 strokes of 200 points, plus 50 recent chat
|
|
292
|
+
messages, in SQL. A refresh reloads that state; no transport replay is promised.
|
|
293
|
+
This demo writer is single-process, not a distributed collaborative document engine.
|
|
294
|
+
|
|
295
|
+
`application.webSockets.presence('/ws/me')` returns `{ userId, connections }[]`
|
|
296
|
+
for authenticated users on that endpoint in this process. Tabs/devices share a
|
|
297
|
+
user entry while retaining separate connection counts. Presence is approximate;
|
|
298
|
+
dead peers disappear after heartbeat detection, not immediately on network loss.
|
|
299
|
+
Presence means connected, not actively looking at the page.
|
|
300
|
+
|
|
301
|
+
Presence is a server API. It is not broadcast automatically because listing other
|
|
302
|
+
users is an application permission decision. A private endpoint can send only the
|
|
303
|
+
current user's entry, as the example does. Shared rooms must authorize membership
|
|
304
|
+
and filter presence by application scope; endpoint paths alone do not create
|
|
305
|
+
tenant isolation, and query strings do not partition this presence list.
|
|
306
|
+
|
|
307
|
+
`onPresence(path, listener)` observes local changes and returns an unsubscribe
|
|
308
|
+
function. For example, an `open` action can push only its account's presence:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import type { WebSocketContext } from '@db3.ai/app/websocket';
|
|
312
|
+
|
|
313
|
+
/** Sends local presence changes for this user and removes the listener on disconnect. */
|
|
314
|
+
async function open({ userId, send, signal }: WebSocketContext) {
|
|
315
|
+
/** Re-reads current counts; send rechecks the recipient's session and endpoint policy. */
|
|
316
|
+
function changed() {
|
|
317
|
+
void send({ type: 'presence', users: application.webSockets.presence('/ws/me').filter(item => item.userId === userId) });
|
|
318
|
+
}
|
|
319
|
+
const unsubscribe = application.webSockets.onPresence('/ws/me', changed);
|
|
320
|
+
signal.addEventListener('abort', unsubscribe, { once: true });
|
|
321
|
+
changed();
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Presence listeners execute without Auth context and do not supply event history.
|
|
326
|
+
Asynchronous listeners are not serialized. Read current presence when handling
|
|
327
|
+
each signal and keep callbacks short; errors go to the configured `onError`.
|
|
328
|
+
|
|
329
|
+
An `open` action can retain `context.send` in an application-owned subscription
|
|
330
|
+
and remove it on `context.signal` abort or `close`. Every later send rechecks
|
|
331
|
+
access. These callbacks and presence belong to the current process. A separate
|
|
332
|
+
worker cannot reach these retained callbacks or lower-level memberships. Use
|
|
333
|
+
named channel events with the HTTP configuration above for cross-process push.
|
|
334
|
+
|
|
335
|
+
## Blue/green deployment
|
|
336
|
+
|
|
337
|
+
The Auth session lives in the shared database while sockets live in an API
|
|
338
|
+
process. Route new upgrades to green, then initiate graceful shutdown on blue.
|
|
339
|
+
Blue closes sockets with 1001; clients reconnect to green using the same token.
|
|
340
|
+
Until old connections drain, each process has only its own presence list and
|
|
341
|
+
subscriptions. HTTP publication reaches the API selected by its URL; events sent
|
|
342
|
+
only to green do not reach sockets still on blue.
|
|
343
|
+
Deployments must close old sockets instead of leaving them alive indefinitely.
|
|
344
|
+
There is no guaranteed delivery across disconnects; applications reload needed
|
|
345
|
+
state themselves after reconnecting.
|
|
346
|
+
|
|
347
|
+
The existing reverse proxy must forward WebSocket upgrades on the endpoint path
|
|
348
|
+
and allow idle timeouts longer than the heartbeat interval. No extra public port
|
|
349
|
+
or shared socket/session service is required. This module cannot change proxy
|
|
350
|
+
routing or implement distributed presence for you.
|
|
351
|
+
|
|
352
|
+
## Limits and verification
|
|
353
|
+
|
|
354
|
+
Defaults: 1000 sockets including pending authentication; 5-second authentication
|
|
355
|
+
deadline; 64 KiB incoming/outgoing frames; 256 KiB outgoing buffer; 16 queued
|
|
356
|
+
incoming actions; 32 concurrent outgoing sends awaiting access checks; 30-second heartbeat; 3-second shutdown deadline. Compression
|
|
357
|
+
is disabled. Slow clients, flooding and oversized payloads are disconnected.
|
|
358
|
+
The `maxPendingSends` limit applies before authentication/resource queries begin,
|
|
359
|
+
so slow access checks cannot build an unlimited per-connection backlog. Overflow
|
|
360
|
+
closes that connection with retryable code 1013; the browser reconnects and the app
|
|
361
|
+
reloads saved state. Pending sends release their payloads when their access checks
|
|
362
|
+
settle; this limit does not cancel database queries already running. Ordinary
|
|
363
|
+
sends are neither throttled nor queued for replay.
|
|
364
|
+
Configure `onError` for server diagnostics; application errors close with 1011
|
|
365
|
+
without sending exception text. Proxy rate limits remain application operations.
|
|
366
|
+
|
|
367
|
+
Run `npm run test:service --workspace @db3.ai/app -- websocket` using the configured
|
|
368
|
+
disposable MariaDB test setup. Tests use real sockets and Auth records, cover
|
|
369
|
+
isolation/revocation, validation, presence, cleanup and reconnect across API
|
|
370
|
+
replacement. Run `npm run check --workspace @db3.ai/app` and
|
|
371
|
+
`npm run framework:package:test` for examples and packed-consumer verification.
|
|
372
|
+
|
|
373
|
+
This module uses the existing-server, heartbeat and bounded transport mechanisms
|
|
374
|
+
described in the [ws documentation](https://github.com/websockets/ws/blob/master/README.md).
|
|
375
|
+
|
|
376
|
+
## Shared board and durable loading recipe
|
|
377
|
+
|
|
378
|
+
The [WebSocket guide](https://db3.ai/docs/realtime-kanban) composes seven
|
|
379
|
+
copyable application examples with real Auth, SQL, Queue, WebSockets and Pinia:
|
|
380
|
+
|
|
381
|
+
- [LiveBoard](./examples/board/LiveBoard.ts) saves authorized card moves under a
|
|
382
|
+
row lock, rejects stale revisions and emits `changed` after commit.
|
|
383
|
+
- [Public snapshot](./examples/board/boardSnapshot.ts) excludes membership and
|
|
384
|
+
validates the browser projection. [HTTP routes](./examples/board/registerBoardRoutes.ts)
|
|
385
|
+
derive identity from the authenticated session.
|
|
386
|
+
- [Pinia store](./examples/board/createBoardStore.ts) rejects stale responses and
|
|
387
|
+
derives loading from persisted job status, including after a full reload.
|
|
388
|
+
- [Channel bindings](./examples/createChannelSync.ts) share one socket, reload
|
|
389
|
+
after subscription acknowledgements and coalesce changes during an HTTP read.
|
|
390
|
+
- [Browser composition](./examples/board/createBoardClient.ts) owns the store and
|
|
391
|
+
connection at app startup, with explicit logout cleanup.
|
|
392
|
+
- [Summary job](./examples/board/SummarizeBoardJob.ts) executes independently of
|
|
393
|
+
the browser, persists completion/failure and publishes through the HTTP bridge.
|
|
394
|
+
|
|
395
|
+
These are app-owned recipes, not automatic ORM subscriptions or a framework
|
|
396
|
+
Pinia dependency. Install Vue/Pinia in the consuming frontend. Register the
|
|
397
|
+
model with the host's migrations and the job with each worker. The recipe uses
|
|
398
|
+
the database queue driver on the same database so loading and dispatch commit
|
|
399
|
+
atomically. Call its mutation methods outside an outer transaction. It keeps
|
|
400
|
+
one active/latest summary per board, not an unlimited run/idempotency history.
|
|
401
|
+
The sample summary is quick; replace that calculation with the actual work.
|
|
402
|
+
Long external operations still need lease fencing, provider idempotency and
|
|
403
|
+
server-side reconciliation of abandoned runs and failed completion hooks.
|
|
404
|
+
|
|
405
|
+
Run `npm test --workspace @db3.ai/app -- src/websocket/tests/examples/liveBoard.test.ts`
|
|
406
|
+
with disposable MariaDB credentials. It tests real two-user Pinia synchronization,
|
|
407
|
+
conflicts/denial, reload/reconnect, SQL dispatch rollback, worker HTTP publication,
|
|
408
|
+
terminal failure, stale-run fencing and listener cleanup. There is no timed inbox
|
|
409
|
+
or board polling. HTTP publication remains best effort: if it fails while a
|
|
410
|
+
client stays connected, that client needs explicit/focus/reconnect refresh.
|
|
411
|
+
Guaranteed eventual notification requires a durable outbox, not only a socket.
|
|
412
|
+
|
|
413
|
+
### Presence, Yjs and AI boundaries
|
|
414
|
+
|
|
415
|
+
Endpoint `presence(path)` does not enumerate named-channel subscribers. A shared
|
|
416
|
+
endpoint needs app-owned document-view/leave actions and per-document ephemeral
|
|
417
|
+
presence, including route departure, multiple tabs, access checks and heartbeat
|
|
418
|
+
cleanup. The starter rooms already demonstrate room presence and saved chat.
|
|
419
|
+
|
|
420
|
+
Yjs uses binary updates and a synchronization/awareness protocol. Existing Yjs
|
|
421
|
+
providers do not speak this service's JSON envelopes. Reusing one connection
|
|
422
|
+
requires an adapter, bounded encoding, state-vector recovery, persistence and
|
|
423
|
+
awareness cleanup. No Yjs adapter is shipped or tested here. See
|
|
424
|
+
[Yjs updates](https://docs.yjs.dev/api/document-updates) and its
|
|
425
|
+
[WebSocket provider](https://docs.yjs.dev/ecosystem/connection-provider/y-websocket).
|
|
426
|
+
|
|
427
|
+
`Agent.queue()` executes on the worker independently of a browser, but queued
|
|
428
|
+
runs currently discard the live event sink. There is no durable token log or
|
|
429
|
+
replay cursor. Reconnectable AI viewing needs saved cumulative checkpoints or
|
|
430
|
+
ordered events, authorized recovery, duplicate/gap handling and attempt fencing.
|
|
431
|
+
The guide explains these design boundaries without advertising a replay API.
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
## Task tutorials
|
|
435
|
+
|
|
436
|
+
Start with the [main WebSocket guide](https://db3.ai/docs/websocket) for shared
|
|
437
|
+
connection ownership, authorized channels, worker HTTP publishing and recovery.
|
|
438
|
+
Then follow the tutorial for the feature you are building:
|
|
439
|
+
|
|
440
|
+
- [Document presence](https://db3.ai/docs/realtime-presence)
|
|
441
|
+
- [Saved-action alerts](https://db3.ai/docs/realtime-actions)
|
|
442
|
+
- [Shared Kanban and Pinia](https://db3.ai/docs/realtime-kanban)
|
|
443
|
+
- [Durable background-job loading](https://db3.ai/docs/realtime-jobs)
|
|
444
|
+
- [Chat rooms](https://db3.ai/docs/realtime-chat)
|
|
445
|
+
- [Deployment progress](https://db3.ai/docs/realtime-deployments)
|
|
446
|
+
- [Yjs adapter design](https://db3.ai/docs/realtime-yjs)
|
|
447
|
+
- [Reconnectable AI design](https://db3.ai/docs/realtime-ai)
|
|
448
|
+
- [Approval requests](https://db3.ai/docs/realtime-approvals)
|
|
449
|
+
- [Upload processing](https://db3.ai/docs/realtime-uploads)
|
|
450
|
+
- [Inventory updates](https://db3.ai/docs/realtime-inventory)
|
|
451
|
+
- [Permission changes](https://db3.ai/docs/realtime-permissions)
|
|
452
|
+
|
|
453
|
+
The deployment tutorial supplies a tested
|
|
454
|
+
[public projection](./examples/deploymentSnapshot.ts) and
|
|
455
|
+
[authorized environment channel](./examples/deploymentEndpoint.ts). The projection
|
|
456
|
+
rejects stale/wrong-resource responses and requires passed health plus the intended
|
|
457
|
+
active release before displaying live. It validates controller assertions; it does
|
|
458
|
+
not perform infrastructure probes, execute deployments or provide a Cloud API.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { WebSocketContext, WebSocketChannelDefinition } from './contracts';
|
|
2
|
+
|
|
3
|
+
/** Local fan-out for trusted server memberships and authorized browser subscriptions. */
|
|
4
|
+
export class WebSocketChannels {
|
|
5
|
+
readonly #subscriptions = new Map<string, Map<string, { context: WebSocketContext<any>; definition: WebSocketChannelDefinition; params: Record<string, string>; leave: () => void }>>();
|
|
6
|
+
readonly #counts = new Map<string, number>();
|
|
7
|
+
readonly #channels = new Map<string, Map<string, { context: WebSocketContext<any>; leave: () => void }>>();
|
|
8
|
+
|
|
9
|
+
/** Joins an already-authorized connection and automatically leaves when its signal aborts. */
|
|
10
|
+
join(name: string, context: WebSocketContext<any>): () => void {
|
|
11
|
+
if (context.signal.aborted) return () => {};
|
|
12
|
+
let members = this.#channels.get(name);
|
|
13
|
+
if (!members) { members = new Map(); this.#channels.set(name, members); }
|
|
14
|
+
const existing = members.get(context.id);
|
|
15
|
+
if (existing) return existing.leave;
|
|
16
|
+
/** Removes only this membership and releases its abort listener. */
|
|
17
|
+
const leave = () => {
|
|
18
|
+
if (members.get(context.id)?.context === context) members.delete(context.id);
|
|
19
|
+
if (!members.size && this.#channels.get(name) === members) this.#channels.delete(name);
|
|
20
|
+
context.signal.removeEventListener('abort', leave);
|
|
21
|
+
};
|
|
22
|
+
context.signal.addEventListener('abort', leave, { once: true });
|
|
23
|
+
members.set(context.id, { context, leave });
|
|
24
|
+
return leave;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Sends JSON to current local members. Returns accepted transport sends, never durable delivery. */
|
|
28
|
+
async publish(name: string, data: unknown): Promise<number> {
|
|
29
|
+
const results = await Promise.all([...this.#channels.get(name)?.values() ?? []].map(member => member.context.send(data)));
|
|
30
|
+
return results.filter(Boolean).length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Handles the reserved subscription protocol. Unknown channels and denied access reveal no payload.
|
|
35
|
+
* @returns Whether this was a framework subscription command.
|
|
36
|
+
*/
|
|
37
|
+
async handle(definitions: readonly WebSocketChannelDefinition[], context: WebSocketContext<any>, data: unknown): Promise<boolean> {
|
|
38
|
+
if (!data || typeof data !== 'object' || !('type' in data) || !['channel.subscribe', 'channel.unsubscribe'].includes(String(data.type))) return false;
|
|
39
|
+
const name = 'channel' in data ? data.channel : null;
|
|
40
|
+
if (typeof name !== 'string' || name.length > 256 || !/^[a-zA-Z0-9_.-]+(?::[a-zA-Z0-9_.-]+)*$/.test(name)) { context.close(); return true; }
|
|
41
|
+
const existing = this.#subscriptions.get(name)?.get(context.id);
|
|
42
|
+
if (data.type === 'channel.unsubscribe') { existing?.leave(); return true; }
|
|
43
|
+
const matches = definitions.map(definition => ({ definition, params: matchChannel(definition.pattern, name) })).filter(match => match.params !== null);
|
|
44
|
+
const match = matches.length === 1 ? matches[0] : undefined;
|
|
45
|
+
if (!context.user || !context.userId || !match || !await match.definition.authorize({ ...context, userId: context.userId, channel: name, params: match.params! })) {
|
|
46
|
+
existing?.leave();
|
|
47
|
+
await context.send({ type: 'channel.denied', channel: name });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (context.signal.aborted) return true;
|
|
51
|
+
if (!existing) {
|
|
52
|
+
if ((this.#counts.get(context.id) ?? 0) >= 128) { await context.send({ type: 'channel.denied', channel: name }); return true; }
|
|
53
|
+
let members = this.#subscriptions.get(name);
|
|
54
|
+
if (!members) { members = new Map(); this.#subscriptions.set(name, members); }
|
|
55
|
+
/** Removes this connection's subscription exactly once, including during in-flight authorization. */
|
|
56
|
+
const leave = () => {
|
|
57
|
+
if (!members.delete(context.id)) return;
|
|
58
|
+
if (!members.size) this.#subscriptions.delete(name);
|
|
59
|
+
const count = (this.#counts.get(context.id) ?? 1) - 1;
|
|
60
|
+
if (count) this.#counts.set(context.id, count); else this.#counts.delete(context.id);
|
|
61
|
+
context.signal.removeEventListener('abort', leave);
|
|
62
|
+
};
|
|
63
|
+
members.set(context.id, { context, definition: match.definition, params: match.params!, leave });
|
|
64
|
+
this.#counts.set(context.id, (this.#counts.get(context.id) ?? 0) + 1);
|
|
65
|
+
context.signal.addEventListener('abort', leave, { once: true });
|
|
66
|
+
}
|
|
67
|
+
await context.send({ type: 'channel.subscribed', channel: name });
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Publishes a named event only to current authorized resource subscribers in this process. */
|
|
72
|
+
async publishEvent(name: string, event: string, data: unknown): Promise<number> {
|
|
73
|
+
if (!event || event.length > 256) throw new Error('Invalid channel event.');
|
|
74
|
+
const members = this.#subscriptions.get(name);
|
|
75
|
+
const results = await Promise.all([...members?.values() ?? []].map(member => member.context.send({ type: 'channel.event', channel: name, event, data }, async context => {
|
|
76
|
+
if (members?.get(context.id) !== member) return false;
|
|
77
|
+
if (!context.user || !context.userId || !await member.definition.authorize({ ...context, userId: context.userId, channel: name, params: member.params })) { member.leave(); return false; }
|
|
78
|
+
return members?.get(context.id) === member;
|
|
79
|
+
})));
|
|
80
|
+
return results.filter(Boolean).length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Drops local memberships during application shutdown. */
|
|
84
|
+
clear(): void { for (const members of this.#subscriptions.values()) for (const member of members.values()) member.leave(); for (const members of this.#channels.values()) for (const member of members.values()) member.leave(); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Matches whole colon-delimited segments; parameter values never include separators. */
|
|
88
|
+
function matchChannel(pattern: string, name: string): Record<string, string> | null {
|
|
89
|
+
const expected = pattern.split(':');
|
|
90
|
+
const actual = name.split(':');
|
|
91
|
+
if (expected.length !== actual.length) return null;
|
|
92
|
+
const params: Record<string, string> = Object.create(null);
|
|
93
|
+
for (let index = 0; index < expected.length; index++) {
|
|
94
|
+
const segment = expected[index]!;
|
|
95
|
+
if (/^\{[a-zA-Z][a-zA-Z0-9_]*\}$/.test(segment)) params[segment.slice(1, -1)] = actual[index]!;
|
|
96
|
+
else if (segment !== actual[index]) return null;
|
|
97
|
+
}
|
|
98
|
+
return params;
|
|
99
|
+
}
|