@ixo/oracle-runtime 0.0.1

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 (583) hide show
  1. package/dist/bootstrap/ambient-factory.d.ts +32 -0
  2. package/dist/bootstrap/ambient-factory.d.ts.map +1 -0
  3. package/dist/bootstrap/ambient-factory.js +128 -0
  4. package/dist/bootstrap/create-oracle-app.d.ts +112 -0
  5. package/dist/bootstrap/create-oracle-app.d.ts.map +1 -0
  6. package/dist/bootstrap/create-oracle-app.js +530 -0
  7. package/dist/bootstrap/graceful-shutdown.d.ts +20 -0
  8. package/dist/bootstrap/graceful-shutdown.d.ts.map +1 -0
  9. package/dist/bootstrap/graceful-shutdown.js +61 -0
  10. package/dist/bootstrap/index.d.ts +13 -0
  11. package/dist/bootstrap/index.d.ts.map +1 -0
  12. package/dist/bootstrap/index.js +6 -0
  13. package/dist/bootstrap/inspect.d.ts +74 -0
  14. package/dist/bootstrap/inspect.d.ts.map +1 -0
  15. package/dist/bootstrap/inspect.js +111 -0
  16. package/dist/bootstrap/plugin-loader.d.ts +50 -0
  17. package/dist/bootstrap/plugin-loader.d.ts.map +1 -0
  18. package/dist/bootstrap/plugin-loader.js +119 -0
  19. package/dist/bootstrap/runtime-app-module.d.ts +38 -0
  20. package/dist/bootstrap/runtime-app-module.d.ts.map +1 -0
  21. package/dist/bootstrap/runtime-app-module.js +114 -0
  22. package/dist/bootstrap/schema-composer.d.ts +46 -0
  23. package/dist/bootstrap/schema-composer.d.ts.map +1 -0
  24. package/dist/bootstrap/schema-composer.js +65 -0
  25. package/dist/config/base-env-config.d.ts +31 -0
  26. package/dist/config/base-env-config.d.ts.map +1 -0
  27. package/dist/config/base-env-config.js +70 -0
  28. package/dist/config/base-env-schema.d.ts +77 -0
  29. package/dist/config/base-env-schema.d.ts.map +1 -0
  30. package/dist/config/base-env-schema.js +102 -0
  31. package/dist/events/scoped-emitter.d.ts +33 -0
  32. package/dist/events/scoped-emitter.d.ts.map +1 -0
  33. package/dist/events/scoped-emitter.js +32 -0
  34. package/dist/graph/index.d.ts +8 -0
  35. package/dist/graph/index.d.ts.map +1 -0
  36. package/dist/graph/index.js +6 -0
  37. package/dist/graph/main-agent-types.d.ts +88 -0
  38. package/dist/graph/main-agent-types.d.ts.map +1 -0
  39. package/dist/graph/main-agent-types.js +20 -0
  40. package/dist/graph/main-agent.d.ts +16 -0
  41. package/dist/graph/main-agent.d.ts.map +1 -0
  42. package/dist/graph/main-agent.js +251 -0
  43. package/dist/graph/middlewares/capability-gate-middleware.d.ts +35 -0
  44. package/dist/graph/middlewares/capability-gate-middleware.d.ts.map +1 -0
  45. package/dist/graph/middlewares/capability-gate-middleware.js +54 -0
  46. package/dist/graph/middlewares/index.d.ts +7 -0
  47. package/dist/graph/middlewares/index.d.ts.map +1 -0
  48. package/dist/graph/middlewares/index.js +6 -0
  49. package/dist/graph/middlewares/page-context-middleware.d.ts +23 -0
  50. package/dist/graph/middlewares/page-context-middleware.d.ts.map +1 -0
  51. package/dist/graph/middlewares/page-context-middleware.js +68 -0
  52. package/dist/graph/middlewares/safety-guardrail-middleware.d.ts +26 -0
  53. package/dist/graph/middlewares/safety-guardrail-middleware.d.ts.map +1 -0
  54. package/dist/graph/middlewares/safety-guardrail-middleware.js +88 -0
  55. package/dist/graph/middlewares/summarization-middleware.d.ts +17 -0
  56. package/dist/graph/middlewares/summarization-middleware.d.ts.map +1 -0
  57. package/dist/graph/middlewares/summarization-middleware.js +62 -0
  58. package/dist/graph/middlewares/tool-repetition-guard-middleware.d.ts +24 -0
  59. package/dist/graph/middlewares/tool-repetition-guard-middleware.d.ts.map +1 -0
  60. package/dist/graph/middlewares/tool-repetition-guard-middleware.js +112 -0
  61. package/dist/graph/middlewares/tool-validation-middleware.d.ts +24 -0
  62. package/dist/graph/middlewares/tool-validation-middleware.d.ts.map +1 -0
  63. package/dist/graph/middlewares/tool-validation-middleware.js +61 -0
  64. package/dist/graph/prompt-composer.d.ts +69 -0
  65. package/dist/graph/prompt-composer.d.ts.map +1 -0
  66. package/dist/graph/prompt-composer.js +315 -0
  67. package/dist/graph/state.d.ts +65 -0
  68. package/dist/graph/state.d.ts.map +1 -0
  69. package/dist/graph/state.js +60 -0
  70. package/dist/graph/sub-agent-fallback.d.ts +56 -0
  71. package/dist/graph/sub-agent-fallback.d.ts.map +1 -0
  72. package/dist/graph/sub-agent-fallback.js +79 -0
  73. package/dist/graph/subagent-as-tool.d.ts +77 -0
  74. package/dist/graph/subagent-as-tool.d.ts.map +1 -0
  75. package/dist/graph/subagent-as-tool.js +197 -0
  76. package/dist/graph/wrap-plugin-tool.d.ts +28 -0
  77. package/dist/graph/wrap-plugin-tool.d.ts.map +1 -0
  78. package/dist/graph/wrap-plugin-tool.js +30 -0
  79. package/dist/index.d.ts +33 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +28 -0
  82. package/dist/llm/index.d.ts +2 -0
  83. package/dist/llm/index.d.ts.map +1 -0
  84. package/dist/llm/index.js +1 -0
  85. package/dist/llm/llm-provider.d.ts +41 -0
  86. package/dist/llm/llm-provider.d.ts.map +1 -0
  87. package/dist/llm/llm-provider.js +130 -0
  88. package/dist/manifest/index.d.ts +6 -0
  89. package/dist/manifest/index.d.ts.map +1 -0
  90. package/dist/manifest/index.js +3 -0
  91. package/dist/manifest/schema.d.ts +31 -0
  92. package/dist/manifest/schema.d.ts.map +1 -0
  93. package/dist/manifest/schema.js +44 -0
  94. package/dist/manifest/tier1-renderer.d.ts +31 -0
  95. package/dist/manifest/tier1-renderer.d.ts.map +1 -0
  96. package/dist/manifest/tier1-renderer.js +68 -0
  97. package/dist/manifest/validator.d.ts +34 -0
  98. package/dist/manifest/validator.d.ts.map +1 -0
  99. package/dist/manifest/validator.js +111 -0
  100. package/dist/matrix/checkpointer/matrix-upload-utils.d.ts +66 -0
  101. package/dist/matrix/checkpointer/matrix-upload-utils.d.ts.map +1 -0
  102. package/dist/matrix/checkpointer/matrix-upload-utils.js +228 -0
  103. package/dist/matrix/checkpointer/type.d.ts +4 -0
  104. package/dist/matrix/checkpointer/type.d.ts.map +1 -0
  105. package/dist/matrix/checkpointer/type.js +1 -0
  106. package/dist/matrix/checkpointer/user-matrix-sqlite-sync-service.module.d.ts +3 -0
  107. package/dist/matrix/checkpointer/user-matrix-sqlite-sync-service.module.d.ts.map +1 -0
  108. package/dist/matrix/checkpointer/user-matrix-sqlite-sync-service.module.js +22 -0
  109. package/dist/matrix/checkpointer/user-matrix-sqlite-sync-service.service.d.ts +93 -0
  110. package/dist/matrix/checkpointer/user-matrix-sqlite-sync-service.service.d.ts.map +1 -0
  111. package/dist/matrix/checkpointer/user-matrix-sqlite-sync-service.service.js +856 -0
  112. package/dist/matrix/room-membership.d.ts +11 -0
  113. package/dist/matrix/room-membership.d.ts.map +1 -0
  114. package/dist/matrix/room-membership.js +33 -0
  115. package/dist/meta-tools/index.d.ts +28 -0
  116. package/dist/meta-tools/index.d.ts.map +1 -0
  117. package/dist/meta-tools/index.js +24 -0
  118. package/dist/meta-tools/list-capabilities.d.ts +12 -0
  119. package/dist/meta-tools/list-capabilities.d.ts.map +1 -0
  120. package/dist/meta-tools/list-capabilities.js +55 -0
  121. package/dist/meta-tools/load-capability.d.ts +24 -0
  122. package/dist/meta-tools/load-capability.d.ts.map +1 -0
  123. package/dist/meta-tools/load-capability.js +82 -0
  124. package/dist/modules/auth/auth-header.middleware.d.ts +37 -0
  125. package/dist/modules/auth/auth-header.middleware.d.ts.map +1 -0
  126. package/dist/modules/auth/auth-header.middleware.js +115 -0
  127. package/dist/modules/auth/auth.module.d.ts +9 -0
  128. package/dist/modules/auth/auth.module.d.ts.map +1 -0
  129. package/dist/modules/auth/auth.module.js +27 -0
  130. package/dist/modules/auth/index.d.ts +3 -0
  131. package/dist/modules/auth/index.d.ts.map +1 -0
  132. package/dist/modules/auth/index.js +2 -0
  133. package/dist/modules/auth/validate-ucan-delegation.d.ts +31 -0
  134. package/dist/modules/auth/validate-ucan-delegation.d.ts.map +1 -0
  135. package/dist/modules/auth/validate-ucan-delegation.js +36 -0
  136. package/dist/modules/blob-store/blob-store.module.d.ts +10 -0
  137. package/dist/modules/blob-store/blob-store.module.d.ts.map +1 -0
  138. package/dist/modules/blob-store/blob-store.module.js +25 -0
  139. package/dist/modules/blob-store/blob-store.service.d.ts +66 -0
  140. package/dist/modules/blob-store/blob-store.service.d.ts.map +1 -0
  141. package/dist/modules/blob-store/blob-store.service.js +108 -0
  142. package/dist/modules/blob-store/index.d.ts +3 -0
  143. package/dist/modules/blob-store/index.d.ts.map +1 -0
  144. package/dist/modules/blob-store/index.js +2 -0
  145. package/dist/modules/health/health.controller.d.ts +18 -0
  146. package/dist/modules/health/health.controller.d.ts.map +1 -0
  147. package/dist/modules/health/health.controller.js +48 -0
  148. package/dist/modules/health/health.module.d.ts +8 -0
  149. package/dist/modules/health/health.module.d.ts.map +1 -0
  150. package/dist/modules/health/health.module.js +21 -0
  151. package/dist/modules/index.d.ts +8 -0
  152. package/dist/modules/index.d.ts.map +1 -0
  153. package/dist/modules/index.js +6 -0
  154. package/dist/modules/messages/__test-fixtures__/deps.d.ts +41 -0
  155. package/dist/modules/messages/__test-fixtures__/deps.d.ts.map +1 -0
  156. package/dist/modules/messages/__test-fixtures__/deps.js +73 -0
  157. package/dist/modules/messages/__test-fixtures__/fake-agent.d.ts +27 -0
  158. package/dist/modules/messages/__test-fixtures__/fake-agent.d.ts.map +1 -0
  159. package/dist/modules/messages/__test-fixtures__/fake-agent.js +41 -0
  160. package/dist/modules/messages/__test-fixtures__/fake-response.d.ts +29 -0
  161. package/dist/modules/messages/__test-fixtures__/fake-response.d.ts.map +1 -0
  162. package/dist/modules/messages/__test-fixtures__/fake-response.js +55 -0
  163. package/dist/modules/messages/agent-builder.d.ts +64 -0
  164. package/dist/modules/messages/agent-builder.d.ts.map +1 -0
  165. package/dist/modules/messages/agent-builder.js +219 -0
  166. package/dist/modules/messages/batch-invoker.d.ts +36 -0
  167. package/dist/modules/messages/batch-invoker.d.ts.map +1 -0
  168. package/dist/modules/messages/batch-invoker.js +58 -0
  169. package/dist/modules/messages/dto/list-messages.dto.d.ts +4 -0
  170. package/dist/modules/messages/dto/list-messages.dto.d.ts.map +1 -0
  171. package/dist/modules/messages/dto/list-messages.dto.js +17 -0
  172. package/dist/modules/messages/dto/send-message.dto.d.ts +73 -0
  173. package/dist/modules/messages/dto/send-message.dto.d.ts.map +1 -0
  174. package/dist/modules/messages/dto/send-message.dto.js +318 -0
  175. package/dist/modules/messages/file-processing-credit-sink.port.d.ts +20 -0
  176. package/dist/modules/messages/file-processing-credit-sink.port.d.ts.map +1 -0
  177. package/dist/modules/messages/file-processing-credit-sink.port.js +1 -0
  178. package/dist/modules/messages/file-processing.service.d.ts +195 -0
  179. package/dist/modules/messages/file-processing.service.d.ts.map +1 -0
  180. package/dist/modules/messages/file-processing.service.js +1278 -0
  181. package/dist/modules/messages/homeserver-cache.d.ts +11 -0
  182. package/dist/modules/messages/homeserver-cache.d.ts.map +1 -0
  183. package/dist/modules/messages/homeserver-cache.js +31 -0
  184. package/dist/modules/messages/matrix-listener-bridge.d.ts +63 -0
  185. package/dist/modules/messages/matrix-listener-bridge.d.ts.map +1 -0
  186. package/dist/modules/messages/matrix-listener-bridge.js +280 -0
  187. package/dist/modules/messages/messages.controller.d.ts +13 -0
  188. package/dist/modules/messages/messages.controller.d.ts.map +1 -0
  189. package/dist/modules/messages/messages.controller.js +95 -0
  190. package/dist/modules/messages/messages.module.d.ts +3 -0
  191. package/dist/modules/messages/messages.module.d.ts.map +1 -0
  192. package/dist/modules/messages/messages.module.js +75 -0
  193. package/dist/modules/messages/messages.service.d.ts +112 -0
  194. package/dist/modules/messages/messages.service.d.ts.map +1 -0
  195. package/dist/modules/messages/messages.service.js +279 -0
  196. package/dist/modules/messages/oracle-runtime-bundle.d.ts +38 -0
  197. package/dist/modules/messages/oracle-runtime-bundle.d.ts.map +1 -0
  198. package/dist/modules/messages/oracle-runtime-bundle.js +40 -0
  199. package/dist/modules/messages/post-message-syncer.d.ts +30 -0
  200. package/dist/modules/messages/post-message-syncer.d.ts.map +1 -0
  201. package/dist/modules/messages/post-message-syncer.js +73 -0
  202. package/dist/modules/messages/request-preparer.d.ts +53 -0
  203. package/dist/modules/messages/request-preparer.d.ts.map +1 -0
  204. package/dist/modules/messages/request-preparer.js +139 -0
  205. package/dist/modules/messages/sse-stream-runner.d.ts +73 -0
  206. package/dist/modules/messages/sse-stream-runner.d.ts.map +1 -0
  207. package/dist/modules/messages/sse-stream-runner.js +352 -0
  208. package/dist/modules/messages/sse.utils.d.ts +29 -0
  209. package/dist/modules/messages/sse.utils.d.ts.map +1 -0
  210. package/dist/modules/messages/sse.utils.js +77 -0
  211. package/dist/modules/messages/user-context-fetcher.d.ts +41 -0
  212. package/dist/modules/messages/user-context-fetcher.d.ts.map +1 -0
  213. package/dist/modules/messages/user-context-fetcher.js +117 -0
  214. package/dist/modules/secrets/index.d.ts +2 -0
  215. package/dist/modules/secrets/index.d.ts.map +1 -0
  216. package/dist/modules/secrets/index.js +1 -0
  217. package/dist/modules/secrets/secrets.service.d.ts +29 -0
  218. package/dist/modules/secrets/secrets.service.d.ts.map +1 -0
  219. package/dist/modules/secrets/secrets.service.js +107 -0
  220. package/dist/modules/sessions/dto/create-session.dto.d.ts +6 -0
  221. package/dist/modules/sessions/dto/create-session.dto.d.ts.map +1 -0
  222. package/dist/modules/sessions/dto/create-session.dto.js +5 -0
  223. package/dist/modules/sessions/dto/delete-session.dto.d.ts +6 -0
  224. package/dist/modules/sessions/dto/delete-session.dto.d.ts.map +1 -0
  225. package/dist/modules/sessions/dto/delete-session.dto.js +5 -0
  226. package/dist/modules/sessions/dto/list-sessions.dto.d.ts +7 -0
  227. package/dist/modules/sessions/dto/list-sessions.dto.d.ts.map +1 -0
  228. package/dist/modules/sessions/dto/list-sessions.dto.js +6 -0
  229. package/dist/modules/sessions/session-history-processor.service.d.ts +48 -0
  230. package/dist/modules/sessions/session-history-processor.service.d.ts.map +1 -0
  231. package/dist/modules/sessions/session-history-processor.service.js +254 -0
  232. package/dist/modules/sessions/sessions.controller.d.ts +13 -0
  233. package/dist/modules/sessions/sessions.controller.d.ts.map +1 -0
  234. package/dist/modules/sessions/sessions.controller.js +113 -0
  235. package/dist/modules/sessions/sessions.module.d.ts +3 -0
  236. package/dist/modules/sessions/sessions.module.d.ts.map +1 -0
  237. package/dist/modules/sessions/sessions.module.js +49 -0
  238. package/dist/modules/sessions/sessions.service.d.ts +23 -0
  239. package/dist/modules/sessions/sessions.service.d.ts.map +1 -0
  240. package/dist/modules/sessions/sessions.service.js +168 -0
  241. package/dist/modules/subscription/subscription.middleware.d.ts +37 -0
  242. package/dist/modules/subscription/subscription.middleware.d.ts.map +1 -0
  243. package/dist/modules/subscription/subscription.middleware.js +141 -0
  244. package/dist/modules/subscription/subscription.module.d.ts +12 -0
  245. package/dist/modules/subscription/subscription.module.d.ts.map +1 -0
  246. package/dist/modules/subscription/subscription.module.js +26 -0
  247. package/dist/modules/throttler/throttler.module.d.ts +11 -0
  248. package/dist/modules/throttler/throttler.module.d.ts.map +1 -0
  249. package/dist/modules/throttler/throttler.module.js +32 -0
  250. package/dist/modules/ucan/index.d.ts +7 -0
  251. package/dist/modules/ucan/index.d.ts.map +1 -0
  252. package/dist/modules/ucan/index.js +6 -0
  253. package/dist/modules/ucan/ucan.config.d.ts +87 -0
  254. package/dist/modules/ucan/ucan.config.d.ts.map +1 -0
  255. package/dist/modules/ucan/ucan.config.js +114 -0
  256. package/dist/modules/ucan/ucan.module.d.ts +29 -0
  257. package/dist/modules/ucan/ucan.module.d.ts.map +1 -0
  258. package/dist/modules/ucan/ucan.module.js +46 -0
  259. package/dist/modules/ucan/ucan.service.d.ts +129 -0
  260. package/dist/modules/ucan/ucan.service.d.ts.map +1 -0
  261. package/dist/modules/ucan/ucan.service.js +582 -0
  262. package/dist/modules/ws/emitter.d.ts +12 -0
  263. package/dist/modules/ws/emitter.d.ts.map +1 -0
  264. package/dist/modules/ws/emitter.js +12 -0
  265. package/dist/modules/ws/ws.gateway.d.ts +34 -0
  266. package/dist/modules/ws/ws.gateway.d.ts.map +1 -0
  267. package/dist/modules/ws/ws.gateway.js +241 -0
  268. package/dist/modules/ws/ws.module.d.ts +3 -0
  269. package/dist/modules/ws/ws.module.d.ts.map +1 -0
  270. package/dist/modules/ws/ws.module.js +21 -0
  271. package/dist/modules/ws/ws.service.d.ts +25 -0
  272. package/dist/modules/ws/ws.service.d.ts.map +1 -0
  273. package/dist/modules/ws/ws.service.js +113 -0
  274. package/dist/plugin-api/define-plugin.d.ts +19 -0
  275. package/dist/plugin-api/define-plugin.d.ts.map +1 -0
  276. package/dist/plugin-api/define-plugin.js +25 -0
  277. package/dist/plugin-api/oracle-plugin.d.ts +92 -0
  278. package/dist/plugin-api/oracle-plugin.d.ts.map +1 -0
  279. package/dist/plugin-api/oracle-plugin.js +15 -0
  280. package/dist/plugin-api/tool-helper.d.ts +33 -0
  281. package/dist/plugin-api/tool-helper.d.ts.map +1 -0
  282. package/dist/plugin-api/tool-helper.js +40 -0
  283. package/dist/plugin-api/types.d.ts +378 -0
  284. package/dist/plugin-api/types.d.ts.map +1 -0
  285. package/dist/plugin-api/types.js +1 -0
  286. package/dist/plugins/agui/agui-agent.d.ts +10 -0
  287. package/dist/plugins/agui/agui-agent.d.ts.map +1 -0
  288. package/dist/plugins/agui/agui-agent.js +110 -0
  289. package/dist/plugins/agui/agui.plugin.d.ts +16 -0
  290. package/dist/plugins/agui/agui.plugin.d.ts.map +1 -0
  291. package/dist/plugins/agui/agui.plugin.js +117 -0
  292. package/dist/plugins/agui/index.d.ts +3 -0
  293. package/dist/plugins/agui/index.d.ts.map +1 -0
  294. package/dist/plugins/agui/index.js +2 -0
  295. package/dist/plugins/composio/composio-tools.d.ts +59 -0
  296. package/dist/plugins/composio/composio-tools.d.ts.map +1 -0
  297. package/dist/plugins/composio/composio-tools.js +88 -0
  298. package/dist/plugins/composio/composio-ucan.d.ts +14 -0
  299. package/dist/plugins/composio/composio-ucan.d.ts.map +1 -0
  300. package/dist/plugins/composio/composio-ucan.js +32 -0
  301. package/dist/plugins/composio/composio.plugin.d.ts +45 -0
  302. package/dist/plugins/composio/composio.plugin.d.ts.map +1 -0
  303. package/dist/plugins/composio/composio.plugin.js +116 -0
  304. package/dist/plugins/composio/index.d.ts +4 -0
  305. package/dist/plugins/composio/index.d.ts.map +1 -0
  306. package/dist/plugins/composio/index.js +3 -0
  307. package/dist/plugins/credits/claim-processing.module.d.ts +28 -0
  308. package/dist/plugins/credits/claim-processing.module.d.ts.map +1 -0
  309. package/dist/plugins/credits/claim-processing.module.js +37 -0
  310. package/dist/plugins/credits/claim-processing.service.d.ts +92 -0
  311. package/dist/plugins/credits/claim-processing.service.d.ts.map +1 -0
  312. package/dist/plugins/credits/claim-processing.service.js +406 -0
  313. package/dist/plugins/credits/credits-middleware.d.ts +23 -0
  314. package/dist/plugins/credits/credits-middleware.d.ts.map +1 -0
  315. package/dist/plugins/credits/credits-middleware.js +154 -0
  316. package/dist/plugins/credits/credits.plugin.d.ts +55 -0
  317. package/dist/plugins/credits/credits.plugin.d.ts.map +1 -0
  318. package/dist/plugins/credits/credits.plugin.js +95 -0
  319. package/dist/plugins/credits/file-processing-sink.module.d.ts +18 -0
  320. package/dist/plugins/credits/file-processing-sink.module.d.ts.map +1 -0
  321. package/dist/plugins/credits/file-processing-sink.module.js +56 -0
  322. package/dist/plugins/credits/index.d.ts +8 -0
  323. package/dist/plugins/credits/index.d.ts.map +1 -0
  324. package/dist/plugins/credits/index.js +7 -0
  325. package/dist/plugins/credits/subscription-sink.module.d.ts +23 -0
  326. package/dist/plugins/credits/subscription-sink.module.d.ts.map +1 -0
  327. package/dist/plugins/credits/subscription-sink.module.js +54 -0
  328. package/dist/plugins/credits/token-limiter.d.ts +139 -0
  329. package/dist/plugins/credits/token-limiter.d.ts.map +1 -0
  330. package/dist/plugins/credits/token-limiter.js +302 -0
  331. package/dist/plugins/domain-indexer/domain-indexer-agent.d.ts +7 -0
  332. package/dist/plugins/domain-indexer/domain-indexer-agent.d.ts.map +1 -0
  333. package/dist/plugins/domain-indexer/domain-indexer-agent.js +48 -0
  334. package/dist/plugins/domain-indexer/domain-indexer-tools.d.ts +8 -0
  335. package/dist/plugins/domain-indexer/domain-indexer-tools.d.ts.map +1 -0
  336. package/dist/plugins/domain-indexer/domain-indexer-tools.js +194 -0
  337. package/dist/plugins/domain-indexer/domain-indexer.plugin.d.ts +20 -0
  338. package/dist/plugins/domain-indexer/domain-indexer.plugin.d.ts.map +1 -0
  339. package/dist/plugins/domain-indexer/domain-indexer.plugin.js +78 -0
  340. package/dist/plugins/domain-indexer/index.d.ts +2 -0
  341. package/dist/plugins/domain-indexer/index.d.ts.map +1 -0
  342. package/dist/plugins/domain-indexer/index.js +1 -0
  343. package/dist/plugins/editor/apply-sandbox-output.d.ts +33 -0
  344. package/dist/plugins/editor/apply-sandbox-output.d.ts.map +1 -0
  345. package/dist/plugins/editor/apply-sandbox-output.js +302 -0
  346. package/dist/plugins/editor/block-actions.d.ts +84 -0
  347. package/dist/plugins/editor/block-actions.d.ts.map +1 -0
  348. package/dist/plugins/editor/block-actions.js +471 -0
  349. package/dist/plugins/editor/blocknote-helper.d.ts +147 -0
  350. package/dist/plugins/editor/blocknote-helper.d.ts.map +1 -0
  351. package/dist/plugins/editor/blocknote-helper.js +542 -0
  352. package/dist/plugins/editor/blocknote-tools.d.ts +364 -0
  353. package/dist/plugins/editor/blocknote-tools.d.ts.map +1 -0
  354. package/dist/plugins/editor/blocknote-tools.js +2123 -0
  355. package/dist/plugins/editor/editor-agent.d.ts +47 -0
  356. package/dist/plugins/editor/editor-agent.d.ts.map +1 -0
  357. package/dist/plugins/editor/editor-agent.js +158 -0
  358. package/dist/plugins/editor/editor-mx.d.ts +56 -0
  359. package/dist/plugins/editor/editor-mx.d.ts.map +1 -0
  360. package/dist/plugins/editor/editor-mx.js +100 -0
  361. package/dist/plugins/editor/editor.plugin.d.ts +32 -0
  362. package/dist/plugins/editor/editor.plugin.d.ts.map +1 -0
  363. package/dist/plugins/editor/editor.plugin.js +189 -0
  364. package/dist/plugins/editor/index.d.ts +6 -0
  365. package/dist/plugins/editor/index.d.ts.map +1 -0
  366. package/dist/plugins/editor/index.js +5 -0
  367. package/dist/plugins/editor/mint-invocation-tool.d.ts +72 -0
  368. package/dist/plugins/editor/mint-invocation-tool.d.ts.map +1 -0
  369. package/dist/plugins/editor/mint-invocation-tool.js +173 -0
  370. package/dist/plugins/editor/page-functions.d.ts +100 -0
  371. package/dist/plugins/editor/page-functions.d.ts.map +1 -0
  372. package/dist/plugins/editor/page-functions.js +317 -0
  373. package/dist/plugins/editor/page-tools.d.ts +75 -0
  374. package/dist/plugins/editor/page-tools.d.ts.map +1 -0
  375. package/dist/plugins/editor/page-tools.js +238 -0
  376. package/dist/plugins/editor/prompts.d.ts +22 -0
  377. package/dist/plugins/editor/prompts.d.ts.map +1 -0
  378. package/dist/plugins/editor/prompts.js +451 -0
  379. package/dist/plugins/editor/provider.d.ts +101 -0
  380. package/dist/plugins/editor/provider.d.ts.map +1 -0
  381. package/dist/plugins/editor/provider.js +249 -0
  382. package/dist/plugins/editor/standalone-editor-tool.d.ts +17 -0
  383. package/dist/plugins/editor/standalone-editor-tool.d.ts.map +1 -0
  384. package/dist/plugins/editor/standalone-editor-tool.js +136 -0
  385. package/dist/plugins/editor/survey-helpers.d.ts +112 -0
  386. package/dist/plugins/editor/survey-helpers.d.ts.map +1 -0
  387. package/dist/plugins/editor/survey-helpers.js +358 -0
  388. package/dist/plugins/firecrawl/firecrawl-agent.d.ts +7 -0
  389. package/dist/plugins/firecrawl/firecrawl-agent.d.ts.map +1 -0
  390. package/dist/plugins/firecrawl/firecrawl-agent.js +84 -0
  391. package/dist/plugins/firecrawl/firecrawl-tools.d.ts +37 -0
  392. package/dist/plugins/firecrawl/firecrawl-tools.d.ts.map +1 -0
  393. package/dist/plugins/firecrawl/firecrawl-tools.js +129 -0
  394. package/dist/plugins/firecrawl/firecrawl.plugin.d.ts +32 -0
  395. package/dist/plugins/firecrawl/firecrawl.plugin.d.ts.map +1 -0
  396. package/dist/plugins/firecrawl/firecrawl.plugin.js +69 -0
  397. package/dist/plugins/firecrawl/index.d.ts +3 -0
  398. package/dist/plugins/firecrawl/index.d.ts.map +1 -0
  399. package/dist/plugins/firecrawl/index.js +2 -0
  400. package/dist/plugins/index.d.ts +47 -0
  401. package/dist/plugins/index.d.ts.map +1 -0
  402. package/dist/plugins/index.js +69 -0
  403. package/dist/plugins/matrix-group-chats/channel-memory.module.d.ts +7 -0
  404. package/dist/plugins/matrix-group-chats/channel-memory.module.d.ts.map +1 -0
  405. package/dist/plugins/matrix-group-chats/channel-memory.module.js +22 -0
  406. package/dist/plugins/matrix-group-chats/channel-memory.repo.d.ts +62 -0
  407. package/dist/plugins/matrix-group-chats/channel-memory.repo.d.ts.map +1 -0
  408. package/dist/plugins/matrix-group-chats/channel-memory.repo.js +311 -0
  409. package/dist/plugins/matrix-group-chats/channel-memory.service.d.ts +89 -0
  410. package/dist/plugins/matrix-group-chats/channel-memory.service.d.ts.map +1 -0
  411. package/dist/plugins/matrix-group-chats/channel-memory.service.js +565 -0
  412. package/dist/plugins/matrix-group-chats/channel-memory.summarizer.d.ts +18 -0
  413. package/dist/plugins/matrix-group-chats/channel-memory.summarizer.d.ts.map +1 -0
  414. package/dist/plugins/matrix-group-chats/channel-memory.summarizer.js +128 -0
  415. package/dist/plugins/matrix-group-chats/channel-memory.types.d.ts +50 -0
  416. package/dist/plugins/matrix-group-chats/channel-memory.types.d.ts.map +1 -0
  417. package/dist/plugins/matrix-group-chats/channel-memory.types.js +2 -0
  418. package/dist/plugins/matrix-group-chats/guard.d.ts +83 -0
  419. package/dist/plugins/matrix-group-chats/guard.d.ts.map +1 -0
  420. package/dist/plugins/matrix-group-chats/guard.js +138 -0
  421. package/dist/plugins/matrix-group-chats/index.d.ts +59 -0
  422. package/dist/plugins/matrix-group-chats/index.d.ts.map +1 -0
  423. package/dist/plugins/matrix-group-chats/index.js +140 -0
  424. package/dist/plugins/matrix-group-chats/middleware.d.ts +31 -0
  425. package/dist/plugins/matrix-group-chats/middleware.d.ts.map +1 -0
  426. package/dist/plugins/matrix-group-chats/middleware.js +186 -0
  427. package/dist/plugins/matrix-group-chats/power-levels.d.ts +15 -0
  428. package/dist/plugins/matrix-group-chats/power-levels.d.ts.map +1 -0
  429. package/dist/plugins/matrix-group-chats/power-levels.js +36 -0
  430. package/dist/plugins/matrix-group-chats/room-info.d.ts +23 -0
  431. package/dist/plugins/matrix-group-chats/room-info.d.ts.map +1 -0
  432. package/dist/plugins/matrix-group-chats/room-info.js +34 -0
  433. package/dist/plugins/matrix-group-chats/tools.d.ts +8 -0
  434. package/dist/plugins/matrix-group-chats/tools.d.ts.map +1 -0
  435. package/dist/plugins/matrix-group-chats/tools.js +154 -0
  436. package/dist/plugins/memory/index.d.ts +5 -0
  437. package/dist/plugins/memory/index.d.ts.map +1 -0
  438. package/dist/plugins/memory/index.js +3 -0
  439. package/dist/plugins/memory/memory-tools.d.ts +52 -0
  440. package/dist/plugins/memory/memory-tools.d.ts.map +1 -0
  441. package/dist/plugins/memory/memory-tools.js +88 -0
  442. package/dist/plugins/memory/memory-ucan.d.ts +17 -0
  443. package/dist/plugins/memory/memory-ucan.d.ts.map +1 -0
  444. package/dist/plugins/memory/memory-ucan.js +43 -0
  445. package/dist/plugins/memory/memory.plugin.d.ts +51 -0
  446. package/dist/plugins/memory/memory.plugin.d.ts.map +1 -0
  447. package/dist/plugins/memory/memory.plugin.js +76 -0
  448. package/dist/plugins/memory/types.d.ts +75 -0
  449. package/dist/plugins/memory/types.d.ts.map +1 -0
  450. package/dist/plugins/memory/types.js +2 -0
  451. package/dist/plugins/portal/index.d.ts +3 -0
  452. package/dist/plugins/portal/index.d.ts.map +1 -0
  453. package/dist/plugins/portal/index.js +2 -0
  454. package/dist/plugins/portal/portal-agent.d.ts +9 -0
  455. package/dist/plugins/portal/portal-agent.d.ts.map +1 -0
  456. package/dist/plugins/portal/portal-agent.js +70 -0
  457. package/dist/plugins/portal/portal.plugin.d.ts +16 -0
  458. package/dist/plugins/portal/portal.plugin.d.ts.map +1 -0
  459. package/dist/plugins/portal/portal.plugin.js +115 -0
  460. package/dist/plugins/sandbox/index.d.ts +3 -0
  461. package/dist/plugins/sandbox/index.d.ts.map +1 -0
  462. package/dist/plugins/sandbox/index.js +2 -0
  463. package/dist/plugins/sandbox/sandbox-mcp.d.ts +46 -0
  464. package/dist/plugins/sandbox/sandbox-mcp.d.ts.map +1 -0
  465. package/dist/plugins/sandbox/sandbox-mcp.js +80 -0
  466. package/dist/plugins/sandbox/sandbox-write-blob.d.ts +22 -0
  467. package/dist/plugins/sandbox/sandbox-write-blob.d.ts.map +1 -0
  468. package/dist/plugins/sandbox/sandbox-write-blob.js +80 -0
  469. package/dist/plugins/sandbox/sandbox.plugin.d.ts +80 -0
  470. package/dist/plugins/sandbox/sandbox.plugin.d.ts.map +1 -0
  471. package/dist/plugins/sandbox/sandbox.plugin.js +204 -0
  472. package/dist/plugins/skills/index.d.ts +3 -0
  473. package/dist/plugins/skills/index.d.ts.map +1 -0
  474. package/dist/plugins/skills/index.js +2 -0
  475. package/dist/plugins/skills/skills-tools.d.ts +20 -0
  476. package/dist/plugins/skills/skills-tools.d.ts.map +1 -0
  477. package/dist/plugins/skills/skills-tools.js +204 -0
  478. package/dist/plugins/skills/skills-ucan.d.ts +24 -0
  479. package/dist/plugins/skills/skills-ucan.d.ts.map +1 -0
  480. package/dist/plugins/skills/skills-ucan.js +28 -0
  481. package/dist/plugins/skills/skills.plugin.d.ts +37 -0
  482. package/dist/plugins/skills/skills.plugin.d.ts.map +1 -0
  483. package/dist/plugins/skills/skills.plugin.js +82 -0
  484. package/dist/plugins/slack/index.d.ts +4 -0
  485. package/dist/plugins/slack/index.d.ts.map +1 -0
  486. package/dist/plugins/slack/index.js +3 -0
  487. package/dist/plugins/slack/slack.module.d.ts +9 -0
  488. package/dist/plugins/slack/slack.module.d.ts.map +1 -0
  489. package/dist/plugins/slack/slack.module.js +27 -0
  490. package/dist/plugins/slack/slack.plugin.d.ts +30 -0
  491. package/dist/plugins/slack/slack.plugin.d.ts.map +1 -0
  492. package/dist/plugins/slack/slack.plugin.js +40 -0
  493. package/dist/plugins/slack/slack.service.d.ts +32 -0
  494. package/dist/plugins/slack/slack.service.d.ts.map +1 -0
  495. package/dist/plugins/slack/slack.service.js +157 -0
  496. package/dist/plugins/user-preferences/index.d.ts +4 -0
  497. package/dist/plugins/user-preferences/index.d.ts.map +1 -0
  498. package/dist/plugins/user-preferences/index.js +3 -0
  499. package/dist/plugins/user-preferences/service/user-preferences.service.d.ts +61 -0
  500. package/dist/plugins/user-preferences/service/user-preferences.service.d.ts.map +1 -0
  501. package/dist/plugins/user-preferences/service/user-preferences.service.js +105 -0
  502. package/dist/plugins/user-preferences/user-preferences-http.module.d.ts +10 -0
  503. package/dist/plugins/user-preferences/user-preferences-http.module.d.ts.map +1 -0
  504. package/dist/plugins/user-preferences/user-preferences-http.module.js +23 -0
  505. package/dist/plugins/user-preferences/user-preferences-tool.d.ts +22 -0
  506. package/dist/plugins/user-preferences/user-preferences-tool.d.ts.map +1 -0
  507. package/dist/plugins/user-preferences/user-preferences-tool.js +103 -0
  508. package/dist/plugins/user-preferences/user-preferences.controller.d.ts +18 -0
  509. package/dist/plugins/user-preferences/user-preferences.controller.d.ts.map +1 -0
  510. package/dist/plugins/user-preferences/user-preferences.controller.js +72 -0
  511. package/dist/plugins/user-preferences/user-preferences.plugin.d.ts +27 -0
  512. package/dist/plugins/user-preferences/user-preferences.plugin.d.ts.map +1 -0
  513. package/dist/plugins/user-preferences/user-preferences.plugin.js +66 -0
  514. package/dist/registries/config-schema-registry.d.ts +24 -0
  515. package/dist/registries/config-schema-registry.d.ts.map +1 -0
  516. package/dist/registries/config-schema-registry.js +27 -0
  517. package/dist/registries/index.d.ts +13 -0
  518. package/dist/registries/index.d.ts.map +1 -0
  519. package/dist/registries/index.js +6 -0
  520. package/dist/registries/manifest-registry.d.ts +49 -0
  521. package/dist/registries/manifest-registry.d.ts.map +1 -0
  522. package/dist/registries/manifest-registry.js +53 -0
  523. package/dist/registries/middleware-registry.d.ts +41 -0
  524. package/dist/registries/middleware-registry.d.ts.map +1 -0
  525. package/dist/registries/middleware-registry.js +52 -0
  526. package/dist/registries/shared-state-registry.d.ts +41 -0
  527. package/dist/registries/shared-state-registry.d.ts.map +1 -0
  528. package/dist/registries/shared-state-registry.js +65 -0
  529. package/dist/registries/subagent-registry.d.ts +55 -0
  530. package/dist/registries/subagent-registry.d.ts.map +1 -0
  531. package/dist/registries/subagent-registry.js +106 -0
  532. package/dist/registries/test-fixtures.d.ts +47 -0
  533. package/dist/registries/test-fixtures.d.ts.map +1 -0
  534. package/dist/registries/test-fixtures.js +168 -0
  535. package/dist/registries/tool-registry.d.ts +74 -0
  536. package/dist/registries/tool-registry.d.ts.map +1 -0
  537. package/dist/registries/tool-registry.js +130 -0
  538. package/dist/runtime-context/ambient.d.ts +118 -0
  539. package/dist/runtime-context/ambient.d.ts.map +1 -0
  540. package/dist/runtime-context/ambient.js +1 -0
  541. package/dist/runtime-context/build-plugin.d.ts +20 -0
  542. package/dist/runtime-context/build-plugin.d.ts.map +1 -0
  543. package/dist/runtime-context/build-plugin.js +16 -0
  544. package/dist/runtime-context/build-runtime.d.ts +60 -0
  545. package/dist/runtime-context/build-runtime.d.ts.map +1 -0
  546. package/dist/runtime-context/build-runtime.js +81 -0
  547. package/dist/testing/create-test-runtime.d.ts +95 -0
  548. package/dist/testing/create-test-runtime.d.ts.map +1 -0
  549. package/dist/testing/create-test-runtime.js +302 -0
  550. package/dist/testing/index.d.ts +5 -0
  551. package/dist/testing/index.d.ts.map +1 -0
  552. package/dist/testing/index.js +5 -0
  553. package/dist/testing/integration/chat-client.d.ts +143 -0
  554. package/dist/testing/integration/chat-client.d.ts.map +1 -0
  555. package/dist/testing/integration/chat-client.js +238 -0
  556. package/dist/testing/integration/harness.d.ts +189 -0
  557. package/dist/testing/integration/harness.d.ts.map +1 -0
  558. package/dist/testing/integration/harness.js +461 -0
  559. package/dist/testing/integration/index.d.ts +14 -0
  560. package/dist/testing/integration/index.d.ts.map +1 -0
  561. package/dist/testing/integration/index.js +18 -0
  562. package/dist/testing/integration/setup.d.ts +2 -0
  563. package/dist/testing/integration/setup.d.ts.map +1 -0
  564. package/dist/testing/integration/setup.js +41 -0
  565. package/dist/testing/integration/sse-parser.d.ts +99 -0
  566. package/dist/testing/integration/sse-parser.d.ts.map +1 -0
  567. package/dist/testing/integration/sse-parser.js +125 -0
  568. package/dist/testing/integration/ucan.d.ts +74 -0
  569. package/dist/testing/integration/ucan.d.ts.map +1 -0
  570. package/dist/testing/integration/ucan.js +95 -0
  571. package/dist/testing/integration/wait-for-matrix-loaded.d.ts +19 -0
  572. package/dist/testing/integration/wait-for-matrix-loaded.d.ts.map +1 -0
  573. package/dist/testing/integration/wait-for-matrix-loaded.js +31 -0
  574. package/dist/testing/mocks.d.ts +64 -0
  575. package/dist/testing/mocks.d.ts.map +1 -0
  576. package/dist/testing/mocks.js +141 -0
  577. package/dist/testing/nest-doubles.d.ts +10 -0
  578. package/dist/testing/nest-doubles.d.ts.map +1 -0
  579. package/dist/testing/nest-doubles.js +19 -0
  580. package/dist/utils/emoji.d.ts +3 -0
  581. package/dist/utils/emoji.d.ts.map +1 -0
  582. package/dist/utils/emoji.js +36 -0
  583. package/package.json +102 -0
