@promptbook/cli 0.104.0-1 → 0.104.0-11

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 (229) hide show
  1. package/apps/agents-server/config.ts +1 -3
  2. package/apps/agents-server/next.config.ts +2 -2
  3. package/apps/agents-server/package.json +7 -3
  4. package/apps/agents-server/public/fonts/OpenMoji-color-cbdt.woff2 +0 -0
  5. package/apps/agents-server/public/swagger.json +115 -0
  6. package/apps/agents-server/scripts/generate-reserved-paths/generate-reserved-paths.ts +54 -0
  7. package/apps/agents-server/scripts/generate-reserved-paths/tsconfig.json +19 -0
  8. package/apps/agents-server/src/app/AddAgentButton.tsx +47 -21
  9. package/apps/agents-server/src/app/actions.ts +22 -5
  10. package/apps/agents-server/src/app/admin/browser-test/BrowserTestClient.tsx +211 -0
  11. package/apps/agents-server/src/app/admin/browser-test/page.tsx +13 -0
  12. package/apps/agents-server/src/app/admin/chat-feedback/ChatFeedbackClient.tsx +221 -274
  13. package/apps/agents-server/src/app/admin/chat-history/ChatHistoryClient.tsx +94 -137
  14. package/apps/agents-server/src/app/admin/files/FilesGalleryClient.tsx +263 -0
  15. package/apps/agents-server/src/app/admin/files/actions.ts +61 -0
  16. package/apps/agents-server/src/app/admin/files/page.tsx +13 -0
  17. package/apps/agents-server/src/app/admin/image-generator-test/ImageGeneratorTestClient.tsx +169 -0
  18. package/apps/agents-server/src/app/admin/image-generator-test/page.tsx +13 -0
  19. package/apps/agents-server/src/app/admin/images/ImagesGalleryClient.tsx +256 -0
  20. package/apps/agents-server/src/app/admin/images/actions.ts +60 -0
  21. package/apps/agents-server/src/app/admin/images/page.tsx +13 -0
  22. package/apps/agents-server/src/app/admin/messages/MessagesClient.tsx +294 -0
  23. package/apps/agents-server/src/app/admin/messages/page.tsx +13 -0
  24. package/apps/agents-server/src/app/admin/messages/send-email/SendEmailClient.tsx +104 -0
  25. package/apps/agents-server/src/app/admin/messages/send-email/actions.ts +35 -0
  26. package/apps/agents-server/src/app/admin/messages/send-email/page.tsx +13 -0
  27. package/apps/agents-server/src/app/admin/metadata/MetadataClient.tsx +23 -19
  28. package/apps/agents-server/src/app/admin/search-engine-test/SearchEngineTestClient.tsx +109 -0
  29. package/apps/agents-server/src/app/admin/search-engine-test/actions.ts +17 -0
  30. package/apps/agents-server/src/app/admin/search-engine-test/page.tsx +13 -0
  31. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +15 -1
  32. package/apps/agents-server/src/app/agents/[agentName]/AgentOptionsMenu.tsx +51 -9
  33. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileChat.tsx +47 -4
  34. package/apps/agents-server/src/app/agents/[agentName]/AgentProfileWrapper.tsx +53 -11
  35. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +23 -3
  36. package/apps/agents-server/src/app/agents/[agentName]/agentLinks.tsx +8 -8
  37. package/apps/agents-server/src/app/agents/[agentName]/api/agents/route.ts +17 -26
  38. package/apps/agents-server/src/app/agents/[agentName]/api/book/route.ts +4 -2
  39. package/apps/agents-server/src/app/agents/[agentName]/api/chat/route.ts +20 -0
  40. package/apps/agents-server/src/app/agents/[agentName]/api/mcp/route.ts +6 -11
  41. package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +5 -1
  42. package/apps/agents-server/src/app/agents/[agentName]/api/voice/route.ts +5 -2
  43. package/apps/agents-server/src/app/agents/[agentName]/book/BookEditorWrapper.tsx +20 -16
  44. package/apps/agents-server/src/app/agents/[agentName]/book/page.tsx +15 -2
  45. package/apps/agents-server/src/app/agents/[agentName]/book+chat/page.tsx +15 -2
  46. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +12 -0
  47. package/apps/agents-server/src/app/agents/[agentName]/code/api/route.ts +68 -0
  48. package/apps/agents-server/src/app/agents/[agentName]/code/page.tsx +223 -0
  49. package/apps/agents-server/src/app/agents/[agentName]/generateAgentMetadata.ts +5 -0
  50. package/apps/agents-server/src/app/agents/[agentName]/history/actions.ts +2 -2
  51. package/apps/agents-server/src/app/agents/[agentName]/history/page.tsx +10 -3
  52. package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/getAgentDefaultAvatarPrompt.ts +31 -0
  53. package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/route.ts +194 -0
  54. package/apps/agents-server/src/app/agents/[agentName]/images/icon-256.png/route.tsx +14 -2
  55. package/apps/agents-server/src/app/agents/[agentName]/images/page.tsx +200 -0
  56. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-fullhd.png/route.tsx +4 -3
  57. package/apps/agents-server/src/app/agents/[agentName]/images/screenshot-phone.png/route.tsx +4 -3
  58. package/apps/agents-server/src/app/agents/[agentName]/integration/page.tsx +10 -3
  59. package/apps/agents-server/src/app/agents/[agentName]/links/page.tsx +11 -4
  60. package/apps/agents-server/src/app/agents/[agentName]/opengraph-image.tsx +11 -2
  61. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +18 -10
  62. package/apps/agents-server/src/app/agents/[agentName]/system-message/page.tsx +100 -0
  63. package/apps/agents-server/src/app/api/admin-email/route.ts +12 -0
  64. package/apps/agents-server/src/app/api/agents/[agentName]/clone/route.ts +13 -14
  65. package/apps/agents-server/src/app/api/agents/[agentName]/restore/route.ts +20 -0
  66. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +43 -1
  67. package/apps/agents-server/src/app/api/agents/route.ts +28 -3
  68. package/apps/agents-server/src/app/api/api-tokens/route.ts +6 -7
  69. package/apps/agents-server/src/app/api/browser-test/act/route.ts +141 -0
  70. package/apps/agents-server/src/app/api/browser-test/screenshot/route.ts +30 -0
  71. package/apps/agents-server/src/app/api/browser-test/scroll-facebook/route.ts +62 -0
  72. package/apps/agents-server/src/app/api/docs/book.md/route.ts +61 -0
  73. package/apps/agents-server/src/app/api/emails/incoming/sendgrid/route.ts +48 -0
  74. package/apps/agents-server/src/app/api/federated-agents/route.ts +12 -0
  75. package/apps/agents-server/src/app/api/images/[filename]/route.ts +128 -0
  76. package/apps/agents-server/src/app/api/messages/route.ts +102 -0
  77. package/apps/agents-server/src/app/api/metadata/route.ts +5 -6
  78. package/apps/agents-server/src/app/api/upload/route.ts +128 -45
  79. package/apps/agents-server/src/app/docs/[docId]/page.tsx +2 -3
  80. package/apps/agents-server/src/app/docs/page.tsx +12 -12
  81. package/apps/agents-server/src/app/globals.css +140 -33
  82. package/apps/agents-server/src/app/humans.txt/route.ts +1 -1
  83. package/apps/agents-server/src/app/layout.tsx +27 -22
  84. package/apps/agents-server/src/app/page.tsx +54 -6
  85. package/apps/agents-server/src/app/recycle-bin/actions.ts +20 -14
  86. package/apps/agents-server/src/app/recycle-bin/page.tsx +27 -41
  87. package/apps/agents-server/src/app/robots.txt/route.ts +1 -1
  88. package/apps/agents-server/src/app/security.txt/route.ts +1 -1
  89. package/apps/agents-server/src/app/sitemap.xml/route.ts +9 -7
  90. package/apps/agents-server/src/app/swagger/page.tsx +14 -0
  91. package/apps/agents-server/src/components/AgentProfile/AgentCapabilityChips.tsx +38 -0
  92. package/apps/agents-server/src/components/AgentProfile/AgentProfile.tsx +44 -116
  93. package/apps/agents-server/src/components/AgentProfile/AgentProfileImage.tsx +92 -0
  94. package/apps/agents-server/src/components/AgentProfile/QrCodeModal.tsx +0 -1
  95. package/apps/agents-server/src/components/AgentProfile/useAgentBackground.ts +97 -0
  96. package/apps/agents-server/src/components/Auth/AuthControls.tsx +5 -4
  97. package/apps/agents-server/src/components/DeletedAgentBanner.tsx +26 -0
  98. package/apps/agents-server/src/components/DocsToolbar/DocsToolbar.tsx +38 -0
  99. package/apps/agents-server/src/components/DocumentationContent/DocumentationContent.tsx +11 -9
  100. package/apps/agents-server/src/components/Footer/Footer.tsx +5 -5
  101. package/apps/agents-server/src/components/ForgottenPasswordDialog/ForgottenPasswordDialog.tsx +61 -0
  102. package/apps/agents-server/src/components/Header/Header.tsx +130 -40
  103. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +150 -23
  104. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +93 -15
  105. package/apps/agents-server/src/components/Homepage/DeletedAgentsList.tsx +66 -0
  106. package/apps/agents-server/src/components/Homepage/ExternalAgentsSection.tsx +12 -3
  107. package/apps/agents-server/src/components/Homepage/ExternalAgentsSectionClient.tsx +19 -10
  108. package/apps/agents-server/src/components/LayoutWrapper/LayoutWrapper.tsx +3 -2
  109. package/apps/agents-server/src/components/LoginForm/LoginForm.tsx +50 -1
  110. package/apps/agents-server/src/components/NewAgentDialog/NewAgentDialog.tsx +88 -0
  111. package/apps/agents-server/src/components/NotFoundPage/NotFoundPage.tsx +7 -2
  112. package/apps/agents-server/src/components/OpenMojiIcon/OpenMojiIcon.tsx +16 -7
  113. package/apps/agents-server/src/components/PrintHeader/PrintHeader.tsx +4 -4
  114. package/apps/agents-server/src/components/RegisterUserDialog/RegisterUserDialog.tsx +61 -0
  115. package/apps/agents-server/src/components/VercelDeploymentCard/VercelDeploymentCard.tsx +2 -0
  116. package/apps/agents-server/src/components/_utils/generateMetaTxt.ts +12 -10
  117. package/apps/agents-server/src/components/_utils/headlessParam.tsx +7 -3
  118. package/apps/agents-server/src/database/$getTableName.ts +1 -0
  119. package/apps/agents-server/src/database/$provideSupabaseForBrowser.ts +3 -3
  120. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +1 -1
  121. package/apps/agents-server/src/database/$provideSupabaseForWorker.ts +3 -3
  122. package/apps/agents-server/src/database/metadataDefaults.ts +19 -1
  123. package/apps/agents-server/src/database/migrate.ts +34 -1
  124. package/apps/agents-server/src/database/migrations/2025-11-0001-initial-schema.sql +1 -3
  125. package/apps/agents-server/src/database/migrations/2025-11-0002-metadata-table.sql +1 -3
  126. package/apps/agents-server/src/database/migrations/2025-12-0240-agent-public-id.sql +3 -0
  127. package/apps/agents-server/src/database/migrations/2025-12-0360-agent-deleted-at.sql +1 -0
  128. package/apps/agents-server/src/database/migrations/2025-12-0370-image-table.sql +19 -0
  129. package/apps/agents-server/src/database/migrations/2025-12-0380-agent-visibility.sql +1 -0
  130. package/apps/agents-server/src/database/migrations/2025-12-0390-upload-tracking.sql +20 -0
  131. package/apps/agents-server/src/database/migrations/2025-12-0401-file-upload-status.sql +13 -0
  132. package/apps/agents-server/src/database/migrations/2025-12-0402-message-table.sql +42 -0
  133. package/apps/agents-server/src/database/migrations/2025-12-0403-generation-lock-table.sql +15 -0
  134. package/apps/agents-server/src/database/migrations/2025-12-0640-openai-assistant-cache.sql +12 -0
  135. package/apps/agents-server/src/database/migrations/2025-12-0820-agent-history-permanent-id.sql +29 -0
  136. package/apps/agents-server/src/database/migrations/2025-12-0830-image-purpose.sql +5 -0
  137. package/apps/agents-server/src/database/migrations/2025-12-0890-file-agent-id.sql +5 -0
  138. package/apps/agents-server/src/database/schema.ts +244 -4
  139. package/apps/agents-server/src/generated/reservedPaths.ts +32 -0
  140. package/apps/agents-server/src/message-providers/email/_common/Email.ts +73 -0
  141. package/apps/agents-server/src/message-providers/email/_common/utils/TODO.txt +1 -0
  142. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.test.ts.todo +108 -0
  143. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddress.ts +62 -0
  144. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.test.ts.todo +117 -0
  145. package/apps/agents-server/src/message-providers/email/_common/utils/parseEmailAddresses.ts +19 -0
  146. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.test.ts.todo +119 -0
  147. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddress.ts +19 -0
  148. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.test.ts.todo +74 -0
  149. package/apps/agents-server/src/message-providers/email/_common/utils/stringifyEmailAddresses.ts +14 -0
  150. package/apps/agents-server/src/message-providers/email/sendgrid/SendgridMessageProvider.ts +44 -0
  151. package/apps/agents-server/src/message-providers/email/sendgrid/parseInboundSendgridEmail.ts +49 -0
  152. package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +51 -0
  153. package/apps/agents-server/src/message-providers/index.ts +13 -0
  154. package/apps/agents-server/src/message-providers/interfaces/MessageProvider.ts +11 -0
  155. package/apps/agents-server/src/middleware.ts +19 -23
  156. package/apps/agents-server/src/tools/$provideBrowserForServer.ts +32 -0
  157. package/apps/agents-server/src/tools/$provideCdnForServer.ts +7 -2
  158. package/apps/agents-server/src/utils/auth.ts +117 -17
  159. package/apps/agents-server/src/utils/cdn/classes/TrackedFilesStorage.ts +57 -0
  160. package/apps/agents-server/src/utils/cdn/classes/VercelBlobStorage.ts +4 -0
  161. package/apps/agents-server/src/utils/cdn/interfaces/IFilesStorage.ts +18 -0
  162. package/apps/agents-server/src/utils/content/extractBodyContentFromHtml.ts +19 -0
  163. package/apps/agents-server/src/utils/getUserIdFromRequest.ts +35 -0
  164. package/apps/agents-server/src/utils/handleChatCompletion.ts +65 -5
  165. package/apps/agents-server/src/utils/messages/sendMessage.ts +91 -0
  166. package/apps/agents-server/src/utils/messagesAdmin.ts +72 -0
  167. package/apps/agents-server/src/utils/normalization/filenameToPrompt.test.ts +36 -0
  168. package/apps/agents-server/src/utils/normalization/filenameToPrompt.ts +25 -0
  169. package/apps/agents-server/src/utils/validateApiKey.ts +7 -11
  170. package/esm/index.es.js +1534 -1330
  171. package/esm/index.es.js.map +1 -1
  172. package/esm/typings/servers.d.ts +8 -0
  173. package/esm/typings/src/_packages/core.index.d.ts +2 -0
  174. package/esm/typings/src/_packages/types.index.d.ts +16 -2
  175. package/esm/typings/src/book-2.0/agent-source/AgentBasicInformation.d.ts +29 -1
  176. package/esm/typings/src/book-2.0/agent-source/createAgentModelRequirements.d.ts +6 -6
  177. package/esm/typings/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments.closed.test.d.ts +1 -0
  178. package/esm/typings/src/book-2.0/utils/generatePlaceholderAgentProfileImageUrl.d.ts +3 -3
  179. package/esm/typings/src/book-components/Chat/Chat/ChatMessageItem.d.ts +5 -1
  180. package/esm/typings/src/book-components/Chat/Chat/ChatProps.d.ts +5 -0
  181. package/esm/typings/src/book-components/Chat/CodeBlock/CodeBlock.d.ts +13 -0
  182. package/esm/typings/src/book-components/Chat/MarkdownContent/MarkdownContent.d.ts +1 -0
  183. package/esm/typings/src/book-components/Chat/types/ChatMessage.d.ts +9 -13
  184. package/esm/typings/src/book-components/_common/Dropdown/Dropdown.d.ts +3 -3
  185. package/esm/typings/src/book-components/_common/HamburgerMenu/HamburgerMenu.d.ts +1 -1
  186. package/esm/typings/src/book-components/_common/MenuHoisting/MenuHoistingContext.d.ts +56 -0
  187. package/esm/typings/src/book-components/icons/AboutIcon.d.ts +1 -1
  188. package/esm/typings/src/book-components/icons/AttachmentIcon.d.ts +1 -1
  189. package/esm/typings/src/book-components/icons/CameraIcon.d.ts +1 -1
  190. package/esm/typings/src/book-components/icons/DownloadIcon.d.ts +1 -1
  191. package/esm/typings/src/book-components/icons/MenuIcon.d.ts +1 -1
  192. package/esm/typings/src/book-components/icons/SaveIcon.d.ts +1 -1
  193. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -12
  194. package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +27 -15
  195. package/esm/typings/src/commitments/DICTIONARY/DICTIONARY.d.ts +46 -0
  196. package/esm/typings/src/commitments/index.d.ts +2 -1
  197. package/esm/typings/src/llm-providers/_common/utils/count-total-usage/countUsage.d.ts +1 -1
  198. package/esm/typings/src/llm-providers/_multiple/MultipleLlmExecutionTools.d.ts +6 -2
  199. package/esm/typings/src/llm-providers/agent/Agent.d.ts +6 -1
  200. package/esm/typings/src/llm-providers/agent/AgentLlmExecutionTools.d.ts +1 -1
  201. package/esm/typings/src/llm-providers/ollama/OllamaExecutionTools.d.ts +1 -1
  202. package/esm/typings/src/llm-providers/openai/createOpenAiCompatibleExecutionTools.d.ts +1 -1
  203. package/esm/typings/src/llm-providers/remote/RemoteLlmExecutionTools.d.ts +1 -0
  204. package/esm/typings/src/remote-server/ui/ServerApp.d.ts +1 -1
  205. package/esm/typings/src/search-engines/SearchEngine.d.ts +9 -0
  206. package/esm/typings/src/search-engines/SearchResult.d.ts +18 -0
  207. package/esm/typings/src/search-engines/bing/BingSearchEngine.d.ts +15 -0
  208. package/esm/typings/src/search-engines/dummy/DummySearchEngine.d.ts +15 -0
  209. package/esm/typings/src/types/Message.d.ts +49 -0
  210. package/esm/typings/src/types/ModelRequirements.d.ts +38 -14
  211. package/esm/typings/src/types/typeAliases.d.ts +23 -1
  212. package/esm/typings/src/utils/color/utils/colorToDataUrl.d.ts +2 -1
  213. package/esm/typings/src/utils/environment/$detectRuntimeEnvironment.d.ts +4 -4
  214. package/esm/typings/src/utils/environment/$isRunningInBrowser.d.ts +1 -1
  215. package/esm/typings/src/utils/environment/$isRunningInJest.d.ts +1 -1
  216. package/esm/typings/src/utils/environment/$isRunningInNode.d.ts +1 -1
  217. package/esm/typings/src/utils/environment/$isRunningInWebWorker.d.ts +1 -1
  218. package/esm/typings/src/utils/markdown/extractAllBlocksFromMarkdown.d.ts +2 -2
  219. package/esm/typings/src/utils/markdown/extractOneBlockFromMarkdown.d.ts +2 -2
  220. package/esm/typings/src/utils/random/$randomAgentPersona.d.ts +3 -2
  221. package/esm/typings/src/utils/random/$randomBase58.d.ts +12 -0
  222. package/esm/typings/src/version.d.ts +1 -1
  223. package/package.json +1 -1
  224. package/umd/index.umd.js +1542 -1338
  225. package/umd/index.umd.js.map +1 -1
  226. package/apps/agents-server/package-lock.json +0 -27
  227. package/apps/agents-server/public/fonts/download-font.js +0 -22
  228. package/apps/agents-server/src/components/PrintButton/PrintButton.tsx +0 -18
  229. package/esm/typings/src/book-2.0/utils/generateGravatarUrl.d.ts +0 -10
