@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,49 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyReply } from 'fastify';
|
|
2
|
+
import type * as health from './contracts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Registers the canonical application health endpoint on a Fastify server.
|
|
6
|
+
*
|
|
7
|
+
* Healthy and degraded reports return HTTP 200 so optional degradation does not
|
|
8
|
+
* remove the process from service. An unhealthy component returns HTTP 503.
|
|
9
|
+
* Compatibility aliases are useful while external monitors migrate to `/health`.
|
|
10
|
+
*
|
|
11
|
+
* @param server - Fastify server that owns the HTTP lifecycle.
|
|
12
|
+
* @param options - Application health service and route paths.
|
|
13
|
+
*/
|
|
14
|
+
export function registerHealthRoute(server: FastifyInstance, options: health.HealthRouteOptions): void {
|
|
15
|
+
const paths = uniquePaths(options.path ?? '/health', options.aliases ?? []);
|
|
16
|
+
|
|
17
|
+
for (const path of paths) {
|
|
18
|
+
server.get(path, async (_request, reply) => healthResponse(options.app.health, reply));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Builds one safe response from the shared health service.
|
|
24
|
+
*
|
|
25
|
+
* @param service - Application health registry.
|
|
26
|
+
* @param reply - Fastify response used to set status and cache policy.
|
|
27
|
+
* @returns Aggregate health report.
|
|
28
|
+
*/
|
|
29
|
+
async function healthResponse(service: health.HealthApplication['health'], reply: FastifyReply): Promise<health.HealthReport> {
|
|
30
|
+
const report = await service.report();
|
|
31
|
+
reply.header('cache-control', 'no-store');
|
|
32
|
+
reply.code(report.status === 'unhealthy' ? 503 : 200);
|
|
33
|
+
return report;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validates and deduplicates canonical and compatibility route paths.
|
|
38
|
+
*
|
|
39
|
+
* @param canonical - Canonical health endpoint path.
|
|
40
|
+
* @param aliases - Optional compatibility paths.
|
|
41
|
+
* @returns Unique validated route paths in registration order.
|
|
42
|
+
*/
|
|
43
|
+
function uniquePaths(canonical: string, aliases: string[]): string[] {
|
|
44
|
+
return [...new Set([canonical, ...aliases].map(path => {
|
|
45
|
+
const normalized = path.trim();
|
|
46
|
+
if (!normalized.startsWith('/')) throw new TypeError(`Health route "${path}" must start with "/".`);
|
|
47
|
+
return normalized;
|
|
48
|
+
}))];
|
|
49
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { ActiveRecord } from '../db';
|
|
4
|
+
import { UserIdentity, type UserIdentityModel } from '../auth';
|
|
5
|
+
import { app } from '../server/appContext';
|
|
6
|
+
import { InAppRecord } from './InAppRecord';
|
|
7
|
+
import { InAppError } from './InAppError';
|
|
8
|
+
import { normalizeMessage, normalizeQuery, normalizeRecipients, normalizeScope, normalizeSend } from './validation';
|
|
9
|
+
import type * as inbox from './contracts';
|
|
10
|
+
|
|
11
|
+
/** Persistent in-app inbox independent of notification classes, email and live delivery. */
|
|
12
|
+
export class InApp {
|
|
13
|
+
readonly #commits = new WeakMap<Knex.Transaction, Promise<boolean>>();
|
|
14
|
+
/** Creates the service; ordinary applications use app().inApp and App's configured Auth identity model. */
|
|
15
|
+
constructor(readonly options: inbox.InAppOptions = {}, readonly identityModel: UserIdentityModel = UserIdentity) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Atomically accepts one message for up to 100 distinct recipients.
|
|
19
|
+
* Trusted server code owns sender authorization. The service validates identities and receive-scope policy.
|
|
20
|
+
* @param recipients - User ID or IDs; an empty list creates no work.
|
|
21
|
+
* @param message - Inbox content with a required plain-text fallback and optional rich body.
|
|
22
|
+
* @param options - Explicit scope, notification type and optional deduplication key.
|
|
23
|
+
* @returns Per-recipient durable acceptance, never a claim of browser delivery.
|
|
24
|
+
* @example await app().inApp.send(userId, { title: 'Ready', body: 'Your report is ready.' }, { scope: { type: 'account' }, type: 'report.ready' });
|
|
25
|
+
*/
|
|
26
|
+
async send(recipients: string | readonly string[], message: inbox.InAppMessage, options: inbox.InAppSendOptions): Promise<inbox.InAppAcceptance[]> {
|
|
27
|
+
const ids = normalizeRecipients(recipients);
|
|
28
|
+
const content = normalizeMessage(message);
|
|
29
|
+
const input = normalizeSend(options);
|
|
30
|
+
const contentHash = hash(content);
|
|
31
|
+
if (!ids.length) return [];
|
|
32
|
+
const connection = InAppRecord.getDb();
|
|
33
|
+
const committed = this.#committed(connection);
|
|
34
|
+
const accepted = await connection.transaction(transaction => ActiveRecord.withDb(transaction, async () => {
|
|
35
|
+
// Lock in stable order so concurrent batches and retries cannot deadlock by recipient order.
|
|
36
|
+
const canonicalIds = new Map<string, string>();
|
|
37
|
+
for (const userId of [...ids].sort((left, right) => left.toLowerCase().localeCompare(right.toLowerCase()))) {
|
|
38
|
+
const row = await this.identityModel.where(this.identityModel.primaryKey, userId).toKnex().forUpdate().first();
|
|
39
|
+
if (!row) throw new InAppError('forbidden', 'Notification recipient is unavailable.');
|
|
40
|
+
const canonicalId = String(this.identityModel.fromDb(row).get(this.identityModel.primaryKey));
|
|
41
|
+
canonicalIds.set(userId, canonicalId);
|
|
42
|
+
await this.#authorize(canonicalId, input.scope, 'receive');
|
|
43
|
+
}
|
|
44
|
+
const results: inbox.InAppAcceptance[] = [];
|
|
45
|
+
for (const userId of new Set(ids.map(id => canonicalIds.get(id)!))) {
|
|
46
|
+
const deduplicationHash = input.key === undefined ? null : hash([userId, input.scope.type, scopeId(input.scope), input.type, input.key]);
|
|
47
|
+
// Request a current read; stale outer snapshots may require a caller-owned transaction retry.
|
|
48
|
+
const existingRow = deduplicationHash ? await InAppRecord.where('deduplicationHash', deduplicationHash).toKnex().forUpdate().first() : null;
|
|
49
|
+
const existing = existingRow ? InAppRecord.fromDb(existingRow) : null;
|
|
50
|
+
if (existing) {
|
|
51
|
+
if (existing.contentHash !== contentHash) throw new InAppError('conflict', 'This notification key already has different content.');
|
|
52
|
+
results.push({ userId, id: existing.id, status: 'existing' });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const record = InAppRecord.create({ userId, scopeType: input.scope.type, scopeId: scopeId(input.scope), type: input.type, message: content, presentation: content.presentation, deduplicationHash, contentHash });
|
|
56
|
+
await record.save();
|
|
57
|
+
results.push({ userId, id: record.id, status: 'created' });
|
|
58
|
+
}
|
|
59
|
+
return results;
|
|
60
|
+
}));
|
|
61
|
+
await this.#changed(accepted.filter(item => item.status === 'created').map(item => item.userId), committed);
|
|
62
|
+
return accepted;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Lists only the authenticated user's unarchived items in an authorized scope.
|
|
67
|
+
* @param query - Scope, page size, optional position and inbox/banner view.
|
|
68
|
+
* @returns Safe content, unread count and next-page position. Reading a page changes no state.
|
|
69
|
+
*/
|
|
70
|
+
async inbox(query: inbox.InAppInboxQuery): Promise<inbox.InAppInboxPage> {
|
|
71
|
+
const input = normalizeQuery(query);
|
|
72
|
+
const userId = await this.#reader(input.scope, 'read');
|
|
73
|
+
const rows = this.#query(userId, input.scope).whereNull('archivedAt');
|
|
74
|
+
if (input.view === 'banners') rows.where('presentation', 'banner').whereNull('dismissedAt');
|
|
75
|
+
if (input.before) rows.where('id', '<', input.before);
|
|
76
|
+
const records = await rows.orderBy('id', 'desc').limit(input.limit + 1).all();
|
|
77
|
+
const unreadCount = await this.#query(userId, input.scope).whereNull('archivedAt').whereNull('readAt').count();
|
|
78
|
+
const items = records.slice(0, input.limit).map(itemView);
|
|
79
|
+
return { items, unreadCount, nextBefore: records.length > input.limit ? items.at(-1)!.id : null };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Changes one owned item's read, dismissal or archive state without coupling those states.
|
|
84
|
+
* @param id - Inbox item ID; a foreign or missing ID returns the same not-found error.
|
|
85
|
+
* @param transition - Explicit recipient action.
|
|
86
|
+
* @param scope - Currently authorized scope, never inferred from the target item.
|
|
87
|
+
*/
|
|
88
|
+
async update(id: string, transition: inbox.InAppTransition, scope: inbox.InAppScope): Promise<void> {
|
|
89
|
+
const input = normalizeScope(scope);
|
|
90
|
+
if (!/^[0-9A-HJKMNP-TV-Z]{26}$/.test(id) || !['read', 'unread', 'dismiss', 'archive'].includes(transition)) throw new InAppError('invalid', 'Invalid inbox state change.');
|
|
91
|
+
const userId = await this.#reader(input, 'update');
|
|
92
|
+
const query = this.#query(userId, input).where('id', id);
|
|
93
|
+
if (!await query.first()) throw new InAppError('not_found', 'Inbox item not found.');
|
|
94
|
+
const field = transition === 'dismiss' ? 'dismissedAt' : transition === 'archive' ? 'archivedAt' : 'readAt';
|
|
95
|
+
const committed = this.#committed(InAppRecord.getDb());
|
|
96
|
+
const update = this.#query(userId, input).where('id', id);
|
|
97
|
+
if (transition !== 'unread') update.whereNull(field);
|
|
98
|
+
const count = await update.patch({ [field]: transition === 'unread' ? null : new Date() });
|
|
99
|
+
if (count) await this.#changed([userId], committed);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Observes all surrounding savepoints and the outer commit, including explicit rollback without an error. */
|
|
103
|
+
#committed(connection: Knex): Promise<boolean> | undefined {
|
|
104
|
+
if (!this.options.onChanged || !connection.isTransaction) return;
|
|
105
|
+
const transactions: Promise<boolean>[] = [];
|
|
106
|
+
let current: Knex.Transaction | undefined = connection as Knex.Transaction;
|
|
107
|
+
while (current) {
|
|
108
|
+
const transaction: Knex.Transaction = current;
|
|
109
|
+
let result = this.#commits.get(transaction);
|
|
110
|
+
if (!result) {
|
|
111
|
+
let rolledBack = false;
|
|
112
|
+
/** The final control statement distinguishes silent rollback from commit or savepoint release. */
|
|
113
|
+
const query = (event: { sql: string }): void => {
|
|
114
|
+
if (/^ROLLBACK/i.test(event.sql)) rolledBack = true;
|
|
115
|
+
else if (/^(COMMIT|RELEASE SAVEPOINT)/i.test(event.sql)) rolledBack = false;
|
|
116
|
+
};
|
|
117
|
+
transaction.on('query', query);
|
|
118
|
+
result = transaction.executionPromise.then(() => !rolledBack, () => false).finally(() => transaction.off('query', query));
|
|
119
|
+
this.#commits.set(transaction, result);
|
|
120
|
+
}
|
|
121
|
+
transactions.push(result);
|
|
122
|
+
current = transaction.parentTransaction;
|
|
123
|
+
}
|
|
124
|
+
return Promise.all(transactions).then(results => results.every(Boolean));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Publishes only committed recipient changes; nested transactions defer without blocking their own commit. */
|
|
128
|
+
async #changed(userIds: string[], committed?: Promise<boolean>): Promise<void> {
|
|
129
|
+
if (!this.options.onChanged || !userIds.length) return;
|
|
130
|
+
const connection = app().db.knex;
|
|
131
|
+
/** Contains push failures so retrying notification delivery cannot duplicate an accepted message. */
|
|
132
|
+
const deliver = async (): Promise<void> => ActiveRecord.withDb(connection, async () => {
|
|
133
|
+
await Promise.all([...new Set(userIds)].map(async userId => {
|
|
134
|
+
try { await this.options.onChanged!(userId); }
|
|
135
|
+
catch (error) { try { this.options.onDeliveryError?.(error); } catch { /* Reporting cannot undo the write. */ } }
|
|
136
|
+
}));
|
|
137
|
+
});
|
|
138
|
+
if (committed) { void committed.then(success => { if (success) return deliver(); }); }
|
|
139
|
+
else await deliver();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Builds an ownership-scoped query; callers never accept a reader ID from a browser. */
|
|
143
|
+
#query(userId: string, scope: inbox.InAppScope) {
|
|
144
|
+
return InAppRecord.where('userId', userId).where('scopeType', scope.type).where('scopeId', scopeId(scope));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Requires isolated request authentication and current scope authorization on every operation. */
|
|
148
|
+
async #reader(scope: inbox.InAppScope, operation: 'read' | 'update'): Promise<string> {
|
|
149
|
+
const application = app();
|
|
150
|
+
const user = application.requestContext.active ? application.auth.user : null;
|
|
151
|
+
if (!user) throw new InAppError('unauthenticated', 'Sign in to access your inbox.');
|
|
152
|
+
const userId = String(user.get(this.identityModel.primaryKey));
|
|
153
|
+
await this.#authorize(userId, scope, operation);
|
|
154
|
+
return userId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Account ownership is built in; application scopes require an explicit policy. */
|
|
158
|
+
async #authorize(userId: string, scope: inbox.InAppScope, operation: 'receive' | 'read' | 'update'): Promise<void> {
|
|
159
|
+
if (scope.type === 'account' && !this.options.authorizeScope) return;
|
|
160
|
+
if (!await this.options.authorizeScope?.(userId, scope, operation)) throw new InAppError('forbidden', 'Notification scope is unavailable.');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Uses a nonempty sentinel only for the explicitly typed account scope. */
|
|
165
|
+
function scopeId(scope: inbox.InAppScope): string { return scope.type === 'account' ? 'account' : scope.id!; }
|
|
166
|
+
|
|
167
|
+
/** Hashes a normalized value; array envelopes avoid ambiguous delimiter concatenation. */
|
|
168
|
+
function hash(value: unknown): string { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
|
|
169
|
+
|
|
170
|
+
/** Serializes only the public inbox contract, never routing or deduplication internals. */
|
|
171
|
+
function itemView(record: InAppRecord): inbox.InAppItem {
|
|
172
|
+
return { id: record.id, type: record.type, message: record.message, createdAt: record.createdAt.toISOString(), readAt: record.readAt?.toISOString() ?? null, dismissedAt: record.dismissedAt?.toISOString() ?? null, archivedAt: record.archivedAt?.toISOString() ?? null };
|
|
173
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Stable service errors that HTTP adapters may translate without exposing storage details. */
|
|
2
|
+
export class InAppError extends Error {
|
|
3
|
+
/** Creates a validation, access, missing-item or conflicting-retry error. */
|
|
4
|
+
constructor(readonly code: 'invalid' | 'unauthenticated' | 'forbidden' | 'not_found' | 'conflict', message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'InAppError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ActiveRecord, type FieldBuilder } from '../db';
|
|
2
|
+
import type { InAppMessage } from './contracts';
|
|
3
|
+
|
|
4
|
+
/** Durable inbox item and independent state for one recipient; install through app migrations. */
|
|
5
|
+
export class InAppRecord extends ActiveRecord {
|
|
6
|
+
static override table = 'in_app_messages';
|
|
7
|
+
static override requestFillable: string[] = [];
|
|
8
|
+
|
|
9
|
+
/** Defines inbox ownership, deduplication and recipient-state storage. */
|
|
10
|
+
static override fields(field: FieldBuilder) {
|
|
11
|
+
return {
|
|
12
|
+
id: field.ulid(),
|
|
13
|
+
userId: field.string({ column: 'user_id', required: true, length: 255, maxLength: 255, indexes: [{ name: 'in_app_recipient_scope', columns: ['user_id', 'scope_type', 'scope_id'] }] }),
|
|
14
|
+
scopeType: field.string({ column: 'scope_type', required: true, length: 80, maxLength: 80 }),
|
|
15
|
+
scopeId: field.string({ column: 'scope_id', required: true, length: 255, maxLength: 255 }),
|
|
16
|
+
type: field.string({ required: true, length: 120, maxLength: 120 }),
|
|
17
|
+
message: field.json<InAppMessage>({ required: true }),
|
|
18
|
+
deduplicationHash: field.string({ column: 'deduplication_hash', length: 64, maxLength: 64, unique: true, hidden: true }),
|
|
19
|
+
contentHash: field.string({ column: 'content_hash', required: true, length: 64, maxLength: 64, hidden: true }),
|
|
20
|
+
presentation: field.string({ required: true, length: 16, maxLength: 16 }),
|
|
21
|
+
readAt: field.timestamp({ column: 'read_at' }),
|
|
22
|
+
dismissedAt: field.timestamp({ column: 'dismissed_at' }),
|
|
23
|
+
archivedAt: field.timestamp({ column: 'archived_at' }),
|
|
24
|
+
createdAt: field.timestamp({ column: 'created_at', auto: 'create' }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare id: string;
|
|
29
|
+
declare userId: string;
|
|
30
|
+
declare scopeType: string;
|
|
31
|
+
declare scopeId: string;
|
|
32
|
+
declare type: string;
|
|
33
|
+
declare message: InAppMessage;
|
|
34
|
+
declare deduplicationHash: string | null;
|
|
35
|
+
declare contentHash: string;
|
|
36
|
+
declare presentation: string;
|
|
37
|
+
declare readAt: Date | null;
|
|
38
|
+
declare dismissedAt: Date | null;
|
|
39
|
+
declare archivedAt: Date | null;
|
|
40
|
+
declare createdAt: Date;
|
|
41
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# In-app messages
|
|
2
|
+
|
|
3
|
+
`@db3.ai/app/in-app` provides `app().inApp`: a durable recipient inbox usable
|
|
4
|
+
directly or through `app().notifications`. It accepts typed content with a required
|
|
5
|
+
plain-text fallback and optional rich HTML body, persists one item per recipient and supports scoped inbox
|
|
6
|
+
queries, unread counts, banner queries and independent read/dismiss/archive state.
|
|
7
|
+
|
|
8
|
+
This first implementation is a server service. It does not yet include HTTP route
|
|
9
|
+
registration, a packaged browser client, mark-all-read, preferences or push
|
|
10
|
+
delivery. The notification coordinator supports application-owned `toInApp()` and
|
|
11
|
+
`toMail()` renderers, but does not yet persist a generic channel outbox. `presentation` records UI intent;
|
|
12
|
+
it does not display a banner or toast by itself.
|
|
13
|
+
|
|
14
|
+
## Install storage
|
|
15
|
+
|
|
16
|
+
Add `InAppRecord` to the application's existing `server/database/models.ts` registry:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { InAppRecord } from '@db3.ai/app/in-app';
|
|
20
|
+
|
|
21
|
+
// Include alongside the existing Auth and application models.
|
|
22
|
+
export const notificationModels = [InAppRecord];
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Generate/review a committed migration with `db3 db:make-migration add_in_app_messages`, then apply it
|
|
26
|
+
with `db3 db:migrate`. The App service getter never creates tables. Tests may use
|
|
27
|
+
`await application.db.install(UserIdentity, InAppRecord)` on a disposable database.
|
|
28
|
+
The service uses ActiveRecord's active connection and joins an outer
|
|
29
|
+
`application.db.transaction(...)` through a savepoint; outer rollback removes
|
|
30
|
+
accepted inbox items too. No `db` argument is propagated through normal calls.
|
|
31
|
+
|
|
32
|
+
## Send from trusted application code
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { app } from '@db3.ai/app/server';
|
|
36
|
+
|
|
37
|
+
const results = await app().inApp.send(
|
|
38
|
+
userId, // Or an array of user IDs.
|
|
39
|
+
{
|
|
40
|
+
title: 'Your content plan is ready',
|
|
41
|
+
body: 'Open the planner to review your articles.',
|
|
42
|
+
bodyHtml: '<p>Open the planner to review your <strong>articles</strong>.</p>',
|
|
43
|
+
severity: 'success',
|
|
44
|
+
presentation: 'banner',
|
|
45
|
+
action: { label: 'View plan', href: '/app/planner' },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
scope: { type: 'account' },
|
|
49
|
+
type: 'content-plan.ready',
|
|
50
|
+
key: completionId,
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See [sendReportReady.ts](./examples/sendReportReady.ts) for the executable
|
|
56
|
+
code-owned example. No notification class or template database is needed.
|
|
57
|
+
|
|
58
|
+
`send()` is a trusted producer API, usable by background jobs. Never expose it
|
|
59
|
+
as an unrestricted browser send endpoint. Application code authorizes the producer
|
|
60
|
+
and chooses recipients. The service verifies identities through the App's configured
|
|
61
|
+
Auth identity model and checks recipient eligibility for the scope. It accepts at
|
|
62
|
+
most 100 input IDs, deduplicates canonical recipient identities, returns an empty
|
|
63
|
+
array for no recipients and accepts a batch atomically. Invalid or unavailable
|
|
64
|
+
recipients cannot leave a partially accepted batch.
|
|
65
|
+
|
|
66
|
+
Each result is `{ userId, id, status: 'created' | 'existing' }`. Acceptance means
|
|
67
|
+
database persistence, not display. `key` identifies an occurrence within recipient,
|
|
68
|
+
scope and `type`; retries return the existing ID without resetting read/dismissal
|
|
69
|
+
state. Different content under the same key raises `InAppError` with code `conflict`.
|
|
70
|
+
Omit the key for a new occurrence every time. Concurrent sends serialize by locking
|
|
71
|
+
recipient identity rows in stable order; a unique stored hash backs deduplication.
|
|
72
|
+
Applications sharing those rows in larger transactions must follow consistent lock
|
|
73
|
+
ordering and retry the entire domain transaction on database deadlocks or stale-snapshot
|
|
74
|
+
errors (including MariaDB ER_CHECKREAD). The service must not retry a fragment of an
|
|
75
|
+
outer transaction or claim acceptance when that transaction failed.
|
|
76
|
+
|
|
77
|
+
Content is copied and validated before writes. Titles, fallback bodies and labels are
|
|
78
|
+
plain text. `bodyHtml` optionally carries richer application-authored content. The
|
|
79
|
+
framework validates its length but does not sanitize it because each application owns
|
|
80
|
+
its permitted markup; every renderer must sanitize it before interpreting HTML. The service allows root-relative or credential-free HTTP(S) actions
|
|
81
|
+
and rejects unsafe schemes, protocol-relative URLs, whitespace and backslashes.
|
|
82
|
+
Renderers must use text-safe bindings; action destinations enforce their own access.
|
|
83
|
+
This is URL-scheme validation, not an application-specific trusted-host allowlist.
|
|
84
|
+
|
|
85
|
+
## Authenticated inbox
|
|
86
|
+
|
|
87
|
+
Authenticate the request using the existing framework Auth flow in an isolated
|
|
88
|
+
`RequestContext`. Then call the service without a client-supplied reader ID:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const page = await app().inApp.inbox({
|
|
92
|
+
scope: { type: 'account' },
|
|
93
|
+
limit: 25,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await app().inApp.update(itemId, 'read', { type: 'account' });
|
|
97
|
+
await app().inApp.update(itemId, 'unread', { type: 'account' });
|
|
98
|
+
await app().inApp.update(itemId, 'dismiss', { type: 'account' });
|
|
99
|
+
await app().inApp.update(itemId, 'archive', { type: 'account' });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`page` contains `items`, the whole scoped/unarchived `unreadCount` and `nextBefore`.
|
|
103
|
+
Supply `before: page.nextBefore` for another page. Limits range from 1 to 100.
|
|
104
|
+
The ordering uses descending ULIDs for stable pagination, not a commit-order
|
|
105
|
+
event cursor or snapshot. Refresh the first page to discover new arrivals; counts
|
|
106
|
+
and pages may change as concurrent actions occur.
|
|
107
|
+
|
|
108
|
+
`view: 'banners'` returns only undismissed/unarchived banner items. All views keep
|
|
109
|
+
the same inbox-wide unread count. Reading a page changes no state. Reading an item
|
|
110
|
+
does not dismiss its banner; dismissal does not mark it read or archive it. Archive
|
|
111
|
+
removes the item from active inbox/banner queries. Repeat state changes preserve the
|
|
112
|
+
first timestamp, except explicitly marking unread clears `readAt`. Rendering clients
|
|
113
|
+
must not replay stored toast items as fresh live alerts when loading an inbox.
|
|
114
|
+
|
|
115
|
+
Inbox operations require active request authentication. Unknown and foreign item IDs
|
|
116
|
+
produce the same `not_found` error. Hosts should map `InAppError.code` appropriately:
|
|
117
|
+
`invalid` to 400, `unauthenticated` to 401, `forbidden` to 403, `not_found` to 404 and
|
|
118
|
+
`conflict` to 409. Ordinary storage failures should remain server errors.
|
|
119
|
+
|
|
120
|
+
## Organization and other application scopes
|
|
121
|
+
|
|
122
|
+
Account ownership is built in. Other scopes fail closed unless configured:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const application = new App({
|
|
126
|
+
inApp: {
|
|
127
|
+
/** Checks current application membership and restrictions for every operation. */
|
|
128
|
+
async authorizeScope(userId, scope, operation) {
|
|
129
|
+
if (operation === 'update' && isReadOnlySession()) return false;
|
|
130
|
+
if (scope.type === 'account') return true;
|
|
131
|
+
return scope.type === 'organization' && await isMember(userId, scope.id);
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The policy functions above are application-owned. The framework invokes the policy
|
|
138
|
+
for `receive`, `read` and `update`. When supplied it also governs account scopes,
|
|
139
|
+
allowing read-only impersonation restrictions. Background producers must supply a
|
|
140
|
+
policy that can check recipients without relying on a logged-in request. Checking
|
|
141
|
+
scope identifiers from headers alone is not authorization.
|
|
142
|
+
|
|
143
|
+
`InAppRecord` is a persistence model, not an authorized HTTP interface. Do not expose
|
|
144
|
+
its unrestricted query/update APIs to clients. It uses opaque recipient identifiers
|
|
145
|
+
rather than a foreign key to a hard-coded users table; application account-deletion
|
|
146
|
+
and retention workflows must remove the corresponding records. Its table belongs in
|
|
147
|
+
the same transactional database as the configured identity model.
|
|
148
|
+
|
|
149
|
+
## Committed change notifications
|
|
150
|
+
|
|
151
|
+
`InAppOptions.onChanged(userId)` is an optional application-owned invalidation
|
|
152
|
+
hook. `send()` calls it for newly created recipients; `update()` calls it after a
|
|
153
|
+
changed row. Reads and deduplicated sends do not trigger it. Surrounding Knex
|
|
154
|
+
transactions/savepoints defer the callback until commit; rollback suppresses it.
|
|
155
|
+
Use the service APIs: direct model writes do not invoke this hook.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const application = new App({
|
|
159
|
+
inApp: {
|
|
160
|
+
/** Sends an empty signal; the browser reloads its authorized HTTP inbox. */
|
|
161
|
+
async onChanged(userId) {
|
|
162
|
+
await application.webSockets.channel(`user:${userId}:notifications`).publish('changed', {});
|
|
163
|
+
},
|
|
164
|
+
/** A push failure cannot undo a committed notification. */
|
|
165
|
+
onDeliveryError() { reportNotificationPushFailure(); },
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`reportNotificationPushFailure` is application-owned logging. Configure the
|
|
171
|
+
WebSocket HTTP receiver and worker publisher for cross-process delivery and register
|
|
172
|
+
a recipient-only channel policy. No WebSocket dependency is enabled automatically.
|
|
173
|
+
The hook is best effort and not a durable outbox. A crash between commit and the
|
|
174
|
+
callback can lose a signal; initial load, reconnect and manual refresh retrieve
|
|
175
|
+
saved inbox state. Callback errors do not reject durable message acceptance.
|
|
176
|
+
|
|
177
|
+
## Ownership and verification
|
|
178
|
+
|
|
179
|
+
This service depends on DB/ActiveRecord, Auth and the active App context. Public
|
|
180
|
+
contracts are in `contracts/`; `@db3.ai/app/in-app/contracts` is type-only and can be
|
|
181
|
+
used by browser TypeScript without importing server implementation.
|
|
182
|
+
|
|
183
|
+
Run `npm run test:service --workspace @db3.ai/app -- in-app` with local test DB
|
|
184
|
+
credentials configured as described in the package README. Tests create and remove
|
|
185
|
+
real disposable databases; missing SQL is a failure, not a skipped pass. They cover
|
|
186
|
+
concurrent retry acceptance, aliases, conflict rollback, outer transaction rollback,
|
|
187
|
+
old transaction snapshots, recipient/scope isolation, state independence and pagination.
|
|
188
|
+
Public changes also require `npm run check --workspace @db3.ai/app` and
|
|
189
|
+
`npm run framework:package:test` for packed runtime and TypeScript consumer checks.
|
|
190
|
+
|
|
191
|
+
The starter application includes app-owned authenticated HTTP routes, an unread badge,
|
|
192
|
+
paginated inbox, persistent banners and a demo toast. Open the inbox or use Refresh
|
|
193
|
+
to retrieve current state; websocket delivery is not connected to this demo yet.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { InAppMessage } from './InAppMessage';
|
|
2
|
+
import type { InAppScope } from './InAppScope';
|
|
3
|
+
|
|
4
|
+
/** A safe recipient-facing view; internal deduplication hashes are never exposed. */
|
|
5
|
+
export interface InAppItem {
|
|
6
|
+
id: string;
|
|
7
|
+
type: string;
|
|
8
|
+
message: InAppMessage;
|
|
9
|
+
createdAt: string;
|
|
10
|
+
readAt: string | null;
|
|
11
|
+
/** Dismissal hides a banner without removing its inbox item or implying it was read. */
|
|
12
|
+
dismissedAt: string | null;
|
|
13
|
+
archivedAt: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Trusted server dispatch metadata. Sending is not a public browser-authorized operation. */
|
|
17
|
+
export interface InAppSendOptions {
|
|
18
|
+
scope: InAppScope;
|
|
19
|
+
/** Stable notification type, for example content-plan.ready. */
|
|
20
|
+
type: string;
|
|
21
|
+
/** Retry identity, unique within type, scope and recipient. Omit to create a new occurrence. */
|
|
22
|
+
key?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One result per distinct recipient, in first-input order. Empty input returns an empty array. */
|
|
26
|
+
export interface InAppAcceptance {
|
|
27
|
+
userId: string;
|
|
28
|
+
id: string;
|
|
29
|
+
status: 'created' | 'existing';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Query for the authenticated user's inbox in one explicit scope. */
|
|
33
|
+
export interface InAppInboxQuery {
|
|
34
|
+
scope: InAppScope;
|
|
35
|
+
/** Defaults to 25; maximum 100. */
|
|
36
|
+
limit?: number;
|
|
37
|
+
/** ID from nextBefore. Pagination is an inbox position, not a realtime/replay cursor. */
|
|
38
|
+
before?: string;
|
|
39
|
+
/** Defaults to inbox; banners excludes dismissed items. All views exclude archived items. */
|
|
40
|
+
view?: 'inbox' | 'banners';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Paginated items and the unread count for the whole unarchived scoped inbox. */
|
|
44
|
+
export interface InAppInboxPage {
|
|
45
|
+
items: InAppItem[];
|
|
46
|
+
unreadCount: number;
|
|
47
|
+
nextBefore: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Explicit independent recipient state transitions. */
|
|
51
|
+
export type InAppTransition = 'read' | 'unread' | 'dismiss' | 'archive';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Recipient-facing content stored in an inbox. */
|
|
2
|
+
export interface InAppMessage {
|
|
3
|
+
title: string;
|
|
4
|
+
/** Plain-text fallback used by compact, accessibility and non-HTML renderers. */
|
|
5
|
+
body: string;
|
|
6
|
+
/**
|
|
7
|
+
* Optional rich body authored by trusted application code.
|
|
8
|
+
*
|
|
9
|
+
* The framework validates and stores this value but does not sanitize it because
|
|
10
|
+
* permitted markup is an application policy. Every HTML renderer must sanitize it.
|
|
11
|
+
*/
|
|
12
|
+
bodyHtml?: string;
|
|
13
|
+
/** Defaults to info. Severity does not determine persistence or prominence. */
|
|
14
|
+
severity?: 'info' | 'success' | 'warning' | 'error';
|
|
15
|
+
/** Defaults to toast. Every presentation remains in the durable inbox. */
|
|
16
|
+
presentation?: 'inbox' | 'toast' | 'banner';
|
|
17
|
+
/** Root-relative or HTTP(S) action; the destination must enforce its own authorization. */
|
|
18
|
+
action?: { label: string; href: string };
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Explicit account or application-owned scope; tenant scopes require an authorization policy. */
|
|
2
|
+
export type InAppScope = { type: 'account'; id?: never } | { type: string; id: string };
|
|
3
|
+
|
|
4
|
+
/** Scope policy invoked for recipients and authenticated readers, including state mutations. */
|
|
5
|
+
export interface InAppOptions {
|
|
6
|
+
/** Best-effort invalidation after committed writes. Failures never undo durable acceptance. */
|
|
7
|
+
onChanged?: (userId: string) => void | Promise<void>;
|
|
8
|
+
/** Reports failed invalidations; inbox state remains authoritative. */
|
|
9
|
+
onDeliveryError?: (error: unknown) => void;
|
|
10
|
+
/** Non-account scopes fail closed when this policy is absent. Check current membership and write permissions. */
|
|
11
|
+
authorizeScope?: (userId: string, scope: InAppScope, operation: 'receive' | 'read' | 'update') => boolean | Promise<boolean>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { app } from '@db3.ai/app/server';
|
|
2
|
+
import type { InAppAcceptance, InAppMessage } from '@db3.ai/app/in-app';
|
|
3
|
+
|
|
4
|
+
/** Sends a code-owned message without notification classes, providers or a template database. */
|
|
5
|
+
export async function sendReportReady(userId: string, reportId: string): Promise<InAppAcceptance[]> {
|
|
6
|
+
const message: InAppMessage = {
|
|
7
|
+
title: 'Your report is ready',
|
|
8
|
+
body: 'Open the report to review your results.',
|
|
9
|
+
presentation: 'banner',
|
|
10
|
+
severity: 'success',
|
|
11
|
+
action: { label: 'View report', href: `/reports/${encodeURIComponent(reportId)}` },
|
|
12
|
+
};
|
|
13
|
+
return app().inApp.send(userId, message, { scope: { type: 'account' }, type: 'report.ready', key: reportId });
|
|
14
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { InAppError } from './InAppError';
|
|
3
|
+
import type * as inbox from './contracts';
|
|
4
|
+
|
|
5
|
+
const identifier = z.string().min(1).max(255).regex(/^\S+$/u);
|
|
6
|
+
const scopeSchema = z.union([
|
|
7
|
+
z.object({ type: z.literal('account') }).strict(),
|
|
8
|
+
z.object({ type: z.string().min(1).max(80).regex(/^[a-zA-Z][a-zA-Z0-9_.-]*$/).refine(value => value !== 'account'), id: identifier }).strict(),
|
|
9
|
+
]);
|
|
10
|
+
const messageSchema = z.object({
|
|
11
|
+
title: z.string().trim().min(1).max(255),
|
|
12
|
+
body: z.string().trim().min(1).max(10_000),
|
|
13
|
+
bodyHtml: z.string().trim().min(1).max(50_000).optional(),
|
|
14
|
+
severity: z.enum(['info', 'success', 'warning', 'error']).default('info'),
|
|
15
|
+
presentation: z.enum(['inbox', 'toast', 'banner']).default('toast'),
|
|
16
|
+
action: z.object({ label: z.string().trim().min(1).max(120), href: z.string().min(1).max(2048).refine(safeAction) }).strict().optional(),
|
|
17
|
+
}).strict();
|
|
18
|
+
const sendSchema = z.object({ scope: scopeSchema, type: z.string().min(1).max(120).regex(/^[a-zA-Z0-9_.-]+$/), key: z.string().min(1).max(255).optional() }).strict();
|
|
19
|
+
const querySchema = z.object({ scope: scopeSchema, limit: z.number().int().min(1).max(100).default(25), before: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/).optional(), view: z.enum(['inbox', 'banners']).default('inbox') }).strict();
|
|
20
|
+
|
|
21
|
+
/** Accepts local absolute paths and credential-free HTTP(S) URLs, rejecting executable and ambiguous forms. */
|
|
22
|
+
function safeAction(value: string): boolean {
|
|
23
|
+
if (/[\s\\\u0000-\u001f\u007f]/u.test(value)) return false;
|
|
24
|
+
if (value.startsWith('/') && !value.startsWith('//')) return true;
|
|
25
|
+
try {
|
|
26
|
+
const url = new URL(value);
|
|
27
|
+
return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password;
|
|
28
|
+
} catch { return false; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Parses public input without leaking database or recipient information. */
|
|
32
|
+
function parse<T>(schema: z.ZodType<T>, value: unknown): T {
|
|
33
|
+
const result = schema.safeParse(value);
|
|
34
|
+
if (!result.success) throw new InAppError('invalid', 'Invalid in-app message input.');
|
|
35
|
+
return result.data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Copies and validates content before it can be persisted. */
|
|
39
|
+
export function normalizeMessage(value: inbox.InAppMessage): inbox.InAppMessage { return parse(messageSchema, value); }
|
|
40
|
+
/** Validates the explicit dispatch scope, type and retry identity. */
|
|
41
|
+
export function normalizeSend(value: inbox.InAppSendOptions): inbox.InAppSendOptions { return parse(sendSchema, value); }
|
|
42
|
+
/** Validates a scoped inbox query and resolves pagination defaults. */
|
|
43
|
+
export function normalizeQuery(value: inbox.InAppInboxQuery) { return parse(querySchema, value); }
|
|
44
|
+
/** Validates a scope for state-changing operations. */
|
|
45
|
+
export function normalizeScope(value: inbox.InAppScope): inbox.InAppScope { return parse(scopeSchema, value); }
|
|
46
|
+
/** Validates the bounded recipient list and removes repeated identities. */
|
|
47
|
+
export function normalizeRecipients(value: string | readonly string[]): string[] {
|
|
48
|
+
return [...new Set(parse(z.array(identifier).max(100), typeof value === 'string' ? [value] : value))];
|
|
49
|
+
}
|