@@ -0,0 +1,1278 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ var FileProcessingService_1;
14
+ import { loadFileFromBuffer } from '@ixo/common';
15
+ import { MatrixManager } from '@ixo/matrix';
16
+ import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
17
+ import { ConfigService } from '@nestjs/config';
18
+ import { UcanService } from '../ucan/ucan.service.js';
19
+ import { FILE_PROCESSING_CREDIT_SINK, } from './file-processing-credit-sink.port.js';
20
+ const MAX_FILE_SIZE = 25 * 1024 * 1024; // 25MB per file
21
+ const MAX_TOTAL_SIZE = 50 * 1024 * 1024; // 50MB total across all attachments
22
+ const MAX_TEXT_LENGTH = 50_000;
23
+ const MATRIX_DOWNLOAD_TIMEOUT_MS = 60_000;
24
+ const AI_PROCESS_TIMEOUT_MS = 120_000;
25
+ const MAX_ERROR_BODY_LENGTH = 1024;
26
+ const SANDBOX_TRUNCATE_LIMIT = 500;
27
+ const SANDBOX_OUTPUT_PREFIX = '/workspace/output';
28
+ const ALLOWED_URI_SCHEMES = /^(mxc|https?):\/\//i;
29
+ const MAX_REDIRECT_COUNT = 5;
30
+ /**
31
+ * Block list for SSRF protection — prevents redirects to internal/cloud
32
+ * metadata endpoints.
33
+ */
34
+ const BLOCKED_HOST_PATTERNS = [
35
+ /^localhost$/i,
36
+ /^127\.\d+\.\d+\.\d+$/,
37
+ /^10\.\d+\.\d+\.\d+$/,
38
+ /^172\.(1[6-9]|2\d|3[01])\.\d+\.\d+$/,
39
+ /^192\.168\.\d+\.\d+$/,
40
+ /^169\.254\.\d+\.\d+$/, // AWS/cloud metadata
41
+ /^0\.0\.0\.0$/,
42
+ /^\[::1?\]$/, // IPv6 loopback (bracketed)
43
+ /^\[::ffff:[^\]]+\]$/i, // IPv4-mapped IPv6 (e.g. [::ffff:127.0.0.1])
44
+ /^\[f[cd][0-9a-f]{2}:.*\]$/i, // IPv6 unique-local (fc00::/7) — RFC 4193
45
+ /^\[fe[89ab][0-9a-f]:.*\]$/i, // IPv6 link-local (fe80::/10)
46
+ /^metadata\.google\.internal$/i,
47
+ ];
48
+ /**
49
+ * Magic byte signatures for common file types.
50
+ */
51
+ const MAGIC_BYTES = [
52
+ // Images
53
+ { bytes: [0x89, 0x50, 0x4e, 0x47], mime: 'image/png' },
54
+ { bytes: [0xff, 0xd8, 0xff], mime: 'image/jpeg' },
55
+ { bytes: [0x47, 0x49, 0x46, 0x38], mime: 'image/gif' },
56
+ { bytes: [0x52, 0x49, 0x46, 0x46], mime: 'image/webp' }, // RIFF (WebP container)
57
+ { bytes: [0x42, 0x4d], mime: 'image/bmp' }, // BMP
58
+ { bytes: [0x49, 0x49, 0x2a, 0x00], mime: 'image/tiff' }, // TIFF little-endian
59
+ { bytes: [0x4d, 0x4d, 0x00, 0x2a], mime: 'image/tiff' }, // TIFF big-endian
60
+ // Documents
61
+ { bytes: [0x25, 0x50, 0x44, 0x46], mime: 'application/pdf' },
62
+ { bytes: [0x50, 0x4b, 0x03, 0x04], mime: 'application/zip' }, // ZIP (docx, xlsx, etc.)
63
+ { bytes: [0xd0, 0xcf, 0x11, 0xe0], mime: 'application/msword' }, // OLE2 (doc, xls, ppt)
64
+ // Audio
65
+ { bytes: [0x49, 0x44, 0x33], mime: 'audio/mpeg' }, // ID3 tag (MP3)
66
+ { bytes: [0xff, 0xfb], mime: 'audio/mpeg' }, // MP3 frame sync
67
+ { bytes: [0xff, 0xf3], mime: 'audio/mpeg' }, // MP3 frame sync
68
+ { bytes: [0x4f, 0x67, 0x67, 0x53], mime: 'audio/ogg' }, // OGG
69
+ { bytes: [0x66, 0x4c, 0x61, 0x43], mime: 'audio/flac' }, // fLaC
70
+ // Video
71
+ { bytes: [0x1a, 0x45, 0xdf, 0xa3], mime: 'video/webm' }, // WebM/MKV (EBML)
72
+ { bytes: [0x66, 0x74, 0x79, 0x70], offset: 4, mime: 'video/mp4' }, // MP4/MOV `ftyp` box at offset 4
73
+ ];
74
+ const MAGIC_MIME_CATEGORIES = {
75
+ 'image/png': ['image'],
76
+ 'image/jpeg': ['image'],
77
+ 'image/gif': ['image'],
78
+ 'image/webp': ['image'],
79
+ 'image/bmp': ['image'],
80
+ 'image/tiff': ['image'],
81
+ 'application/pdf': ['document'],
82
+ 'application/zip': ['document'],
83
+ 'application/msword': ['document'],
84
+ 'audio/mpeg': ['audio'],
85
+ 'audio/ogg': ['audio'],
86
+ 'audio/flac': ['audio'],
87
+ 'video/webm': ['video', 'audio'],
88
+ 'video/mp4': ['video', 'audio'],
89
+ };
90
+ const PROMPTS = {
91
+ document: 'Extract all text content from this document verbatim.',
92
+ image: 'Describe this image in detail. Include all text, numbers, labels, and visual elements.',
93
+ audio: 'Transcribe this audio completely. Include all spoken words.',
94
+ video: 'Describe this video in detail. Include actions, text overlays, and spoken content.',
95
+ };
96
+ /**
97
+ * Provider config getter. Forks supply this at module composition time;
98
+ * the default throws so a missing wiring is caught at the first attachment.
99
+ */
100
+ let providerConfigGetter = () => {
101
+ throw new Error('FileProcessingService provider config not initialised — call setFileProcessingProvider() at boot');
102
+ };
103
+ export function setFileProcessingProvider(getter) {
104
+ providerConfigGetter = getter;
105
+ }
106
+ let FileProcessingService = FileProcessingService_1 = class FileProcessingService {
107
+ config;
108
+ ucanService;
109
+ creditSink;
110
+ logger = new Logger(FileProcessingService_1.name);
111
+ providerApiKey;
112
+ providerBaseURL;
113
+ providerHeaders;
114
+ processingModel;
115
+ constructor(config, ucanService, creditSink) {
116
+ this.config = config;
117
+ this.ucanService = ucanService;
118
+ this.creditSink = creditSink;
119
+ const providerCfg = providerConfigGetter();
120
+ this.providerApiKey = providerCfg.apiKey;
121
+ this.providerBaseURL = providerCfg.baseURL.replace(/\/+$/, '');
122
+ this.providerHeaders = providerCfg.headers;
123
+ this.processingModel = providerCfg.model;
124
+ }
125
+ /**
126
+ * Build the sandbox upload config for the current user. Returns `undefined`
127
+ * when sandbox archival isn't possible — `SANDBOX_MCP_URL` unset, the user
128
+ * has no cached UCAN delegation, or the oracle has no signing key. The
129
+ * caller treats `undefined` as "skip upload"; processing still happens.
130
+ *
131
+ * Auth header shape matches `plugins/sandbox/sandbox-mcp.ts`
132
+ * (`createDefaultAuthBuilder`) exactly — same `Authorization: Bearer <ucan>`
133
+ * + `X-Auth-Type: ucan` the sandbox MCP client sends for tool calls.
134
+ */
135
+ async buildSandboxConfig(userDid) {
136
+ const sandboxMcpUrl = this.config.get('SANDBOX_MCP_URL');
137
+ if (!sandboxMcpUrl)
138
+ return undefined;
139
+ const invocation = await this.ucanService.createServiceInvocation(sandboxMcpUrl, userDid, 'ixo:sandbox');
140
+ if (!invocation) {
141
+ this.logger.debug(`[FileProcessing] Skipping sandbox archive for ${userDid} — UCAN invocation unavailable`);
142
+ return undefined;
143
+ }
144
+ return {
145
+ sandboxMcpUrl,
146
+ authHeaders: {
147
+ Authorization: `Bearer ${invocation}`,
148
+ 'X-Auth-Type': 'ucan',
149
+ },
150
+ };
151
+ }
152
+ /**
153
+ * Sanitize a filename/path for the sandbox upload endpoint.
154
+ * Only alphanumeric, dots, dashes, underscores, and slashes are allowed,
155
+ * and `..` path segments are removed so the sandbox can't be escaped.
156
+ */
157
+ sanitizeSandboxPath(p) {
158
+ const charset = p.replace(/[^a-zA-Z0-9._\-/]/g, '_');
159
+ // Strip any `..` segment surrounded by `/` or string boundaries — a
160
+ // payload like `/workspace/../etc` reduces to `/workspace/etc`.
161
+ return charset
162
+ .split('/')
163
+ .filter((seg) => seg !== '..')
164
+ .join('/');
165
+ }
166
+ async uploadToSandbox(buffer, filename, destPath, sandboxConfig, mimetype) {
167
+ const baseUrl = sandboxConfig.sandboxMcpUrl.replace(/\/mcp\/?$/, '');
168
+ const uploadUrl = `${baseUrl}/artifacts/upload`;
169
+ const resolvedMime = mimetype ??
170
+ this.guessMimeFromFilename(filename) ??
171
+ 'application/octet-stream';
172
+ const safeFilename = this.sanitizeSandboxPath(filename);
173
+ const safePath = this.sanitizeSandboxPath(destPath);
174
+ const formData = new FormData();
175
+ const file = new File([new Uint8Array(buffer)], safeFilename, {
176
+ type: resolvedMime,
177
+ });
178
+ formData.set('file', file);
179
+ formData.set('path', safePath);
180
+ const response = await fetch(uploadUrl, {
181
+ method: 'POST',
182
+ headers: { ...sandboxConfig.authHeaders },
183
+ body: formData,
184
+ });
185
+ if (!response.ok) {
186
+ const errorText = await response.text().catch(() => '');
187
+ throw new Error(`Sandbox upload failed (${response.status}): ${errorText.slice(0, MAX_ERROR_BODY_LENGTH)}`);
188
+ }
189
+ const result = (await response.json());
190
+ return {
191
+ path: result.path,
192
+ url: result.url,
193
+ previewUrl: result.previewUrl,
194
+ };
195
+ }
196
+ async processAttachments(attachments, roomId, userDid) {
197
+ this.logger.log(`Processing ${attachments.length} attachment(s) in room ${roomId}`);
198
+ const reportedTotal = attachments.reduce((sum, a) => sum + (a.size ?? 0), 0);
199
+ if (reportedTotal > MAX_TOTAL_SIZE) {
200
+ throw new Error(`Total attachment size (${Math.round(reportedTotal / 1024 / 1024)} MB) exceeds budget (${Math.round(MAX_TOTAL_SIZE / 1024 / 1024)} MB)`);
201
+ }
202
+ // Mint the sandbox UCAN once per request — the cached invocation is reused
203
+ // for every attachment so we don't re-resolve did:web or re-sign per file.
204
+ const sandboxConfig = userDid
205
+ ? await this.buildSandboxConfig(userDid)
206
+ : undefined;
207
+ if (sandboxConfig) {
208
+ this.logger.log(`[FileProcessing] Sandbox archival enabled for this request → ${sandboxConfig.sandboxMcpUrl}`);
209
+ }
210
+ // Process sequentially so the running download total is enforced *before*
211
+ // the next attachment is fetched. Parallel `Promise.all` would let N
212
+ // attachments each download up to MAX_FILE_SIZE before any cumulative
213
+ // check fired, blowing past MAX_TOTAL_SIZE by a factor of N in the worst
214
+ // case.
215
+ const results = [];
216
+ let totalDownloaded = 0;
217
+ for (const attachment of attachments) {
218
+ this.logger.log(`Attachment: "${attachment.filename}" (${attachment.mimetype}, ${attachment.size ?? 'unknown'} bytes) — source: ${attachment.eventId ? `eventId=${attachment.eventId}` : `mxcUri=${attachment.mxcUri}`}`);
219
+ try {
220
+ const { text, downloadedSize, sandboxPath, usage } = await this.processAttachment(attachment, totalDownloaded, roomId, sandboxConfig);
221
+ totalDownloaded += downloadedSize;
222
+ if (totalDownloaded > MAX_TOTAL_SIZE) {
223
+ throw new Error(`Total downloaded size (${Math.round(totalDownloaded / 1024 / 1024)} MB) exceeds budget (${Math.round(MAX_TOTAL_SIZE / 1024 / 1024)} MB)`);
224
+ }
225
+ this.logger.log(`Attachment "${attachment.filename}" processed — downloaded ${downloadedSize} bytes, text extracted: ${text ? text.length + ' chars' : 'none'}`);
226
+ const category = this.categorizeFile(attachment.mimetype);
227
+ results.push({
228
+ text,
229
+ downloadedSize,
230
+ usage,
231
+ metadata: text
232
+ ? {
233
+ filename: attachment.filename,
234
+ mimetype: attachment.mimetype,
235
+ size: attachment.size,
236
+ mxcUri: attachment.mxcUri,
237
+ eventId: attachment.eventId,
238
+ category: category === 'unsupported' ? 'document' : category,
239
+ sandboxPath,
240
+ }
241
+ : null,
242
+ });
243
+ }
244
+ catch (error) {
245
+ this.logger.error(`Failed to process attachment ${attachment.filename}: ${error instanceof Error ? error.message : String(error)}`);
246
+ const errorText = `[File "${this.sanitizeFilename(attachment.filename)}" (${attachment.mimetype}) failed to process: ${error instanceof Error ? error.message : 'unknown error'}. Let the user know this file could not be read.]`;
247
+ const category = this.categorizeFile(attachment.mimetype);
248
+ results.push({
249
+ text: errorText,
250
+ downloadedSize: 0,
251
+ metadata: {
252
+ filename: attachment.filename,
253
+ mimetype: attachment.mimetype,
254
+ size: attachment.size,
255
+ mxcUri: attachment.mxcUri,
256
+ eventId: attachment.eventId,
257
+ category: category === 'unsupported' ? 'document' : category,
258
+ },
259
+ });
260
+ }
261
+ }
262
+ const texts = [];
263
+ const metadata = [];
264
+ const totalUsage = { cost: 0, promptTokens: 0, completionTokens: 0 };
265
+ for (const result of results) {
266
+ if (result.text) {
267
+ texts.push(result.text);
268
+ }
269
+ if (result.metadata) {
270
+ metadata.push(result.metadata);
271
+ }
272
+ if (result.usage) {
273
+ totalUsage.cost += result.usage.cost ?? 0;
274
+ totalUsage.promptTokens += result.usage.promptTokens ?? 0;
275
+ totalUsage.completionTokens += result.usage.completionTokens ?? 0;
276
+ }
277
+ }
278
+ const aiCallsMade = results.filter((r) => r.usage).length;
279
+ this.logger.log(`Attachments done — ${texts.length} text result(s), ${aiCallsMade} AI call(s), total downloaded: ${totalDownloaded} bytes, usage: cost=$${totalUsage.cost} tokens=${totalUsage.promptTokens + totalUsage.completionTokens}`);
280
+ if (this.creditSink && userDid && aiCallsMade > 0) {
281
+ try {
282
+ await this.creditSink.deductForFileProcessing(userDid, totalUsage);
283
+ }
284
+ catch (error) {
285
+ // Non-blocking: the file was already processed; surface and continue.
286
+ this.logger.warn(`[FileProcessing] Credit deduction failed: ${error instanceof Error ? error.message : String(error)}`);
287
+ }
288
+ }
289
+ return { texts, metadata, totalUsage };
290
+ }
291
+ async processAttachment(attachment, currentTotalSize, roomId, sandboxConfig) {
292
+ if (!attachment.eventId && !attachment.mxcUri) {
293
+ throw new Error('Either mxcUri or eventId must be provided');
294
+ }
295
+ if (attachment.mxcUri && !ALLOWED_URI_SCHEMES.test(attachment.mxcUri)) {
296
+ throw new Error('Invalid URI scheme');
297
+ }
298
+ if (attachment.size && attachment.size > MAX_FILE_SIZE) {
299
+ throw new Error('File exceeds maximum size');
300
+ }
301
+ if (attachment.size &&
302
+ currentTotalSize + attachment.size > MAX_TOTAL_SIZE) {
303
+ throw new Error('Total attachment size budget exceeded');
304
+ }
305
+ const category = this.categorizeFile(attachment.mimetype);
306
+ if (category === 'unsupported') {
307
+ this.logger.warn(`Unsupported file type: ${attachment.mimetype} for ${attachment.filename}`);
308
+ return {
309
+ text: `[File "${this.sanitizeFilename(attachment.filename)}" (${attachment.mimetype}) is not a supported file type and could not be processed. Let the user know this file type is not supported.]`,
310
+ downloadedSize: 0,
311
+ };
312
+ }
313
+ // For HTTP image/video URLs, try AI URL passthrough first (no download needed).
314
+ const isHttpUrl = attachment.mxcUri &&
315
+ !attachment.eventId &&
316
+ /^https?:\/\//i.test(attachment.mxcUri);
317
+ if (isHttpUrl && (category === 'image' || category === 'video')) {
318
+ try {
319
+ const { content, usage } = await this.aiProcessFromUrl(attachment.mxcUri, attachment.mimetype, category, attachment.filename);
320
+ if (content && content.trim().length > 0) {
321
+ const text = this.formatContent('Description', this.sanitizeFilename(attachment.filename), content);
322
+ return { text, downloadedSize: 0, usage };
323
+ }
324
+ }
325
+ catch (error) {
326
+ this.logger.warn(`URL passthrough failed for "${attachment.filename}", falling back to download: ${error instanceof Error ? error.message : String(error)}`);
327
+ }
328
+ }
329
+ let buffer;
330
+ if (attachment.eventId) {
331
+ buffer = await this.downloadFromMatrixEvent(roomId, attachment.eventId);
332
+ }
333
+ else if (attachment.mxcUri.startsWith('mxc://')) {
334
+ buffer = await this.downloadFromMatrix(attachment.mxcUri);
335
+ }
336
+ else {
337
+ const result = await this.downloadFromUrl(attachment.mxcUri);
338
+ buffer = result.data;
339
+ }
340
+ if (buffer.length > MAX_FILE_SIZE) {
341
+ throw new Error('File exceeds maximum size');
342
+ }
343
+ this.verifyMagicBytes(buffer, category, attachment);
344
+ let text;
345
+ let usage;
346
+ switch (category) {
347
+ case 'document':
348
+ ({ text, usage } = await this.processDocument(buffer, attachment));
349
+ break;
350
+ case 'image':
351
+ ({ text, usage } = await this.processImage(buffer, attachment));
352
+ break;
353
+ case 'audio':
354
+ ({ text, usage } = await this.processAudio(buffer, attachment));
355
+ break;
356
+ case 'video':
357
+ ({ text, usage } = await this.processVideo(buffer, attachment));
358
+ break;
359
+ }
360
+ if (sandboxConfig) {
361
+ const safeName = this.sanitizeFilename(attachment.filename);
362
+ const destPath = `${SANDBOX_OUTPUT_PREFIX}/${safeName}`;
363
+ try {
364
+ await this.uploadToSandbox(buffer, safeName, destPath, sandboxConfig, attachment.mimetype);
365
+ const actualPath = this.sanitizeSandboxPath(destPath);
366
+ this.logger.log(`Attachment "${attachment.filename}" uploaded to sandbox at ${actualPath}`);
367
+ // For AI-processed files (image/video/audio), save analysis as .md
368
+ let analysisPath;
369
+ if (category === 'image' ||
370
+ category === 'video' ||
371
+ category === 'audio') {
372
+ const analysisContent = this.buildAnalysisMarkdown(safeName, attachment.mimetype, buffer.length, category, text);
373
+ const analysisBuf = Buffer.from(analysisContent, 'utf-8');
374
+ const analysisFilename = `${safeName.replace(/\.[^.]+$/, '')}-analysis.md`;
375
+ const analysisDestPath = `${SANDBOX_OUTPUT_PREFIX}/${analysisFilename}`;
376
+ try {
377
+ await this.uploadToSandbox(analysisBuf, analysisFilename, analysisDestPath, sandboxConfig, 'text/markdown');
378
+ analysisPath = this.sanitizeSandboxPath(analysisDestPath);
379
+ this.logger.log(`Analysis for "${attachment.filename}" saved to sandbox at ${analysisPath}`);
380
+ }
381
+ catch (error) {
382
+ this.logger.warn(`Analysis .md upload failed for "${attachment.filename}": ${error instanceof Error ? error.message : String(error)}`);
383
+ }
384
+ }
385
+ if (text.length > SANDBOX_TRUNCATE_LIMIT) {
386
+ const paths = analysisPath
387
+ ? `\n\n[Full analysis saved to sandbox at ${analysisPath}]\n[Original file saved to sandbox at ${actualPath}]`
388
+ : `\n\n[Full file saved to sandbox at ${actualPath}]`;
389
+ return {
390
+ text: text.slice(0, SANDBOX_TRUNCATE_LIMIT) + paths,
391
+ downloadedSize: buffer.length,
392
+ sandboxPath: actualPath,
393
+ usage,
394
+ };
395
+ }
396
+ const suffix = analysisPath
397
+ ? `\n\n[Analysis saved to sandbox at ${analysisPath}]\n[File also saved to sandbox at ${actualPath}]`
398
+ : `\n\n[File also saved to sandbox at ${actualPath}]`;
399
+ return {
400
+ text: text + suffix,
401
+ downloadedSize: buffer.length,
402
+ sandboxPath: actualPath,
403
+ usage,
404
+ };
405
+ }
406
+ catch (error) {
407
+ this.logger.warn(`Sandbox upload failed for "${attachment.filename}": ${error instanceof Error ? error.message : String(error)}`);
408
+ return {
409
+ text: text +
410
+ `\n\n[Warning: sandbox upload failed — file content is included above]`,
411
+ downloadedSize: buffer.length,
412
+ usage,
413
+ };
414
+ }
415
+ }
416
+ return { text, downloadedSize: buffer.length, usage };
417
+ }
418
+ async downloadFromMatrix(mxcUri) {
419
+ const client = MatrixManager.getInstance().getClient();
420
+ if (!client) {
421
+ throw new Error('Matrix client not available');
422
+ }
423
+ const abortController = new AbortController();
424
+ const timeout = setTimeout(() => abortController.abort(), MATRIX_DOWNLOAD_TIMEOUT_MS);
425
+ try {
426
+ const result = await client.mxClient.downloadContent(mxcUri);
427
+ return result.data;
428
+ }
429
+ finally {
430
+ clearTimeout(timeout);
431
+ }
432
+ }
433
+ async downloadFromMatrixEvent(roomId, eventId) {
434
+ this.logger.log(`Fetching Matrix event ${eventId} from room ${roomId}`);
435
+ const client = MatrixManager.getInstance().getClient();
436
+ if (!client) {
437
+ throw new Error('Matrix client not available');
438
+ }
439
+ const event = await client.mxClient.getEvent(roomId, eventId);
440
+ if (!event.content) {
441
+ throw new Error('Event has no content');
442
+ }
443
+ const isEncrypted = !!event.content.file;
444
+ this.logger.log(`Event ${eventId} — encrypted: ${isEncrypted}, type: ${event.content.msgtype ?? event.type}`);
445
+ let data;
446
+ if (isEncrypted) {
447
+ data = await client.mxClient.crypto.decryptMedia(event.content.file);
448
+ }
449
+ else {
450
+ if (!event.content.url) {
451
+ throw new Error('Event has no media URL');
452
+ }
453
+ const result = await client.mxClient.downloadContent(event.content.url);
454
+ data = result.data;
455
+ }
456
+ this.logger.log(`Downloaded ${data.length} bytes from event ${eventId}`);
457
+ return data;
458
+ }
459
+ /**
460
+ * Validate that a URL does not point to an internal/private network address.
461
+ * Prevents SSRF attacks via crafted or redirected URLs.
462
+ */
463
+ validateUrlTarget(targetUrl) {
464
+ let parsed;
465
+ try {
466
+ parsed = new URL(targetUrl);
467
+ }
468
+ catch {
469
+ throw new Error('Invalid URL');
470
+ }
471
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
472
+ throw new Error(`Blocked URL scheme: ${parsed.protocol}`);
473
+ }
474
+ const hostname = parsed.hostname;
475
+ for (const pattern of BLOCKED_HOST_PATTERNS) {
476
+ if (pattern.test(hostname)) {
477
+ throw new Error('URL points to a blocked internal address');
478
+ }
479
+ }
480
+ }
481
+ async downloadFromUrl(url) {
482
+ this.validateUrlTarget(url);
483
+ const abortController = new AbortController();
484
+ const timeout = setTimeout(() => abortController.abort(), MATRIX_DOWNLOAD_TIMEOUT_MS);
485
+ try {
486
+ // Follow redirects manually so each hop is SSRF-validated.
487
+ let currentUrl = url;
488
+ let response;
489
+ for (let i = 0; i <= MAX_REDIRECT_COUNT; i++) {
490
+ response = await fetch(currentUrl, {
491
+ signal: abortController.signal,
492
+ redirect: 'manual',
493
+ });
494
+ if (response.status >= 300 &&
495
+ response.status < 400 &&
496
+ response.headers.get('location')) {
497
+ const location = response.headers.get('location');
498
+ currentUrl = new URL(location, currentUrl).toString();
499
+ this.validateUrlTarget(currentUrl);
500
+ this.logger.debug(`[downloadFromUrl] Redirect ${i + 1} → ${currentUrl}`);
501
+ continue;
502
+ }
503
+ break;
504
+ }
505
+ if (!response || (response.status >= 300 && response.status < 400)) {
506
+ throw new Error('Too many redirects');
507
+ }
508
+ if (!response.ok) {
509
+ throw new Error(`HTTP ${response.status} downloading ${url}`);
510
+ }
511
+ const contentType = response.headers.get('content-type')?.split(';')[0]?.trim() ??
512
+ undefined;
513
+ const contentLength = response.headers.get('content-length');
514
+ if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
515
+ throw new Error(`File too large: server reports ${Math.round(parseInt(contentLength, 10) / 1024 / 1024)} MB (limit: ${Math.round(MAX_FILE_SIZE / 1024 / 1024)} MB)`);
516
+ }
517
+ // Stream the body with running size check to avoid OOM.
518
+ const reader = response.body?.getReader();
519
+ if (!reader) {
520
+ throw new Error('Response body is not readable');
521
+ }
522
+ const chunks = [];
523
+ let totalBytes = 0;
524
+ while (true) {
525
+ const { done, value } = await reader.read();
526
+ if (done)
527
+ break;
528
+ totalBytes += value.byteLength;
529
+ if (totalBytes > MAX_FILE_SIZE) {
530
+ void reader.cancel();
531
+ throw new Error(`File exceeds maximum size (${Math.round(MAX_FILE_SIZE / 1024 / 1024)} MB) — download aborted`);
532
+ }
533
+ chunks.push(value);
534
+ }
535
+ const finalUrl = currentUrl !== url ? currentUrl : undefined;
536
+ return {
537
+ data: Buffer.concat(chunks),
538
+ contentType,
539
+ finalUrl,
540
+ };
541
+ }
542
+ finally {
543
+ clearTimeout(timeout);
544
+ }
545
+ }
546
+ /**
547
+ * HEAD a URL to determine Content-Type and Content-Length without
548
+ * downloading the body. Follows redirects with SSRF validation.
549
+ */
550
+ async headUrl(url) {
551
+ this.validateUrlTarget(url);
552
+ const abortController = new AbortController();
553
+ const timeout = setTimeout(() => abortController.abort(), MATRIX_DOWNLOAD_TIMEOUT_MS);
554
+ try {
555
+ let currentUrl = url;
556
+ let response;
557
+ for (let i = 0; i <= MAX_REDIRECT_COUNT; i++) {
558
+ response = await fetch(currentUrl, {
559
+ method: 'HEAD',
560
+ signal: abortController.signal,
561
+ redirect: 'manual',
562
+ });
563
+ if (response.status >= 300 &&
564
+ response.status < 400 &&
565
+ response.headers.get('location')) {
566
+ const location = response.headers.get('location');
567
+ currentUrl = new URL(location, currentUrl).toString();
568
+ this.validateUrlTarget(currentUrl);
569
+ continue;
570
+ }
571
+ break;
572
+ }
573
+ if (!response || (response.status >= 300 && response.status < 400)) {
574
+ throw new Error('Too many redirects');
575
+ }
576
+ if (!response.ok) {
577
+ throw new Error(`HTTP ${response.status} from HEAD ${url}`);
578
+ }
579
+ const contentType = response.headers.get('content-type')?.split(';')[0]?.trim() ??
580
+ undefined;
581
+ const clHeader = response.headers.get('content-length');
582
+ const contentLength = clHeader ? parseInt(clHeader, 10) : undefined;
583
+ return { contentType, contentLength, finalUrl: currentUrl };
584
+ }
585
+ finally {
586
+ clearTimeout(timeout);
587
+ }
588
+ }
589
+ /**
590
+ * Detect the actual MIME type from the file's magic bytes.
591
+ * Returns null if no known signature matches.
592
+ */
593
+ detectMimeFromMagicBytes(buffer) {
594
+ if (buffer.length < 4)
595
+ return null;
596
+ for (const sig of MAGIC_BYTES) {
597
+ const offset = sig.offset ?? 0;
598
+ if (buffer.length < offset + sig.bytes.length)
599
+ continue;
600
+ let match = true;
601
+ for (let i = 0; i < sig.bytes.length; i++) {
602
+ if (buffer[offset + i] !== sig.bytes[i]) {
603
+ match = false;
604
+ break;
605
+ }
606
+ }
607
+ if (match)
608
+ return sig.mime;
609
+ }
610
+ return null;
611
+ }
612
+ /**
613
+ * Verify that the file's magic bytes are consistent with the claimed
614
+ * mimetype category. For binary formats (images, audio, video, PDF)
615
+ * this is strict. For text-based formats (text/plain, text/html, etc.)
616
+ * we skip magic byte checks since they don't have reliable signatures.
617
+ */
618
+ verifyMagicBytes(buffer, claimedCategory, attachment) {
619
+ if (this.isPlainTextType(attachment.mimetype)) {
620
+ return;
621
+ }
622
+ const detectedMime = this.detectMimeFromMagicBytes(buffer);
623
+ if (!detectedMime) {
624
+ this.logger.warn(`No magic bytes match for ${attachment.filename} (claimed: ${attachment.mimetype})`);
625
+ return;
626
+ }
627
+ const allowedCategories = MAGIC_MIME_CATEGORIES[detectedMime];
628
+ if (!allowedCategories || !allowedCategories.includes(claimedCategory)) {
629
+ throw new Error(`File content mismatch: claimed ${attachment.mimetype} but detected ${detectedMime}`);
630
+ }
631
+ }
632
+ categorizeFile(mimetype) {
633
+ if (this.isDocumentType(mimetype))
634
+ return 'document';
635
+ if (mimetype.startsWith('image/'))
636
+ return 'image';
637
+ if (mimetype.startsWith('audio/'))
638
+ return 'audio';
639
+ if (mimetype.startsWith('video/'))
640
+ return 'video';
641
+ return 'unsupported';
642
+ }
643
+ isDocumentType(mimetype) {
644
+ return (mimetype.startsWith('text/') ||
645
+ mimetype === 'application/pdf' ||
646
+ mimetype === 'application/msword' ||
647
+ mimetype === 'application/json' ||
648
+ mimetype === 'application/xml' ||
649
+ mimetype === 'application/rtf' ||
650
+ mimetype.startsWith('application/vnd.openxmlformats-officedocument.') ||
651
+ mimetype === 'application/vnd.ms-excel' ||
652
+ mimetype === 'application/vnd.ms-powerpoint');
653
+ }
654
+ async processDocument(buffer, attachment) {
655
+ const safeFilename = this.sanitizeFilename(attachment.filename);
656
+ if (this.isPlainTextType(attachment.mimetype)) {
657
+ const text = buffer.toString('utf-8');
658
+ return {
659
+ text: this.formatContent('Content', safeFilename, this.truncateText(text)),
660
+ };
661
+ }
662
+ try {
663
+ const docs = await loadFileFromBuffer(buffer, attachment.mimetype, attachment.filename);
664
+ // PDFs return one Document per page — preserve the page boundary so the
665
+ // model can cite "page N" downstream. Other doc types return one chunk.
666
+ const isPdf = attachment.mimetype === 'application/pdf';
667
+ const text = isPdf && docs.length > 1
668
+ ? docs
669
+ .map((doc, i) => `## Page ${i + 1}\n\n${doc.pageContent}`)
670
+ .join('\n\n')
671
+ : docs.map((doc) => doc.pageContent).join('\n\n');
672
+ if (text.trim().length > 0) {
673
+ return {
674
+ text: this.formatContent('Content', safeFilename, this.truncateText(text)),
675
+ };
676
+ }
677
+ }
678
+ catch (error) {
679
+ this.logger.warn(`Local parsing failed for ${attachment.filename}, falling back to AI: ${error instanceof Error ? error.message : String(error)}`);
680
+ }
681
+ const { content, usage } = await this.aiProcess(buffer, attachment.mimetype, 'document', attachment.filename);
682
+ return {
683
+ text: this.formatContent('Content', safeFilename, this.truncateText(content)),
684
+ usage,
685
+ };
686
+ }
687
+ async processImage(buffer, attachment) {
688
+ const { content, usage } = await this.aiProcess(buffer, attachment.mimetype, 'image', attachment.filename);
689
+ return {
690
+ text: this.formatContent('Description', this.sanitizeFilename(attachment.filename), content),
691
+ usage,
692
+ };
693
+ }
694
+ async processAudio(buffer, attachment) {
695
+ const { content, usage } = await this.aiProcess(buffer, attachment.mimetype, 'audio', attachment.filename);
696
+ return {
697
+ text: this.formatContent('Transcription', this.sanitizeFilename(attachment.filename), content),
698
+ usage,
699
+ };
700
+ }
701
+ async processVideo(buffer, attachment) {
702
+ const { content, usage } = await this.aiProcess(buffer, attachment.mimetype, 'video', attachment.filename);
703
+ return {
704
+ text: this.formatContent('Description', this.sanitizeFilename(attachment.filename), content),
705
+ usage,
706
+ };
707
+ }
708
+ /**
709
+ * Send a public URL directly to OpenRouter for image/video processing
710
+ * without downloading the file first. Faster and avoids OOM for large files.
711
+ */
712
+ async aiProcessFromUrl(url, _mimetype, category, _filename) {
713
+ // The upstream model fetches `url` from its own infrastructure, so this
714
+ // path bypasses our normal `downloadFromUrl` SSRF check. Validate here
715
+ // so we can't be coerced into asking an external LLM to read internal
716
+ // metadata endpoints or private network hosts.
717
+ this.validateUrlTarget(url);
718
+ const prompt = PROMPTS[category];
719
+ const contentParts = [
720
+ { type: 'text', text: prompt },
721
+ ];
722
+ if (category === 'image') {
723
+ contentParts.push({
724
+ type: 'image_url',
725
+ image_url: { url },
726
+ });
727
+ }
728
+ else {
729
+ contentParts.push({
730
+ type: 'video_url',
731
+ video_url: { url },
732
+ });
733
+ }
734
+ const abortController = new AbortController();
735
+ const timeout = setTimeout(() => abortController.abort(), AI_PROCESS_TIMEOUT_MS);
736
+ try {
737
+ const response = await fetch(`${this.providerBaseURL}/chat/completions`, {
738
+ method: 'POST',
739
+ headers: {
740
+ Authorization: `Bearer ${this.providerApiKey}`,
741
+ 'Content-Type': 'application/json',
742
+ ...this.providerHeaders,
743
+ },
744
+ body: JSON.stringify({
745
+ model: this.processingModel,
746
+ messages: [
747
+ {
748
+ role: 'user',
749
+ content: contentParts,
750
+ },
751
+ ],
752
+ }),
753
+ signal: abortController.signal,
754
+ });
755
+ if (!response.ok) {
756
+ const errorText = await response.text();
757
+ this.logger.error(`OpenRouter API error (${response.status}): ${errorText.slice(0, MAX_ERROR_BODY_LENGTH)}`);
758
+ throw new Error(`AI processing failed (${response.status})`);
759
+ }
760
+ const result = (await response.json());
761
+ this.logger.log(`[aiProcessFromUrl] model=${result.model ?? 'unknown'} usage=${JSON.stringify(result.usage ?? null)}`);
762
+ return {
763
+ content: result.choices[0]?.message?.content ?? '',
764
+ usage: result.usage
765
+ ? {
766
+ cost: result.usage.cost,
767
+ promptTokens: result.usage.prompt_tokens,
768
+ completionTokens: result.usage.completion_tokens,
769
+ }
770
+ : undefined,
771
+ };
772
+ }
773
+ finally {
774
+ clearTimeout(timeout);
775
+ }
776
+ }
777
+ async aiProcess(buffer, mimetype, category, filename) {
778
+ const base64 = buffer.toString('base64');
779
+ const dataUri = `data:${mimetype};base64,${base64}`;
780
+ const prompt = PROMPTS[category];
781
+ const contentParts = [
782
+ { type: 'text', text: prompt },
783
+ ];
784
+ if (category === 'image') {
785
+ contentParts.push({
786
+ type: 'image_url',
787
+ image_url: { url: dataUri },
788
+ });
789
+ }
790
+ else if (category === 'audio') {
791
+ contentParts.push({
792
+ type: 'input_audio',
793
+ input_audio: { data: base64, format: this.getAudioFormat(mimetype) },
794
+ });
795
+ }
796
+ else if (category === 'document') {
797
+ contentParts.push({
798
+ type: 'file',
799
+ file: { filename, file_data: dataUri },
800
+ });
801
+ }
802
+ else if (category === 'video') {
803
+ contentParts.push({
804
+ type: 'video_url',
805
+ video_url: { url: dataUri },
806
+ });
807
+ }
808
+ const abortController = new AbortController();
809
+ const timeout = setTimeout(() => abortController.abort(), AI_PROCESS_TIMEOUT_MS);
810
+ try {
811
+ const response = await fetch(`${this.providerBaseURL}/chat/completions`, {
812
+ method: 'POST',
813
+ headers: {
814
+ Authorization: `Bearer ${this.providerApiKey}`,
815
+ 'Content-Type': 'application/json',
816
+ ...this.providerHeaders,
817
+ },
818
+ body: JSON.stringify({
819
+ model: this.processingModel,
820
+ messages: [
821
+ {
822
+ role: 'user',
823
+ content: contentParts,
824
+ },
825
+ ],
826
+ }),
827
+ signal: abortController.signal,
828
+ });
829
+ if (!response.ok) {
830
+ const errorText = await response.text();
831
+ this.logger.error(`OpenRouter API error (${response.status}): ${errorText.slice(0, MAX_ERROR_BODY_LENGTH)}`);
832
+ throw new Error(`AI processing failed (${response.status})`);
833
+ }
834
+ const result = (await response.json());
835
+ this.logger.log(`[aiProcess] model=${result.model ?? 'unknown'} usage=${JSON.stringify(result.usage ?? null)}`);
836
+ return {
837
+ content: result.choices[0]?.message?.content ?? '',
838
+ usage: result.usage
839
+ ? {
840
+ cost: result.usage.cost,
841
+ promptTokens: result.usage.prompt_tokens,
842
+ completionTokens: result.usage.completion_tokens,
843
+ }
844
+ : undefined,
845
+ };
846
+ }
847
+ finally {
848
+ clearTimeout(timeout);
849
+ }
850
+ }
851
+ getAudioFormat(mimetype) {
852
+ if (mimetype.includes('mp3') || mimetype.includes('mpeg'))
853
+ return 'mp3';
854
+ if (mimetype.includes('wav'))
855
+ return 'wav';
856
+ if (mimetype.includes('ogg'))
857
+ return 'ogg';
858
+ if (mimetype.includes('flac'))
859
+ return 'flac';
860
+ if (mimetype.includes('webm'))
861
+ return 'webm';
862
+ if (mimetype.includes('mp4') || mimetype.includes('m4a'))
863
+ return 'mp4';
864
+ if (mimetype.includes('aac'))
865
+ return 'aac';
866
+ return 'mp3';
867
+ }
868
+ /**
869
+ * Strip control characters and bracket sequences from filename
870
+ * to prevent prompt injection when interpolated into LLM context.
871
+ */
872
+ sanitizeFilename(filename) {
873
+ return (filename
874
+ // eslint-disable-next-line no-control-regex
875
+ .replace(/[-]/g, '')
876
+ .replace(/[[\]]/g, '')
877
+ .slice(0, 255));
878
+ }
879
+ /**
880
+ * Plain-text mimetypes that can be read directly as UTF-8 without AI
881
+ * processing or local parsers.
882
+ */
883
+ isPlainTextType(mimetype) {
884
+ return (mimetype.startsWith('text/') ||
885
+ mimetype === 'application/json' ||
886
+ mimetype === 'application/xml' ||
887
+ mimetype === 'application/rtf');
888
+ }
889
+ buildAnalysisMarkdown(filename, mimetype, sizeBytes, category, content) {
890
+ const labels = {
891
+ image: 'Image Description',
892
+ video: 'Video Description',
893
+ audio: 'Audio Transcription',
894
+ };
895
+ const label = labels[category] ?? 'Analysis';
896
+ return [
897
+ `# ${label}: ${filename}`,
898
+ '',
899
+ `- **File:** ${filename}`,
900
+ `- **Type:** ${mimetype}`,
901
+ `- **Size:** ${(sizeBytes / 1024).toFixed(1)} KB`,
902
+ `- **Processed:** ${new Date().toISOString()}`,
903
+ '',
904
+ '---',
905
+ '',
906
+ content,
907
+ '',
908
+ ].join('\n');
909
+ }
910
+ formatContent(label, filename, content) {
911
+ return `[${label} of ${filename}]:\n${content}`;
912
+ }
913
+ truncateText(text) {
914
+ if (text.length <= MAX_TEXT_LENGTH)
915
+ return text;
916
+ return text.slice(0, MAX_TEXT_LENGTH) + '\n\n[...truncated]';
917
+ }
918
+ /**
919
+ * Download and process a file in one pass, returning both the raw buffer
920
+ * and extracted text. Used by the process_file tool when copy_to_sandbox
921
+ * is enabled so the file isn't downloaded twice.
922
+ */
923
+ async downloadAndProcessFile(source, hints) {
924
+ let buffer;
925
+ let httpContentType;
926
+ let finalUrl;
927
+ if ('eventId' in source) {
928
+ const roomId = source.roomId;
929
+ if (!roomId) {
930
+ throw new Error('roomId is required when using eventId');
931
+ }
932
+ buffer = await this.downloadFromMatrixEvent(roomId, source.eventId);
933
+ }
934
+ else {
935
+ const url = source.url;
936
+ if (!ALLOWED_URI_SCHEMES.test(url)) {
937
+ throw new Error('Invalid URI scheme — only http, https, and mxc are allowed');
938
+ }
939
+ if (url.startsWith('mxc://')) {
940
+ buffer = await this.downloadFromMatrix(url);
941
+ }
942
+ else {
943
+ const result = await this.downloadFromUrl(url);
944
+ buffer = result.data;
945
+ httpContentType = result.contentType;
946
+ finalUrl = result.finalUrl;
947
+ }
948
+ }
949
+ this.logger.log(`[downloadAndProcessFile] Downloaded ${buffer.length} bytes`);
950
+ if (buffer.length > MAX_FILE_SIZE) {
951
+ throw new Error('File exceeds maximum size (25 MB)');
952
+ }
953
+ const url = 'url' in source ? source.url : undefined;
954
+ const filename = hints?.filename ??
955
+ (finalUrl ? this.extractFilenameFromUrl(finalUrl) : null) ??
956
+ (url ? this.extractFilenameFromUrl(url) : null) ??
957
+ 'file';
958
+ const extensionMime = this.guessMimeFromFilename(filename);
959
+ const magicMime = this.detectMimeFromMagicBytes(buffer);
960
+ const mimetype = hints?.mimetype ??
961
+ extensionMime ??
962
+ magicMime ??
963
+ httpContentType ??
964
+ 'application/octet-stream';
965
+ this.logger.log(`[downloadAndProcessFile] Resolved — filename="${filename}", mimetype="${mimetype}" ` +
966
+ `(extension=${extensionMime}, magic=${magicMime}, http=${httpContentType})`);
967
+ const category = this.categorizeFile(mimetype);
968
+ let text;
969
+ if (category === 'unsupported') {
970
+ const fallbackMime = magicMime ?? httpContentType;
971
+ const fallbackCategory = fallbackMime
972
+ ? this.categorizeFile(fallbackMime)
973
+ : 'unsupported';
974
+ if (fallbackCategory !== 'unsupported' && fallbackMime) {
975
+ const attachment = { filename, mimetype: fallbackMime };
976
+ this.verifyMagicBytes(buffer, fallbackCategory, attachment);
977
+ text = await this.processCategory(buffer, fallbackCategory, attachment);
978
+ }
979
+ else {
980
+ text = `[File "${this.sanitizeFilename(filename)}" (${mimetype}) is not a supported file type and could not be processed.]`;
981
+ }
982
+ }
983
+ else {
984
+ const attachment = { filename, mimetype };
985
+ this.verifyMagicBytes(buffer, category, attachment);
986
+ text = await this.processCategory(buffer, category, attachment);
987
+ }
988
+ return {
989
+ buffer,
990
+ text,
991
+ resolvedFilename: filename,
992
+ resolvedMimetype: mimetype,
993
+ };
994
+ }
995
+ /**
996
+ * Process a file from a Matrix event ID — downloads via
997
+ * downloadFromMatrixEvent (handles encrypted + unencrypted) and routes
998
+ * through the standard processCategory pipeline.
999
+ */
1000
+ async processFileFromEventId(roomId, eventId, hints) {
1001
+ this.logger.log(`[processFileFromEventId] Starting — roomId=${roomId}, eventId=${eventId}, hints=${JSON.stringify(hints)}`);
1002
+ const buffer = await this.downloadFromMatrixEvent(roomId, eventId);
1003
+ if (buffer.length > MAX_FILE_SIZE) {
1004
+ throw new Error('File exceeds maximum size (25 MB)');
1005
+ }
1006
+ const filename = hints?.filename ?? 'file';
1007
+ const magicMime = this.detectMimeFromMagicBytes(buffer);
1008
+ const extensionMime = this.guessMimeFromFilename(filename);
1009
+ const mimetype = hints?.mimetype ??
1010
+ extensionMime ??
1011
+ magicMime ??
1012
+ 'application/octet-stream';
1013
+ this.logger.log(`[processFileFromEventId] Resolved — filename="${filename}", mimetype="${mimetype}" ` +
1014
+ `(hints=${hints?.mimetype}, extension=${extensionMime}, magic=${magicMime})`);
1015
+ const category = this.categorizeFile(mimetype);
1016
+ if (category === 'unsupported') {
1017
+ const fallbackMime = magicMime;
1018
+ const fallbackCategory = fallbackMime
1019
+ ? this.categorizeFile(fallbackMime)
1020
+ : 'unsupported';
1021
+ if (fallbackCategory !== 'unsupported' && fallbackMime) {
1022
+ const attachment = { filename, mimetype: fallbackMime };
1023
+ this.verifyMagicBytes(buffer, fallbackCategory, attachment);
1024
+ return this.processCategory(buffer, fallbackCategory, attachment);
1025
+ }
1026
+ return `[File "${this.sanitizeFilename(filename)}" (${mimetype}) is not a supported file type and could not be processed.]`;
1027
+ }
1028
+ const attachment = { filename, mimetype };
1029
+ this.verifyMagicBytes(buffer, category, attachment);
1030
+ return this.processCategory(buffer, category, attachment);
1031
+ }
1032
+ /**
1033
+ * Process a file from a URL and extract its content as text.
1034
+ * For HTTPS image/video URLs, passes the URL directly to the AI model (no download).
1035
+ * For audio/documents/mxc, downloads first then processes locally or via AI.
1036
+ * If the type can't be determined, tries AI passthrough before falling back to download.
1037
+ */
1038
+ async processFileFromUrl(url, hints) {
1039
+ this.logger.log(`[processFileFromUrl] Starting — url=${url}, hints=${JSON.stringify(hints)}`);
1040
+ if (!ALLOWED_URI_SCHEMES.test(url)) {
1041
+ throw new Error('Invalid URI scheme — only http, https, and mxc are allowed');
1042
+ }
1043
+ if (url.startsWith('mxc://')) {
1044
+ return this.downloadAndProcess(url, hints);
1045
+ }
1046
+ const filename = hints?.filename ?? this.extractFilenameFromUrl(url);
1047
+ const extensionMime = this.guessMimeFromFilename(filename);
1048
+ const knownMime = hints?.mimetype ?? extensionMime;
1049
+ const knownCategory = knownMime ? this.categorizeFile(knownMime) : null;
1050
+ if (knownCategory === 'image' || knownCategory === 'video') {
1051
+ this.logger.log(`[processFileFromUrl] URL passthrough (${knownCategory}) — "${filename}" (${knownMime})`);
1052
+ return this.tryUrlPassthrough(url, knownMime, knownCategory, filename);
1053
+ }
1054
+ if (knownCategory === 'audio' || knownCategory === 'document') {
1055
+ this.logger.log(`[processFileFromUrl] Known ${knownCategory} — downloading "${filename}" (${knownMime})`);
1056
+ return this.downloadAndProcess(url, hints);
1057
+ }
1058
+ this.logger.log(`[processFileFromUrl] Unknown type for "${filename}" — trying HEAD`);
1059
+ let headContentType;
1060
+ let resolvedUrl = url;
1061
+ try {
1062
+ const head = await this.headUrl(url);
1063
+ headContentType = head.contentType;
1064
+ resolvedUrl = head.finalUrl;
1065
+ }
1066
+ catch (error) {
1067
+ this.logger.warn(`[processFileFromUrl] HEAD failed: ${error instanceof Error ? error.message : String(error)}`);
1068
+ }
1069
+ if (headContentType) {
1070
+ // Ignore text/html — web pages usually wrap embedded media (YouTube, etc.)
1071
+ // and should fall through to the AI video passthrough below.
1072
+ const isHtmlPage = headContentType.startsWith('text/html');
1073
+ const headCategory = isHtmlPage
1074
+ ? 'unsupported'
1075
+ : this.categorizeFile(headContentType);
1076
+ if (headCategory === 'image' || headCategory === 'video') {
1077
+ this.logger.log(`[processFileFromUrl] HEAD says ${headCategory} (${headContentType}) — URL passthrough`);
1078
+ return this.tryUrlPassthrough(resolvedUrl, headContentType, headCategory, filename);
1079
+ }
1080
+ if (headCategory === 'audio' || headCategory === 'document') {
1081
+ this.logger.log(`[processFileFromUrl] HEAD says ${headCategory} (${headContentType}) — downloading`);
1082
+ return this.downloadAndProcess(url, hints);
1083
+ }
1084
+ }
1085
+ // Try passing the URL to AI as video — Gemini natively handles YouTube,
1086
+ // Vimeo, and many other platforms that serve HTML pages with embedded video.
1087
+ // SSRF validation is performed inside `aiProcessFromUrl`.
1088
+ this.logger.log(`[processFileFromUrl] Type still unknown (HEAD Content-Type: ${headContentType ?? 'none'}) — trying AI video passthrough as fallback`);
1089
+ try {
1090
+ const { content } = await this.aiProcessFromUrl(resolvedUrl, 'video/mp4', 'video', filename);
1091
+ if (content && content.trim().length > 0) {
1092
+ this.logger.log(`[processFileFromUrl] AI video passthrough succeeded for "${filename}"`);
1093
+ return this.formatContent('Description', this.sanitizeFilename(filename), content);
1094
+ }
1095
+ }
1096
+ catch (error) {
1097
+ this.logger.warn(`[processFileFromUrl] AI video passthrough failed, falling back to download: ${error instanceof Error ? error.message : String(error)}`);
1098
+ }
1099
+ this.logger.log(`[processFileFromUrl] All passthrough attempts failed — downloading "${filename}"`);
1100
+ return this.downloadAndProcess(url, hints);
1101
+ }
1102
+ /**
1103
+ * Try passing a URL directly to AI for image/video processing.
1104
+ * Falls back to download + process if the AI rejects it.
1105
+ */
1106
+ async tryUrlPassthrough(url, mimetype, category, filename) {
1107
+ try {
1108
+ const { content } = await this.aiProcessFromUrl(url, mimetype, category, filename);
1109
+ if (content && content.trim().length > 0) {
1110
+ return this.formatContent('Description', this.sanitizeFilename(filename), content);
1111
+ }
1112
+ this.logger.warn(`[tryUrlPassthrough] AI returned empty response for "${filename}", falling back to download`);
1113
+ }
1114
+ catch (error) {
1115
+ this.logger.warn(`[tryUrlPassthrough] AI passthrough failed for "${filename}", falling back to download: ${error instanceof Error ? error.message : String(error)}`);
1116
+ }
1117
+ return this.downloadAndProcess(url, { filename, mimetype });
1118
+ }
1119
+ /**
1120
+ * Download a URL (or mxc:// resource) into memory and process from the buffer.
1121
+ * Handles size validation, mime detection from bytes, and category routing.
1122
+ */
1123
+ async downloadAndProcess(url, hints) {
1124
+ let buffer;
1125
+ let httpContentType;
1126
+ let finalUrl;
1127
+ if (url.startsWith('mxc://')) {
1128
+ buffer = await this.downloadFromMatrix(url);
1129
+ }
1130
+ else {
1131
+ const result = await this.downloadFromUrl(url);
1132
+ buffer = result.data;
1133
+ httpContentType = result.contentType;
1134
+ finalUrl = result.finalUrl;
1135
+ }
1136
+ this.logger.log(`[downloadAndProcess] Downloaded ${buffer.length} bytes, HTTP Content-Type: ${httpContentType ?? 'none'}` +
1137
+ (finalUrl ? `, redirected to: ${finalUrl}` : ''));
1138
+ if (buffer.length > MAX_FILE_SIZE) {
1139
+ throw new Error('File exceeds maximum size (25 MB)');
1140
+ }
1141
+ const filename = hints?.filename ??
1142
+ (finalUrl ? this.extractFilenameFromUrl(finalUrl) : null) ??
1143
+ this.extractFilenameFromUrl(url);
1144
+ const extensionMime = this.guessMimeFromFilename(filename);
1145
+ const magicMime = this.detectMimeFromMagicBytes(buffer);
1146
+ const mimetype = hints?.mimetype ??
1147
+ extensionMime ??
1148
+ magicMime ??
1149
+ httpContentType ??
1150
+ 'application/octet-stream';
1151
+ this.logger.log(`[downloadAndProcess] Resolved — filename="${filename}", mimetype="${mimetype}" ` +
1152
+ `(extension=${extensionMime}, magic=${magicMime}, http=${httpContentType})`);
1153
+ const category = this.categorizeFile(mimetype);
1154
+ if (category === 'unsupported') {
1155
+ const fallbackMime = magicMime ?? httpContentType;
1156
+ const fallbackCategory = fallbackMime
1157
+ ? this.categorizeFile(fallbackMime)
1158
+ : 'unsupported';
1159
+ if (fallbackCategory !== 'unsupported' && fallbackMime) {
1160
+ this.logger.log(`[downloadAndProcess] Fallback mime "${fallbackMime}" → ${fallbackCategory}`);
1161
+ const attachment = { filename, mimetype: fallbackMime };
1162
+ this.verifyMagicBytes(buffer, fallbackCategory, attachment);
1163
+ return this.processCategory(buffer, fallbackCategory, attachment);
1164
+ }
1165
+ this.logger.warn(`[downloadAndProcess] Unsupported file type: ${mimetype} for ${filename}`);
1166
+ return `[File "${this.sanitizeFilename(filename)}" (${mimetype}) is not a supported file type and could not be processed.]`;
1167
+ }
1168
+ const attachment = { filename, mimetype };
1169
+ this.verifyMagicBytes(buffer, category, attachment);
1170
+ return this.processCategory(buffer, category, attachment);
1171
+ }
1172
+ /**
1173
+ * Route a validated buffer to the correct processor by category.
1174
+ */
1175
+ async processCategory(buffer, category, attachment) {
1176
+ this.logger.log(`[processCategory] Processing "${attachment.filename}" as ${category} (${attachment.mimetype}, ${buffer.length} bytes)`);
1177
+ let result;
1178
+ switch (category) {
1179
+ case 'document':
1180
+ result = await this.processDocument(buffer, attachment);
1181
+ break;
1182
+ case 'image':
1183
+ result = await this.processImage(buffer, attachment);
1184
+ break;
1185
+ case 'audio':
1186
+ result = await this.processAudio(buffer, attachment);
1187
+ break;
1188
+ case 'video':
1189
+ result = await this.processVideo(buffer, attachment);
1190
+ break;
1191
+ }
1192
+ return result.text;
1193
+ }
1194
+ /**
1195
+ * Best-effort filename extraction from a URL path.
1196
+ * Falls back to 'download' if nothing useful can be derived.
1197
+ */
1198
+ extractFilenameFromUrl(url) {
1199
+ try {
1200
+ const pathname = new URL(url).pathname;
1201
+ const lastSegment = pathname.split('/').filter(Boolean).pop();
1202
+ if (lastSegment) {
1203
+ return decodeURIComponent(lastSegment).slice(0, 255);
1204
+ }
1205
+ }
1206
+ catch {
1207
+ // Malformed URL — fall through.
1208
+ }
1209
+ return 'download';
1210
+ }
1211
+ /**
1212
+ * Map common file extensions to MIME types.
1213
+ * Returns null if the extension is unknown.
1214
+ */
1215
+ guessMimeFromFilename(filename) {
1216
+ const ext = filename.split('.').pop()?.toLowerCase();
1217
+ if (!ext)
1218
+ return null;
1219
+ const map = {
1220
+ // Documents
1221
+ pdf: 'application/pdf',
1222
+ doc: 'application/msword',
1223
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1224
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1225
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
1226
+ xls: 'application/vnd.ms-excel',
1227
+ ppt: 'application/vnd.ms-powerpoint',
1228
+ rtf: 'application/rtf',
1229
+ // Text / code
1230
+ txt: 'text/plain',
1231
+ md: 'text/markdown',
1232
+ html: 'text/html',
1233
+ htm: 'text/html',
1234
+ csv: 'text/csv',
1235
+ json: 'application/json',
1236
+ xml: 'application/xml',
1237
+ css: 'text/css',
1238
+ js: 'text/javascript',
1239
+ ts: 'text/plain',
1240
+ py: 'text/x-python',
1241
+ yaml: 'text/yaml',
1242
+ yml: 'text/yaml',
1243
+ // Images
1244
+ png: 'image/png',
1245
+ jpg: 'image/jpeg',
1246
+ jpeg: 'image/jpeg',
1247
+ gif: 'image/gif',
1248
+ webp: 'image/webp',
1249
+ svg: 'image/svg+xml',
1250
+ bmp: 'image/bmp',
1251
+ tiff: 'image/tiff',
1252
+ tif: 'image/tiff',
1253
+ // Audio
1254
+ mp3: 'audio/mpeg',
1255
+ ogg: 'audio/ogg',
1256
+ flac: 'audio/flac',
1257
+ wav: 'audio/wav',
1258
+ m4a: 'audio/mp4',
1259
+ aac: 'audio/aac',
1260
+ // Video
1261
+ webm: 'video/webm',
1262
+ mp4: 'video/mp4',
1263
+ mov: 'video/quicktime',
1264
+ avi: 'video/x-msvideo',
1265
+ mkv: 'video/x-matroska',
1266
+ '3gp': 'video/3gpp',
1267
+ };
1268
+ return map[ext] ?? null;
1269
+ }
1270
+ };
1271
+ FileProcessingService = FileProcessingService_1 = __decorate([
1272
+ Injectable(),
1273
+ __param(2, Optional()),
1274
+ __param(2, Inject(FILE_PROCESSING_CREDIT_SINK)),
1275
+ __metadata("design:paramtypes", [ConfigService,
1276
+ UcanService, Object])
1277
+ ], FileProcessingService);
1278
+ export { FileProcessingService };