@@ -0,0 +1,32 @@
1
+ import { locateChrome } from 'locate-app';
2
+ import { join } from 'path';
3
+ import { BrowserContext, chromium } from 'playwright';
4
+
5
+ /**
6
+ * Cache of browser instance
7
+ *
8
+ * @private internal cache for `$provideBrowserForServer`
9
+ */
10
+ let browserInstance: BrowserContext | null = null;
11
+
12
+ /**
13
+ * @@@
14
+ */
15
+ export async function $provideBrowserForServer(): Promise<BrowserContext> {
16
+ if (browserInstance !== null && browserInstance.browser() && browserInstance.browser()!.isConnected()) {
17
+ return browserInstance;
18
+ }
19
+
20
+ console.log('Launching new browser instance...');
21
+ browserInstance = await chromium.launchPersistentContext(
22
+ join(process.cwd(), '.promptbook', 'puppeteer', 'user-data'),
23
+ {
24
+ executablePath: await locateChrome(),
25
+ headless: false,
26
+ // defaultViewport: null,
27
+ // downloadsPath
28
+ },
29
+ );
30
+
31
+ return browserInstance;
32
+ }
@@ -1,3 +1,5 @@
1
+ import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
2
+ import { TrackedFilesStorage } from '../utils/cdn/classes/TrackedFilesStorage';
1
3
  import { VercelBlobStorage } from '../utils/cdn/classes/VercelBlobStorage';
