@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.
Files changed (653) hide show
  1. package/README.md +75 -17
  2. package/agent-instructions.md +76 -0
  3. package/dist/ai/AIErrors.d.ts +7 -1
  4. package/dist/ai/AIErrors.js +8 -1
  5. package/dist/ai/AIFailover.d.ts +10 -0
  6. package/dist/ai/AIFailover.js +3 -2
  7. package/dist/ai/AIProviderAdmission.d.ts +101 -0
  8. package/dist/ai/AIProviderAdmission.js +336 -0
  9. package/dist/ai/AIRateLimiter.d.ts +11 -2
  10. package/dist/ai/AIRateLimiter.js +125 -10
  11. package/dist/ai/AIRequestTrackingError.d.ts +22 -0
  12. package/dist/ai/AIRequestTrackingError.js +33 -0
  13. package/dist/ai/Agent.d.ts +7 -7
  14. package/dist/ai/Agent.js +84 -56
  15. package/dist/ai/Ai.d.ts +11 -3
  16. package/dist/ai/Ai.js +94 -44
  17. package/dist/ai/OpenAIProviderError.js +1 -1
  18. package/dist/ai/ProviderSseObserver.d.ts +19 -0
  19. package/dist/ai/ProviderSseObserver.js +98 -0
  20. package/dist/ai/aiServiceTier.d.ts +5 -0
  21. package/dist/ai/aiServiceTier.js +14 -0
  22. package/dist/ai/chunkEmbeddingText.d.ts +14 -0
  23. package/dist/ai/chunkEmbeddingText.js +147 -0
  24. package/dist/ai/contracts/AI.d.ts +16 -0
  25. package/dist/ai/contracts/AIRequestErrorDiagnostics.d.ts +12 -0
  26. package/dist/ai/contracts/AIRequestErrorDiagnostics.js +1 -0
  27. package/dist/ai/contracts/AIServiceTier.d.ts +2 -0
  28. package/dist/ai/contracts/AIServiceTier.js +1 -0
  29. package/dist/ai/contracts/Agent.d.ts +3 -0
  30. package/dist/ai/contracts/EmbeddingTextChunk.d.ts +25 -0
  31. package/dist/ai/contracts/EmbeddingTextChunk.js +1 -0
  32. package/dist/ai/contracts/Pricing.d.ts +7 -1
  33. package/dist/ai/contracts/Providers.d.ts +2 -0
  34. package/dist/ai/contracts/index.d.ts +2 -0
  35. package/dist/ai/contracts/index.js +1 -0
  36. package/dist/ai/index.d.ts +8 -2
  37. package/dist/ai/index.js +5 -2
  38. package/dist/ai/{Models.d.ts → modelPricing.d.ts} +2 -2
  39. package/dist/ai/{Models.js → modelPricing.js} +38 -4
  40. package/dist/apps/AppInstallation.d.ts +21 -0
  41. package/dist/apps/AppInstallation.js +16 -0
  42. package/dist/apps/AppRouteError.d.ts +6 -0
  43. package/dist/apps/AppRouteError.js +10 -0
  44. package/dist/apps/AppService.d.ts +24 -0
  45. package/dist/apps/AppService.js +21 -0
  46. package/dist/apps/Apps.d.ts +59 -0
  47. package/dist/apps/Apps.js +566 -0
  48. package/dist/apps/appsPlugin.d.ts +3 -0
  49. package/dist/apps/appsPlugin.js +36 -0
  50. package/dist/apps/commands/disableApp.d.ts +2 -0
  51. package/dist/apps/commands/disableApp.js +5 -0
  52. package/dist/apps/commands/enableApp.d.ts +2 -0
  53. package/dist/apps/commands/enableApp.js +5 -0
  54. package/dist/apps/commands/index.d.ts +10 -0
  55. package/dist/apps/commands/index.js +27 -0
  56. package/dist/apps/commands/installApp.d.ts +2 -0
  57. package/dist/apps/commands/installApp.js +5 -0
  58. package/dist/apps/commands/listApps.d.ts +2 -0
  59. package/dist/apps/commands/listApps.js +5 -0
  60. package/dist/apps/commands/makeAppMigration.d.ts +2 -0
  61. package/dist/apps/commands/makeAppMigration.js +9 -0
  62. package/dist/apps/commands/rollbackApp.d.ts +2 -0
  63. package/dist/apps/commands/rollbackApp.js +5 -0
  64. package/dist/apps/commands/uninstallApp.d.ts +2 -0
  65. package/dist/apps/commands/uninstallApp.js +5 -0
  66. package/dist/apps/contracts/AppAction.d.ts +2 -0
  67. package/dist/apps/contracts/AppAction.js +1 -0
  68. package/dist/apps/contracts/AppDefinition.d.ts +45 -0
  69. package/dist/apps/contracts/AppDefinition.js +1 -0
  70. package/dist/apps/contracts/AppDescription.d.ts +32 -0
  71. package/dist/apps/contracts/AppDescription.js +1 -0
  72. package/dist/apps/contracts/AppManifest.d.ts +30 -0
  73. package/dist/apps/contracts/AppManifest.js +1 -0
  74. package/dist/apps/contracts/AppNavigation.d.ts +9 -0
  75. package/dist/apps/contracts/AppNavigation.js +1 -0
  76. package/dist/apps/contracts/AppNavigationBadge.d.ts +6 -0
  77. package/dist/apps/contracts/AppNavigationBadge.js +1 -0
  78. package/dist/apps/contracts/AppNavigationContext.d.ts +8 -0
  79. package/dist/apps/contracts/AppNavigationContext.js +1 -0
  80. package/dist/apps/contracts/AppNavigationResult.d.ts +12 -0
  81. package/dist/apps/contracts/AppNavigationResult.js +1 -0
  82. package/dist/apps/contracts/AppRoute.d.ts +19 -0
  83. package/dist/apps/contracts/AppRoute.js +1 -0
  84. package/dist/apps/contracts/DiscoveredApp.d.ts +10 -0
  85. package/dist/apps/contracts/DiscoveredApp.js +1 -0
  86. package/dist/apps/contracts/InstalledApps.d.ts +3 -0
  87. package/dist/apps/contracts/InstalledApps.js +1 -0
  88. package/dist/apps/contracts/index.d.ts +11 -0
  89. package/dist/apps/contracts/index.js +1 -0
  90. package/dist/apps/defineApp.d.ts +7 -0
  91. package/dist/apps/defineApp.js +8 -0
  92. package/dist/apps/discoverApps.d.ts +3 -0
  93. package/dist/apps/discoverApps.js +62 -0
  94. package/dist/apps/fastify.d.ts +1 -0
  95. package/dist/apps/fastify.js +1 -0
  96. package/dist/apps/index.d.ts +7 -0
  97. package/dist/apps/index.js +7 -0
  98. package/dist/apps/normalizeAppNavigation.d.ts +3 -0
  99. package/dist/apps/normalizeAppNavigation.js +30 -0
  100. package/dist/apps/registerAppRoutes.d.ts +6 -0
  101. package/dist/apps/registerAppRoutes.js +52 -0
  102. package/dist/apps/validateAppDefinitions.d.ts +3 -0
  103. package/dist/apps/validateAppDefinitions.js +65 -0
  104. package/dist/apps/vite.d.ts +1 -0
  105. package/dist/apps/vite.js +1 -0
  106. package/dist/apps/writeAppTypes.d.ts +2 -0
  107. package/dist/apps/writeAppTypes.js +13 -0
  108. package/dist/auth/Auth.d.ts +18 -0
  109. package/dist/auth/Auth.js +89 -13
  110. package/dist/auth/PasswordLoginAttempt.d.ts +50 -0
  111. package/dist/auth/PasswordLoginAttempt.js +60 -0
  112. package/dist/auth/PasswordSuspendedError.d.ts +10 -0
  113. package/dist/auth/PasswordSuspendedError.js +13 -0
  114. package/dist/auth/contracts/PasswordAuthProviderOptions.d.ts +26 -0
  115. package/dist/auth/contracts/PasswordAuthProviderOptions.js +1 -0
  116. package/dist/auth/contracts/PasswordSuspension.d.ts +7 -0
  117. package/dist/auth/contracts/PasswordSuspension.js +1 -0
  118. package/dist/auth/contracts/PasswordSuspensionOptions.d.ts +10 -0
  119. package/dist/auth/contracts/PasswordSuspensionOptions.js +1 -0
  120. package/dist/auth/contracts/index.d.ts +3 -0
  121. package/dist/auth/contracts/index.js +3 -0
  122. package/dist/auth/index.d.ts +2 -0
  123. package/dist/auth/index.js +2 -0
  124. package/dist/auth/providers/PasswordAuthProvider.d.ts +19 -23
  125. package/dist/auth/providers/PasswordAuthProvider.js +96 -8
  126. package/dist/auth/providers/resolveAuthProviders.js +1 -0
  127. package/dist/cli/Cli.d.ts +2 -0
  128. package/dist/cli/Cli.js +8 -0
  129. package/dist/cli/commands/repl.js +2 -1
  130. package/dist/cli/contracts/CliCommand.d.ts +2 -0
  131. package/dist/cli/contracts/CliCommandInfo.d.ts +12 -0
  132. package/dist/cli/contracts/CliCommandInfo.js +1 -0
  133. package/dist/cli/contracts/index.d.ts +1 -0
  134. package/dist/cli/index.d.ts +1 -0
  135. package/dist/cli/index.js +1 -0
  136. package/dist/cli/listDb3Commands.d.ts +8 -0
  137. package/dist/cli/listDb3Commands.js +10 -0
  138. package/dist/cli/runDb3.d.ts +3 -6
  139. package/dist/cli/runDb3.js +5 -59
  140. package/dist/cli/withDb3Cli.d.ts +4 -0
  141. package/dist/cli/withDb3Cli.js +60 -0
  142. package/dist/db/ActiveQueryBuilder.d.ts +8 -0
  143. package/dist/db/ActiveQueryBuilder.js +11 -0
  144. package/dist/db/ActiveRecord.d.ts +6 -0
  145. package/dist/db/ActiveRecord.js +8 -0
  146. package/dist/db/connection.js +14 -2
  147. package/dist/db/fields/TimestampField.d.ts +1 -1
  148. package/dist/db/fields/TimestampField.js +7 -12
  149. package/dist/db/migrations/DatabaseMigrationManager.d.ts +3 -1
  150. package/dist/db/migrations/DatabaseMigrationManager.js +9 -3
  151. package/dist/db/migrations/contracts/DatabaseMigration.d.ts +7 -0
  152. package/dist/db/migrations/index.d.ts +1 -1
  153. package/dist/db/migrations/index.js +1 -1
  154. package/dist/db/migrations/{KnexMigrationRenderer.d.ts → renderKnexMigration.d.ts} +4 -3
  155. package/dist/db/migrations/{KnexMigrationRenderer.js → renderKnexMigration.js} +7 -6
  156. package/dist/flows/FlowCompiler.js +1 -1
  157. package/dist/flows/Flows.js +1 -1
  158. package/dist/flows/index.d.ts +1 -1
  159. package/dist/flows/index.js +1 -1
  160. package/dist/health/Health.d.ts +41 -0
  161. package/dist/health/Health.js +153 -0
  162. package/dist/health/contracts/Health.d.ts +36 -0
  163. package/dist/health/contracts/Health.js +1 -0
  164. package/dist/health/contracts/HealthHttp.d.ts +15 -0
  165. package/dist/health/contracts/HealthHttp.js +1 -0
  166. package/dist/health/contracts/index.d.ts +2 -0
  167. package/dist/health/contracts/index.js +1 -0
  168. package/dist/health/databaseHealthCheck.d.ts +15 -0
  169. package/dist/health/databaseHealthCheck.js +18 -0
  170. package/dist/health/fastify.d.ts +13 -0
  171. package/dist/health/fastify.js +44 -0
  172. package/dist/health/index.d.ts +3 -0
  173. package/dist/health/index.js +2 -0
  174. package/dist/in-app/InApp.d.ts +33 -0
  175. package/dist/in-app/InApp.js +191 -0
  176. package/dist/in-app/InAppError.d.ts +6 -0
  177. package/dist/in-app/InAppError.js +10 -0
  178. package/dist/in-app/InAppRecord.d.ts +36 -0
  179. package/dist/in-app/InAppRecord.js +24 -0
  180. package/dist/in-app/contracts/InAppInbox.d.ts +45 -0
  181. package/dist/in-app/contracts/InAppInbox.js +1 -0
  182. package/dist/in-app/contracts/InAppMessage.d.ts +22 -0
  183. package/dist/in-app/contracts/InAppMessage.js +1 -0
  184. package/dist/in-app/contracts/InAppScope.d.ts +17 -0
  185. package/dist/in-app/contracts/InAppScope.js +1 -0
  186. package/dist/in-app/contracts/index.d.ts +3 -0
  187. package/dist/in-app/contracts/index.js +3 -0
  188. package/dist/in-app/index.d.ts +4 -0
  189. package/dist/in-app/index.js +4 -0
  190. package/dist/in-app/validation.d.ts +21 -0
  191. package/dist/in-app/validation.js +50 -0
  192. package/dist/index.d.ts +2 -14
  193. package/dist/index.js +2 -5
  194. package/dist/logging/DevelopmentConsole.d.ts +42 -0
  195. package/dist/logging/DevelopmentConsole.js +162 -0
  196. package/dist/logging/Log.d.ts +3 -1
  197. package/dist/logging/Log.js +5 -2
  198. package/dist/logging/PrettyLogFormatter.d.ts +16 -0
  199. package/dist/logging/PrettyLogFormatter.js +151 -0
  200. package/dist/logging/acquireDevelopmentConsole.d.ts +6 -0
  201. package/dist/logging/acquireDevelopmentConsole.js +21 -0
  202. package/dist/logging/contracts/DevelopmentConsoleOptions.d.ts +16 -0
  203. package/dist/logging/contracts/DevelopmentConsoleOptions.js +1 -0
  204. package/dist/logging/contracts/EmailLogOptions.d.ts +8 -0
  205. package/dist/logging/contracts/EmailLogOptions.js +1 -0
  206. package/dist/logging/contracts/HttpExchangeMonitorOptions.d.ts +12 -0
  207. package/dist/logging/contracts/HttpExchangeMonitorOptions.js +1 -0
  208. package/dist/logging/contracts/LogTransportOptions.d.ts +19 -0
  209. package/dist/logging/contracts/LogTransportOptions.js +1 -0
  210. package/dist/logging/contracts/LoggingOptions.d.ts +12 -1
  211. package/dist/logging/contracts/index.d.ts +4 -0
  212. package/dist/logging/contracts/index.js +4 -0
  213. package/dist/logging/drivers/PinoLoggerDriver.d.ts +3 -1
  214. package/dist/logging/drivers/PinoLoggerDriver.js +75 -16
  215. package/dist/logging/formatLogEmail.d.ts +4 -0
  216. package/dist/logging/formatLogEmail.js +21 -0
  217. package/dist/logging/highlightQueueMessage.d.ts +11 -0
  218. package/dist/logging/highlightQueueMessage.js +36 -0
  219. package/dist/logging/httpBodyCapture.d.ts +24 -0
  220. package/dist/logging/httpBodyCapture.js +321 -0
  221. package/dist/logging/index.d.ts +2 -0
  222. package/dist/logging/index.js +2 -0
  223. package/dist/logging/redactLogEmail.d.ts +2 -0
  224. package/dist/logging/redactLogEmail.js +12 -0
  225. package/dist/logging/registerHttpExchangeMonitor.d.ts +10 -0
  226. package/dist/logging/registerHttpExchangeMonitor.js +43 -0
  227. package/dist/logging/transports/EmailLogTransport.d.ts +18 -0
  228. package/dist/logging/transports/EmailLogTransport.js +47 -0
  229. package/dist/mail/Mail.d.ts +2 -0
  230. package/dist/mail/Mail.js +3 -0
  231. package/dist/mail/MailDeliveryError.d.ts +9 -0
  232. package/dist/mail/MailDeliveryError.js +16 -0
  233. package/dist/mail/index.d.ts +1 -0
  234. package/dist/mail/index.js +1 -0
  235. package/dist/mail/transports/MailgunTransport.js +2 -0
  236. package/dist/mail/transports/ResendTransport.d.ts +3 -2
  237. package/dist/mail/transports/ResendTransport.js +22 -14
  238. package/dist/media/MediaManager.d.ts +4 -2
  239. package/dist/media/MediaManager.js +40 -8
  240. package/dist/network/BlockedUrlError.d.ts +16 -0
  241. package/dist/network/BlockedUrlError.js +19 -0
  242. package/dist/network/assertPublicUrl.d.ts +22 -0
  243. package/dist/network/assertPublicUrl.js +67 -0
  244. package/dist/network/contracts/GuardedFetch.d.ts +35 -0
  245. package/dist/network/contracts/GuardedFetch.js +1 -0
  246. package/dist/network/contracts/PublicUrlOptions.d.ts +17 -0
  247. package/dist/network/contracts/PublicUrlOptions.js +1 -0
  248. package/dist/network/contracts/index.d.ts +2 -0
  249. package/dist/network/contracts/index.js +2 -0
  250. package/dist/network/guardedFetch.d.ts +29 -0
  251. package/dist/network/guardedFetch.js +133 -0
  252. package/dist/network/index.d.ts +6 -0
  253. package/dist/network/index.js +6 -0
  254. package/dist/network/playwright/GuardedBrowserProxy.d.ts +32 -0
  255. package/dist/network/playwright/GuardedBrowserProxy.js +172 -0
  256. package/dist/network/playwright/contracts/GuardedBrowserContextOptions.d.ts +37 -0
  257. package/dist/network/playwright/contracts/GuardedBrowserContextOptions.js +1 -0
  258. package/dist/network/playwright/contracts/index.d.ts +1 -0
  259. package/dist/network/playwright/contracts/index.js +1 -0
  260. package/dist/network/playwright/index.d.ts +2 -0
  261. package/dist/network/playwright/index.js +2 -0
  262. package/dist/network/playwright/newGuardedBrowserContext.d.ts +21 -0
  263. package/dist/network/playwright/newGuardedBrowserContext.js +86 -0
  264. package/dist/network/privateNetworkAddresses.d.ts +44 -0
  265. package/dist/network/privateNetworkAddresses.js +157 -0
  266. package/dist/network/publicAddressLookup.d.ts +26 -0
  267. package/dist/network/publicAddressLookup.js +42 -0
  268. package/dist/notifications/Notifications.d.ts +27 -0
  269. package/dist/notifications/Notifications.js +99 -0
  270. package/dist/notifications/contracts/Notification.d.ts +56 -0
  271. package/dist/notifications/contracts/Notification.js +1 -0
  272. package/dist/notifications/contracts/NotificationDelivery.d.ts +11 -0
  273. package/dist/notifications/contracts/NotificationDelivery.js +1 -0
  274. package/dist/notifications/contracts/index.d.ts +2 -0
  275. package/dist/notifications/contracts/index.js +2 -0
  276. package/dist/notifications/index.d.ts +2 -0
  277. package/dist/notifications/index.js +2 -0
  278. package/dist/queue/JobRunAttempt.d.ts +16 -0
  279. package/dist/queue/JobRunAttempt.js +17 -0
  280. package/dist/queue/JobRunEvent.d.ts +15 -0
  281. package/dist/queue/JobRunEvent.js +16 -0
  282. package/dist/queue/JobRunStoppedError.d.ts +6 -0
  283. package/dist/queue/JobRunStoppedError.js +10 -0
  284. package/dist/queue/Queue.d.ts +19 -4
  285. package/dist/queue/Queue.js +122 -9
  286. package/dist/queue/QueueTerminalError.d.ts +9 -0
  287. package/dist/queue/QueueTerminalError.js +12 -0
  288. package/dist/queue/QueueWorker.d.ts +12 -5
  289. package/dist/queue/QueueWorker.js +94 -9
  290. package/dist/queue/QueueWorkerReporter.d.ts +17 -0
  291. package/dist/queue/QueueWorkerReporter.js +71 -0
  292. package/dist/queue/QueueableJob.d.ts +2 -2
  293. package/dist/queue/RunnableJob.d.ts +78 -0
  294. package/dist/queue/RunnableJob.js +284 -0
  295. package/dist/queue/console.d.ts +8 -0
  296. package/dist/queue/console.js +53 -19
  297. package/dist/queue/contracts/QueueDriver.d.ts +8 -0
  298. package/dist/queue/contracts/QueueJobRunner.d.ts +4 -0
  299. package/dist/queue/contracts/QueueJobRunner.js +1 -0
  300. package/dist/queue/contracts/QueuePayload.d.ts +1 -1
  301. package/dist/queue/contracts/QueueSelection.d.ts +11 -0
  302. package/dist/queue/contracts/QueueSelection.js +1 -0
  303. package/dist/queue/contracts/QueueService.d.ts +18 -4
  304. package/dist/queue/contracts/QueueWorkerLifecycle.d.ts +2 -2
  305. package/dist/queue/contracts/QueueWorkerSnapshot.d.ts +14 -0
  306. package/dist/queue/contracts/QueueWorkerSnapshot.js +1 -0
  307. package/dist/queue/contracts/QueueableJob.d.ts +7 -3
  308. package/dist/queue/contracts/QueueableJobContract.d.ts +12 -0
  309. package/dist/queue/contracts/QueueableJobContract.js +1 -0
  310. package/dist/queue/contracts/index.d.ts +4 -0
  311. package/dist/queue/contracts/index.js +2 -0
  312. package/dist/queue/drivers/DatabaseQueueDriver.d.ts +4 -0
  313. package/dist/queue/drivers/DatabaseQueueDriver.js +27 -3
  314. package/dist/queue/drivers/RedisQueueDriver.d.ts +7 -0
  315. package/dist/queue/drivers/RedisQueueDriver.js +37 -0
  316. package/dist/queue/index.d.ts +5 -0
  317. package/dist/queue/index.js +5 -0
  318. package/dist/queue/jobEnvelopeHasPrefix.d.ts +3 -0
  319. package/dist/queue/jobEnvelopeHasPrefix.js +4 -0
  320. package/dist/scheduler/ScheduledEvent.d.ts +8 -4
  321. package/dist/scheduler/ScheduledEvent.js +17 -3
  322. package/dist/scheduler/ScheduledJob.d.ts +1 -1
  323. package/dist/scheduler/Scheduler.d.ts +5 -0
  324. package/dist/scheduler/Scheduler.js +92 -28
  325. package/dist/scheduler/SchedulerCheckpointRecord.d.ts +16 -0
  326. package/dist/scheduler/SchedulerCheckpointRecord.js +53 -0
  327. package/dist/scheduler/SchedulerWorker.d.ts +3 -5
  328. package/dist/scheduler/SchedulerWorker.js +58 -14
  329. package/dist/scheduler/console.js +3 -1
  330. package/dist/scheduler/contracts/Schedule.d.ts +19 -0
  331. package/dist/scheduler/contracts/Schedule.js +1 -0
  332. package/dist/scheduler/contracts/Scheduler.d.ts +18 -6
  333. package/dist/scheduler/contracts/SchedulerCheckpoint.d.ts +23 -0
  334. package/dist/scheduler/contracts/SchedulerCheckpoint.js +1 -0
  335. package/dist/scheduler/contracts/SchedulerConsole.d.ts +5 -0
  336. package/dist/scheduler/contracts/SchedulerWorker.d.ts +6 -1
  337. package/dist/scheduler/contracts/index.d.ts +2 -0
  338. package/dist/scheduler/contracts/index.js +2 -0
  339. package/dist/scheduler/index.d.ts +1 -0
  340. package/dist/scheduler/index.js +1 -0
  341. package/dist/server/App.d.ts +36 -4
  342. package/dist/server/App.js +57 -7
  343. package/dist/server/contracts/HttpErrorHandlerOptions.d.ts +29 -0
  344. package/dist/server/contracts/HttpErrorHandlerOptions.js +1 -0
  345. package/dist/server/contracts/index.d.ts +1 -0
  346. package/dist/server/contracts/index.js +1 -0
  347. package/dist/server/index.d.ts +4 -0
  348. package/dist/server/index.js +4 -0
  349. package/dist/server/publicServerErrorMessage.d.ts +11 -0
  350. package/dist/server/publicServerErrorMessage.js +16 -0
  351. package/dist/server/registerBrowserJsonFormatting.d.ts +16 -0
  352. package/dist/server/registerBrowserJsonFormatting.js +100 -0
  353. package/dist/server/registerHttpErrorHandler.d.ts +20 -0
  354. package/dist/server/registerHttpErrorHandler.js +93 -0
  355. package/dist/ssr/vite/contracts/ViteSsrOptions.d.ts +2 -0
  356. package/dist/ssr/vite/fastifyViteSsr.js +4 -3
  357. package/dist/storage/drivers/FlystorageDisk.js +2 -2
  358. package/dist/storage/index.d.ts +2 -2
  359. package/dist/storage/index.js +2 -2
  360. package/dist/websocket/WebSocketChannels.d.ts +18 -0
  361. package/dist/websocket/WebSocketChannels.js +129 -0
  362. package/dist/websocket/WebSocketConnection.d.ts +25 -0
  363. package/dist/websocket/WebSocketConnection.js +244 -0
  364. package/dist/websocket/WebSockets.d.ts +41 -0
  365. package/dist/websocket/WebSockets.js +194 -0
  366. package/dist/websocket/client/WebSocketClient.d.ts +21 -0
  367. package/dist/websocket/client/WebSocketClient.js +230 -0
  368. package/dist/websocket/client/index.d.ts +3 -0
  369. package/dist/websocket/client/index.js +1 -0
  370. package/dist/websocket/contracts/WebSocketChannel.d.ts +20 -0
  371. package/dist/websocket/contracts/WebSocketChannel.js +1 -0
  372. package/dist/websocket/contracts/WebSocketClient.d.ts +45 -0
  373. package/dist/websocket/contracts/WebSocketClient.js +1 -0
  374. package/dist/websocket/contracts/WebSocketEndpoint.d.ts +43 -0
  375. package/dist/websocket/contracts/WebSocketEndpoint.js +1 -0
  376. package/dist/websocket/contracts/WebSocketOptions.d.ts +48 -0
  377. package/dist/websocket/contracts/WebSocketOptions.js +1 -0
  378. package/dist/websocket/contracts/WebSocketPresence.d.ts +6 -0
  379. package/dist/websocket/contracts/WebSocketPresence.js +1 -0
  380. package/dist/websocket/contracts/WebSocketServiceOptions.d.ts +12 -0
  381. package/dist/websocket/contracts/WebSocketServiceOptions.js +1 -0
  382. package/dist/websocket/contracts/index.d.ts +6 -0
  383. package/dist/websocket/contracts/index.js +1 -0
  384. package/dist/websocket/defineChannel.d.ts +8 -0
  385. package/dist/websocket/defineChannel.js +13 -0
  386. package/dist/websocket/defineWebSocket.d.ts +14 -0
  387. package/dist/websocket/defineWebSocket.js +4 -0
  388. package/dist/websocket/fastify.d.ts +11 -0
  389. package/dist/websocket/fastify.js +38 -0
  390. package/dist/websocket/index.d.ts +5 -0
  391. package/dist/websocket/index.js +4 -0
  392. package/package.json +108 -7
  393. package/src/ai/AIErrors.ts +10 -0
  394. package/src/ai/AIFailover.ts +3 -2
  395. package/src/ai/AIProviderAdmission.ts +329 -0
  396. package/src/ai/AIRateLimiter.ts +134 -10
  397. package/src/ai/AIRequestTrackingError.ts +37 -0
  398. package/src/ai/Agent.ts +85 -71
  399. package/src/ai/Ai.ts +104 -48
  400. package/src/ai/OpenAIProviderError.ts +1 -1
  401. package/src/ai/ProviderSseObserver.ts +89 -0
  402. package/src/ai/README.md +216 -11
  403. package/src/ai/aiServiceTier.ts +14 -0
  404. package/src/ai/chunkEmbeddingText.ts +137 -0
  405. package/src/ai/contracts/AI.ts +17 -0
  406. package/src/ai/contracts/AIRequestErrorDiagnostics.ts +12 -0
  407. package/src/ai/contracts/AIServiceTier.ts +2 -0
  408. package/src/ai/contracts/Agent.ts +3 -0
  409. package/src/ai/contracts/EmbeddingTextChunk.ts +26 -0
  410. package/src/ai/contracts/Pricing.ts +7 -1
  411. package/src/ai/contracts/Providers.ts +2 -0
  412. package/src/ai/contracts/index.ts +2 -0
  413. package/src/ai/examples/prepareDocumentEmbeddings.ts +20 -0
  414. package/src/ai/index.ts +9 -2
  415. package/src/ai/{Models.ts → modelPricing.ts} +37 -4
  416. package/src/apps/AppInstallation.ts +25 -0
  417. package/src/apps/AppRouteError.ts +8 -0
  418. package/src/apps/AppService.ts +30 -0
  419. package/src/apps/Apps.ts +508 -0
  420. package/src/apps/README.md +354 -0
  421. package/src/apps/appsPlugin.ts +34 -0
  422. package/src/apps/commands/disableApp.ts +6 -0
  423. package/src/apps/commands/enableApp.ts +6 -0
  424. package/src/apps/commands/index.ts +29 -0
  425. package/src/apps/commands/installApp.ts +6 -0
  426. package/src/apps/commands/listApps.ts +6 -0
  427. package/src/apps/commands/makeAppMigration.ts +9 -0
  428. package/src/apps/commands/rollbackApp.ts +6 -0
  429. package/src/apps/commands/uninstallApp.ts +6 -0
  430. package/src/apps/contracts/AppAction.ts +2 -0
  431. package/src/apps/contracts/AppDefinition.ts +49 -0
  432. package/src/apps/contracts/AppDescription.ts +27 -0
  433. package/src/apps/contracts/AppManifest.ts +32 -0
  434. package/src/apps/contracts/AppNavigation.ts +10 -0
  435. package/src/apps/contracts/AppNavigationBadge.ts +6 -0
  436. package/src/apps/contracts/AppNavigationContext.ts +6 -0
  437. package/src/apps/contracts/AppNavigationResult.ts +9 -0
  438. package/src/apps/contracts/AppRoute.ts +18 -0
  439. package/src/apps/contracts/DiscoveredApp.ts +11 -0
  440. package/src/apps/contracts/InstalledApps.ts +2 -0
  441. package/src/apps/contracts/index.ts +11 -0
  442. package/src/apps/defineApp.ts +10 -0
  443. package/src/apps/discoverApps.ts +56 -0
  444. package/src/apps/fastify.ts +1 -0
  445. package/src/apps/index.ts +7 -0
  446. package/src/apps/normalizeAppNavigation.ts +29 -0
  447. package/src/apps/registerAppRoutes.ts +48 -0
  448. package/src/apps/validateAppDefinitions.ts +52 -0
  449. package/src/apps/vite.ts +1 -0
  450. package/src/apps/writeAppTypes.ts +14 -0
  451. package/src/auth/Auth.ts +89 -23
  452. package/src/auth/PasswordLoginAttempt.ts +72 -0
  453. package/src/auth/PasswordSuspendedError.ts +13 -0
  454. package/src/auth/README.md +94 -4
  455. package/src/auth/contracts/PasswordAuthProviderOptions.ts +30 -0
  456. package/src/auth/contracts/PasswordSuspension.ts +7 -0
  457. package/src/auth/contracts/PasswordSuspensionOptions.ts +10 -0
  458. package/src/auth/contracts/index.ts +3 -0
  459. package/src/auth/examples/runPasswordAuth.ts +6 -6
  460. package/src/auth/index.ts +2 -0
  461. package/src/auth/providers/PasswordAuthProvider.ts +96 -30
  462. package/src/auth/providers/resolveAuthProviders.ts +1 -0
  463. package/src/cli/Cli.ts +9 -0
  464. package/src/cli/README.md +19 -0
  465. package/src/cli/commands/repl.ts +2 -1
  466. package/src/cli/contracts/CliCommand.ts +2 -0
  467. package/src/cli/contracts/CliCommandInfo.ts +13 -0
  468. package/src/cli/contracts/index.ts +1 -0
  469. package/src/cli/index.ts +1 -0
  470. package/src/cli/listDb3Commands.ts +12 -0
  471. package/src/cli/runDb3.ts +5 -51
  472. package/src/cli/withDb3Cli.ts +55 -0
  473. package/src/db/ActiveQueryBuilder.ts +12 -0
  474. package/src/db/ActiveRecord.ts +9 -0
  475. package/src/db/README.md +45 -1
  476. package/src/db/connection.ts +12 -1
  477. package/src/db/examples/FieldNote.ts +7 -13
  478. package/src/db/examples/KnowledgeNote.ts +7 -13
  479. package/src/db/examples/runNoteMigrations.ts +11 -11
  480. package/src/db/fields/TimestampField.ts +8 -17
  481. package/src/db/migrations/DatabaseMigrationManager.ts +12 -4
  482. package/src/db/migrations/contracts/DatabaseMigration.ts +9 -0
  483. package/src/db/migrations/index.ts +1 -1
  484. package/src/db/migrations/{KnexMigrationRenderer.ts → renderKnexMigration.ts} +8 -14
  485. package/src/flows/FlowCompiler.ts +1 -1
  486. package/src/flows/Flows.ts +1 -1
  487. package/src/flows/index.ts +1 -1
  488. package/src/health/Health.ts +163 -0
  489. package/src/health/README.md +46 -0
  490. package/src/health/contracts/Health.ts +47 -0
  491. package/src/health/contracts/HealthHttp.ts +19 -0
  492. package/src/health/contracts/index.ts +2 -0
  493. package/src/health/databaseHealthCheck.ts +21 -0
  494. package/src/health/fastify.ts +49 -0
  495. package/src/health/index.ts +3 -0
  496. package/src/in-app/InApp.ts +173 -0
  497. package/src/in-app/InAppError.ts +8 -0
  498. package/src/in-app/InAppRecord.ts +41 -0
  499. package/src/in-app/README.md +193 -0
  500. package/src/in-app/contracts/InAppInbox.ts +51 -0
  501. package/src/in-app/contracts/InAppMessage.ts +19 -0
  502. package/src/in-app/contracts/InAppScope.ts +12 -0
  503. package/src/in-app/contracts/index.ts +3 -0
  504. package/src/in-app/examples/sendReportReady.ts +14 -0
  505. package/src/in-app/index.ts +4 -0
  506. package/src/in-app/validation.ts +49 -0
  507. package/src/index.ts +2 -22
  508. package/src/logging/DevelopmentConsole.ts +154 -0
  509. package/src/logging/Log.ts +5 -2
  510. package/src/logging/PrettyLogFormatter.ts +165 -0
  511. package/src/logging/README.md +52 -109
  512. package/src/logging/acquireDevelopmentConsole.ts +19 -0
  513. package/src/logging/contracts/DevelopmentConsoleOptions.ts +13 -0
  514. package/src/logging/contracts/EmailLogOptions.ts +9 -0
  515. package/src/logging/contracts/HttpExchangeMonitorOptions.ts +13 -0
  516. package/src/logging/contracts/LogTransportOptions.ts +10 -0
  517. package/src/logging/contracts/LoggingOptions.ts +12 -1
  518. package/src/logging/contracts/index.ts +4 -0
  519. package/src/logging/development/README.md +88 -0
  520. package/src/logging/drivers/PinoLoggerDriver.ts +72 -20
  521. package/src/logging/examples/runDevelopmentLogs.ts +36 -0
  522. package/src/logging/formatLogEmail.ts +24 -0
  523. package/src/logging/highlightQueueMessage.ts +38 -0
  524. package/src/logging/httpBodyCapture.ts +393 -0
  525. package/src/logging/index.ts +2 -0
  526. package/src/logging/redactLogEmail.ts +12 -0
  527. package/src/logging/registerHttpExchangeMonitor.ts +45 -0
  528. package/src/logging/transports/EmailLogTransport.ts +46 -0
  529. package/src/mail/Mail.ts +5 -0
  530. package/src/mail/MailDeliveryError.ts +8 -0
  531. package/src/mail/README.md +11 -1
  532. package/src/mail/index.ts +1 -0
  533. package/src/mail/transports/MailgunTransport.ts +2 -0
  534. package/src/mail/transports/ResendTransport.ts +24 -22
  535. package/src/media/MediaManager.ts +31 -10
  536. package/src/media/README.md +4 -0
  537. package/src/media/examples/runPrivateFiles.ts +2 -2
  538. package/src/network/BlockedUrlError.ts +19 -0
  539. package/src/network/README.md +133 -0
  540. package/src/network/assertPublicUrl.ts +77 -0
  541. package/src/network/contracts/GuardedFetch.ts +40 -0
  542. package/src/network/contracts/PublicUrlOptions.ts +17 -0
  543. package/src/network/contracts/index.ts +2 -0
  544. package/src/network/examples/runGuardedFetch.ts +35 -0
  545. package/src/network/guardedFetch.ts +150 -0
  546. package/src/network/index.ts +6 -0
  547. package/src/network/playwright/GuardedBrowserProxy.ts +172 -0
  548. package/src/network/playwright/contracts/GuardedBrowserContextOptions.ts +43 -0
  549. package/src/network/playwright/contracts/index.ts +1 -0
  550. package/src/network/playwright/index.ts +2 -0
  551. package/src/network/playwright/newGuardedBrowserContext.ts +74 -0
  552. package/src/network/privateNetworkAddresses.ts +166 -0
  553. package/src/network/publicAddressLookup.ts +49 -0
  554. package/src/notifications/Notifications.ts +105 -0
  555. package/src/notifications/README.md +59 -0
  556. package/src/notifications/contracts/Notification.ts +65 -0
  557. package/src/notifications/contracts/NotificationDelivery.ts +11 -0
  558. package/src/notifications/contracts/index.ts +2 -0
  559. package/src/notifications/examples/ReportReadyNotification.ts +31 -0
  560. package/src/notifications/index.ts +2 -0
  561. package/src/queue/JobRunAttempt.ts +17 -0
  562. package/src/queue/JobRunEvent.ts +16 -0
  563. package/src/queue/JobRunStoppedError.ts +8 -0
  564. package/src/queue/Queue.ts +112 -11
  565. package/src/queue/QueueTerminalError.ts +12 -0
  566. package/src/queue/QueueWorker.ts +86 -7
  567. package/src/queue/QueueWorkerReporter.ts +59 -0
  568. package/src/queue/QueueableJob.ts +2 -2
  569. package/src/queue/README.md +142 -1
  570. package/src/queue/RunnableJob.ts +272 -0
  571. package/src/queue/console.ts +54 -21
  572. package/src/queue/contracts/QueueDriver.ts +9 -0
  573. package/src/queue/contracts/QueueJobRunner.ts +4 -0
  574. package/src/queue/contracts/QueuePayload.ts +1 -1
  575. package/src/queue/contracts/QueueSelection.ts +11 -0
  576. package/src/queue/contracts/QueueService.ts +19 -4
  577. package/src/queue/contracts/QueueWorkerLifecycle.ts +2 -2
  578. package/src/queue/contracts/QueueWorkerSnapshot.ts +15 -0
  579. package/src/queue/contracts/QueueableJob.ts +7 -3
  580. package/src/queue/contracts/QueueableJobContract.ts +13 -0
  581. package/src/queue/contracts/index.ts +6 -0
  582. package/src/queue/drivers/DatabaseQueueDriver.ts +25 -3
  583. package/src/queue/drivers/RedisQueueDriver.ts +36 -0
  584. package/src/queue/examples/runQueueSelection.ts +34 -0
  585. package/src/queue/index.ts +7 -0
  586. package/src/queue/jobEnvelopeHasPrefix.ts +6 -0
  587. package/src/scheduler/README.md +50 -9
  588. package/src/scheduler/ScheduledEvent.ts +20 -5
  589. package/src/scheduler/ScheduledJob.ts +1 -1
  590. package/src/scheduler/Scheduler.ts +73 -27
  591. package/src/scheduler/SchedulerCheckpointRecord.ts +51 -0
  592. package/src/scheduler/SchedulerWorker.ts +54 -19
  593. package/src/scheduler/console.ts +3 -1
  594. package/src/scheduler/contracts/Schedule.ts +22 -0
  595. package/src/scheduler/contracts/Scheduler.ts +21 -6
  596. package/src/scheduler/contracts/SchedulerCheckpoint.ts +23 -0
  597. package/src/scheduler/contracts/SchedulerConsole.ts +5 -0
  598. package/src/scheduler/contracts/SchedulerWorker.ts +6 -1
  599. package/src/scheduler/contracts/index.ts +2 -0
  600. package/src/scheduler/examples/runSchedulerWindow.ts +54 -0
  601. package/src/scheduler/index.ts +1 -0
  602. package/src/server/App.ts +73 -10
  603. package/src/server/README.md +95 -2
  604. package/src/server/contracts/HttpErrorHandlerOptions.ts +29 -0
  605. package/src/server/contracts/index.ts +1 -0
  606. package/src/server/examples/createFirstServer.ts +5 -3
  607. package/src/server/index.ts +4 -0
  608. package/src/server/publicServerErrorMessage.ts +17 -0
  609. package/src/server/registerBrowserJsonFormatting.ts +94 -0
  610. package/src/server/registerHttpErrorHandler.ts +96 -0
  611. package/src/ssr/README.md +6 -0
  612. package/src/ssr/vite/contracts/ViteSsrOptions.ts +2 -0
  613. package/src/ssr/vite/fastifyViteSsr.ts +4 -3
  614. package/src/storage/drivers/FlystorageDisk.ts +2 -2
  615. package/src/storage/index.ts +2 -2
  616. package/src/websocket/README.md +458 -0
  617. package/src/websocket/WebSocketChannels.ts +99 -0
  618. package/src/websocket/WebSocketConnection.ts +162 -0
  619. package/src/websocket/WebSockets.ts +157 -0
  620. package/src/websocket/client/WebSocketClient.ts +188 -0
  621. package/src/websocket/client/index.ts +3 -0
  622. package/src/websocket/contracts/WebSocketChannel.ts +23 -0
  623. package/src/websocket/contracts/WebSocketClient.ts +48 -0
  624. package/src/websocket/contracts/WebSocketEndpoint.ts +46 -0
  625. package/src/websocket/contracts/WebSocketOptions.ts +50 -0
  626. package/src/websocket/contracts/WebSocketPresence.ts +6 -0
  627. package/src/websocket/contracts/WebSocketServiceOptions.ts +12 -0
  628. package/src/websocket/contracts/index.ts +6 -0
  629. package/src/websocket/defineChannel.ts +14 -0
  630. package/src/websocket/defineWebSocket.ts +15 -0
  631. package/src/websocket/examples/board/LiveBoard.ts +97 -0
  632. package/src/websocket/examples/board/SummarizeBoardJob.ts +52 -0
  633. package/src/websocket/examples/board/boardSnapshot.ts +16 -0
  634. package/src/websocket/examples/board/createBoardClient.ts +43 -0
  635. package/src/websocket/examples/board/createBoardStore.ts +58 -0
  636. package/src/websocket/examples/board/registerBoardRoutes.ts +31 -0
  637. package/src/websocket/examples/createChannelSync.ts +66 -0
  638. package/src/websocket/examples/deploymentEndpoint.ts +13 -0
  639. package/src/websocket/examples/deploymentSnapshot.ts +36 -0
  640. package/src/websocket/examples/resourceChannels.ts +18 -0
  641. package/src/websocket/examples/userEndpoint.ts +27 -0
  642. package/src/websocket/fastify.ts +39 -0
  643. package/src/websocket/index.ts +5 -0
  644. package/templates/AGENTS.md +33 -2
  645. /package/dist/flows/{FlowValueValidator.d.ts → flowValueValidator.d.ts} +0 -0
  646. /package/dist/flows/{FlowValueValidator.js → flowValueValidator.js} +0 -0
  647. /package/dist/storage/{mime.d.ts → mimeTypeFromPath.d.ts} +0 -0
  648. /package/dist/storage/{mime.js → mimeTypeFromPath.js} +0 -0
  649. /package/dist/storage/{contents.d.ts → storageContentsToBuffer.d.ts} +0 -0
  650. /package/dist/storage/{contents.js → storageContentsToBuffer.js} +0 -0
  651. /package/src/flows/{FlowValueValidator.ts → flowValueValidator.ts} +0 -0
  652. /package/src/storage/{mime.ts → mimeTypeFromPath.ts} +0 -0
  653. /package/src/storage/{contents.ts → storageContentsToBuffer.ts} +0 -0
