@db3.ai/app 0.1.0-beta.1 → 0.1.0
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 +75 -17
- 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,11 @@
|
|
|
1
|
+
export type { AppManifest, AppMenuItem } from './AppManifest';
|
|
2
|
+
export type { AppDefinition, AppDefinitions, AppServices, AppStartContext } from './AppDefinition';
|
|
3
|
+
export type { AppRoute, AppRouteContext } from './AppRoute';
|
|
4
|
+
export type { AppDescription, AppInstallationState, AppClientDefinition } from './AppDescription';
|
|
5
|
+
export type { DiscoveredApp } from './DiscoveredApp';
|
|
6
|
+
export type { InstalledApps } from './InstalledApps';
|
|
7
|
+
export type { AppAction } from './AppAction';
|
|
8
|
+
export type { AppNavigation } from './AppNavigation';
|
|
9
|
+
export type { AppNavigationBadge } from './AppNavigationBadge';
|
|
10
|
+
export type { AppNavigationContext } from './AppNavigationContext';
|
|
11
|
+
export type { AppNavigationResult } from './AppNavigationResult';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AppDefinition } from './contracts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Defines an app without installing, connecting or registering it globally.
|
|
5
|
+
* @param definition - Public service, metadata and app-owned contributions.
|
|
6
|
+
* @returns Definition preserving the inferred public service type.
|
|
7
|
+
*/
|
|
8
|
+
export function defineApp<TService>(definition: AppDefinition<TService>): AppDefinition<TService> {
|
|
9
|
+
return definition;
|
|
10
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import type { DiscoveredApp } from './contracts';
|
|
5
|
+
import { validateAppDefinitions } from './validateAppDefinitions';
|
|
6
|
+
|
|
7
|
+
/** Discovers local apps and opted-in direct npm dependencies without executing their code. */
|
|
8
|
+
export function discoverApps(directory: string): DiscoveredApp[] {
|
|
9
|
+
const found: DiscoveredApp[] = [];
|
|
10
|
+
const root = join(directory, 'apps');
|
|
11
|
+
if (existsSync(root)) {
|
|
12
|
+
for (const item of readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
13
|
+
if (item.isDirectory() && existsSync(join(root, item.name, 'manifest.json'))) found.push(readApp(join(root, item.name), item.name));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const packageFile = join(directory, 'package.json');
|
|
17
|
+
if (existsSync(packageFile)) {
|
|
18
|
+
const host = JSON.parse(readFileSync(packageFile, 'utf8'));
|
|
19
|
+
const require = createRequire(packageFile);
|
|
20
|
+
for (const name of Object.keys({ ...host.dependencies, ...host.optionalDependencies }).sort()) {
|
|
21
|
+
let file: string;
|
|
22
|
+
try { file = require.resolve(`${name}/package.json`); }
|
|
23
|
+
catch { continue; } // Packages must export package.json to opt into metadata-only discovery.
|
|
24
|
+
const metadata = JSON.parse(readFileSync(file, 'utf8'));
|
|
25
|
+
if (metadata.db3?.app !== true) continue;
|
|
26
|
+
found.push(readApp(dirname(file), basename(name), name, metadata.db3));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const definitions: Record<string, any> = Object.create(null);
|
|
30
|
+
for (const app of found) {
|
|
31
|
+
const id = app.manifest.id;
|
|
32
|
+
if (Object.hasOwn(definitions, id)) throw new Error(`Duplicate app "${id}". Keep either the local folder or the npm package.`);
|
|
33
|
+
definitions[id] = { manifest: app.manifest, create: () => undefined };
|
|
34
|
+
}
|
|
35
|
+
validateAppDefinitions(definitions, [...Object.getOwnPropertyNames(Object.prototype), 'then', 'toJSON']);
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Reads one conventional root manifest and resolves entries contained in that app's directory. */
|
|
40
|
+
function readApp(directory: string, id: string, packageName?: string, entries?: { entry?: string; client?: string }): DiscoveredApp {
|
|
41
|
+
const manifest = JSON.parse(readFileSync(join(directory, 'manifest.json'), 'utf8'));
|
|
42
|
+
if (manifest.id !== undefined && manifest.id !== id) throw new Error(`App folder "${id}" disagrees with manifest identifier "${manifest.id}".`);
|
|
43
|
+
const entry = containedEntry(directory, entries?.entry ?? 'App.ts');
|
|
44
|
+
const clientPath = entries?.client ?? 'client/index.ts';
|
|
45
|
+
const client = containedEntry(directory, clientPath);
|
|
46
|
+
return { manifest: { ...manifest, id }, directory, entry, ...(existsSync(client) ? { client } : {}), ...(packageName ? { packageName } : {}) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Rejects entry paths that escape the package or local app root. */
|
|
50
|
+
function containedEntry(directory: string, entry: string): string {
|
|
51
|
+
if (typeof entry !== 'string' || isAbsolute(entry)) throw new Error('App entries must be relative paths.');
|
|
52
|
+
const target = resolve(directory, entry);
|
|
53
|
+
const path = relative(directory, target);
|
|
54
|
+
if (!path || path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)) throw new Error('App entries must remain inside their app directory.');
|
|
55
|
+
return target;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerAppRoutes } from './registerAppRoutes';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './contracts';
|
|
2
|
+
export { defineApp } from './defineApp';
|
|
3
|
+
export { Apps } from './Apps';
|
|
4
|
+
export { AppRouteError } from './AppRouteError';
|
|
5
|
+
export { AppService } from './AppService';
|
|
6
|
+
export { discoverApps } from './discoverApps';
|
|
7
|
+
export { writeAppTypes } from './writeAppTypes';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type * as apps from './contracts';
|
|
2
|
+
|
|
3
|
+
/** Validates a contribution and copies only supported, browser-safe fields into the response. */
|
|
4
|
+
export function normalizeAppNavigation(value: apps.AppNavigation): apps.AppNavigation {
|
|
5
|
+
if (!value || !Array.isArray(value.items)) throw new Error('Navigation must provide an items array.');
|
|
6
|
+
const ids = new Set<string>();
|
|
7
|
+
return {
|
|
8
|
+
...details(value),
|
|
9
|
+
items: value.items.map(item => {
|
|
10
|
+
if (!item || typeof item.id !== 'string' || !item.id.trim() || ids.has(item.id) || typeof item.label !== 'string' || !item.label.trim() || typeof item.path !== 'string' || !/^(?:[a-z0-9-]+(?:\/[a-z0-9-]+)*)?$/.test(item.path)) throw new Error('Invalid or duplicate navigation item.');
|
|
11
|
+
ids.add(item.id);
|
|
12
|
+
return { id: item.id, label: item.label, path: item.path, ...details(item) };
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Copies optional display information without passing through arbitrary provider data. */
|
|
18
|
+
function details(value: { description?: string; badge?: apps.AppNavigationBadge }): Pick<apps.AppNavigation, 'description' | 'badge'> {
|
|
19
|
+
const result: Pick<apps.AppNavigation, 'description' | 'badge'> = {};
|
|
20
|
+
if (value.description !== undefined) {
|
|
21
|
+
if (typeof value.description !== 'string') throw new Error('Navigation descriptions must be plain text.');
|
|
22
|
+
result.description = value.description;
|
|
23
|
+
}
|
|
24
|
+
if (value.badge !== undefined) {
|
|
25
|
+
if (!value.badge || !Number.isSafeInteger(value.badge.count) || value.badge.count < 0 || typeof value.badge.label !== 'string' || !value.badge.label.trim()) throw new Error('Navigation badges require a non-negative integer count and an accessible label.');
|
|
26
|
+
result.badge = { count: value.badge.count, label: value.badge.label };
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import type { Apps } from './Apps';
|
|
3
|
+
import { AppRouteError } from './AppRouteError';
|
|
4
|
+
|
|
5
|
+
/** Mounts dynamic app routes behind mandatory host authorization; newly installed apps become reachable immediately. */
|
|
6
|
+
export function registerAppRoutes(server: FastifyInstance, apps: Apps<any>, authorize: (request: FastifyRequest, appId: string) => Promise<{ id: string }>): void {
|
|
7
|
+
for (const url of ['/api/apps/:appId', '/api/apps/:appId/*']) {
|
|
8
|
+
server.route({
|
|
9
|
+
method: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'], url,
|
|
10
|
+
/** Drains in-flight requests before lifecycle changes and passes only a verified actor to app code. */
|
|
11
|
+
handler: async (request, reply) => {
|
|
12
|
+
try {
|
|
13
|
+
return await apps.run(async () => {
|
|
14
|
+
const { appId, '*': suffix = '' } = request.params as Record<string, string>;
|
|
15
|
+
const service = apps.get(appId);
|
|
16
|
+
if (service === undefined) return reply.code(404).send({ message: 'App is unavailable.' });
|
|
17
|
+
const candidates = apps.routes().filter(item => item.appId === appId && item.route.method === request.method).sort((a, b) => (a.route.path.match(/:/g)?.length ?? 0) - (b.route.path.match(/:/g)?.length ?? 0));
|
|
18
|
+
for (const { route } of candidates) {
|
|
19
|
+
const params = matchPath(route.path, `/${suffix}`);
|
|
20
|
+
if (!params) continue;
|
|
21
|
+
const actor = await authorize(request, appId);
|
|
22
|
+
if (!actor?.id) throw new AppRouteError(401, 'Please sign in.');
|
|
23
|
+
const result = await route.handle({ service, actor, body: request.body, params, query: request.query as Record<string, unknown> });
|
|
24
|
+
return reply.code(route.status ?? 200).send(result);
|
|
25
|
+
}
|
|
26
|
+
return reply.code(404).send({ message: 'App route not found.' });
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof AppRouteError) return reply.code(error.statusCode).send({ message: error.message });
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Matches validated route segments and captures named parameters without evaluating arbitrary regular expressions. */
|
|
38
|
+
function matchPath(pattern: string, path: string): Record<string, string> | undefined {
|
|
39
|
+
const expected = pattern.split('/');
|
|
40
|
+
const actual = path.split('/');
|
|
41
|
+
if (expected.length !== actual.length) return;
|
|
42
|
+
const params: Record<string, string> = Object.create(null);
|
|
43
|
+
for (let index = 0; index < expected.length; index++) {
|
|
44
|
+
if (expected[index].startsWith(':') && actual[index]) params[expected[index].slice(1)] = actual[index];
|
|
45
|
+
else if (expected[index] !== actual[index]) return;
|
|
46
|
+
}
|
|
47
|
+
return params;
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type * as apps from './contracts';
|
|
2
|
+
import { normalizeAppNavigation } from './normalizeAppNavigation';
|
|
3
|
+
import { isQueueableJobClass, queueableJobName } from '../queue';
|
|
4
|
+
|
|
5
|
+
/** Validates names, contribution ownership and dependency order before any app code is started. */
|
|
6
|
+
export function validateAppDefinitions(definitions: apps.AppDefinitions, reserved: readonly string[]): string[] {
|
|
7
|
+
const ids = Object.keys(definitions);
|
|
8
|
+
const tables = new Set<string>();
|
|
9
|
+
for (const id of ids) {
|
|
10
|
+
const definition = definitions[id];
|
|
11
|
+
const manifest = definition.manifest;
|
|
12
|
+
if (!/^[a-z][a-z0-9_]{0,39}$/.test(id) || /^db3(?:_|$)/.test(id) || reserved.includes(id) || manifest.id !== id) throw new Error(`Invalid or reserved app identifier "${id}".`);
|
|
13
|
+
if (!manifest.name?.trim() || !/^\d+\.\d+\.\d+$/.test(manifest.version) || !Number.isSafeInteger(manifest.apiVersion ?? 1) || (manifest.apiVersion ?? 1) < 1) throw new Error(`Invalid app manifest for "${id}"; use a stable x.y.z release and a positive API generation.`);
|
|
14
|
+
if (typeof definition.create !== 'function') throw new Error(`App "${id}" must create a public service.`);
|
|
15
|
+
const jobs = new Set<string>();
|
|
16
|
+
for (const Job of definition.jobs ?? []) {
|
|
17
|
+
if (!isQueueableJobClass(Job) || !Job.jobName?.startsWith(`${id}.`) || jobs.has(queueableJobName(Job))) throw new Error(`Invalid or duplicate job in "${id}"; declare a stable ${id}. jobName.`);
|
|
18
|
+
jobs.add(queueableJobName(Job));
|
|
19
|
+
}
|
|
20
|
+
for (const Model of definition.database?.models ?? []) {
|
|
21
|
+
if (!Model.table.startsWith(`${id}_`) || tables.has(Model.table)) throw new Error(`App "${id}" cannot own table "${Model.table}".`);
|
|
22
|
+
tables.add(Model.table);
|
|
23
|
+
}
|
|
24
|
+
const paths = new Set<string>();
|
|
25
|
+
for (const route of definition.routes ?? []) {
|
|
26
|
+
const key = `${route.method} ${route.path}`;
|
|
27
|
+
if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(route.method) || !/^\/(?:[a-zA-Z0-9_:-]+(?:\/[a-zA-Z0-9_:-]+)*)?$/.test(route.path) || paths.has(key) || typeof route.handle !== 'function') throw new Error(`Invalid or duplicate route "${key}" in "${id}".`);
|
|
28
|
+
paths.add(key);
|
|
29
|
+
}
|
|
30
|
+
try { normalizeAppNavigation({ items: manifest.navigation ?? [] }); }
|
|
31
|
+
catch { throw new Error(`Invalid navigation in "${id}".`); }
|
|
32
|
+
for (const [dependency, generation] of Object.entries({ ...manifest.requires, ...manifest.optional })) {
|
|
33
|
+
if (!/^[a-z][a-z0-9_]{0,39}$/.test(dependency) || reserved.includes(dependency) || dependency === id || !Number.isSafeInteger(generation) || generation < 1) throw new Error(`Invalid dependency "${dependency}" in "${id}".`);
|
|
34
|
+
if (manifest.requires?.[dependency] !== undefined && manifest.optional?.[dependency] !== undefined) throw new Error(`Dependency "${dependency}" is both required and optional in "${id}".`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const visiting = new Set<string>();
|
|
38
|
+
const ordered = new Set<string>();
|
|
39
|
+
/** Topologically orders available dependencies and rejects composition cycles. */
|
|
40
|
+
function visit(id: string): void {
|
|
41
|
+
if (ordered.has(id)) return;
|
|
42
|
+
if (visiting.has(id)) throw new Error(`App dependency cycle at "${id}".`);
|
|
43
|
+
visiting.add(id);
|
|
44
|
+
for (const dependency of Object.keys({ ...definitions[id].manifest.requires, ...definitions[id].manifest.optional })) {
|
|
45
|
+
if (definitions[dependency]) visit(dependency);
|
|
46
|
+
}
|
|
47
|
+
visiting.delete(id);
|
|
48
|
+
ordered.add(id);
|
|
49
|
+
}
|
|
50
|
+
ids.forEach(visit);
|
|
51
|
+
return [...ordered];
|
|
52
|
+
}
|
package/src/apps/vite.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { appsPlugin } from './appsPlugin';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { discoverApps } from './discoverApps';
|
|
4
|
+
|
|
5
|
+
/** Regenerates optional app().social types from discovery during development and builds, without loading server code. */
|
|
6
|
+
export async function writeAppTypes(directory: string): Promise<void> {
|
|
7
|
+
const target = join(directory, '.db3/apps.d.ts');
|
|
8
|
+
const properties = discoverApps(directory).map(app => {
|
|
9
|
+
const specifier = app.packageName ?? `./${relative(dirname(target), app.entry).replaceAll('\\', '/').replace(/\.ts$/, '.js')}`;
|
|
10
|
+
return `\t\treadonly ${app.manifest.id}?: InstanceType<typeof import(${JSON.stringify(specifier)}).default>;`;
|
|
11
|
+
});
|
|
12
|
+
await mkdir(dirname(target), { recursive: true });
|
|
13
|
+
await writeFile(target, `// Generated by DB3 app discovery. Do not edit.\nimport '@db3.ai/app/apps/contracts';\ndeclare module '@db3.ai/app/apps/contracts' {\n\tinterface InstalledApps {\n${properties.join('\n')}\n\t}\n}\n`);
|
|
14
|
+
}
|
package/src/auth/Auth.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ActiveRecord, type Database } from '../db';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
2
3
|
import { isPromiseLike } from '@db3.ai/pure';
|
|
3
4
|
import type { Config } from '../config';
|
|
4
5
|
import { AuthProvider, PASSWORD_AUTH_PROVIDER } from './AuthProvider';
|
|
@@ -649,6 +650,8 @@ export class Auth<TIdentity extends UserIdentity = UserIdentity> {
|
|
|
649
650
|
|
|
650
651
|
/**
|
|
651
652
|
* Changes a user's password provider after verifying the current password.
|
|
653
|
+
* Password replacement, recovery-link invalidation and revocation of every
|
|
654
|
+
* bearer session (including the current session) commit together.
|
|
652
655
|
*
|
|
653
656
|
* @param user - User whose password should change.
|
|
654
657
|
* @param currentPassword - Current plaintext password for verification.
|
|
@@ -660,13 +663,19 @@ export class Auth<TIdentity extends UserIdentity = UserIdentity> {
|
|
|
660
663
|
currentPassword: string,
|
|
661
664
|
nextPassword: string,
|
|
662
665
|
): Promise<boolean> {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
);
|
|
669
|
-
|
|
666
|
+
const userId = this.userPrimaryKey(user);
|
|
667
|
+
if (!userId) return false;
|
|
668
|
+
|
|
669
|
+
const changed = await this.db.knex.transaction(transaction => ActiveRecord.withDb(transaction, async () => {
|
|
670
|
+
const lockedUser = await this.#lockIdentity(userId, transaction);
|
|
671
|
+
if (!lockedUser) return false;
|
|
672
|
+
if (!await this.passwordProvider().changePassword(lockedUser, currentPassword, nextPassword)) return false;
|
|
673
|
+
await this.#invalidatePasswordRecovery(userId, transaction);
|
|
674
|
+
return true;
|
|
675
|
+
}));
|
|
676
|
+
|
|
677
|
+
if (changed) this.#forgetRevokedUser(userId);
|
|
678
|
+
return changed;
|
|
670
679
|
}
|
|
671
680
|
|
|
672
681
|
/**
|
|
@@ -762,6 +771,8 @@ export class Auth<TIdentity extends UserIdentity = UserIdentity> {
|
|
|
762
771
|
|
|
763
772
|
/**
|
|
764
773
|
* Resets the password provider using a valid password reset token.
|
|
774
|
+
* Serializes recovery for an account and atomically invalidates every existing
|
|
775
|
+
* session and recovery link. Failed validation leaves credentials unchanged.
|
|
765
776
|
*
|
|
766
777
|
* @param input - Reset token, new password, and optional identity value.
|
|
767
778
|
* @returns Identity whose password changed, or null when redemption fails.
|
|
@@ -782,25 +793,57 @@ export class Auth<TIdentity extends UserIdentity = UserIdentity> {
|
|
|
782
793
|
return null;
|
|
783
794
|
}
|
|
784
795
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
this.
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
796
|
+
if (!tokenRecord.userId) return null;
|
|
797
|
+
const user = await this.db.knex.transaction(transaction => ActiveRecord.withDb(transaction, async () => {
|
|
798
|
+
const lockedUser = await this.#lockIdentity(tokenRecord.userId!, transaction);
|
|
799
|
+
if (!lockedUser) return null;
|
|
800
|
+
|
|
801
|
+
// A locking read sees a competing redemption's committed state after
|
|
802
|
+
// waiting for the account lock, including under repeatable-read SQL.
|
|
803
|
+
const row = await this.PasswordResetTokenModel.query(transaction)
|
|
804
|
+
.where('id', tokenRecord.id).toKnex().forUpdate().first();
|
|
805
|
+
const current = row ? this.PasswordResetTokenModel.fromDb(row, transaction) : null;
|
|
806
|
+
if (!current?.isUsable() || !await this.passwordResetIdentityMatches(input, lockedUser)) return null;
|
|
807
|
+
|
|
808
|
+
await this.passwordProvider().recoverPassword(lockedUser, input.password);
|
|
809
|
+
await this.#invalidatePasswordRecovery(tokenRecord.userId!, transaction);
|
|
810
|
+
return lockedUser;
|
|
811
|
+
}));
|
|
812
|
+
|
|
813
|
+
if (user) {
|
|
814
|
+
user.setDb(this.db.knex);
|
|
815
|
+
this.#forgetRevokedUser(this.userPrimaryKey(user)!);
|
|
792
816
|
}
|
|
817
|
+
return user;
|
|
818
|
+
}
|
|
793
819
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
820
|
+
/** Locks the account row to serialize concurrent password replacements. */
|
|
821
|
+
async #lockIdentity(userId: string, transaction: Knex.Transaction): Promise<TIdentity | null> {
|
|
822
|
+
const row = await this.IdentityModel.query(transaction)
|
|
823
|
+
.where(this.IdentityModel.primaryKey, userId).toKnex().forUpdate().first();
|
|
824
|
+
return row ? this.IdentityModel.fromDb(row, transaction) as TIdentity : null;
|
|
825
|
+
}
|
|
797
826
|
|
|
798
|
-
|
|
827
|
+
/** Invalidates recovery links and sessions within the password transaction. */
|
|
828
|
+
async #invalidatePasswordRecovery(userId: string, transaction: Knex.Transaction): Promise<void> {
|
|
829
|
+
await this.PasswordResetTokenModel.query(transaction)
|
|
830
|
+
.where('userId', userId).whereNull('usedAt').patch({ usedAt: new Date() });
|
|
831
|
+
await this.#revokeTokens(userId, transaction);
|
|
832
|
+
}
|
|
799
833
|
|
|
800
|
-
|
|
801
|
-
|
|
834
|
+
/** Applies session revocation on the owning connection or password transaction. */
|
|
835
|
+
async #revokeTokens(userId: string, connection: Knex): Promise<number> {
|
|
836
|
+
return this.TokenModel.query(connection)
|
|
837
|
+
.where('userId', userId).whereNull('revokedAt').patch({ revokedAt: new Date() });
|
|
838
|
+
}
|
|
802
839
|
|
|
803
|
-
|
|
840
|
+
/** Discards cached bearer authentication after committed account revocation. */
|
|
841
|
+
#forgetRevokedUser(userId: string): void {
|
|
842
|
+
if (this.requestContext?.active) {
|
|
843
|
+
const key = `${AUTH_TOKEN_CONTEXT_KEY_PREFIX}.${this.TokenModel.table}.revision`;
|
|
844
|
+
this.requestContext.set(key, (this.requestContext.get<number>(key) ?? 0) + 1);
|
|
845
|
+
}
|
|
846
|
+
if (this.user && this.userPrimaryKey(this.user) === userId) this.logout();
|
|
804
847
|
}
|
|
805
848
|
|
|
806
849
|
/**
|
|
@@ -897,6 +940,26 @@ export class Auth<TIdentity extends UserIdentity = UserIdentity> {
|
|
|
897
940
|
return tokens.filter(token => token.isActive());
|
|
898
941
|
}
|
|
899
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Revokes all bearer sessions for an account, including the current device.
|
|
945
|
+
*
|
|
946
|
+
* Apps must authorize the supplied account before calling this method. Tokens
|
|
947
|
+
* are rejected on subsequent requests; already-running requests can finish.
|
|
948
|
+
* Revoked rows are retained for audit. New sign-ins remain possible.
|
|
949
|
+
*
|
|
950
|
+
* @param user - Authorized account whose sessions should end.
|
|
951
|
+
* @returns Number of previously unrevoked token rows changed, including expired rows.
|
|
952
|
+
* @example
|
|
953
|
+
* await app.auth.revokeAllTokens(authenticatedUser);
|
|
954
|
+
*/
|
|
955
|
+
async revokeAllTokens(user: TIdentity): Promise<number> {
|
|
956
|
+
const userId = this.userPrimaryKey(user);
|
|
957
|
+
if (!userId) return 0;
|
|
958
|
+
const count = await this.#revokeTokens(userId, this.db.knex);
|
|
959
|
+
this.#forgetRevokedUser(userId);
|
|
960
|
+
return count;
|
|
961
|
+
}
|
|
962
|
+
|
|
900
963
|
/**
|
|
901
964
|
* Revokes one bearer session owned by a user.
|
|
902
965
|
*
|
|
@@ -947,7 +1010,9 @@ export class Auth<TIdentity extends UserIdentity = UserIdentity> {
|
|
|
947
1010
|
* @returns Request-context cache key.
|
|
948
1011
|
*/
|
|
949
1012
|
private tokenContextKey(tokenHash: string): string {
|
|
950
|
-
|
|
1013
|
+
const prefix = `${AUTH_TOKEN_CONTEXT_KEY_PREFIX}.${this.TokenModel.table}`;
|
|
1014
|
+
const revision = this.requestContext?.get<number>(`${prefix}.revision`) ?? 0;
|
|
1015
|
+
return `${prefix}.${revision}.${tokenHash}`;
|
|
951
1016
|
}
|
|
952
1017
|
|
|
953
1018
|
/**
|
|
@@ -1299,7 +1364,8 @@ export class Auth<TIdentity extends UserIdentity = UserIdentity> {
|
|
|
1299
1364
|
return true;
|
|
1300
1365
|
}
|
|
1301
1366
|
|
|
1302
|
-
const inputUser = await this.
|
|
1367
|
+
const inputUser = await this.IdentityModel.query()
|
|
1368
|
+
.where(identityField, inputIdentity).first();
|
|
1303
1369
|
|
|
1304
1370
|
return inputUser?.get(this.IdentityModel.primaryKey) === user.get(this.IdentityModel.primaryKey);
|
|
1305
1371
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ActiveRecord, type FieldBuilder } from '../db';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Durable failed-password state for any submitted identity, known or unknown.
|
|
6
|
+
*
|
|
7
|
+
* The unique normalized identity digest provides a database-wide serialization
|
|
8
|
+
* point without requiring an account. The normalized submitted email is also
|
|
9
|
+
* retained so operators can investigate attempts against known or unknown users.
|
|
10
|
+
* This table is intentionally independent of users and authentication providers.
|
|
11
|
+
*/
|
|
12
|
+
export class PasswordLoginAttempt extends ActiveRecord {
|
|
13
|
+
static override table = 'auth_password_login_attempts';
|
|
14
|
+
|
|
15
|
+
/** Defines the identity key, failure counter and persistent suspension. */
|
|
16
|
+
static override fields(field: FieldBuilder) {
|
|
17
|
+
return {
|
|
18
|
+
id: field.ulid(),
|
|
19
|
+
identityHash: field.string({ column: 'identity_hash', required: true, unique: true, length: 64, maxLength: 64, comment: 'SHA-256 of the normalized submitted identity; not tied to account existence.' }),
|
|
20
|
+
identity: field.string({ length: 255, maxLength: 255, comment: 'Normalized submitted email or configured identity; null for legacy digest-only rows until their next attempt.' }),
|
|
21
|
+
failedAttempts: field.integer({ column: 'failed_attempts', required: true, unsigned: true, min: 0, default: 0, comment: 'Consecutive wrong passwords for this identity.' }),
|
|
22
|
+
suspendedAt: field.timestamp({ column: 'suspended_at', comment: 'When password recovery became required; no automatic expiry.' }),
|
|
23
|
+
suspensionId: field.string({ column: 'suspension_id', length: 36, maxLength: 36, comment: 'Unique suspension occurrence; prevents delayed recovery notices from referring to a later suspension.' }),
|
|
24
|
+
createdAt: field.timestamp({ column: 'created_at', auto: 'create' }),
|
|
25
|
+
updatedAt: field.timestamp({ column: 'updated_at', auto: 'both', index: true, comment: 'Latest password sign-in or recovery activity, including refused suspended attempts.' }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
declare id: string | null;
|
|
30
|
+
declare identityHash: string;
|
|
31
|
+
/** Readable submitted identity, populated on the next attempt for legacy rows. */
|
|
32
|
+
declare identity: string | null;
|
|
33
|
+
declare failedAttempts: number;
|
|
34
|
+
declare suspendedAt: Date | null;
|
|
35
|
+
/** Unique occurrence identifier, cleared only by verified password recovery. */
|
|
36
|
+
declare suspensionId: string | null;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Derives an exact stable key unaffected by the database's email collation.
|
|
40
|
+
* @param identity - Submitted email or configured password identity.
|
|
41
|
+
* @returns SHA-256 of the trimmed, lowercase identity.
|
|
42
|
+
*/
|
|
43
|
+
static identityKey(identity: string): string {
|
|
44
|
+
return createHash('sha256').update(identity.trim().toLowerCase()).digest('hex');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Serializes one verification or recovery with every operation for this identity.
|
|
49
|
+
*
|
|
50
|
+
* The unique-key upsert handles concurrent first attempts. Locking reads see
|
|
51
|
+
* the latest committed state, including under repeatable-read isolation.
|
|
52
|
+
* Nested use during recovery participates through a transaction savepoint.
|
|
53
|
+
*
|
|
54
|
+
* Records the normalized identity and latest activity even when already suspended.
|
|
55
|
+
* @param identity - Password identity, whether or not registered.
|
|
56
|
+
* @param callback - Operation that may update and save the locked attempt row.
|
|
57
|
+
* @returns Callback result after its state commits.
|
|
58
|
+
*/
|
|
59
|
+
static async withIdentityLock<TResult>(identity: string, callback: (attempt: PasswordLoginAttempt) => Promise<TResult>): Promise<TResult> {
|
|
60
|
+
return this.getDb().transaction(transaction => ActiveRecord.withDb(transaction, async () => {
|
|
61
|
+
const identityHash = this.identityKey(identity);
|
|
62
|
+
const candidate = this.create({ identityHash, identity: identity.trim().toLowerCase() });
|
|
63
|
+
// Framework-level upsert requires Knex; the fields still own every value
|
|
64
|
+
// and column conversion. Updating activity takes an exclusive key lock;
|
|
65
|
+
// INSERT IGNORE would take shared locks and can deadlock on FOR UPDATE.
|
|
66
|
+
await transaction(this.table).insert(await candidate.getDataForDb()).onConflict(this.getFields().identityHash!.column).merge([this.getFields().identity!.column, this.getFields().updatedAt!.column]);
|
|
67
|
+
const attempt = await this.where('identityHash', identityHash).forUpdate().first() as PasswordLoginAttempt | null;
|
|
68
|
+
if (!attempt) throw new Error('Unable to lock password login state.');
|
|
69
|
+
return callback(attempt);
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PasswordSuspension } from './contracts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Password sign-in for the submitted identity requires email recovery.
|
|
5
|
+
* The same error is used for existing and unknown emails, with no account data.
|
|
6
|
+
*/
|
|
7
|
+
export class PasswordSuspendedError extends Error {
|
|
8
|
+
/** Creates a generic refusal, optionally carrying a newly committed transition for server use. */
|
|
9
|
+
constructor(readonly suspension: PasswordSuspension | null = null) {
|
|
10
|
+
super('Too many sign-in attempts. Use password recovery to continue.');
|
|
11
|
+
this.name = 'PasswordSuspendedError';
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/auth/README.md
CHANGED
|
@@ -162,6 +162,81 @@ Password-reset routes should find the user without revealing whether the email
|
|
|
162
162
|
exists, call `createPasswordResetToken(user)`, send the plaintext token once,
|
|
163
163
|
and later redeem it with `resetPassword(...)`.
|
|
164
164
|
|
|
165
|
+
Successful `resetPassword(...)` and `changePasswordProvider(...)` calls replace
|
|
166
|
+
the password, invalidate all outstanding password-reset links, and revoke every
|
|
167
|
+
bearer session in one database transaction. This includes the current device;
|
|
168
|
+
the app should clear its browser credentials and return to sign-in after a
|
|
169
|
+
successful password change. Account-row locks serialize concurrent password
|
|
170
|
+
replacements so two recovery links cannot both succeed. Invalid input or a
|
|
171
|
+
database failure rolls back the complete operation.
|
|
172
|
+
|
|
173
|
+
`setPasswordProvider(...)` is the low-level credential setup operation. Use the
|
|
174
|
+
reset/change methods for account recovery and authenticated password changes.
|
|
175
|
+
|
|
176
|
+
### Failed-password suspension
|
|
177
|
+
|
|
178
|
+
Register `PasswordLoginAttempt` alongside `AuthProvider`, `AuthToken` and
|
|
179
|
+
`PasswordResetToken` in the application's model registry and committed migrations.
|
|
180
|
+
The `auth_password_login_attempts` table keeps one unique normalized-identity
|
|
181
|
+
SHA-256 key, readable normalized `identity` (email), consecutive failure count and
|
|
182
|
+
nullable `suspendedAt`. It has no user foreign key and records attempts for unknown
|
|
183
|
+
emails exactly as for existing ones. `createdAt` records when the identity was
|
|
184
|
+
first seen; `updatedAt` records the latest password sign-in or recovery activity,
|
|
185
|
+
including refused attempts after suspension. Passwords are never recorded here.
|
|
186
|
+
The digest remains the exact unique key independently of SQL text collation;
|
|
187
|
+
it does not conceal the email. Legacy digest-only rows gain their readable
|
|
188
|
+
identity on the next attempt without clearing their failure count or suspension.
|
|
189
|
+
|
|
190
|
+
After 20 failures by default, password sign-in for that email requires recovery.
|
|
191
|
+
There is no timed unlock and a correct password cannot clear suspension. Before
|
|
192
|
+
suspension, successful verification resets consecutive failures. A completed,
|
|
193
|
+
identity-matched password reset clears the state in the same transaction as the
|
|
194
|
+
credential replacement and session revocation. Asking for a reset or submitting
|
|
195
|
+
an invalid/expired reset link does not clear it. Google sign-in remains available.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
password: {
|
|
199
|
+
driver: 'password',
|
|
200
|
+
suspension: { maxFailedAttempts: 20 }, // or suspension: false
|
|
201
|
+
},
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Known and unknown identities receive the same `PasswordSuspendedError` and public
|
|
205
|
+
message. Unknown identities also perform password-hash verification with the
|
|
206
|
+
configured hasher (default-cost scrypt by default) before recording failure.
|
|
207
|
+
No account ID, timestamp, automatic retry deadline or
|
|
208
|
+
account-existence flag belongs in the response. An HTTP adapter can map this error
|
|
209
|
+
to `429 too_many_attempts`, with no `Retry-After` because recovery is required.
|
|
210
|
+
The public message is "Too many sign-in attempts. Use password recovery to continue."
|
|
211
|
+
|
|
212
|
+
Only the error that first commits a suspension carries a `suspension` value:
|
|
213
|
+
`{ attemptId, suspensionId }`. Later blocked requests carry `null`. These opaque
|
|
214
|
+
identifiers exist equally for registered and unknown identities and are for
|
|
215
|
+
server-side work only; serialize the public code/message, not the whole error.
|
|
216
|
+
An application can enqueue one recovery-notice job after catching this transition.
|
|
217
|
+
The worker should find the attempt, confirm its `suspensionId` is still current,
|
|
218
|
+
resolve an exactly matching account and send that owner a secure link from
|
|
219
|
+
`Auth.createPasswordResetToken`. Never email unregistered submitted addresses.
|
|
220
|
+
Templates, queueing and delivery policy remain application-owned. A reset clears
|
|
221
|
+
the occurrence ID; a later suspension receives a new ID, so delayed jobs can be
|
|
222
|
+
discarded without confusing separate suspension periods.
|
|
223
|
+
|
|
224
|
+
Verification, failure counting and suspension share a per-identity SQL row lock.
|
|
225
|
+
Concurrent first attempts use a unique-key upsert, and each verifier sees committed
|
|
226
|
+
state before checking a password. Recovery takes the same lock. An attempt already
|
|
227
|
+
verified before a later suspension is ordered before it; a verifier waiting behind
|
|
228
|
+
the threshold-reaching failure cannot authenticate or clear the suspension.
|
|
229
|
+
|
|
230
|
+
Keep IP-based limits and explicit trusted-proxy configuration: suspension does not
|
|
231
|
+
prevent password spraying across many identities or intentional denial of password
|
|
232
|
+
sign-in for an email. Records are retained without automatic deletion so operators
|
|
233
|
+
can inspect targeted emails and recent activity. This is current suspension state,
|
|
234
|
+
not a per-attempt event history: successful verification resets the counter before
|
|
235
|
+
suspension, and verified recovery clears suspension. Operational logs may supplement
|
|
236
|
+
investigations but do not replace this transactional state. A later manual cleanup
|
|
237
|
+
or retention job should exclude rows with `suspendedAt` set; deleting those rows
|
|
238
|
+
would remove the reset requirement. An email in this table is not proof of an account.
|
|
239
|
+
|
|
165
240
|
## Google Provider
|
|
166
241
|
|
|
167
242
|
The built-in Google driver accepts a Google Identity Services ID token. It
|
|
@@ -336,6 +411,7 @@ List and revoke active sessions through the authenticated account:
|
|
|
336
411
|
const sessions = await app.auth.tokensFor(user);
|
|
337
412
|
await app.auth.revokeToken(user, sessionId);
|
|
338
413
|
await app.auth.revokeCurrentToken();
|
|
414
|
+
await app.auth.revokeAllTokens(user);
|
|
339
415
|
```
|
|
340
416
|
|
|
341
417
|
Revocation sets `revokedAt`; the next bearer-token authentication rejects the
|
|
@@ -343,12 +419,25 @@ session before loading its user. `tokensFor(...)` excludes revoked and expired
|
|
|
343
419
|
sessions. The current authenticated token is available as `app.auth.token`
|
|
344
420
|
inside the request context so an app can mark the current browser session.
|
|
345
421
|
|
|
422
|
+
`revokeAllTokens(user)` implements **Log out all devices**. The app must authorize
|
|
423
|
+
the supplied account first; never accept a user id from the request as authority.
|
|
424
|
+
It revokes all previously unrevoked tokens, including expired rows, and returns
|
|
425
|
+
the number of rows changed. Revoked rows remain available for audit. Authentication
|
|
426
|
+
cached in the calling request is invalidated and the current user is cleared
|
|
427
|
+
when that account is logged out. Other requests already in progress may finish;
|
|
428
|
+
subsequent authentication rejects the old tokens. A new sign-in can create a new
|
|
429
|
+
session. This does not disconnect existing application-owned streams or revoke
|
|
430
|
+
external provider credentials. Show failures and retain browser state if the
|
|
431
|
+
all-devices request fails; clear browser credentials after successful confirmation.
|
|
432
|
+
|
|
346
433
|
## Security Responsibilities
|
|
347
434
|
|
|
348
435
|
The auth package validates credentials and tokens, but the application still
|
|
349
436
|
owns HTTP security controls:
|
|
350
437
|
|
|
351
438
|
- Rate-limit public sign-in, sign-up, provider, forgot-password, and reset routes.
|
|
439
|
+
The password provider's per-identity suspension complements, and does not replace,
|
|
440
|
+
these IP limits.
|
|
352
441
|
- Use generic invalid-credential and forgot-password responses to reduce account
|
|
353
442
|
enumeration.
|
|
354
443
|
- Accept bearer tokens only over HTTPS in production.
|
|
@@ -383,10 +472,11 @@ dedicated test account allowed to create/drop `db3_app_test_*` databases.
|
|
|
383
472
|
It creates a unique database and removes it in `finally`; no email is sent and
|
|
384
473
|
no token is logged. It is not a public HTTP auth endpoint.
|
|
385
474
|
|
|
386
|
-
The lab
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
475
|
+
The lab verifies account-wide logout and automatic revocation after password
|
|
476
|
+
reset. The service's SQL regression tests additionally cover concurrent reset
|
|
477
|
+
redemption, transaction rollback and cross-account isolation. Concurrent
|
|
478
|
+
registration is a separate workflow. The website's Auth page includes the
|
|
479
|
+
exact lab test to copy into an independent application.
|
|
390
480
|
|
|
391
481
|
Run the auth package tests from the repository root:
|
|
392
482
|
|