@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,393 @@
|
|
|
1
|
+
import type { HttpBodyLog } from './contracts';
|
|
2
|
+
|
|
3
|
+
const maximumCapturedProperties = 2_000;
|
|
4
|
+
const maximumCaptureDepth = 12;
|
|
5
|
+
const maximumHeaderValueLength = 4_096;
|
|
6
|
+
const redactedValue = '[Redacted]';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mutable limits used while producing a bounded JSON-safe body value.
|
|
10
|
+
*/
|
|
11
|
+
interface BodySanitizationState {
|
|
12
|
+
/** Objects already visited while guarding against circular structures. */
|
|
13
|
+
seen: WeakSet<object>;
|
|
14
|
+
/** Remaining object properties or array items allowed in the capture. */
|
|
15
|
+
remainingProperties: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Captures one request or response body without retaining unbounded payloads.
|
|
20
|
+
*
|
|
21
|
+
* @param payload - Parsed request body or outgoing Fastify payload.
|
|
22
|
+
* @param contentType - Media type reported by the HTTP message.
|
|
23
|
+
* @param maxBodyBytes - Maximum UTF-8 bytes retained in the log record.
|
|
24
|
+
* @returns Bounded body metadata and an inspectable value when safe.
|
|
25
|
+
*/
|
|
26
|
+
export function captureBody(
|
|
27
|
+
payload: unknown,
|
|
28
|
+
contentType: string | undefined,
|
|
29
|
+
maxBodyBytes: number,
|
|
30
|
+
): HttpBodyLog {
|
|
31
|
+
if (payload === undefined || payload === null || payload === '') {
|
|
32
|
+
return {
|
|
33
|
+
kind: 'empty',
|
|
34
|
+
contentType,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isStreamLike(payload)) {
|
|
39
|
+
return {
|
|
40
|
+
kind: 'stream',
|
|
41
|
+
contentType,
|
|
42
|
+
note: 'Streaming response body omitted.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (Buffer.isBuffer(payload) || payload instanceof Uint8Array) {
|
|
47
|
+
const buffer = Buffer.from(payload);
|
|
48
|
+
|
|
49
|
+
if (!isTextContentType(contentType)) {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'binary',
|
|
52
|
+
contentType,
|
|
53
|
+
sizeBytes: buffer.byteLength,
|
|
54
|
+
note: 'Binary body omitted.',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return captureText(buffer.toString('utf8'), contentType, maxBodyBytes);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (typeof payload === 'string') {
|
|
62
|
+
return captureText(payload, contentType, maxBodyBytes);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return captureJson(payload, contentType, maxBodyBytes);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Captures textual content, parsing JSON so nested secrets can be redacted.
|
|
70
|
+
*
|
|
71
|
+
* @param value - Original textual payload.
|
|
72
|
+
* @param contentType - Media type reported by the HTTP message.
|
|
73
|
+
* @param maxBodyBytes - Maximum UTF-8 bytes retained in the log record.
|
|
74
|
+
* @returns Text or JSON body capture.
|
|
75
|
+
*/
|
|
76
|
+
function captureText(
|
|
77
|
+
value: string,
|
|
78
|
+
contentType: string | undefined,
|
|
79
|
+
maxBodyBytes: number,
|
|
80
|
+
): HttpBodyLog {
|
|
81
|
+
if (isFormContentType(contentType)) {
|
|
82
|
+
return captureJson(formBodyFromText(value), contentType, maxBodyBytes);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (isJsonContentType(contentType) || looksLikeJson(value)) {
|
|
86
|
+
try {
|
|
87
|
+
return captureJson(JSON.parse(value), contentType, maxBodyBytes);
|
|
88
|
+
} catch {
|
|
89
|
+
// Malformed JSON remains useful as bounded response text.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sizeBytes = Buffer.byteLength(value);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
kind: 'text',
|
|
97
|
+
contentType,
|
|
98
|
+
sizeBytes,
|
|
99
|
+
truncated: sizeBytes > maxBodyBytes,
|
|
100
|
+
value: truncateUtf8(value, maxBodyBytes),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Converts URL-encoded form text into a shape that supports field redaction.
|
|
106
|
+
*
|
|
107
|
+
* Repeated form keys remain arrays so the inspector does not silently discard
|
|
108
|
+
* submitted values.
|
|
109
|
+
*
|
|
110
|
+
* @param value - URL-encoded request body.
|
|
111
|
+
* @returns Parsed form object suitable for normal JSON sanitization.
|
|
112
|
+
*/
|
|
113
|
+
function formBodyFromText(value: string): Record<string, string | string[]> {
|
|
114
|
+
const captured: Record<string, string | string[]> = {};
|
|
115
|
+
|
|
116
|
+
for (const [key, item] of new URLSearchParams(value)) {
|
|
117
|
+
const existing = captured[key];
|
|
118
|
+
|
|
119
|
+
if (existing === undefined) {
|
|
120
|
+
captured[key] = item;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
captured[key] = Array.isArray(existing)
|
|
125
|
+
? [...existing, item]
|
|
126
|
+
: [existing, item];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return captured;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Produces a JSON-safe, recursively redacted representation of a payload.
|
|
134
|
+
*
|
|
135
|
+
* @param value - Parsed body value.
|
|
136
|
+
* @param contentType - Media type reported by the HTTP message.
|
|
137
|
+
* @param maxBodyBytes - Maximum UTF-8 bytes retained in the log record.
|
|
138
|
+
* @returns JSON body capture.
|
|
139
|
+
*/
|
|
140
|
+
function captureJson(
|
|
141
|
+
value: unknown,
|
|
142
|
+
contentType: string | undefined,
|
|
143
|
+
maxBodyBytes: number,
|
|
144
|
+
): HttpBodyLog {
|
|
145
|
+
const sanitized = sanitizeBodyValue(value, '', {
|
|
146
|
+
seen: new WeakSet<object>(),
|
|
147
|
+
remainingProperties: maximumCapturedProperties,
|
|
148
|
+
});
|
|
149
|
+
const serialized = JSON.stringify(sanitized);
|
|
150
|
+
|
|
151
|
+
if (serialized === undefined) {
|
|
152
|
+
return captureText(String(value), contentType, maxBodyBytes);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sizeBytes = Buffer.byteLength(serialized);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
kind: 'json',
|
|
159
|
+
contentType,
|
|
160
|
+
sizeBytes,
|
|
161
|
+
truncated: sizeBytes > maxBodyBytes,
|
|
162
|
+
value: sizeBytes > maxBodyBytes
|
|
163
|
+
? truncateUtf8(serialized, maxBodyBytes)
|
|
164
|
+
: sanitized,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Recursively removes sensitive fields and bounds complex object traversal.
|
|
170
|
+
*
|
|
171
|
+
* @param value - Current body value.
|
|
172
|
+
* @param key - Owning object key used for sensitive-name detection.
|
|
173
|
+
* @param state - Shared circular-reference and property-count state.
|
|
174
|
+
* @param depth - Current traversal depth.
|
|
175
|
+
* @returns JSON-safe value suitable for structured logging.
|
|
176
|
+
*/
|
|
177
|
+
function sanitizeBodyValue(
|
|
178
|
+
value: unknown,
|
|
179
|
+
key: string,
|
|
180
|
+
state: BodySanitizationState,
|
|
181
|
+
depth = 0,
|
|
182
|
+
): unknown {
|
|
183
|
+
if (isSensitiveName(key)) return redactedValue;
|
|
184
|
+
if (value === null || value === undefined) return value;
|
|
185
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
if (typeof value === 'bigint') return value.toString();
|
|
189
|
+
if (typeof value !== 'object') return String(value);
|
|
190
|
+
if (state.seen.has(value)) return '[Circular]';
|
|
191
|
+
if (depth >= maximumCaptureDepth) return '[Maximum depth reached]';
|
|
192
|
+
|
|
193
|
+
state.seen.add(value);
|
|
194
|
+
|
|
195
|
+
if (value instanceof Date) return value.toISOString();
|
|
196
|
+
if (value instanceof Error) {
|
|
197
|
+
return {
|
|
198
|
+
name: value.name,
|
|
199
|
+
message: value.message,
|
|
200
|
+
stack: value.stack,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
204
|
+
return `[Binary ${value.byteLength} bytes]`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
const captured: unknown[] = [];
|
|
209
|
+
|
|
210
|
+
for (const item of value) {
|
|
211
|
+
if (state.remainingProperties <= 0) {
|
|
212
|
+
captured.push('[Capture limit reached]');
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
state.remainingProperties -= 1;
|
|
217
|
+
captured.push(sanitizeBodyValue(item, '', state, depth + 1));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return captured;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const captured: Record<string, unknown> = {};
|
|
224
|
+
|
|
225
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
226
|
+
if (state.remainingProperties <= 0) {
|
|
227
|
+
captured['[capture]'] = 'Property limit reached';
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
state.remainingProperties -= 1;
|
|
232
|
+
captured[childKey] = sanitizeBodyValue(childValue, childKey, state, depth + 1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return captured;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Redacts credential-bearing headers before a record reaches Pino.
|
|
240
|
+
*
|
|
241
|
+
* @param headers - Fastify request or response header collection.
|
|
242
|
+
* @returns Header object safe for local request inspection.
|
|
243
|
+
*/
|
|
244
|
+
export function redactHeaders(
|
|
245
|
+
headers: Readonly<Record<string, unknown>>,
|
|
246
|
+
): Record<string, unknown> {
|
|
247
|
+
const captured: Record<string, unknown> = {};
|
|
248
|
+
|
|
249
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
250
|
+
captured[name] = isSensitiveName(name)
|
|
251
|
+
? redactedValue
|
|
252
|
+
: boundedHeaderValue(value);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return captured;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Bounds one response or request header value.
|
|
260
|
+
*
|
|
261
|
+
* @param value - Original Fastify header value.
|
|
262
|
+
* @returns Original scalar shape with long strings shortened.
|
|
263
|
+
*/
|
|
264
|
+
function boundedHeaderValue(value: unknown): unknown {
|
|
265
|
+
if (typeof value === 'string') {
|
|
266
|
+
return value.length > maximumHeaderValueLength
|
|
267
|
+
? `${value.slice(0, maximumHeaderValueLength)}…`
|
|
268
|
+
: value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (Array.isArray(value)) {
|
|
272
|
+
return value.map(item => boundedHeaderValue(item));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Detects common credential and session field names.
|
|
280
|
+
*
|
|
281
|
+
* @param name - Header or JSON property name.
|
|
282
|
+
* @returns True when the value must be removed from the capture.
|
|
283
|
+
*/
|
|
284
|
+
function isSensitiveName(name: string): boolean {
|
|
285
|
+
const normalized = name.toLowerCase().replace(/[-_]/g, '');
|
|
286
|
+
|
|
287
|
+
return normalized === 'authorization'
|
|
288
|
+
|| normalized === 'proxyauthorization'
|
|
289
|
+
|| normalized === 'cookie'
|
|
290
|
+
|| normalized === 'setcookie'
|
|
291
|
+
|| normalized === 'password'
|
|
292
|
+
|| normalized === 'passphrase'
|
|
293
|
+
|| normalized === 'secret'
|
|
294
|
+
|| normalized === 'token'
|
|
295
|
+
|| normalized === 'accesstoken'
|
|
296
|
+
|| normalized === 'refreshtoken'
|
|
297
|
+
|| normalized === 'apikey'
|
|
298
|
+
|| normalized === 'xapikey';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Converts a Fastify header into one displayable content-type string.
|
|
303
|
+
*
|
|
304
|
+
* @param value - Request or response content-type header.
|
|
305
|
+
* @returns First string value when present.
|
|
306
|
+
*/
|
|
307
|
+
export function headerValue(value: unknown): string | undefined {
|
|
308
|
+
if (typeof value === 'string') return value;
|
|
309
|
+
if (Array.isArray(value) && typeof value[0] === 'string') return value[0];
|
|
310
|
+
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Identifies payloads that Fastify will stream instead of serialize.
|
|
316
|
+
*
|
|
317
|
+
* @param value - Candidate outgoing response payload.
|
|
318
|
+
* @returns True when the value exposes the Node stream pipe contract.
|
|
319
|
+
*/
|
|
320
|
+
function isStreamLike(value: unknown): boolean {
|
|
321
|
+
return typeof value === 'object'
|
|
322
|
+
&& value !== null
|
|
323
|
+
&& 'pipe' in value
|
|
324
|
+
&& typeof value.pipe === 'function';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Identifies media types that are safe to decode as UTF-8 text.
|
|
329
|
+
*
|
|
330
|
+
* @param contentType - HTTP content type.
|
|
331
|
+
* @returns True for JSON and common textual media types.
|
|
332
|
+
*/
|
|
333
|
+
function isTextContentType(contentType: string | undefined): boolean {
|
|
334
|
+
const normalized = (contentType || '').toLowerCase();
|
|
335
|
+
|
|
336
|
+
return normalized.startsWith('text/')
|
|
337
|
+
|| normalized.includes('json')
|
|
338
|
+
|| normalized.includes('xml')
|
|
339
|
+
|| normalized.includes('javascript')
|
|
340
|
+
|| normalized.includes('x-www-form-urlencoded')
|
|
341
|
+
|| normalized.includes('svg');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Identifies JSON media types.
|
|
346
|
+
*
|
|
347
|
+
* @param contentType - HTTP content type.
|
|
348
|
+
* @returns True when the media type uses JSON.
|
|
349
|
+
*/
|
|
350
|
+
function isJsonContentType(contentType: string | undefined): boolean {
|
|
351
|
+
return (contentType || '').toLowerCase().includes('json');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Identifies URL-encoded form media types requiring field-level redaction.
|
|
356
|
+
*
|
|
357
|
+
* @param contentType - HTTP content type.
|
|
358
|
+
* @returns True when the body uses URL-encoded form syntax.
|
|
359
|
+
*/
|
|
360
|
+
function isFormContentType(contentType: string | undefined): boolean {
|
|
361
|
+
return (contentType || '').toLowerCase().includes('x-www-form-urlencoded');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Detects likely JSON text when a content type was omitted or incorrect.
|
|
366
|
+
*
|
|
367
|
+
* @param value - Textual payload.
|
|
368
|
+
* @returns True when the trimmed body begins with a JSON container.
|
|
369
|
+
*/
|
|
370
|
+
function looksLikeJson(value: string): boolean {
|
|
371
|
+
const trimmed = value.trim();
|
|
372
|
+
|
|
373
|
+
return (trimmed.startsWith('{') && trimmed.endsWith('}'))
|
|
374
|
+
|| (trimmed.startsWith('[') && trimmed.endsWith(']'));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Truncates text by UTF-8 byte length rather than JavaScript character count.
|
|
379
|
+
*
|
|
380
|
+
* @param value - Original text.
|
|
381
|
+
* @param maxBytes - Maximum bytes to retain.
|
|
382
|
+
* @returns Original or shortened UTF-8 text.
|
|
383
|
+
*/
|
|
384
|
+
function truncateUtf8(value: string, maxBytes: number): string {
|
|
385
|
+
const buffer = Buffer.from(value);
|
|
386
|
+
|
|
387
|
+
if (buffer.byteLength <= maxBytes) return value;
|
|
388
|
+
|
|
389
|
+
const marker = '…';
|
|
390
|
+
const contentBytes = Math.max(0, maxBytes - Buffer.byteLength(marker));
|
|
391
|
+
|
|
392
|
+
return `${buffer.subarray(0, contentBytes).toString('utf8')}…`;
|
|
393
|
+
}
|
package/src/logging/index.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Redacts common credential forms while preserving exception wording and stack locations. */
|
|
2
|
+
export function redactLogEmail(text: string): string {
|
|
3
|
+
return text
|
|
4
|
+
.replace(/\b(?:Bearer|Basic)\s+[^\s"',;]+/gi, '[REDACTED authorization]')
|
|
5
|
+
.replace(/\b(?:sk-|sk_|rk_|ghp_|github_pat_)[A-Za-z0-9_-]+/g, '[REDACTED key]')
|
|
6
|
+
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, '[REDACTED token]')
|
|
7
|
+
.replace(/(["']?(?:password|passwd|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|authorization|token|secret)["']?\s*[:=]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\s,;]+)/gi, '$1[REDACTED]')
|
|
8
|
+
.replace(/((?:set-cookie|cookie)\s*:\s*)[^\r\n]+/gi, '$1[REDACTED]')
|
|
9
|
+
.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"']+/gi, url => url
|
|
10
|
+
.replace(/(\/\/)[^/@\s]+@/, '$1[REDACTED]@')
|
|
11
|
+
.replace(/[?#].*$/, '?[REDACTED parameters]'));
|
|
12
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import { isInteractiveDevelopmentEnvironment } from '../devtools';
|
|
3
|
+
import { captureBody, headerValue, redactHeaders } from './httpBodyCapture';
|
|
4
|
+
import type * as logging from './contracts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Captures bounded, redacted payloads for grouped console logs and devtools.
|
|
8
|
+
* Register before routes on a Fastify host using app.log.logger. Fastify's own
|
|
9
|
+
* arrival/completion records provide lifecycle timing; streams are never consumed.
|
|
10
|
+
* @param server - Caller-owned HTTP server.
|
|
11
|
+
* @param options - Development capture and private-route exclusion policy.
|
|
12
|
+
*/
|
|
13
|
+
export function registerHttpExchangeMonitor(server: FastifyInstance, options: logging.HttpExchangeMonitorOptions = {}): void {
|
|
14
|
+
if (options.enabled === false || !isInteractiveDevelopmentEnvironment(options.environment ?? process.env.NODE_ENV)) return;
|
|
15
|
+
const configured = options.maxBodyBytes;
|
|
16
|
+
const maxBodyBytes = configured && Number.isFinite(configured) && configured > 0 ? Math.min(65536, Math.floor(configured)) : 16384;
|
|
17
|
+
const ended = new WeakSet<FastifyRequest>();
|
|
18
|
+
|
|
19
|
+
/** Reports one terminal interruption even if timeout and socket close both fire. */
|
|
20
|
+
function interrupted(request: FastifyRequest, message: string): void {
|
|
21
|
+
if (ended.has(request)) return;
|
|
22
|
+
ended.add(request);
|
|
23
|
+
request.log.warn(message);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
server.addHook('onRequest', (request, reply, done) => {
|
|
27
|
+
reply.raw.once('close', () => {
|
|
28
|
+
if (!reply.raw.writableFinished) interrupted(request, 'request aborted');
|
|
29
|
+
});
|
|
30
|
+
done();
|
|
31
|
+
});
|
|
32
|
+
server.addHook('onTimeout', (request, _reply, done) => { interrupted(request, 'request timed out'); done(); });
|
|
33
|
+
server.addHook('onRequestAbort', (request, done) => { interrupted(request, 'request aborted'); done(); });
|
|
34
|
+
server.addHook('onResponse', (request, _reply, done) => { ended.add(request); done(); });
|
|
35
|
+
server.addHook('onSend', (request, reply, payload, done) => {
|
|
36
|
+
if (!options.exclude?.(request)) {
|
|
37
|
+
const httpExchange: logging.HttpExchangeLog = {
|
|
38
|
+
request: { method: request.method, url: request.url, headers: redactHeaders(request.headers), body: captureBody(request.body, headerValue(request.headers['content-type']), maxBodyBytes) },
|
|
39
|
+
response: { statusCode: reply.statusCode, headers: redactHeaders(reply.getHeaders()), body: captureBody(payload, headerValue(reply.getHeader('content-type')), maxBodyBytes) },
|
|
40
|
+
};
|
|
41
|
+
request.log.info({ httpExchange }, 'request exchange');
|
|
42
|
+
}
|
|
43
|
+
done(null, payload);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { Mail } from '../../mail';
|
|
3
|
+
import type { EmailLogOptions } from '../contracts';
|
|
4
|
+
import { formatLogEmail } from '../formatLogEmail';
|
|
5
|
+
|
|
6
|
+
const deliveringEmail = new AsyncLocalStorage<boolean>();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sends each serialized error/fatal log through Mail once, in arrival order.
|
|
10
|
+
* Pending sends live in memory; flush/close drains them. Provider failures go to
|
|
11
|
+
* stderr without recursive logging, retry, cooldown, grouping or suppression of later records.
|
|
12
|
+
*/
|
|
13
|
+
export class EmailLogTransport {
|
|
14
|
+
#pending: Promise<void> = Promise.resolve();
|
|
15
|
+
|
|
16
|
+
/** Resolves Mail lazily so constructing an application logger does not initialize mail providers. */
|
|
17
|
+
constructor(readonly options: EmailLogOptions, readonly mail: () => Mail) {}
|
|
18
|
+
|
|
19
|
+
/** Accepts Pino JSON lines after serialization and configured field redaction. */
|
|
20
|
+
write(line: string): void {
|
|
21
|
+
if (deliveringEmail.getStore()) return;
|
|
22
|
+
try {
|
|
23
|
+
const record = JSON.parse(line) as Record<string, unknown>;
|
|
24
|
+
const message = formatLogEmail(record, this.options);
|
|
25
|
+
this.#pending = this.#pending.then(() => deliveringEmail.run(true, async () => {
|
|
26
|
+
try {
|
|
27
|
+
await this.mail().send(message);
|
|
28
|
+
} catch {
|
|
29
|
+
// Never include provider errors here: they can contain credentials or request bodies.
|
|
30
|
+
process.stderr.write('[logging] Error email delivery failed; original record remains in configured log destinations.\n');
|
|
31
|
+
}
|
|
32
|
+
}));
|
|
33
|
+
} catch {
|
|
34
|
+
process.stderr.write('[logging] Error email formatting failed; original record remains in configured log destinations.\n');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Waits for all accepted sends, including records appended while a flush is in progress. */
|
|
39
|
+
async flush(): Promise<void> {
|
|
40
|
+
let pending: Promise<void>;
|
|
41
|
+
do {
|
|
42
|
+
pending = this.#pending;
|
|
43
|
+
await pending;
|
|
44
|
+
} while (pending !== this.#pending);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/mail/Mail.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface MailMessage {
|
|
|
26
26
|
html?: string;
|
|
27
27
|
/** Extra message headers; File and Resend preserve them, Mailgun currently does not. */
|
|
28
28
|
headers?: Record<string, string>;
|
|
29
|
+
/** Stable request identity for provider retries. Resend deduplicates identical requests for 24 hours; other built-in transports ignore this value. */
|
|
30
|
+
idempotencyKey?: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** Transport input with a default sender resolved and recipients normalized to an array. */
|
|
@@ -83,6 +85,9 @@ export class Mail {
|
|
|
83
85
|
|
|
84
86
|
/** Resolves the sender and recipient list without changing application content. */
|
|
85
87
|
private resolveMessage(message: MailMessage): ResolvedMailMessage {
|
|
88
|
+
if (message.idempotencyKey !== undefined && !/^[\x21-\x7e]{1,256}$/.test(message.idempotencyKey)) {
|
|
89
|
+
throw new Error('Mail idempotencyKey must contain 1 to 256 printable non-space ASCII characters.');
|
|
90
|
+
}
|
|
86
91
|
const to = Array.isArray(message.to) ? message.to : [message.to];
|
|
87
92
|
|
|
88
93
|
if (to.length === 0) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Structured provider refusal; consumers decide retry policy without parsing diagnostic messages. */
|
|
2
|
+
export class MailDeliveryError extends Error {
|
|
3
|
+
/** Creates a refusal retaining the provider code, HTTP status and optional validated retry time. */
|
|
4
|
+
constructor(message: string, readonly provider: string, readonly status: number, readonly code: string | null, readonly retryAt: Date | null = null) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'MailDeliveryError';
|
|
7
|
+
}
|
|
8
|
+
}
|
package/src/mail/README.md
CHANGED
|
@@ -177,7 +177,7 @@ The framework does not retry a failed send or persist an outbox. Dispatch a
|
|
|
177
177
|
dedicated queued job when the application needs retry policy, idempotency, or
|
|
178
178
|
delivery that survives process shutdown.
|
|
179
179
|
|
|
180
|
-
Queue retry alone does not prevent duplicate email. A timeout can follow provider acceptance.
|
|
180
|
+
Queue retry alone does not prevent duplicate email. A timeout can follow provider acceptance. Set `MailMessage.idempotencyKey` to a stable request identity when using Resend. The transport forwards it as the API `Idempotency-Key` header, separately from email headers. Resend deduplicates identical requests for 24 hours; applications must preserve the exact payload and manage retries within that window. Keys accept 1–256 printable non-space ASCII characters. File and Mailgun transports ignore this field. Mail does not persist an outbox or promise exactly-once delivery across all transports. See [Resend idempotency](https://resend.com/docs/dashboard/emails/idempotency-keys).
|
|
181
181
|
|
|
182
182
|
`accepted` means transport/provider acceptance, not inbox delivery. The built-ins have no attachments, CC/BCC, delivery tracking, scheduled sending or explicit request timeout. File and Resend preserve custom `headers`; Mailgun currently does not forward them. `App` has no built-in `mail` getter, and Mail has no close method. Own any resources introduced by your custom transport.
|
|
183
183
|
|
|
@@ -204,3 +204,13 @@ External mail APIs are controlled through the injectable transport or a stubbed
|
|
|
204
204
|
in [`Mail.ts`](./Mail.ts) and [`transports/`](./transports). Behavioural tests
|
|
205
205
|
live at `packages/app/src/mail/tests/` in the source repository and are not
|
|
206
206
|
included in the installed runtime package.
|
|
207
|
+
|
|
208
|
+
Resend and Mailgun HTTP submissions now reject redirects and have a 15-second
|
|
209
|
+
request timeout. A timeout may follow provider acceptance, so callers must still
|
|
210
|
+
use stable idempotency identities and tolerate at-least-once delivery.
|
|
211
|
+
|
|
212
|
+
Resend refusals throw the exported `MailDeliveryError`, retaining `provider`,
|
|
213
|
+
HTTP `status`, a bounded machine-readable `code`, and an optional validated
|
|
214
|
+
`retryAt` parsed from `Retry-After` seconds or an HTTP date. Missing, malformed or
|
|
215
|
+
past timing is `null`. `Mail` submits once and leaves retry policy to consumers;
|
|
216
|
+
error messages remain diagnostic content rather than classification signals.
|
package/src/mail/index.ts
CHANGED
|
@@ -57,6 +57,8 @@ export class MailgunTransport implements MailTransport {
|
|
|
57
57
|
|
|
58
58
|
const response = await fetch(`${this.baseUrl}/${this.domain}/messages`, {
|
|
59
59
|
method: 'POST',
|
|
60
|
+
signal: AbortSignal.timeout(15000),
|
|
61
|
+
redirect: 'error',
|
|
60
62
|
headers: {
|
|
61
63
|
authorization: `Basic ${Buffer.from(`api:${this.apiKey}`).toString('base64')}`,
|
|
62
64
|
},
|
|
@@ -1,35 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
mailAddressEmail,
|
|
4
|
-
type MailDelivery,
|
|
5
|
-
type MailTransport,
|
|
6
|
-
type ResolvedMailMessage,
|
|
7
|
-
} from '../Mail.js';
|
|
1
|
+
import { MailDeliveryError } from '../MailDeliveryError.js';
|
|
2
|
+
import { formatMailAddress, mailAddressEmail, type MailDelivery, type MailTransport, type ResolvedMailMessage } from '../Mail.js';
|
|
8
3
|
|
|
9
4
|
export interface ResendTransportOptions {
|
|
10
5
|
apiKey: string;
|
|
11
6
|
baseUrl?: string;
|
|
12
7
|
}
|
|
13
8
|
|
|
14
|
-
class ResendTransportError extends Error {
|
|
15
|
-
constructor(message: string) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = 'ResendTransportError';
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
9
|
/**
|
|
22
10
|
* Resend API transport.
|
|
23
11
|
*/
|
|
24
12
|
export class ResendTransport implements MailTransport {
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
readonly #apiKey: string;
|
|
14
|
+
readonly #baseUrl: string;
|
|
27
15
|
|
|
16
|
+
/** Creates a bounded Resend transport; credentials remain private to this instance. */
|
|
28
17
|
constructor(options: ResendTransportOptions) {
|
|
29
|
-
this
|
|
30
|
-
this
|
|
18
|
+
this.#apiKey = options.apiKey;
|
|
19
|
+
this.#baseUrl = (options.baseUrl || 'https://api.resend.com').replace(/\/+$/, '');
|
|
31
20
|
}
|
|
32
21
|
|
|
22
|
+
/** Sends one message, forwarding a stable retry identity as an API header rather than an email header. */
|
|
33
23
|
async send(message: ResolvedMailMessage): Promise<MailDelivery> {
|
|
34
24
|
const payload: Record<string, unknown> = {
|
|
35
25
|
from: formatMailAddress(message.from),
|
|
@@ -49,17 +39,21 @@ export class ResendTransport implements MailTransport {
|
|
|
49
39
|
payload.headers = message.headers;
|
|
50
40
|
}
|
|
51
41
|
|
|
52
|
-
const response = await fetch(`${this
|
|
42
|
+
const response = await fetch(`${this.#baseUrl}/emails`, {
|
|
53
43
|
method: 'POST',
|
|
44
|
+
signal: AbortSignal.timeout(15000),
|
|
45
|
+
redirect: 'error',
|
|
54
46
|
headers: {
|
|
55
|
-
authorization: `Bearer ${this
|
|
47
|
+
authorization: `Bearer ${this.#apiKey}`,
|
|
56
48
|
'content-type': 'application/json',
|
|
49
|
+
...(message.idempotencyKey ? { 'Idempotency-Key': message.idempotencyKey } : {}),
|
|
57
50
|
},
|
|
58
51
|
body: JSON.stringify(payload),
|
|
59
52
|
});
|
|
60
53
|
const result = await response.json().catch(() => null) as {
|
|
61
54
|
id?: unknown;
|
|
62
55
|
message?: unknown;
|
|
56
|
+
name?: unknown;
|
|
63
57
|
} | null;
|
|
64
58
|
|
|
65
59
|
if (!response.ok) {
|
|
@@ -67,7 +61,8 @@ export class ResendTransport implements MailTransport {
|
|
|
67
61
|
? result.message
|
|
68
62
|
: response.statusText;
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
const code = typeof result?.name === 'string' && /^[a-z_]{1,100}$/.test(result.name) ? result.name : null;
|
|
65
|
+
throw new MailDeliveryError(`Resend rejected the message: ${detail}`, 'resend', response.status, code, retryTime(response.headers.get('retry-after')));
|
|
71
66
|
}
|
|
72
67
|
|
|
73
68
|
return {
|
|
@@ -77,4 +72,11 @@ export class ResendTransport implements MailTransport {
|
|
|
77
72
|
rejected: [],
|
|
78
73
|
};
|
|
79
74
|
}
|
|
80
|
-
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Parses standard Retry-After seconds or HTTP dates, ignoring malformed, overflowing and past values. */
|
|
78
|
+
function retryTime(value: string | null): Date | null {
|
|
79
|
+
if (!value) return null;
|
|
80
|
+
const milliseconds = /^\d+$/.test(value) ? Date.now() + Number(value) * 1000 : Date.parse(value);
|
|
81
|
+
return Number.isFinite(milliseconds) && milliseconds > Date.now() && milliseconds <= 8640000000000000 ? new Date(milliseconds) : null;
|
|
82
|
+
}
|