@@ -0,0 +1,172 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { createServer, request as httpRequest, type IncomingMessage, type ServerResponse, type OutgoingHttpHeaders } from 'node:http';
3
+ import { connect, type Socket } from 'node:net';
4
+ import type { Duplex } from 'node:stream';
5
+ import { assertPublicUrl } from '../assertPublicUrl';
6
+ import { BlockedUrlError } from '../BlockedUrlError';
7
+ import { normalizedUrlHostname } from '../privateNetworkAddresses';
8
+ import { publicAddressLookup } from '../publicAddressLookup';
9
+
10
+ /** Internal connection policy; the browser adapter owns request attribution. */
11
+ interface ProxyOptions {
12
+ allowPrivate?: boolean;
13
+ requestTimeoutMs?: number;
14
+ onBlocked: (url: string, error: BlockedUrlError) => void;
15
+ }
16
+
17
+ /**
18
+ * Context-owned loopback proxy enforcing the destination of every browser socket.
19
+ *
20
+ * HTTP bodies stream without buffering. HTTPS remains end-to-end TLS through a
21
+ * validated CONNECT tunnel, preserving browser certificates, cookies and origins.
22
+ * Redirects create normal browser requests and cannot escape the proxy's policy.
23
+ */
24
+ export class GuardedBrowserProxy {
25
+ readonly #options: ProxyOptions;
26
+ readonly #sockets = new Set<Duplex>();
27
+ readonly #server = createServer();
28
+ readonly #username = 'db3-browser';
29
+ readonly #password = randomBytes(32).toString('hex');
30
+ #closed = false;
31
+
32
+ /** Creates an unstarted, isolated browser proxy. */
33
+ constructor(options: ProxyOptions) {
34
+ this.#options = options;
35
+ this.#server.on('connection', socket => this.#track(socket));
36
+ this.#server.on('request', (request, response) => { void this.#forward(request, response); });
37
+ this.#server.on('connect', (request, socket, head) => { void this.#tunnel(request, socket, head); });
38
+ this.#server.on('clientError', (_error, socket) => socket.destroy());
39
+ }
40
+
41
+ /**
42
+ * Starts the proxy on an ephemeral loopback port.
43
+ * @returns Chromium proxy options; the bypass rule includes loopback traffic.
44
+ */
45
+ async start(): Promise<{ server: string; bypass: string; username: string; password: string }> {
46
+ await new Promise<void>((resolve, reject) => {
47
+ this.#server.once('error', reject);
48
+ this.#server.listen(0, '127.0.0.1', () => {
49
+ this.#server.off('error', reject);
50
+ resolve();
51
+ });
52
+ });
53
+ const address = this.#server.address();
54
+ if (!address || typeof address === 'string') throw new Error('Browser proxy did not open a TCP port.');
55
+ return { server: `http://127.0.0.1:${address.port}`, bypass: '<-loopback>', username: this.#username, password: this.#password };
56
+ }
57
+
58
+ /** Stops accepting requests and synchronously tears down every owned socket. */
59
+ close(): void {
60
+ if (this.#closed) return;
61
+ this.#closed = true;
62
+ for (const socket of this.#sockets) socket.destroy();
63
+ this.#server.close();
64
+ }
65
+
66
+ /**
67
+ * Registers socket cleanup and an inactivity timeout without buffering data.
68
+ * @param socket - Accepted browser socket or outgoing destination connection.
69
+ */
70
+ #track(socket: Duplex): void {
71
+ if (this.#closed) { socket.destroy(); return; }
72
+ this.#sockets.add(socket);
73
+ socket.once('close', () => this.#sockets.delete(socket));
74
+ socket.on('error', () => socket.destroy());
75
+ if ('setTimeout' in socket) (socket as Socket).setTimeout(this.#options.requestTimeoutMs ?? 30_000, () => socket.destroy());
76
+ }
77
+
78
+ /** Checks the per-context proxy credential supplied by Chromium. */
79
+ #authorized(request: IncomingMessage): boolean {
80
+ return request.headers['proxy-authorization'] === `Basic ${Buffer.from(`${this.#username}:${this.#password}`).toString('base64')}`;
81
+ }
82
+
83
+ /**
84
+ * Streams a normal HTTP proxy request through a validated connection.
85
+ * @param request - Browser's absolute-form request.
86
+ * @param response - Response streamed back to the browser.
87
+ */
88
+ async #forward(request: IncomingMessage, response: ServerResponse): Promise<void> {
89
+ if (!this.#authorized(request)) {
90
+ response.writeHead(407, { 'proxy-authenticate': 'Basic realm="db3-browser"' }).end();
91
+ return;
92
+ }
93
+ const url = request.url ?? '';
94
+ try {
95
+ await assertPublicUrl(url, this.#options);
96
+ const destination = new URL(url);
97
+ if (destination.protocol !== 'http:') throw new BlockedUrlError('HTTPS browser requests must use a CONNECT tunnel.');
98
+ if (this.#closed || response.destroyed) return;
99
+ const headers: OutgoingHttpHeaders = { ...request.headers, host: destination.host };
100
+ delete headers['proxy-authorization'];
101
+ delete headers['proxy-connection'];
102
+ const outgoing = httpRequest(destination, {
103
+ method: request.method,
104
+ headers,
105
+ agent: false,
106
+ lookup: this.#options.allowPrivate ? undefined : publicAddressLookup,
107
+ }, upstream => {
108
+ response.writeHead(upstream.statusCode ?? 502, upstream.headers);
109
+ upstream.on('error', () => response.destroy());
110
+ upstream.pipe(response);
111
+ });
112
+ outgoing.on('socket', socket => this.#track(socket));
113
+ outgoing.on('error', error => {
114
+ this.#reportBlocked(url, error);
115
+ response.destroy();
116
+ });
117
+ request.on('aborted', () => outgoing.destroy());
118
+ request.on('error', () => outgoing.destroy());
119
+ response.on('close', () => outgoing.destroy());
120
+ request.pipe(outgoing);
121
+ } catch (error) {
122
+ this.#reportBlocked(url, error);
123
+ response.destroy();
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Opens a TLS tunnel only after validating its actual destination connection.
129
+ * @param request - Chromium CONNECT authority.
130
+ * @param browser - Browser side of the opaque tunnel.
131
+ * @param head - Bytes received with the CONNECT request.
132
+ */
133
+ async #tunnel(request: IncomingMessage, browser: Duplex, head: Buffer): Promise<void> {
134
+ if (!this.#authorized(request)) {
135
+ browser.end('HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm="db3-browser"\r\nContent-Length: 0\r\n\r\n');
136
+ return;
137
+ }
138
+ const url = `https://${request.url ?? ''}/`;
139
+ try {
140
+ await assertPublicUrl(url, this.#options);
141
+ if (this.#closed || browser.destroyed) return;
142
+ const destination = new URL(url);
143
+ const upstream = connect({
144
+ host: normalizedUrlHostname(destination.hostname),
145
+ port: Number(destination.port || 443),
146
+ lookup: this.#options.allowPrivate ? undefined : publicAddressLookup,
147
+ });
148
+ this.#track(upstream);
149
+ browser.once('close', () => upstream.destroy());
150
+ upstream.once('close', () => browser.destroy());
151
+ upstream.on('error', error => {
152
+ this.#reportBlocked(url, error);
153
+ browser.destroy();
154
+ });
155
+ upstream.once('connect', () => {
156
+ browser.write('HTTP/1.1 200 Connection Established\r\n\r\n');
157
+ if (head.length) upstream.write(head);
158
+ upstream.pipe(browser);
159
+ browser.pipe(upstream);
160
+ });
161
+ } catch (error) {
162
+ this.#reportBlocked(url, error);
163
+ browser.destroy();
164
+ }
165
+ }
166
+
167
+ /** Records policy refusals without allowing observer failures to escape I/O. */
168
+ #reportBlocked(url: string, error: unknown): void {
169
+ if (!(error instanceof BlockedUrlError)) return;
170
+ try { this.#options.onBlocked(url, error); } catch { /* Observers cannot change network enforcement. */ }
171
+ }
172
+ }
@@ -0,0 +1,43 @@
1
+ import type { Request } from 'playwright';
2
+ import type { BlockedUrlError } from '../../BlockedUrlError';
3
+ import type { PublicUrlOptions } from '../../contracts';
4
+
5
+ /**
6
+ * Details passed to the observer when the guard refuses a browser request.
7
+ */
8
+ export interface BlockedBrowserRequest {
9
+ /** URL the page tried to load. */
10
+ url: string;
11
+
12
+ /** Guard rejection with a caller-safe message. */
13
+ error: BlockedUrlError;
14
+
15
+ /**
16
+ * Playwright request that was aborted.
17
+ *
18
+ * Use `request.isNavigationRequest()` and `request.frame()` to tell whether
19
+ * the page's own navigation was refused rather than an embedded resource.
20
+ */
21
+ request: Request;
22
+ }
23
+
24
+ /**
25
+ * Network policy for a browser context whose traffic is routed through the guard.
26
+ */
27
+ export interface GuardedBrowserContextOptions extends PublicUrlOptions {
28
+ /**
29
+ * Playwright resource types aborted without a network request, for example
30
+ * `['image', 'font', 'media']` when only the rendered DOM is needed.
31
+ */
32
+ blockedResourceTypes?: readonly string[];
33
+
34
+ /** Socket inactivity timeout in milliseconds. Defaults to 30 seconds. */
35
+ requestTimeoutMs?: number;
36
+
37
+ /**
38
+ * Observer called for each request refused by the destination policy.
39
+ *
40
+ * The request is aborted whether or not an observer is supplied.
41
+ */
42
+ onBlocked?: (blocked: BlockedBrowserRequest) => void;
43
+ }
@@ -0,0 +1 @@
1
+ export * from './GuardedBrowserContextOptions';
@@ -0,0 +1,2 @@
1
+ export * from './contracts';
2
+ export * from './newGuardedBrowserContext';
@@ -0,0 +1,74 @@
1
+ import type { Browser, BrowserContext, BrowserContextOptions, Request } from 'playwright';
2
+ import { BlockedUrlError } from '../BlockedUrlError';
3
+ import { assertPublicUrl } from '../assertPublicUrl';
4
+ import { GuardedBrowserProxy } from './GuardedBrowserProxy';
5
+ import type * as playwrightNetwork from './contracts';
6
+
7
+ /**
8
+ * Creates a Chromium context with connection-level private-network protection.
9
+ *
10
+ * All HTTP requests and HTTPS tunnels use a context-owned authenticated proxy.
11
+ * Its socket lookup validates DNS at connect time, including redirect hops that
12
+ * Playwright does not route again. Chromium retains native redirects, cookies,
13
+ * response streaming and TLS verification. Closing the context closes its proxy.
14
+ * Service workers and WebSockets are disabled; callers must also disable WebRTC
15
+ * non-proxied UDP when launching Chromium for untrusted pages.
16
+ *
17
+ * @param browser - Launched Chromium browser.
18
+ * @param contextOptions - Browser settings; upstream proxy overrides are refused.
19
+ * @param options - Destination policy, resource filters and refusal observer.
20
+ * @returns Isolated browser context, owned and closed by the caller.
21
+ * @example
22
+ * const context = await newGuardedBrowserContext(browser);
23
+ * try { await (await context.newPage()).goto(url); } finally { await context.close(); }
24
+ */
25
+ export async function newGuardedBrowserContext(browser: Browser, contextOptions: BrowserContextOptions = {}, options: playwrightNetwork.GuardedBrowserContextOptions = {}): Promise<BrowserContext> {
26
+ if (browser.browserType().name() !== 'chromium') throw new Error('Guarded browser contexts require Chromium.');
27
+ if (contextOptions.proxy) throw new Error('Guarded browser contexts own their proxy configuration.');
28
+ const requests = new Set<Request>();
29
+ const reported = new WeakSet<Request>();
30
+ const blockedOrigins = new Map<string, BlockedUrlError>();
31
+ const blockedTypes = new Set(options.blockedResourceTypes ?? []);
32
+ /** Attributes a connection rejection without exposing observer errors to I/O. */
33
+ function report(request: Request, error: BlockedUrlError): void {
34
+ if (reported.has(request)) return;
35
+ reported.add(request);
36
+ try { options.onBlocked?.({ url: request.url(), request, error }); } catch { /* Observers cannot change network enforcement. */ }
37
+ }
38
+ const proxy = new GuardedBrowserProxy({
39
+ ...options,
40
+ onBlocked: (url, error) => {
41
+ const origin = new URL(url).origin;
42
+ blockedOrigins.set(origin, error);
43
+ for (const request of requests) if (new URL(request.url()).origin === origin) report(request, error);
44
+ },
45
+ });
46
+ let context: BrowserContext | undefined;
47
+ try {
48
+ context = await browser.newContext({ ...contextOptions, proxy: await proxy.start(), serviceWorkers: 'block' });
49
+ context.once('close', () => { proxy.close(); requests.clear(); blockedOrigins.clear(); });
50
+ context.on('request', request => {
51
+ requests.add(request);
52
+ const error = blockedOrigins.get(new URL(request.url()).origin);
53
+ if (error) report(request, error);
54
+ });
55
+ context.on('requestfinished', request => requests.delete(request));
56
+ context.on('requestfailed', request => requests.delete(request));
57
+ await context.route('**/*', async route => {
58
+ try {
59
+ if (blockedTypes.has(route.request().resourceType())) { await route.abort('blockedbyclient'); return; }
60
+ await assertPublicUrl(route.request().url(), options);
61
+ await route.continue();
62
+ } catch (error) {
63
+ if (error instanceof BlockedUrlError) report(route.request(), error);
64
+ await route.abort(error instanceof BlockedUrlError ? 'blockedbyclient' : 'failed').catch(() => undefined);
65
+ }
66
+ });
67
+ await context.routeWebSocket('**/*', socket => socket.close());
68
+ return context;
69
+ } catch (error) {
70
+ proxy.close();
71
+ await context?.close();
72
+ throw error;
73
+ }
74
+ }
@@ -0,0 +1,166 @@
1
+ import { isIP } from 'node:net';
2
+
3
+ /**
4
+ * Normalizes a URL hostname for address classification.
5
+ *
6
+ * WHATWG URL parsing already canonicalizes numeric IPv4 forms such as
7
+ * `2130706433` and `0x7f.1`. This helper additionally lowercases the value,
8
+ * removes IPv6 brackets and drops one trailing root-label dot so `localhost.`
9
+ * and `localhost` are treated the same.
10
+ *
11
+ * @param hostname - `URL.hostname` value to normalize.
12
+ * @returns Lowercase hostname or IP literal without brackets or a trailing dot.
13
+ *
14
+ * @example
15
+ * normalizedUrlHostname(new URL('http://[::1]/').hostname); // '::1'
16
+ */
17
+ export function normalizedUrlHostname(hostname: string): string {
18
+ return hostname.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
19
+ }
20
+
21
+ /**
22
+ * Checks whether a hostname is reserved for the local machine.
23
+ *
24
+ * DNS names are otherwise judged by the addresses they resolve to, so this
25
+ * only covers names that never need a DNS query to reach loopback.
26
+ *
27
+ * @param hostname - Hostname normalized with {@link normalizedUrlHostname}.
28
+ * @returns True for `localhost` and any `*.localhost` name.
29
+ */
30
+ export function isPrivateHostname(hostname: string): boolean {
31
+ return hostname === 'localhost' || hostname.endsWith('.localhost');
32
+ }
33
+
34
+ /**
35
+ * Checks whether an IP address must not be reached by server-side requests.
36
+ *
37
+ * The policy blocks loopback, private, link-local (including cloud metadata),
38
+ * carrier-grade NAT, benchmarking, documentation, multicast and reserved
39
+ * ranges. IPv6 forms that embed an IPv4 address (mapped, NAT64 and 6to4) are
40
+ * judged by the embedded address, and tunnelling or deprecated forms that can
41
+ * reach IPv4 hosts indirectly are blocked outright. Values that are not valid
42
+ * IP literals return false so callers can resolve them as hostnames instead.
43
+ *
44
+ * @param address - IPv4 or IPv6 literal, with or without IPv6 brackets.
45
+ * @returns True when the address is private, local or reserved.
46
+ *
47
+ * @example
48
+ * isPrivateIpAddress('169.254.169.254'); // true
49
+ * isPrivateIpAddress('::ffff:7f00:1'); // true (IPv4-mapped 127.0.0.1)
50
+ * isPrivateIpAddress('93.184.216.34'); // false
51
+ */
52
+ export function isPrivateIpAddress(address: string): boolean {
53
+ const normalized = normalizedUrlHostname(address).replace(/%.*$/, '');
54
+ const version = isIP(normalized);
55
+
56
+ if (version === 4) return isPrivateIpv4Octets(normalized.split('.').map(Number));
57
+ if (version === 6) return isPrivateIpv6Hextets(expandIpv6(normalized));
58
+
59
+ return false;
60
+ }
61
+
62
+ /**
63
+ * Applies the IPv4 reserved-range policy to four parsed octets.
64
+ *
65
+ * @param octets - Parsed IPv4 octets.
66
+ * @returns True when the address belongs to a blocked range.
67
+ */
68
+ function isPrivateIpv4Octets(octets: number[]): boolean {
69
+ if (octets.length !== 4 || octets.some(octet => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
70
+ return true;
71
+ }
72
+
73
+ const [first, second, third] = octets as [number, number, number, number];
74
+
75
+ return first === 0
76
+ || first === 10
77
+ || first === 127
78
+ || (first === 100 && second >= 64 && second <= 127)
79
+ || (first === 169 && second === 254)
80
+ || (first === 172 && second >= 16 && second <= 31)
81
+ || (first === 192 && second === 0 && (third === 0 || third === 2))
82
+ || (first === 192 && second === 88 && third === 99)
83
+ || (first === 192 && second === 168)
84
+ || (first === 198 && (second === 18 || second === 19))
85
+ || (first === 198 && second === 51 && third === 100)
86
+ || (first === 203 && second === 0 && third === 113)
87
+ || first >= 224;
88
+ }
89
+
90
+ /**
91
+ * Applies the IPv6 reserved-range policy to eight expanded hextets.
92
+ *
93
+ * @param hextets - Expanded IPv6 address, or null when it could not be parsed.
94
+ * @returns True when the address belongs to a blocked range.
95
+ */
96
+ function isPrivateIpv6Hextets(hextets: number[] | null): boolean {
97
+ if (!hextets) return true;
98
+
99
+ const [h0, h1, h2, h3, h4, h5, h6, h7] = hextets as [number, number, number, number, number, number, number, number];
100
+ const leadingZeros = h0 === 0 && h1 === 0 && h2 === 0 && h3 === 0 && h4 === 0;
101
+
102
+ // ::ffff:a.b.c.d IPv4-mapped addresses reach the embedded IPv4 host.
103
+ if (leadingZeros && h5 === 0xffff) return isPrivateIpv4Octets(ipv4FromHextets(h6, h7));
104
+ // ::, ::1 and deprecated ::a.b.c.d IPv4-compatible addresses.
105
+ if (leadingZeros && h5 === 0) return true;
106
+ // 64:ff9b::/96 well-known NAT64 prefix translates to the embedded IPv4 host.
107
+ if (h0 === 0x64 && h1 === 0xff9b && h2 === 0 && h3 === 0 && h4 === 0 && h5 === 0) {
108
+ return isPrivateIpv4Octets(ipv4FromHextets(h6, h7));
109
+ }
110
+ // 64:ff9b:1::/48 local-use NAT64.
111
+ if (h0 === 0x64 && h1 === 0xff9b && h2 === 1) return true;
112
+ // 2002::/16 6to4 embeds the relay's IPv4 address in the next 32 bits.
113
+ if (h0 === 0x2002) return isPrivateIpv4Octets(ipv4FromHextets(h1, h2));
114
+ // 2001::/32 Teredo tunnelling and 2001:db8::/32 documentation.
115
+ if (h0 === 0x2001 && (h1 === 0 || h1 === 0xdb8)) return true;
116
+ // 100::/64 discard-only.
117
+ if (h0 === 0x100 && h1 === 0 && h2 === 0 && h3 === 0) return true;
118
+
119
+ return (h0 & 0xfe00) === 0xfc00 // fc00::/7 unique local
120
+ || (h0 & 0xffc0) === 0xfe80 // fe80::/10 link-local
121
+ || (h0 & 0xffc0) === 0xfec0 // fec0::/10 deprecated site-local
122
+ || (h0 & 0xff00) === 0xff00; // ff00::/8 multicast
123
+ }
124
+
125
+ /**
126
+ * Converts two hextets holding an embedded IPv4 address into octets.
127
+ *
128
+ * @param high - Hextet containing the first two octets.
129
+ * @param low - Hextet containing the last two octets.
130
+ * @returns Four IPv4 octets.
131
+ */
132
+ function ipv4FromHextets(high: number, low: number): number[] {
133
+ return [high >> 8, high & 0xff, low >> 8, low & 0xff];
134
+ }
135
+
136
+ /**
137
+ * Expands a valid IPv6 literal into eight numeric hextets.
138
+ *
139
+ * Supports `::` compression and a trailing dotted IPv4 suffix.
140
+ *
141
+ * @param address - IPv6 literal already validated by `net.isIP`.
142
+ * @returns Eight hextets, or null when the literal cannot be expanded.
143
+ */
144
+ function expandIpv6(address: string): number[] | null {
145
+ let value = address;
146
+ const dotted = value.match(/^(.*:)(\d+\.\d+\.\d+\.\d+)$/);
147
+
148
+ if (dotted) {
149
+ const octets = dotted[2]!.split('.').map(Number);
150
+ value = `${dotted[1]}${((octets[0]! << 8) | octets[1]!).toString(16)}:${((octets[2]! << 8) | octets[3]!).toString(16)}`;
151
+ }
152
+
153
+ const [head, tail] = value.split('::') as [string, string | undefined];
154
+ const headParts = head ? head.split(':') : [];
155
+ const tailParts = tail === undefined ? [] : tail ? tail.split(':') : [];
156
+ const missing = 8 - headParts.length - tailParts.length;
157
+
158
+ if (tail === undefined ? missing !== 0 : missing < 0) return null;
159
+
160
+ const hextets = [...headParts, ...Array<string>(tail === undefined ? 0 : missing).fill('0'), ...tailParts]
161
+ .map(part => Number.parseInt(part, 16));
162
+
163
+ return hextets.length === 8 && hextets.every(part => Number.isInteger(part) && part >= 0 && part <= 0xffff)
164
+ ? hextets
165
+ : null;
166
+ }
@@ -0,0 +1,49 @@
1
+ import { lookup, type LookupAddress, type LookupOptions } from 'node:dns';
2
+ import { BlockedUrlError } from './BlockedUrlError';
3
+ import { PRIVATE_DESTINATION_MESSAGE } from './assertPublicUrl';
4
+ import { isPrivateIpAddress } from './privateNetworkAddresses';
5
+
6
+ /** Callback shape used by `dns.lookup` for both single and `all` results. */
7
+ type LookupCallback = (error: NodeJS.ErrnoException | null, address: string | LookupAddress[], family?: number) => void;
8
+
9
+ /**
10
+ * DNS lookup that refuses to connect to private, local or reserved addresses.
11
+ *
12
+ * It has the same signature as `dns.lookup`, so it can be passed as the
13
+ * `lookup` socket option of Node's `http`, `https`, `net` and `tls` modules and
14
+ * to clients built on them (for example undici's `connect.lookup` or got's
15
+ * `dnsLookup`). Because the check runs when the socket connects, the address
16
+ * that is validated is the address that is used, which closes the DNS
17
+ * rebinding gap left by checking a hostname before a separate request.
18
+ *
19
+ * Node does not call `lookup` for IP-literal hosts, so literals (including
20
+ * redirect targets) must still be checked with {@link assertPublicUrl}.
21
+ *
22
+ * @param hostname - Hostname being connected to.
23
+ * @param options - Lookup options supplied by the socket layer.
24
+ * @param callback - Receives the validated address(es) or a `BlockedUrlError`.
25
+ *
26
+ * @example
27
+ * import { Agent } from 'undici';
28
+ * const dispatcher = new Agent({ connect: { lookup: publicAddressLookup } });
29
+ */
30
+ export function publicAddressLookup(hostname: string, options: LookupOptions, callback: LookupCallback): void {
31
+ lookup(hostname, { ...options, all: true }, (error, addresses) => {
32
+ if (error) {
33
+ callback(error, []);
34
+ return;
35
+ }
36
+
37
+ if (addresses.length === 0 || addresses.some(entry => isPrivateIpAddress(entry.address))) {
38
+ callback(Object.assign(new BlockedUrlError(PRIVATE_DESTINATION_MESSAGE), { code: 'ERR_BLOCKED_ADDRESS' }), []);
39
+ return;
40
+ }
41
+
42
+ if (options.all) {
43
+ callback(null, addresses);
44
+ return;
45
+ }
46
+
47
+ callback(null, addresses[0]!.address, addresses[0]!.family);
48
+ });
49
+ }
@@ -0,0 +1,105 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { UserIdentity, UserIdentityModel } from '../auth';
3
+ import type { InApp } from '../in-app';
4
+ import type { Mail, MailMessage } from '../mail';
5
+ import type * as notifications from './contracts';
6
+
7
+ /** Coordinates application-owned notification classes with framework channels. */
8
+ export class Notifications<TUser extends UserIdentity = UserIdentity> {
9
+ /**
10
+ * Creates the notification coordinator used by app().notifications.
11
+ *
12
+ * @param inApp - Durable framework inbox service.
13
+ * @param mail - Provider-neutral framework mail service.
14
+ * @param identityModel - Application identity model used to resolve ID recipients.
15
+ */
16
+ constructor(
17
+ private readonly inApp: InApp,
18
+ private readonly mail: Mail,
19
+ private readonly identityModel: UserIdentityModel<TUser>,
20
+ ) {}
21
+
22
+ /**
23
+ * Delivers one notification to one or more users through its `via()` channels.
24
+ *
25
+ * @param recipients - User model, ID, or an array mixing both forms.
26
+ * @param notification - Application-owned routing and channel renderers.
27
+ * @returns One delivery result per distinct resolved user.
28
+ */
29
+ async send(
30
+ recipients: notifications.NotificationRecipient<TUser> | readonly notifications.NotificationRecipient<TUser>[],
31
+ notification: notifications.Notification<TUser>,
32
+ ): Promise<notifications.NotificationDelivery[]> {
33
+ if (!notification.type?.trim()) throw new Error('Notification type is required.');
34
+ const users = await this.#resolveUsers(recipients);
35
+ const deliveries: notifications.NotificationDelivery[] = [];
36
+
37
+ for (const user of users) deliveries.push(await this.#deliver(user, notification));
38
+
39
+ return deliveries;
40
+ }
41
+
42
+ /**
43
+ * Delivers selected channels for one canonical user.
44
+ *
45
+ * @param user - Resolved application identity.
46
+ * @param notification - Application-owned routing and content definition.
47
+ * @returns Accepted in-app and mail outcomes for the recipient.
48
+ */
49
+ async #deliver(user: TUser, notification: notifications.Notification<TUser>): Promise<notifications.NotificationDelivery> {
50
+ const userId = String(user.id);
51
+ const channels = [...new Set(notification.via(user))];
52
+ if (!channels.length) return { userId, inApp: null, mail: null };
53
+ if (channels.some(channel => !['inApp', 'mail'].includes(channel))) throw new Error(`Notification "${notification.type}" selected an unsupported channel.`);
54
+ const scope = notification.scope?.(user) ?? { type: 'account' as const };
55
+ const key = notification.key?.(user);
56
+ let inApp = null;
57
+ let mail = null;
58
+
59
+ if (channels.includes('inApp')) {
60
+ if (!notification.toInApp) throw new Error(`Notification "${notification.type}" selected inApp without a toInApp() renderer.`);
61
+ [inApp] = await this.inApp.send(userId, notification.toInApp(user), { scope, type: notification.type, key });
62
+ }
63
+ if (channels.includes('mail')) {
64
+ if (!notification.toMail) throw new Error(`Notification "${notification.type}" selected mail without a toMail() renderer.`);
65
+ mail = await this.mail.send(withDeliveryKey(notification.toMail(user), notification.type, key, userId));
66
+ }
67
+
68
+ return { userId, inApp, mail };
69
+ }
70
+
71
+ /**
72
+ * Resolves model and ID inputs, rejecting missing identities before delivery.
73
+ *
74
+ * @param recipients - User model, ID, or bounded array of either form.
75
+ * @returns Distinct canonical users in first-seen order.
76
+ */
77
+ async #resolveUsers(recipients: notifications.NotificationRecipient<TUser> | readonly notifications.NotificationRecipient<TUser>[]): Promise<TUser[]> {
78
+ const input = Array.isArray(recipients) ? recipients : [recipients];
79
+ if (input.length > 100) throw new Error('Notifications accepts at most 100 recipients per send.');
80
+ const users = new Map<string, TUser>();
81
+
82
+ for (const recipient of input) {
83
+ const user = typeof recipient === 'string' ? await this.identityModel.find(recipient) : recipient;
84
+ if (!user?.id) throw new Error('Notification recipient is unavailable.');
85
+ users.set(String(user.id), user);
86
+ }
87
+
88
+ return [...users.values()];
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Adds a stable provider retry identity when the renderer did not provide one.
94
+ *
95
+ * @param message - Provider-neutral mail content rendered by the notification.
96
+ * @param type - Stable notification type.
97
+ * @param key - Optional application-owned occurrence key.
98
+ * @param userId - Canonical recipient identity.
99
+ * @returns Original or copied message with an idempotency key when possible.
100
+ */
101
+ function withDeliveryKey(message: MailMessage, type: string, key: string | undefined, userId: string): MailMessage {
102
+ if (message.idempotencyKey || !key) return message;
103
+ const hash = createHash('sha256').update(JSON.stringify([type, key, userId])).digest('hex');
104
+ return { ...message, idempotencyKey: `notification/${hash}` };
105
+ }
@@ -0,0 +1,59 @@
1
+ # Notifications
2
+
3
+ `@db3.ai/app/notifications` coordinates application-owned notification classes
4
+ with the framework's durable in-app and mail services. A notification owns its
5
+ channel policy and renderers; it returns content and never performs delivery.
6
+
7
+ ```ts
8
+ import type { Notification } from '@db3.ai/app/notifications';
9
+ import type { User } from '../models/User.js';
10
+
11
+ class ReportReadyNotification implements Notification<User> {
12
+ readonly type = 'report.ready';
13
+
14
+ constructor(private readonly reportId: string) {}
15
+
16
+ via(user: User) {
17
+ return user.email ? ['inApp', 'mail'] as const : ['inApp'] as const;
18
+ }
19
+
20
+ key(_user: User) {
21
+ return this.reportId;
22
+ }
23
+
24
+ toInApp(_user: User) {
25
+ return { title: 'Your report is ready', body: 'Open the report to review it.' };
26
+ }
27
+
28
+ toMail(user: User) {
29
+ return { to: user.email!, subject: 'Your report is ready', text: 'Open the app to review it.' };
30
+ }
31
+ }
32
+
33
+ await app().notifications.send(user, new ReportReadyNotification(reportId));
34
+ await app().notifications.send(users, new ReportReadyNotification(reportId));
35
+ ```
36
+
37
+ An app may add a small convenience method to its user model:
38
+
39
+ ```ts
40
+ public async notify(notification: Notification<this>) {
41
+ return app().notifications.send(this, notification);
42
+ }
43
+
44
+ await user.notify(new ReportReadyNotification(reportId));
45
+ ```
46
+
47
+ The service resolves IDs through the configured auth identity model, removes
48
+ duplicate recipients, then stores in-app content before submitting email for
49
+ each user. A stable notification `key()` deduplicates inbox retries and supplies
50
+ a stable mail-provider idempotency key.
51
+
52
+ The coordinator does not yet persist a generic per-channel delivery log, resolve
53
+ notification preferences, or discover notification classes automatically.
54
+
55
+ ## Error reporting
56
+
57
+ Use `app().log.error({ err: error }, 'Operation failed')` for operational errors.
58
+ Logging owns configurable email and other transports; application notifications
59
+ remain recipient-facing messages. See [logging](../logging/README.md).