@db3.ai/app 0.1.0-beta.1 → 0.1.0-beta.4

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