2
4
  import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
3
5
 
@@ -9,16 +11,19 @@ import { IIFilesStorageWithCdn } from '../utils/cdn/interfaces/IFilesStorage';
9
11
  let cdn: IIFilesStorageWithCdn | null = null;
10
12
 
11
13
  /**
12
- * [🐱‍🚀]
14
+ * @@@
13
15
  */
14
16
  export function $provideCdnForServer(): IIFilesStorageWithCdn {
15
17
  if (!cdn) {
16
- cdn = new VercelBlobStorage({
18
+ const inner = new VercelBlobStorage({
17
19
  token: process.env.VERCEL_BLOB_READ_WRITE_TOKEN!,
18
20
  pathPrefix: process.env.NEXT_PUBLIC_CDN_PATH_PREFIX!,
19
21
  cdnPublicUrl: new URL(process.env.NEXT_PUBLIC_CDN_PUBLIC_URL!),
20
22
  });
21
23
 
24
+ const supabase = $provideSupabaseForServer();
25
+ cdn = new TrackedFilesStorage(inner, supabase);
26
+
22
27
  /*
23
28
  cdn = new DigitalOceanSpaces({
24
29
  bucket: process.env.CDN_BUCKET!,
@@ -1,33 +1,133 @@
1
- import { randomBytes, scrypt, timingSafeEqual } from 'crypto';
1
+ import { createHash, randomBytes, scrypt, timingSafeEqual } from 'crypto';
2
2
  import { promisify } from 'util';
3
+ import { PASSWORD_SECURITY_CONFIG } from '../../../../security.config';
3
4
 
4
5
  const scryptAsync = promisify(scrypt);
5
6
 
6
7
  /**
7
- * Hashes a password using scrypt
8
- *
9
- * @param password The plain text password
8
+ * Validates password input to prevent edge cases and DoS attacks
9
+ *
10
+ * @param password The password to validate
11
+ * @throws Error if password is invalid
12
+ */
13
+ function validatePasswordInput(password: string): void {
14
+ if (typeof password !== 'string') {
15
+ throw new Error('Password must be a string');
16
+ }
17
+ if (password.length === 0) {
18
+ throw new Error('Password cannot be empty');
19
+ }
20
+ if (password.length < PASSWORD_SECURITY_CONFIG.MIN_PASSWORD_LENGTH) {
21
+ throw new Error(`Password must be at least ${PASSWORD_SECURITY_CONFIG.MIN_PASSWORD_LENGTH} characters`);
22
+ }
23
+ // Note: No hard max limit - long passwords are compacted via compactPassword()
24
+ }
25
+
26
+ /**
27
+ * Compacts a password for secure processing
28
+ *
29
+ * If the password is within MAX_PASSWORD_LENGTH, it is returned as-is.
30
+ * If longer, the password is split at MAX_PASSWORD_LENGTH and the second part
31
+ * is hashed with SHA256 before being appended to the first part.
32
+ *
33
+ * This prevents DoS attacks via extremely long passwords while still utilizing
34
+ * the full entropy of longer passwords.
35
+ *
36
+ * @param password The password to compact
37
+ * @returns The compacted password
38
+ */
39
+ function compactPassword(password: string): string {
40
+ if (password.length <= PASSWORD_SECURITY_CONFIG.MAX_PASSWORD_LENGTH) {
41
+ return password;
42
+ }
43
+
44
+ const firstPart = password.slice(0, PASSWORD_SECURITY_CONFIG.MAX_PASSWORD_LENGTH);
45
+ const secondPart = password.slice(PASSWORD_SECURITY_CONFIG.MAX_PASSWORD_LENGTH);
46
+
47
+ // Hash the overflow part with SHA256 to bound its length while preserving entropy
48
+ const secondPartHash = createHash('sha256').update(secondPart, 'utf8').digest('hex');
49
+
50
+ return firstPart + secondPartHash;
51
+ }
52
+
53
+ /**
54
+ * Hashes a password using scrypt with secure parameters
55
+ *
56
+ * @param password The plain text password (minimum 8 characters, no maximum - long passwords are compacted)
10
57
  * @returns The salt and hash formatted as "salt:hash"
58
+ * @throws Error if password validation fails
11
59
  */
12
60
  export async function hashPassword(password: string): Promise<string> {
13
- const salt = randomBytes(16).toString('hex');
14
- const derivedKey = (await scryptAsync(password, salt, 64)) as Buffer;
61
+ validatePasswordInput(password);
62
+
63
+ // Compact long passwords to prevent DoS while preserving entropy
64
+ const compactedPassword = compactPassword(password);
65
+
66
+ const salt = randomBytes(PASSWORD_SECURITY_CONFIG.SALT_LENGTH).toString('hex');
67
+ const derivedKey = (await scryptAsync(compactedPassword, salt, PASSWORD_SECURITY_CONFIG.KEY_LENGTH)) as Buffer;
68
+
69
+ // Clear password from memory as soon as possible (best effort)
70
+ // Note: JavaScript strings are immutable, so this is limited in effectiveness
15
71
  return `${salt}:${derivedKey.toString('hex')}`;
16
72
  }
17
73
 
18
74
  /**
19
- * Verifies a password against a stored hash
20
- *
21
- * @param password The plain text password
75
+ * Verifies a password against a stored hash using constant-time comparison
76
+ *
77
+ * @param password The plain text password to verify
22
78
  * @param storedHash The stored hash in format "salt:hash"
23
- * @returns True if the password matches
79
+ * @returns True if the password matches, false otherwise
24
80
  */
25
81
  export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
26
- const [salt, key] = storedHash.split(':');
27
- if (!salt || !key) return false;
28
-
29
- const derivedKey = (await scryptAsync(password, salt, 64)) as Buffer;
30
- const keyBuffer = Buffer.from(key, 'hex');
31
-
32
- return timingSafeEqual(derivedKey, keyBuffer);
82
+ // Validate inputs
83
+ if (typeof password !== 'string' || typeof storedHash !== 'string') {
84
+ return false;
85
+ }
86
+
87
+ if (password.length === 0) {
88
+ return false;
89
+ }
90
+
91
+ // Compact long passwords the same way as during hashing
92
+ const compactedPassword = compactPassword(password);
93
+
94
+ const parts = storedHash.split(':');
95
+ if (parts.length !== 2) {
96
+ return false;
97
+ }
98
+
99
+ const [salt, key] = parts;
100
+ if (!salt || !key) {
101
+ return false;
102
+ }
103
+
104
+ // Validate salt and key format (should be hex strings of expected length)
105
+ const expectedSaltLength = PASSWORD_SECURITY_CONFIG.SALT_LENGTH * 2; // hex encoding doubles length
106
+ const expectedKeyLength = PASSWORD_SECURITY_CONFIG.KEY_LENGTH * 2;
107
+
108
+ if (salt.length !== expectedSaltLength || key.length !== expectedKeyLength) {
109
+ return false;
110
+ }
111
+
112
+ // Validate hex format
113
+ const hexRegex = /^[0-9a-fA-F]+$/;
114
+ if (!hexRegex.test(salt) || !hexRegex.test(key)) {
115
+ return false;
116
+ }
117
+
118
+ try {
119
+ const derivedKey = (await scryptAsync(compactedPassword, salt, PASSWORD_SECURITY_CONFIG.KEY_LENGTH)) as Buffer;
120
+ const keyBuffer = Buffer.from(key, 'hex');
121
+
122
+ // Ensure buffers are same length before timing-safe comparison
123
+ // This should always be true given our validation, but defense in depth
124
+ if (derivedKey.length !== keyBuffer.length) {
125
+ return false;
126
+ }
127
+
128
+ return timingSafeEqual(derivedKey, keyBuffer);
129
+ } catch {
130
+ // Any error during verification should return false, not leak information
131
+ return false;
132
+ }
33
133
  }
@@ -0,0 +1,57 @@
1
+ import { SupabaseClient } from '@supabase/supabase-js';
2
+ import { $getTableName } from '../../../database/$getTableName';
3
+ import { AgentsServerDatabase } from '../../../database/schema';
4
+ import type { IFile, IIFilesStorageWithCdn } from '../interfaces/IFilesStorage';
5
+
6
+ export class TrackedFilesStorage implements IIFilesStorageWithCdn {
7
+ public constructor(
8
+ private readonly inner: IIFilesStorageWithCdn,
9
+ private readonly supabase: SupabaseClient<AgentsServerDatabase>,
10
+ ) {}
11
+
12
+ public get cdnPublicUrl(): URL {
13
+ return this.inner.cdnPublicUrl;
14
+ }
15
+
16
+ public get pathPrefix(): string | undefined {
17
+ return this.inner.pathPrefix;
18
+ }
19
+
20
+ public getItemUrl(key: string): URL {
21
+ return this.inner.getItemUrl(key);
22
+ }
23
+
24
+ public async getItem(key: string): Promise<IFile | null> {
25
+ return this.inner.getItem(key);
26
+ }
27
+
28
+ public async removeItem(key: string): Promise<void> {
29
+ return this.inner.removeItem(key);
30
+ }
31
+
32
+ public async setItem(key: string, file: IFile): Promise<void> {
33
+ console.log('!!! 0', { key, file });
34
+
35
+ await this.inner.setItem(key, file);
36
+
37
+ try {
38
+ const { userId, purpose } = file;
39
+ const cdnUrl = this.getItemUrl(key).href;
40
+
41
+ console.log('!!! 1', { userId, purpose, cdnUrl, key, file });
42
+
43
+ await this.supabase.from(await $getTableName('File')).insert({
44
+ userId: userId || null,
45
+ fileName: key,
46
+ fileSize: file.fileSize ?? file.data.length,
47
+ fileType: file.type,
48
+ cdnUrl,
49
+ purpose: purpose || 'UNKNOWN',
50
+ });
51
+
52
+ console.log('!!! 2', { userId, purpose, cdnUrl, key, file });
53
+ } catch (error) {
54
+ console.error('Failed to track upload:', error);
55
+ }
56
+ }
57
+ }
@@ -14,6 +14,10 @@ export class VercelBlobStorage implements IIFilesStorageWithCdn {
14
14
  return this.config.cdnPublicUrl;
15
15
  }
16
16
 
17
+ public get pathPrefix() {
18
+ return this.config.pathPrefix;
19
+ }
20
+
17
21
  public constructor(private readonly config: IVercelBlobStorageConfig) {}
18
22
 
19
23
  public getItemUrl(key: string): URL {
@@ -5,6 +5,23 @@ export type IFile = {
5
5
  // Maybe TODO name: string_name;
6
6
  type: string_mime_type;
7
7
  data: Buffer;
8
+
9
+ /**
10
+ * User who uploaded the file
11
+ */
12
+ userId?: number;
13
+
14
+ /**
15
+ * Purpose of the upload (e.g. KNOWLEDGE, SERVER_FAVICON_URL)
16
+ */
17
+ purpose?: string;
18
+
19
+ /**
20
+ * Size of the file in bytes
21
+ *
22
+ * Note: This is optional, if not provided, the size of the buffer is used
23
+ */
24
+ fileSize?: number;
8
25
  };
9
26
 
10
27
  /**
@@ -17,6 +34,7 @@ export type IFilesStorage = Omit<IStorage<IFile>, 'length' | 'clear' | 'key'>;
17
34
  */
18
35
  export type IIFilesStorageWithCdn = IFilesStorage & {
19
36
  readonly cdnPublicUrl: URL;
37
+ readonly pathPrefix?: string;
20
38
  getItemUrl(key: string): URL;
21
39
  };
22
40
 
@@ -0,0 +1,19 @@
1
+ import type { string_html } from '@promptbook-local/types';
2
+
3
+ /**
4
+ * Extract the first heading from HTML
5
+ *
6
+ * @param contentText HTML
7
+ * @returns heading
8
+ */
9
+ export function extractBodyContentFromHtml(html: string_html): string_html {
10
+ // Note: Not using DOMParser, because it's overkill for this simple task
11
+
12
+ const match = html.match(/<body[^>]*>(?<bodyContent>[\s\S]*)<\/body>/s);
13
+
14
+ if (!match) {
15
+ return html;
16
+ }
17
+
18
+ return match.groups!.bodyContent!;
19
+ }
@@ -0,0 +1,35 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { keepUnused } from '../../../../src/utils/organization/keepUnused';
3
+ import { $getTableName } from '../database/$getTableName';
4
+ import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
5
+ import { getSession } from './session';
6
+
7
+ export async function getUserIdFromRequest(request: NextRequest): Promise<number | null> {
8
+ keepUnused(request); // Unused because we get user from session cookie for now
9
+
10
+ try {
11
+ // 1. Try to get user from session (cookie)
12
+ const session = await getSession();
13
+ if (session && session.username) {
14
+ const supabase = $provideSupabaseForServer();
15
+ const { data } = await supabase
16
+ .from(await $getTableName('User'))
17
+ .select('id')
18
+ .eq('username', session.username)
19
+ .single();
20
+
21
+ if (data) {
22
+ return data.id;
23
+ }
24
+ }
25
+
26
+ // 2. Try to get user from API key (Authorization header)
27
+ // TODO: [🧠] Implement linking API keys to users if needed
28
+ // const authHeader = request.headers.get('authorization');
29
+ // ...
30
+ } catch (error) {
31
+ console.error('Error getting user ID from request:', error);
32
+ }
33
+
34
+ return null;
35
+ }
@@ -2,10 +2,11 @@ import { $getTableName } from '@/src/database/$getTableName';
2
2
  import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
3
3
  import { $provideAgentCollectionForServer } from '@/src/tools/$provideAgentCollectionForServer';
4
4
  import { $provideOpenAiAssistantExecutionToolsForServer } from '@/src/tools/$provideOpenAiAssistantExecutionToolsForServer';
5
- import { Agent, computeAgentHash, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
5
+ import { Agent, computeAgentHash, parseAgentSource, PROMPTBOOK_ENGINE_VERSION } from '@promptbook-local/core';
6
6
  import { ChatMessage, ChatPromptResult, Prompt, string_book, TODO_any } from '@promptbook-local/types';
7
7
  import { computeHash } from '@promptbook-local/utils';
8
8
  import { NextRequest, NextResponse } from 'next/server';
9
+ import { isAgentDeleted } from '../app/agents/[agentName]/_utils';
9
10
  import { validateApiKey } from './validateApiKey';
10
11
 
11
12
  export async function handleChatCompletion(
@@ -60,6 +61,19 @@ export async function handleChatCompletion(
60
61
  );
61
62
  }
62
63
 
64
+ // Check if agent is deleted
65
+ if (await isAgentDeleted(agentName)) {
66
+ return NextResponse.json(
67
+ {
68
+ error: {
69
+ message: 'This agent has been deleted. You can restore it from the Recycle Bin.',
70
+ type: 'agent_deleted',
71
+ },
72
+ },
73
+ { status: 410 }, // Gone - indicates the resource is no longer available
74
+ );
75
+ }
76
+
63
77
  const collection = await $provideAgentCollectionForServer();
64
78
  let agentSource: string_book;
65
79
  try {
@@ -99,7 +113,54 @@ export async function handleChatCompletion(
99
113
  );
100
114
  }
101
115
 
102
- const openAiAssistantExecutionTools = await $provideOpenAiAssistantExecutionToolsForServer();
116
+ const agentHash = computeAgentHash(agentSource);
117
+ const supabase = $provideSupabaseForServer();
118
+ const { data: assistantCache } = await supabase
119
+ .from(await $getTableName('OpenAiAssistantCache'))
120
+ .select('assistantId')
121
+ .eq('agentHash', agentHash)
122
+ .single();
123
+
124
+ let openAiAssistantExecutionTools = await $provideOpenAiAssistantExecutionToolsForServer();
125
+
126
+ if (assistantCache?.assistantId) {
127
+ console.log(
128
+ `[🐱‍🚀] Reusing assistant ${assistantCache.assistantId} for agent ${agentName} (hash: ${agentHash})`,
129
+ );
130
+ openAiAssistantExecutionTools = openAiAssistantExecutionTools.getAssistant(assistantCache.assistantId);
131
+ } else {
132
+ console.log(`[🐱‍🚀] Creating NEW assistant for agent ${agentName} (hash: ${agentHash})`);
133
+ // Parse to get instructions and name
134
+ const parsed = parseAgentSource(agentSource);
135
+ const name = parsed.agentName || agentName;
136
+ // Extract PERSONA
137
+ const baseInstructions = parsed.personaDescription || 'You are a helpful assistant.';
138
+
139
+ // Note: Append context to instructions
140
+ const contextLines = agentSource.split('\n').filter((line) => line.startsWith('CONTEXT '));
141
+ const contextInstructions = contextLines.join('\n');
142
+ const instructions = contextInstructions
143
+ ? `${baseInstructions}\n\n${contextInstructions}`
144
+ : baseInstructions;
145
+
146
+ // Create assistant
147
+ const newAssistantTools = await openAiAssistantExecutionTools.createNewAssistant({
148
+ name,
149
+ instructions,
150
+ // knowledgeSources?
151
+ });
152
+
153
+ // Save to cache
154
+ const newAssistantId = newAssistantTools.assistantId;
155
+ if (newAssistantId) {
156
+ await supabase.from(await $getTableName('OpenAiAssistantCache')).insert({
157
+ agentHash,
158
+ assistantId: newAssistantId,
159
+ });
160
+ openAiAssistantExecutionTools = newAssistantTools;
161
+ }
162
+ }
163
+
103
164
  const agent = new Agent({
104
165
  agentSource,
105
166
  executionTools: {
@@ -108,7 +169,6 @@ export async function handleChatCompletion(
108
169
  isVerbose: true, // or false
109
170
  });
110
171
 
111
- const agentHash = computeAgentHash(agentSource);
112
172
  const userAgent = request.headers.get('user-agent');
113
173
  const ip =
114
174
  request.headers.get('x-forwarded-for') ||
@@ -125,8 +185,9 @@ export async function handleChatCompletion(
125
185
  const previousMessages = threadMessages.slice(0, -1);
126
186
 
127
187
  const thread: ChatMessage[] = previousMessages.map((msg: TODO_any, index: number) => ({
188
+ // channel: 'PROMPTBOOK_CHAT',
128
189
  id: `msg-${index}`, // Placeholder ID
129
- from: msg.role === 'assistant' ? 'agent' : 'user', // Mapping standard OpenAI roles
190
+ sender: msg.role === 'assistant' ? 'agent' : 'user', // Mapping standard OpenAI roles
130
191
  content: msg.content,
131
192
  isComplete: true,
132
193
  date: new Date(), // We don't have the real date, using current
@@ -138,7 +199,6 @@ export async function handleChatCompletion(
138
199
  content: lastMessage.content,
139
200
  };
140
201
 
141
- const supabase = $provideSupabaseForServer();
142
202
  await supabase.from(await $getTableName('ChatHistory')).insert({
143
203
  createdAt: new Date().toISOString(),
144
204
  messageHash: computeHash(userMessageContent),
@@ -0,0 +1,91 @@
1
+ import type { really_any } from '@promptbook-local/types';
2
+ import { serializeError } from '@promptbook-local/utils';
3
+ import { assertsError } from '../../../../../src/errors/assertsError';
4
+ import { $getTableName } from '../../database/$getTableName';
5
+ import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
6
+ import { EMAIL_PROVIDERS } from '../../message-providers';
7
+ import { OutboundEmail } from '../../message-providers/email/_common/Email';
8
+
9
+ /**
10
+ * Sends a message
11
+ */
12
+ export async function sendMessage(message: OutboundEmail): Promise<void> {
13
+ const supabase = await $provideSupabaseForServer();
14
+
15
+ // 1. Insert message
16
+ const { data: insertedMessage, error: insertError } = await supabase
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ .from(await $getTableName('Message'))
19
+ .insert({
20
+ channel: message.channel || 'UNKNOWN',
21
+ direction: message.direction || 'OUTBOUND',
22
+ sender: message.sender,
23
+ recipients: message.recipients,
24
+ content: message.content,
25
+ threadId: message.threadId,
26
+ metadata: message.metadata,
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ } as any)
29
+ .select()
30
+ .single();
31
+
32
+ if (insertError) {
33
+ throw new Error(`Failed to insert message: ${insertError.message}`);
34
+ }
35
+
36
+ if (!insertedMessage) {
37
+ throw new Error('Failed to insert message: No data returned');
38
+ }
39
+
40
+ // 2. If outbound and email, try to send
41
+ if (message.direction === 'OUTBOUND' && message.channel === 'EMAIL') {
42
+ const providers = Object.keys(EMAIL_PROVIDERS);
43
+
44
+ if (providers.length === 0) {
45
+ console.warn('No email providers configured');
46
+ return;
47
+ }
48
+
49
+ let isSent = false;
50
+
51
+ for (const providerName of providers) {
52
+ const provider = EMAIL_PROVIDERS[providerName];
53
+ let isSuccessful = false;
54
+ let raw: really_any = null;
55
+
56
+ try {
57
+ console.log(`📤 Sending email via ${providerName}`);
58
+ raw = await provider.send(message);
59
+ isSuccessful = true;
60
+ isSent = true;
61
+ } catch (error) {
62
+ assertsError(error);
63
+ console.error(`Failed to send email via ${providerName}`, error);
64
+ raw = { error: serializeError(error) };
65
+ }
66
+
67
+ // 3. Log attempt
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ await supabase.from(await $getTableName('MessageSendAttempt')).insert({
70
+ // @ts-expect-error: insertedMessage is any
71
+ messageId: insertedMessage.id,
72
+ providerName,
73
+ isSuccessful,
74
+ raw,
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ } as any);
77
+
78
+ if (isSuccessful) {
79
+ break;
80
+ }
81
+ }
82
+
83
+ if (!isSent) {
84
+ throw new Error('Failed to send email via any provider');
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * TODO: !!!! Move to `message-providers` and rename `message-providers` -> `messages`
91
+ */
@@ -0,0 +1,72 @@
1
+ import type { Json } from '../database/schema';
2
+
3
+ export type MessageRow = {
4
+ id: number;
5
+ createdAt: string;
6
+ channel: string;
7
+ direction: string;
8
+ sender: Json;
9
+ recipients: Json;
10
+ content: string;
11
+ threadId: string | null;
12
+ metadata: Json;
13
+ // Joined fields
14
+ sendAttempts?: MessageSendAttemptRow[];
15
+ };
16
+
17
+ export type MessageSendAttemptRow = {
18
+ id: number;
19
+ createdAt: string;
20
+ messageId: number;
21
+ providerName: string;
22
+ isSuccessful: boolean;
23
+ raw: Json;
24
+ };
25
+
26
+ export type MessagesListResponse = {
27
+ items: MessageRow[];
28
+ total: number;
29
+ page: number;
30
+ pageSize: number;
31
+ };
32
+
33
+ export type MessagesListParams = {
34
+ page?: number;
35
+ pageSize?: number;
36
+ search?: string;
37
+ channel?: string;
38
+ direction?: string;
39
+ };
40
+
41
+ /**
42
+ * Build query string for messages listing.
43
+ */
44
+ function buildQuery(params: MessagesListParams): string {
45
+ const searchParams = new URLSearchParams();
46
+
47
+ if (params.page && params.page > 0) searchParams.set('page', String(params.page));
48
+ if (params.pageSize && params.pageSize > 0) searchParams.set('pageSize', String(params.pageSize));
49
+ if (params.search) searchParams.set('search', params.search);
50
+ if (params.channel) searchParams.set('channel', params.channel);
51
+ if (params.direction) searchParams.set('direction', params.direction);
52
+
53
+ const qs = searchParams.toString();
54
+ return qs ? `?${qs}` : '';
55
+ }
56
+
57
+ /**
58
+ * Fetch messages from the admin API.
59
+ */
60
+ export async function $fetchMessages(params: MessagesListParams = {}): Promise<MessagesListResponse> {
61
+ const qs = buildQuery(params);
62
+ const response = await fetch(`/api/messages${qs}`, {
63
+ method: 'GET',
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const data = await response.json().catch(() => ({}));
68
+ throw new Error(data.error || 'Failed to load messages');
69
+ }
70
+
71
+ return (await response.json()) as MessagesListResponse;
72
+ }