@aurabase/aurabase-js 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/.changeset/config.json +11 -0
  2. package/CHANGELOG.md +30 -0
  3. package/LICENSE +21 -0
  4. package/README.md +833 -0
  5. package/package.json +90 -0
  6. package/packages/ai/LICENSE +21 -0
  7. package/packages/ai/README.md +106 -0
  8. package/packages/ai/dist/index.cjs +229 -0
  9. package/packages/ai/dist/index.d.cts +139 -0
  10. package/packages/ai/dist/index.d.ts +139 -0
  11. package/packages/ai/dist/index.js +226 -0
  12. package/packages/ai/node_modules/.bin/esbuild +19 -0
  13. package/packages/ai/node_modules/.bin/tsc +22 -0
  14. package/packages/ai/node_modules/.bin/tsserver +22 -0
  15. package/packages/ai/node_modules/.bin/tsup +22 -0
  16. package/packages/ai/node_modules/.bin/tsup-node +22 -0
  17. package/packages/ai/node_modules/.bin/vite +22 -0
  18. package/packages/ai/node_modules/.bin/vitest +22 -0
  19. package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  20. package/packages/ai/package.json +61 -0
  21. package/packages/ai/src/AiService.ts +226 -0
  22. package/packages/ai/src/index.ts +18 -0
  23. package/packages/ai/src/sse.ts +141 -0
  24. package/packages/ai/src/types.ts +30 -0
  25. package/packages/ai/tests/chat-stream.test.ts +151 -0
  26. package/packages/ai/tests/rag-namespaces.test.ts +54 -0
  27. package/packages/ai/tests/sse.test.ts +69 -0
  28. package/packages/ai/tsconfig.json +5 -0
  29. package/packages/ai/tsup.config.ts +11 -0
  30. package/packages/aurabase/LICENSE +21 -0
  31. package/packages/aurabase/README.md +64 -0
  32. package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.cts +77 -0
  33. package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.ts +77 -0
  34. package/packages/aurabase/dist/index.cjs +237 -0
  35. package/packages/aurabase/dist/index.d.cts +46 -0
  36. package/packages/aurabase/dist/index.d.ts +46 -0
  37. package/packages/aurabase/dist/index.js +177 -0
  38. package/packages/aurabase/dist/ssr.cjs +224 -0
  39. package/packages/aurabase/dist/ssr.d.cts +65 -0
  40. package/packages/aurabase/dist/ssr.d.ts +65 -0
  41. package/packages/aurabase/dist/ssr.js +214 -0
  42. package/packages/aurabase/node_modules/.bin/esbuild +19 -0
  43. package/packages/aurabase/node_modules/.bin/tsc +22 -0
  44. package/packages/aurabase/node_modules/.bin/tsserver +22 -0
  45. package/packages/aurabase/node_modules/.bin/tsup +22 -0
  46. package/packages/aurabase/node_modules/.bin/tsup-node +22 -0
  47. package/packages/aurabase/node_modules/.bin/vite +22 -0
  48. package/packages/aurabase/node_modules/.bin/vitest +22 -0
  49. package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  50. package/packages/aurabase/package.json +80 -0
  51. package/packages/aurabase/src/AuraClient.ts +237 -0
  52. package/packages/aurabase/src/index.ts +152 -0
  53. package/packages/aurabase/src/ssr.ts +156 -0
  54. package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +82 -0
  55. package/packages/aurabase/tests/ai.test.ts +111 -0
  56. package/packages/aurabase/tests/auth-admin.test.ts +515 -0
  57. package/packages/aurabase/tests/auth-e2e.test.ts +135 -0
  58. package/packages/aurabase/tests/auth-hardening.test.ts +206 -0
  59. package/packages/aurabase/tests/auth.test.ts +947 -0
  60. package/packages/aurabase/tests/database.test.ts +230 -0
  61. package/packages/aurabase/tests/db-types-e2e.test.ts +217 -0
  62. package/packages/aurabase/tests/dispose.test.ts +23 -0
  63. package/packages/aurabase/tests/foreign-token.e2e.test.ts +129 -0
  64. package/packages/aurabase/tests/functions.test.ts +107 -0
  65. package/packages/aurabase/tests/integration.test.ts +413 -0
  66. package/packages/aurabase/tests/live-production-validation.js +241 -0
  67. package/packages/aurabase/tests/live-sdk-production-validation.js +293 -0
  68. package/packages/aurabase/tests/mocks.ts +139 -0
  69. package/packages/aurabase/tests/notifications.test.ts +314 -0
  70. package/packages/aurabase/tests/observability-backup.test.ts +186 -0
  71. package/packages/aurabase/tests/observability-hook.test.ts +130 -0
  72. package/packages/aurabase/tests/production-robustness.test.ts +268 -0
  73. package/packages/aurabase/tests/public-api-surface.test.ts +53 -0
  74. package/packages/aurabase/tests/query-builder.test.ts +329 -0
  75. package/packages/aurabase/tests/readme-smoke-e2e.test.ts +231 -0
  76. package/packages/aurabase/tests/realtime-filter-e2e.test.ts +344 -0
  77. package/packages/aurabase/tests/refresh-locks.test.ts +210 -0
  78. package/packages/aurabase/tests/retry-option.test.ts +17 -0
  79. package/packages/aurabase/tests/rpc-e2e.test.ts +254 -0
  80. package/packages/aurabase/tests/rs256-local-e2e.test.ts +114 -0
  81. package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +235 -0
  82. package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +124 -0
  83. package/packages/aurabase/tests/service-role-guard.test.ts +99 -0
  84. package/packages/aurabase/tests/ssr.test.ts +91 -0
  85. package/packages/aurabase/tests/storage-backend-e2e.test.ts +481 -0
  86. package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +104 -0
  87. package/packages/aurabase/tests/storage-transform-e2e.test.ts +684 -0
  88. package/packages/aurabase/tests/storage.test.ts +55 -0
  89. package/packages/aurabase/tests/types/client-generic.test-d.ts +79 -0
  90. package/packages/aurabase/tests/types/docs-snippets.test-d.ts +571 -0
  91. package/packages/aurabase/tests/types/error-code.test-d.ts +53 -0
  92. package/packages/aurabase/tsconfig.json +5 -0
  93. package/packages/aurabase/tsconfig.test-d.json +5 -0
  94. package/packages/aurabase/tsup.config.ts +11 -0
  95. package/packages/aurabase/vitest.config.ts +16 -0
  96. package/packages/aurabase/vitest.e2e.config.ts +9 -0
  97. package/packages/auth/LICENSE +21 -0
  98. package/packages/auth/README.md +215 -0
  99. package/packages/auth/dist/index.cjs +1175 -0
  100. package/packages/auth/dist/index.d.cts +397 -0
  101. package/packages/auth/dist/index.d.ts +397 -0
  102. package/packages/auth/dist/index.js +1170 -0
  103. package/packages/auth/node_modules/.bin/esbuild +19 -0
  104. package/packages/auth/node_modules/.bin/tsc +22 -0
  105. package/packages/auth/node_modules/.bin/tsserver +22 -0
  106. package/packages/auth/node_modules/.bin/tsup +22 -0
  107. package/packages/auth/node_modules/.bin/tsup-node +22 -0
  108. package/packages/auth/node_modules/.bin/vite +22 -0
  109. package/packages/auth/node_modules/.bin/vitest +22 -0
  110. package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  111. package/packages/auth/package.json +61 -0
  112. package/packages/auth/src/AuthAdminService.ts +149 -0
  113. package/packages/auth/src/AuthService.ts +1166 -0
  114. package/packages/auth/src/SessionManager.ts +92 -0
  115. package/packages/auth/src/index.ts +17 -0
  116. package/packages/auth/src/jwks.ts +337 -0
  117. package/packages/auth/src/pkce.ts +27 -0
  118. package/packages/auth/src/types.ts +89 -0
  119. package/packages/auth/tests/auth-lifecycle.test.ts +149 -0
  120. package/packages/auth/tests/detect-session.test.ts +361 -0
  121. package/packages/auth/tests/getclaims.test.ts +780 -0
  122. package/packages/auth/tests/is-mfa-challenge.test.ts +27 -0
  123. package/packages/auth/tests/password-recovery.test.ts +124 -0
  124. package/packages/auth/tests/pkce.test.ts +192 -0
  125. package/packages/auth/tests/session-manager.test.ts +103 -0
  126. package/packages/auth/tests/set-session.test.ts +139 -0
  127. package/packages/auth/tests/verify-otp.test.ts +153 -0
  128. package/packages/auth/tsconfig.json +5 -0
  129. package/packages/auth/tsup.config.ts +11 -0
  130. package/packages/core/LICENSE +21 -0
  131. package/packages/core/README.md +132 -0
  132. package/packages/core/dist/index.cjs +695 -0
  133. package/packages/core/dist/index.d.cts +1137 -0
  134. package/packages/core/dist/index.d.ts +1137 -0
  135. package/packages/core/dist/index.js +673 -0
  136. package/packages/core/node_modules/.bin/esbuild +22 -0
  137. package/packages/core/node_modules/.bin/tsc +22 -0
  138. package/packages/core/node_modules/.bin/tsserver +22 -0
  139. package/packages/core/node_modules/.bin/tsup +22 -0
  140. package/packages/core/node_modules/.bin/tsup-node +22 -0
  141. package/packages/core/node_modules/.bin/vite +22 -0
  142. package/packages/core/node_modules/.bin/vitest +22 -0
  143. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  144. package/packages/core/package.json +57 -0
  145. package/packages/core/src/AuraError.ts +89 -0
  146. package/packages/core/src/BaseService.ts +44 -0
  147. package/packages/core/src/client.ts +34 -0
  148. package/packages/core/src/cookie-utils.ts +42 -0
  149. package/packages/core/src/fetch.ts +435 -0
  150. package/packages/core/src/index.ts +22 -0
  151. package/packages/core/src/retry.ts +63 -0
  152. package/packages/core/src/serialize.ts +6 -0
  153. package/packages/core/src/stores/BaseAuthStore.ts +164 -0
  154. package/packages/core/src/stores/LocalAuthStore.ts +199 -0
  155. package/packages/core/src/types.ts +1042 -0
  156. package/packages/core/tests/auth-store.test.ts +300 -0
  157. package/packages/core/tests/error-code.test.ts +61 -0
  158. package/packages/core/tests/error.test.ts +24 -0
  159. package/packages/core/tests/fetch.test.ts +286 -0
  160. package/packages/core/tests/mocks.ts +139 -0
  161. package/packages/core/tests/observability.test.ts +86 -0
  162. package/packages/core/tests/refresh-hardening.test.ts +164 -0
  163. package/packages/core/tests/refresh-observability.test.ts +111 -0
  164. package/packages/core/tests/retry.test.ts +106 -0
  165. package/packages/core/tests/serialize.test.ts +12 -0
  166. package/packages/core/tests/timeout.test.ts +79 -0
  167. package/packages/core/tsconfig.json +5 -0
  168. package/packages/core/tsup.config.ts +11 -0
  169. package/packages/db/LICENSE +21 -0
  170. package/packages/db/README.md +152 -0
  171. package/packages/db/VENDORED.md +93 -0
  172. package/packages/db/dist/index.cjs +544 -0
  173. package/packages/db/dist/index.d.cts +1111 -0
  174. package/packages/db/dist/index.d.ts +1111 -0
  175. package/packages/db/dist/index.js +541 -0
  176. package/packages/db/node_modules/.bin/esbuild +19 -0
  177. package/packages/db/node_modules/.bin/tsc +22 -0
  178. package/packages/db/node_modules/.bin/tsserver +22 -0
  179. package/packages/db/node_modules/.bin/tsup +22 -0
  180. package/packages/db/node_modules/.bin/tsup-node +22 -0
  181. package/packages/db/node_modules/.bin/vite +22 -0
  182. package/packages/db/node_modules/.bin/vitest +22 -0
  183. package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  184. package/packages/db/package.json +62 -0
  185. package/packages/db/src/DatabaseService.ts +170 -0
  186. package/packages/db/src/QueryBuilder.ts +830 -0
  187. package/packages/db/src/index.ts +2 -0
  188. package/packages/db/src/pgrst-types/common/common.ts +86 -0
  189. package/packages/db/src/pgrst-types/common/rpc.ts +151 -0
  190. package/packages/db/src/pgrst-types/feature-flags.ts +20 -0
  191. package/packages/db/src/pgrst-types/types.ts +134 -0
  192. package/packages/db/src/select-query-parser/parser.ts +479 -0
  193. package/packages/db/src/select-query-parser/result.ts +553 -0
  194. package/packages/db/src/select-query-parser/types.ts +132 -0
  195. package/packages/db/src/select-query-parser/utils.ts +707 -0
  196. package/packages/db/tests/filters.test.ts +81 -0
  197. package/packages/db/tests/mutations-filtered.e2e.test.ts +132 -0
  198. package/packages/db/tests/mutations-filtered.test.ts +188 -0
  199. package/packages/db/tests/order.test.ts +45 -0
  200. package/packages/db/tests/rpc.test.ts +41 -0
  201. package/packages/db/tests/scale/build-large-schema.ts +94 -0
  202. package/packages/db/tests/scale/scale-compile.test.ts +167 -0
  203. package/packages/db/tests/schema.test.ts +77 -0
  204. package/packages/db/tests/single-accept.e2e.test.ts +123 -0
  205. package/packages/db/tests/single.test.ts +211 -0
  206. package/packages/db/tests/types/filters.test-d.ts +52 -0
  207. package/packages/db/tests/types/fixtures.ts +40 -0
  208. package/packages/db/tests/types/from-views.test-d.ts +18 -0
  209. package/packages/db/tests/types/mutations.test-d.ts +23 -0
  210. package/packages/db/tests/types/rpc.test-d.ts +60 -0
  211. package/packages/db/tests/types/schema-shape.test-d.ts +8 -0
  212. package/packages/db/tests/types/select-inference.test-d.ts +38 -0
  213. package/packages/db/tests/vendored/fixtures.ts +83 -0
  214. package/packages/db/tests/vendored/get-result.test-d.ts +75 -0
  215. package/packages/db/tsconfig.json +5 -0
  216. package/packages/db/tsconfig.test-d.json +5 -0
  217. package/packages/db/tsup.config.ts +11 -0
  218. package/packages/db/vitest.config.ts +8 -0
  219. package/packages/functions/LICENSE +21 -0
  220. package/packages/functions/README.md +96 -0
  221. package/packages/functions/dist/index.cjs +173 -0
  222. package/packages/functions/dist/index.d.cts +81 -0
  223. package/packages/functions/dist/index.d.ts +81 -0
  224. package/packages/functions/dist/index.js +168 -0
  225. package/packages/functions/node_modules/.bin/esbuild +19 -0
  226. package/packages/functions/node_modules/.bin/tsc +22 -0
  227. package/packages/functions/node_modules/.bin/tsserver +22 -0
  228. package/packages/functions/node_modules/.bin/tsup +22 -0
  229. package/packages/functions/node_modules/.bin/tsup-node +22 -0
  230. package/packages/functions/node_modules/.bin/vite +22 -0
  231. package/packages/functions/node_modules/.bin/vitest +22 -0
  232. package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  233. package/packages/functions/package.json +60 -0
  234. package/packages/functions/src/FunctionsService.ts +188 -0
  235. package/packages/functions/src/errors.ts +70 -0
  236. package/packages/functions/src/index.ts +6 -0
  237. package/packages/functions/tests/invoke.test.ts +97 -0
  238. package/packages/functions/tests/response-type-errors.test.ts +135 -0
  239. package/packages/functions/tsconfig.json +5 -0
  240. package/packages/functions/tsup.config.ts +11 -0
  241. package/packages/gen/LICENSE +21 -0
  242. package/packages/gen/README.md +84 -0
  243. package/packages/gen/dist/cli.cjs +334 -0
  244. package/packages/gen/dist/cli.d.cts +1 -0
  245. package/packages/gen/dist/cli.d.ts +1 -0
  246. package/packages/gen/dist/cli.js +332 -0
  247. package/packages/gen/dist/index.cjs +257 -0
  248. package/packages/gen/dist/index.d.cts +12 -0
  249. package/packages/gen/dist/index.d.ts +12 -0
  250. package/packages/gen/dist/index.js +255 -0
  251. package/packages/gen/node_modules/.bin/esbuild +19 -0
  252. package/packages/gen/node_modules/.bin/tsc +22 -0
  253. package/packages/gen/node_modules/.bin/tsserver +22 -0
  254. package/packages/gen/node_modules/.bin/tsup +22 -0
  255. package/packages/gen/node_modules/.bin/tsup-node +22 -0
  256. package/packages/gen/node_modules/.bin/vite +22 -0
  257. package/packages/gen/node_modules/.bin/vitest +22 -0
  258. package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  259. package/packages/gen/package.json +66 -0
  260. package/packages/gen/src/cli.ts +67 -0
  261. package/packages/gen/src/generate.ts +0 -0
  262. package/packages/gen/src/index.ts +2 -0
  263. package/packages/gen/tests/compile.test.ts +213 -0
  264. package/packages/gen/tests/e2e.test.ts +130 -0
  265. package/packages/gen/tests/generate.test.ts +396 -0
  266. package/packages/gen/tsconfig.json +5 -0
  267. package/packages/gen/tsup.config.ts +11 -0
  268. package/packages/notifications/LICENSE +21 -0
  269. package/packages/notifications/README.md +134 -0
  270. package/packages/notifications/dist/index.cjs +128 -0
  271. package/packages/notifications/dist/index.d.cts +81 -0
  272. package/packages/notifications/dist/index.d.ts +81 -0
  273. package/packages/notifications/dist/index.js +125 -0
  274. package/packages/notifications/node_modules/.bin/esbuild +19 -0
  275. package/packages/notifications/node_modules/.bin/tsc +22 -0
  276. package/packages/notifications/node_modules/.bin/tsserver +22 -0
  277. package/packages/notifications/node_modules/.bin/tsup +22 -0
  278. package/packages/notifications/node_modules/.bin/tsup-node +22 -0
  279. package/packages/notifications/node_modules/.bin/vite +22 -0
  280. package/packages/notifications/node_modules/.bin/vitest +22 -0
  281. package/packages/notifications/package.json +60 -0
  282. package/packages/notifications/src/NotificationsService.ts +230 -0
  283. package/packages/notifications/src/index.ts +19 -0
  284. package/packages/notifications/tsconfig.json +5 -0
  285. package/packages/notifications/tsup.config.ts +11 -0
  286. package/packages/realtime/LICENSE +21 -0
  287. package/packages/realtime/README.md +183 -0
  288. package/packages/realtime/dist/index.cjs +977 -0
  289. package/packages/realtime/dist/index.d.cts +259 -0
  290. package/packages/realtime/dist/index.d.ts +259 -0
  291. package/packages/realtime/dist/index.js +973 -0
  292. package/packages/realtime/node_modules/.bin/esbuild +19 -0
  293. package/packages/realtime/node_modules/.bin/tsc +22 -0
  294. package/packages/realtime/node_modules/.bin/tsserver +22 -0
  295. package/packages/realtime/node_modules/.bin/tsup +22 -0
  296. package/packages/realtime/node_modules/.bin/tsup-node +22 -0
  297. package/packages/realtime/node_modules/.bin/vite +22 -0
  298. package/packages/realtime/node_modules/.bin/vitest +22 -0
  299. package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  300. package/packages/realtime/package.json +60 -0
  301. package/packages/realtime/src/AuraChannel.ts +485 -0
  302. package/packages/realtime/src/HeartbeatManager.ts +73 -0
  303. package/packages/realtime/src/RealtimeService.ts +505 -0
  304. package/packages/realtime/src/columnFilter.ts +301 -0
  305. package/packages/realtime/src/index.ts +4 -0
  306. package/packages/realtime/tests/ack-routing.test.ts +81 -0
  307. package/packages/realtime/tests/column-filter.test.ts +144 -0
  308. package/packages/realtime/tests/dispatch.test.ts +191 -0
  309. package/packages/realtime/tests/heartbeat.test.ts +67 -0
  310. package/packages/realtime/tests/http-broadcast.test.ts +68 -0
  311. package/packages/realtime/tests/realtime-token.test.ts +43 -0
  312. package/packages/realtime/tests/reconnect-race.test.ts +149 -0
  313. package/packages/realtime/tests/ref-id.test.ts +34 -0
  314. package/packages/realtime/tests/set-auth.test.ts +70 -0
  315. package/packages/realtime/tsconfig.json +5 -0
  316. package/packages/realtime/tsup.config.ts +11 -0
  317. package/packages/storage/LICENSE +21 -0
  318. package/packages/storage/README.md +141 -0
  319. package/packages/storage/dist/index.cjs +333 -0
  320. package/packages/storage/dist/index.d.cts +185 -0
  321. package/packages/storage/dist/index.d.ts +185 -0
  322. package/packages/storage/dist/index.js +329 -0
  323. package/packages/storage/node_modules/.bin/esbuild +19 -0
  324. package/packages/storage/node_modules/.bin/tsc +22 -0
  325. package/packages/storage/node_modules/.bin/tsserver +22 -0
  326. package/packages/storage/node_modules/.bin/tsup +22 -0
  327. package/packages/storage/node_modules/.bin/tsup-node +22 -0
  328. package/packages/storage/node_modules/.bin/vite +22 -0
  329. package/packages/storage/node_modules/.bin/vitest +22 -0
  330. package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  331. package/packages/storage/package.json +60 -0
  332. package/packages/storage/src/DownloadBuilder.ts +60 -0
  333. package/packages/storage/src/StorageService.ts +323 -0
  334. package/packages/storage/src/index.ts +2 -0
  335. package/packages/storage/src/path.ts +35 -0
  336. package/packages/storage/src/transform.ts +15 -0
  337. package/packages/storage/tests/download.test.ts +64 -0
  338. package/packages/storage/tests/parity.test.ts +154 -0
  339. package/packages/storage/tests/path.test.ts +87 -0
  340. package/packages/storage/tests/transform.test.ts +79 -0
  341. package/packages/storage/tsconfig.json +5 -0
  342. package/packages/storage/tsup.config.ts +11 -0
  343. package/pnpm-workspace.yaml +24 -0
  344. package/tsconfig.base.json +15 -0
@@ -0,0 +1,226 @@
1
+ import { BaseService } from '@aurabase/core'
2
+ import { AuraError, isTokenExpired, refreshOnce, isAbortError } from '@aurabase/core'
3
+ import type {
4
+ AuraResponse,
5
+ ChatMessage,
6
+ ChatResponse,
7
+ EmbedRequest,
8
+ EmbedResponse,
9
+ RagIngestRequest,
10
+ RagIngestResponse,
11
+ RagRequest,
12
+ RagResponse,
13
+ SemanticSearchResult,
14
+ } from '@aurabase/core'
15
+ import type { AuraClientLike } from '@aurabase/core'
16
+ import { parseSSE, type ChatStreamChunk, type ChatStreamFinal } from './sse'
17
+ import type { RagNamespace, RagDeleteResponse, RagReindexResponse } from './types'
18
+
19
+ export class AiService extends BaseService {
20
+ constructor(client: AuraClientLike) {
21
+ super(client)
22
+ }
23
+
24
+ private get prefix(): string {
25
+ return `/v1/ai/${this.client.projectId}`
26
+ }
27
+
28
+ /**
29
+ * Send a chat completion request.
30
+ */
31
+ async chat(
32
+ messages: ChatMessage[],
33
+ options?: { model?: string; temperature?: number; max_tokens?: number },
34
+ ): Promise<AuraResponse<ChatResponse>> {
35
+ return this.request('POST', `${this.prefix}/chat`, {
36
+ messages,
37
+ ...options,
38
+ })
39
+ }
40
+
41
+ /**
42
+ * Chat completion streamé (SSE). Renvoie un `AsyncIterable` de deltas de tokens,
43
+ * consommable par `for await (const { delta } of ai.chatStream(msgs)) { … }`.
44
+ *
45
+ * Court-circuite volontairement le pipeline JSON (`@aurabase/core/fetch.ts`) et
46
+ * emprunte le chemin transport BRUT `client.fetchFn` (patron `StorageService.download`) :
47
+ * - refresh proactif du token expiré (même règle que `download`, marge 30 s) ;
48
+ * - pose manuelle de `Authorization: Bearer` + `apikey`, `Accept: text/event-stream` ;
49
+ * - lecture de `res.body` via `parseSSE`, mapping des events serveur :
50
+ * `delta` (`{ content }`) → `yield { delta }`, `error` (`{ error }`) → lève une
51
+ * `AuraError`, `done` → termine l'itération.
52
+ *
53
+ * `options.signal` interrompt proprement le flux (annulation du `fetch` + fin
54
+ * d'itération sans throw). Sur `!res.ok`, lève `AuraError(res.status)`.
55
+ *
56
+ * Backend route: POST /v1/ai/{project_id}/chat/stream
57
+ */
58
+ async *chatStream(
59
+ messages: ChatMessage[],
60
+ options?: { model?: string; temperature?: number; max_tokens?: number; signal?: AbortSignal },
61
+ ): AsyncGenerator<ChatStreamChunk> {
62
+ const client = this.client
63
+ const store = client.authStore
64
+ // Refresh proactif AVANT d'ouvrir le flux (aligné sur download()).
65
+ if (store.token && store.refreshToken && isTokenExpired(store.token, 30)) {
66
+ await refreshOnce(client)
67
+ }
68
+ const headers: Record<string, string> = {
69
+ 'Content-Type': 'application/json',
70
+ Accept: 'text/event-stream',
71
+ }
72
+ if (store.token) headers['Authorization'] = `Bearer ${store.token}`
73
+ if (client.apiKey) headers['apikey'] = client.apiKey
74
+
75
+ const body = JSON.stringify({
76
+ messages,
77
+ model: options?.model,
78
+ temperature: options?.temperature,
79
+ max_tokens: options?.max_tokens,
80
+ })
81
+
82
+ try {
83
+ const res = await client.fetchFn(this.url(`${this.prefix}/chat/stream`), {
84
+ method: 'POST',
85
+ headers,
86
+ body,
87
+ signal: options?.signal,
88
+ })
89
+ if (!res.ok) {
90
+ let parsed: { error?: { code?: string; message?: string }; message?: string } | null = null
91
+ try {
92
+ parsed = (await res.json()) as typeof parsed
93
+ } catch {
94
+ // corps non-JSON / vide → AuraError générique sur le status
95
+ }
96
+ throw new AuraError(res.status, parsed)
97
+ }
98
+ if (!res.body) {
99
+ throw new AuraError(0, {
100
+ error: { code: 'invalid_response', message: 'chatStream: la réponse SSE n\'a pas de corps lisible' },
101
+ })
102
+ }
103
+ for await (const msg of parseSSE(res.body as ReadableStream<Uint8Array>)) {
104
+ if (msg.event === 'delta') {
105
+ let content = ''
106
+ try {
107
+ content = (JSON.parse(msg.data) as { content?: string }).content ?? ''
108
+ } catch {
109
+ content = ''
110
+ }
111
+ if (content) yield { delta: content }
112
+ } else if (msg.event === 'error') {
113
+ let message = 'AI stream error'
114
+ try {
115
+ message = (JSON.parse(msg.data) as { error?: string }).error ?? message
116
+ } catch {
117
+ // conserve le message par défaut
118
+ }
119
+ throw new AuraError(0, { error: { code: 'ai_stream_error', message } })
120
+ } else if (msg.event === 'done') {
121
+ // Le serveur joint au `done` le provider effectif, la bascule éventuelle,
122
+ // le motif d'arrêt et l'usage réel. Les jeter privait l'application de
123
+ // toute visibilité sur une réponse tronquée et sur sa consommation.
124
+ let final: ChatStreamFinal | undefined
125
+ try {
126
+ const parsed = JSON.parse(msg.data) as ChatStreamFinal & { status?: string }
127
+ const { status: _status, ...rest } = parsed
128
+ // Un chunk final n'est émis QUE s'il apporte quelque chose : face à un
129
+ // serveur plus ancien (`done` réduit à `[DONE]`), l'itération se termine
130
+ // exactement comme avant — aucun chunk supplémentaire à filtrer.
131
+ if (Object.keys(rest).length > 0) final = rest
132
+ } catch {
133
+ final = undefined
134
+ }
135
+ if (final) yield { delta: '', final }
136
+ return
137
+ }
138
+ }
139
+ } catch (err) {
140
+ // Annulation demandée → fin d'itération PROPRE (pas de throw), parité avec
141
+ // une consommation interrompue via AbortController.
142
+ if (options?.signal?.aborted || isAbortError(err)) return
143
+ throw err
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Generate embeddings for the given input(s).
149
+ */
150
+ async embed(req: EmbedRequest): Promise<AuraResponse<EmbedResponse>> {
151
+ return this.request('POST', `${this.prefix}/embed`, req)
152
+ }
153
+
154
+ /**
155
+ * Retrieval-Augmented Generation: query a collection and get an answer.
156
+ */
157
+ async rag(req: RagRequest): Promise<AuraResponse<RagResponse>> {
158
+ return this.request('POST', `${this.prefix}/rag/query`, req)
159
+ }
160
+
161
+ /**
162
+ * Ingest a document into a RAG namespace.
163
+ */
164
+ async ragIngest(req: RagIngestRequest): Promise<AuraResponse<RagIngestResponse>> {
165
+ return this.request('POST', `${this.prefix}/rag/ingest`, req)
166
+ }
167
+
168
+ /**
169
+ * Liste les namespaces RAG du projet.
170
+ * Backend route: GET /v1/ai/{project_id}/rag/namespaces (→ `Vec<String>`).
171
+ */
172
+ async listNamespaces(): Promise<AuraResponse<RagNamespace[]>> {
173
+ return this.request('GET', `${this.prefix}/rag/namespaces`)
174
+ }
175
+
176
+ /**
177
+ * Supprime un namespace RAG (et son corpus). Le segment `namespace` est
178
+ * URL-encodé (anti-injection de chemin).
179
+ * Backend route: DELETE /v1/ai/{project_id}/rag/{namespace} (→ `{ deleted }`).
180
+ */
181
+ async deleteNamespace(namespace: string): Promise<AuraResponse<RagDeleteResponse>> {
182
+ return this.request('DELETE', `${this.prefix}/rag/${encodeURIComponent(namespace)}`)
183
+ }
184
+
185
+ /**
186
+ * Ré-indexe un namespace RAG avec le modèle d'embedding courant (remédiation
187
+ * après un changement de modèle/dimension). Le segment `namespace` est URL-encodé.
188
+ * Backend route: POST /v1/ai/{project_id}/rag/{namespace}/reindex (→ `ReindexResponse`).
189
+ */
190
+ async reindexNamespace(namespace: string): Promise<AuraResponse<RagReindexResponse>> {
191
+ return this.request('POST', `${this.prefix}/rag/${encodeURIComponent(namespace)}/reindex`)
192
+ }
193
+
194
+ /**
195
+ * Semantic search over an embedding namespace.
196
+ */
197
+ async semanticSearch(
198
+ query: string,
199
+ namespace: string,
200
+ options?: { top_k?: number; threshold?: number },
201
+ ): Promise<AuraResponse<SemanticSearchResult[]>> {
202
+ const params = new URLSearchParams({ query, namespace })
203
+ if (options?.top_k !== undefined) params.set('top_k', String(options.top_k))
204
+ if (options?.threshold !== undefined) params.set('threshold', String(options.threshold))
205
+ return this.request('GET', `${this.prefix}/search?${params.toString()}`)
206
+ }
207
+
208
+ /**
209
+ * Natural-language to SQL: convert a question into a SQL query.
210
+ */
211
+ async nl2sql(
212
+ question: string,
213
+ schema: string,
214
+ options?: {
215
+ dialect?: string
216
+ examples?: Array<{ question: string; sql: string }>
217
+ },
218
+ ): Promise<AuraResponse<{ sql: string; explanation: string }>> {
219
+ return this.request('POST', `${this.prefix}/nl2sql`, {
220
+ question,
221
+ schema,
222
+ dialect: options?.dialect,
223
+ examples: options?.examples,
224
+ })
225
+ }
226
+ }
@@ -0,0 +1,18 @@
1
+ export { AiService } from './AiService'
2
+ export { parseSSE } from './sse'
3
+ export type { SSEMessage, ChatStreamChunk } from './sse'
4
+ export type { RagNamespace, RagDeleteResponse, RagReindexResponse } from './types'
5
+
6
+ // Types publics (utilisables en consommant `@aurabase/ai` seul, sans importer core).
7
+ export type {
8
+ AuraResponse,
9
+ ChatMessage,
10
+ ChatResponse,
11
+ EmbedRequest,
12
+ EmbedResponse,
13
+ RagRequest,
14
+ RagResponse,
15
+ RagIngestRequest,
16
+ RagIngestResponse,
17
+ SemanticSearchResult,
18
+ } from '@aurabase/core'
@@ -0,0 +1,141 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Parseur SSE (Server-Sent Events) — utilitaire de streaming côté SDK.
3
+ //
4
+ // Extrait pour être testable en isolation (trames partielles, multi-events par
5
+ // chunk, lignes keep-alive `:`) et réutilisable (chat streaming, futur SSE
6
+ // realtime/functions). NE DÉPEND PAS de `@aurabase/core/fetch.ts` : il opère sur
7
+ // un `ReadableStream<Uint8Array>` brut obtenu via `client.fetchFn` (patron Storage).
8
+ // ---------------------------------------------------------------------------
9
+
10
+ /** Un message SSE dispatché : nom d'event (`message` par défaut) + données jointes. */
11
+ export interface SSEMessage {
12
+ event: string
13
+ data: string
14
+ }
15
+
16
+ /** Usage de tokens rapporté par le provider à la fin d'un flux. */
17
+ export interface ChatStreamUsage {
18
+ input_tokens?: number | null
19
+ /** Tokens de sortie facturés, raisonnement inclus. */
20
+ output_tokens?: number | null
21
+ /** Part de `output_tokens` consommée par le raisonnement du modèle. */
22
+ reasoning_tokens?: number | null
23
+ /** `true` si les compteurs sont estimés faute d'usage rapporté par le provider. */
24
+ estimated?: boolean
25
+ }
26
+
27
+ /**
28
+ * Dernier chunk d'un flux de chat, émis avant la fin de l'itération.
29
+ *
30
+ * Le serveur envoie ces informations dans l'event SSE `done` ; elles étaient
31
+ * jusqu'ici ignorées par le SDK, si bien qu'une réponse tronquée (`length`) ou la
32
+ * consommation réelle de tokens restaient invisibles côté application.
33
+ */
34
+ export interface ChatStreamFinal {
35
+ /** Provider ayant effectivement répondu. */
36
+ provider?: string
37
+ /** `true` si une bascule de fallback a eu lieu. */
38
+ fallback_used?: boolean
39
+ /** Motif d'arrêt normalisé : `stop` | `length` | `content_filter` | `tool_use` | `other`. */
40
+ finish_reason?: string | null
41
+ /** Raccourci : la réponse a été coupée par la limite de tokens. */
42
+ truncated?: boolean
43
+ usage?: ChatStreamUsage
44
+ }
45
+
46
+ /**
47
+ * Chunk de streaming de chat.
48
+ *
49
+ * `delta` porte le texte incrémental ; le **dernier** chunk du flux porte `final`
50
+ * (sans `delta`) avec l'usage et le motif d'arrêt. Les consommateurs existants
51
+ * qui n'utilisent que `delta` continuent de fonctionner : ils reçoivent une
52
+ * chaîne vide sur ce dernier chunk.
53
+ */
54
+ export interface ChatStreamChunk {
55
+ delta: string
56
+ final?: ChatStreamFinal
57
+ }
58
+
59
+ /**
60
+ * Parse un flux SSE en `AsyncGenerator<SSEMessage>`.
61
+ *
62
+ * Conforme au protocole EventSource :
63
+ * - événements séparés par une ligne vide (`\n\n`) ;
64
+ * - champs `event:` / `data:` (espace optionnel après le `:`), autres champs ignorés ;
65
+ * - plusieurs `data:` d'un même event sont jointes par `\n` ;
66
+ * - lignes commençant par `:` = commentaires/keep-alive → ignorées ;
67
+ * - CRLF toléré (le `\r` de fin de ligne est retiré).
68
+ *
69
+ * Gère les trames partielles : un event scindé sur plusieurs chunks est
70
+ * bufferisé jusqu'à réception de la ligne vide. Un éventuel event final non
71
+ * terminé par une ligne vide est tout de même livré à la fermeture du flux.
72
+ */
73
+ export async function* parseSSE(
74
+ stream: ReadableStream<Uint8Array>,
75
+ ): AsyncGenerator<SSEMessage> {
76
+ const reader = stream.getReader()
77
+ const decoder = new TextDecoder()
78
+ let buffer = ''
79
+ let eventName = 'message'
80
+ let dataLines: string[] = []
81
+
82
+ // Consomme une ligne (déjà débarrassée de son `\n` et de son éventuel `\r`).
83
+ // Renvoie un SSEMessage à dispatcher sur ligne vide, sinon null.
84
+ const consumeLine = (raw: string): SSEMessage | null => {
85
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw
86
+ if (line === '') {
87
+ // Fin d'event : dispatch si des données ont été accumulées.
88
+ if (dataLines.length === 0) {
89
+ eventName = 'message'
90
+ return null
91
+ }
92
+ const msg: SSEMessage = { event: eventName, data: dataLines.join('\n') }
93
+ eventName = 'message'
94
+ dataLines = []
95
+ return msg
96
+ }
97
+ if (line.startsWith(':')) return null // commentaire / keep-alive
98
+ const colon = line.indexOf(':')
99
+ let field: string
100
+ let value: string
101
+ if (colon === -1) {
102
+ field = line
103
+ value = ''
104
+ } else {
105
+ field = line.slice(0, colon)
106
+ value = line.slice(colon + 1)
107
+ if (value.startsWith(' ')) value = value.slice(1)
108
+ }
109
+ if (field === 'event') eventName = value
110
+ else if (field === 'data') dataLines.push(value)
111
+ // id / retry : ignorés (non pertinents pour le SDK)
112
+ return null
113
+ }
114
+
115
+ try {
116
+ for (;;) {
117
+ const { done, value } = await reader.read()
118
+ if (done) break
119
+ buffer += decoder.decode(value, { stream: true })
120
+ let idx: number
121
+ while ((idx = buffer.indexOf('\n')) !== -1) {
122
+ const raw = buffer.slice(0, idx)
123
+ buffer = buffer.slice(idx + 1)
124
+ const msg = consumeLine(raw)
125
+ if (msg) yield msg
126
+ }
127
+ }
128
+ // Vidange du décodeur + traitement d'une éventuelle dernière ligne sans `\n`.
129
+ buffer += decoder.decode()
130
+ if (buffer.length > 0) {
131
+ const msg = consumeLine(buffer)
132
+ if (msg) yield msg
133
+ }
134
+ // Event final non terminé par une ligne vide (data accumulée sans dispatch).
135
+ if (dataLines.length > 0) {
136
+ yield { event: eventName, data: dataLines.join('\n') }
137
+ }
138
+ } finally {
139
+ reader.releaseLock()
140
+ }
141
+ }
@@ -0,0 +1,30 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Types locaux du package @aurabase/ai (non mutualisés dans @aurabase/core).
3
+ // ---------------------------------------------------------------------------
4
+
5
+ /**
6
+ * Un namespace RAG. Le backend (`GET /rag/namespaces` → `Vec<String>`) renvoie
7
+ * de simples chaînes ; le type reste un alias pour documenter l'intention et
8
+ * autoriser une évolution ultérieure vers un objet sans casser les appelants.
9
+ */
10
+ export type RagNamespace = string
11
+
12
+ /**
13
+ * Réponse de `DELETE /rag/{namespace}` — forme réelle du handler
14
+ * (`rag_delete_namespace` → `{ "deleted": <count> }`).
15
+ */
16
+ export interface RagDeleteResponse {
17
+ /** Nombre d'entrées supprimées du namespace. */
18
+ deleted: number
19
+ }
20
+
21
+ /**
22
+ * Réponse de `POST /rag/{namespace}/reindex` — forme réelle du handler
23
+ * (`ReindexResponse` dans `services/aura-ai/src/rag/mod.rs`).
24
+ */
25
+ export interface RagReindexResponse {
26
+ /** Nombre de chunks ré-embeddés sous le modèle courant. */
27
+ reindexed: number
28
+ /** Modèle d'embedding désormais appliqué au namespace. */
29
+ embedding_model: string
30
+ }
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { AiService } from '../src/AiService'
3
+ import { BaseAuthStore, isAuraError } from '@aurabase/core'
4
+ import type { AuraClientLike } from '@aurabase/core'
5
+
6
+ function b64url(o: unknown) { return btoa(JSON.stringify(o)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') }
7
+ function expiredJwt() { return `${b64url({ alg: 'HS256' })}.${b64url({ exp: Math.floor(Date.now() / 1000) - 60 })}.s` }
8
+ function validJwt() { return `${b64url({ alg: 'HS256' })}.${b64url({ exp: Math.floor(Date.now() / 1000) + 3600 })}.s` }
9
+ function fakeUser() { return { id: 'u', email: null, role: 'authenticated', email_verified: true, is_banned: false, is_anonymous: false, mfa_enabled: false, user_metadata: {}, app_metadata: {}, created_at: '2025-01-01T00:00:00Z' } }
10
+
11
+ function streamResponse(chunks: string[], status = 200): Response {
12
+ const enc = new TextEncoder()
13
+ let i = 0
14
+ const body = new ReadableStream<Uint8Array>({
15
+ pull(controller) {
16
+ if (i < chunks.length) controller.enqueue(enc.encode(chunks[i++]))
17
+ else controller.close()
18
+ },
19
+ })
20
+ return new Response(body, { status, headers: { 'Content-Type': 'text/event-stream' } })
21
+ }
22
+
23
+ function make(
24
+ responder: (url: string, init: RequestInit) => Response | Promise<Response>,
25
+ store = new BaseAuthStore(),
26
+ ) {
27
+ const calls: Array<{ url: string; init: RequestInit }> = []
28
+ const client: AuraClientLike = {
29
+ baseUrl: 'http://x', projectId: 'p', apiKey: 'k', authStore: store,
30
+ fetchFn: (async (url: string, init: RequestInit) => { calls.push({ url: String(url), init }); return responder(String(url), init) }) as unknown as typeof globalThis.fetch,
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ auth: { refreshSession: async () => ({ data: null, error: null }) as any },
33
+ }
34
+ return { ai: new AiService(client), calls }
35
+ }
36
+
37
+ const delta = (c: string) => `event: delta\ndata: ${JSON.stringify({ content: c })}\n\n`
38
+ const done = () => `event: done\ndata: ${JSON.stringify({ status: '[DONE]' })}\n\n`
39
+
40
+ const doneRiche = () =>
41
+ `event: done\ndata: ${JSON.stringify({
42
+ status: '[DONE]',
43
+ provider: 'google',
44
+ fallback_used: false,
45
+ finish_reason: 'length',
46
+ truncated: true,
47
+ usage: { input_tokens: 11, output_tokens: 76, reasoning_tokens: 74, estimated: false },
48
+ })}\n\n`
49
+
50
+ describe('ai.chatStream — SSE via fetchFn brut', () => {
51
+ it('livre un chunk final avec usage et motif d’arrêt quand le serveur les fournit', async () => {
52
+ const { ai } = make(() => streamResponse([delta('Ah, les'), doneRiche()]))
53
+ const chunks: { delta: string; final?: Record<string, unknown> }[] = []
54
+ for await (const c of ai.chatStream([{ role: 'user', content: 'hi' }])) chunks.push(c)
55
+
56
+ // Le texte reste livré à l'identique…
57
+ expect(chunks.map((c) => c.delta).join('')).toBe('Ah, les')
58
+ // …et le dernier chunk porte de quoi détecter une réponse TRONQUÉE, qui était
59
+ // jusqu'ici indiscernable d'une réponse complète côté application.
60
+ const final = chunks[chunks.length - 1].final as Record<string, unknown>
61
+ expect(final).toBeDefined()
62
+ expect(final.finish_reason).toBe('length')
63
+ expect(final.truncated).toBe(true)
64
+ expect(final.provider).toBe('google')
65
+ // Usage réel, raisonnement inclus (74 des 76 tokens de sortie).
66
+ expect((final.usage as Record<string, number>).output_tokens).toBe(76)
67
+ expect((final.usage as Record<string, number>).reasoning_tokens).toBe(74)
68
+ })
69
+
70
+ it('serveur ancien (done sans détail) → aucun chunk final ajouté', async () => {
71
+ const { ai } = make(() => streamResponse([delta('a'), delta('b'), done()]))
72
+ const out: string[] = []
73
+ for await (const c of ai.chatStream([{ role: 'user', content: 'hi' }])) out.push(c.delta)
74
+ expect(out).toEqual(['a', 'b'])
75
+ })
76
+
77
+ it('livre des deltas incrémentaux puis termine sur done', async () => {
78
+ const { ai } = make(() => streamResponse([delta('Hel'), delta('lo'), delta('!'), done()]))
79
+ const out: string[] = []
80
+ for await (const chunk of ai.chatStream([{ role: 'user', content: 'hi' }])) {
81
+ out.push(chunk.delta)
82
+ }
83
+ expect(out).toEqual(['Hel', 'lo', '!'])
84
+ expect(out.join('')).toBe('Hello!')
85
+ })
86
+
87
+ it('POST vers /chat/stream avec Accept text/event-stream + apikey + Authorization', async () => {
88
+ const store = new BaseAuthStore()
89
+ store.save(validJwt(), 'rt', fakeUser())
90
+ const { ai, calls } = make(() => streamResponse([done()]), store)
91
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
92
+ for await (const _ of ai.chatStream([{ role: 'user', content: 'hi' }])) { /* drain */ }
93
+ expect(calls[0]!.url).toBe('http://x/v1/ai/p/chat/stream')
94
+ expect(calls[0]!.init.method).toBe('POST')
95
+ const h = calls[0]!.init.headers as Record<string, string>
96
+ expect(h['Accept']).toBe('text/event-stream')
97
+ expect(h['apikey']).toBe('k')
98
+ expect(h['Authorization']).toBe(`Bearer ${store.token}`)
99
+ })
100
+
101
+ it('event error → lève une AuraError exploitable', async () => {
102
+ const { ai } = make(() => streamResponse([delta('a'), `event: error\ndata: ${JSON.stringify({ error: 'boom' })}\n\n`]))
103
+ await expect(async () => {
104
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
105
+ for await (const _ of ai.chatStream([{ role: 'user', content: 'hi' }])) { /* drain */ }
106
+ }).rejects.toSatisfy((e: unknown) => isAuraError(e) && (e as { message: string }).message.includes('boom'))
107
+ })
108
+
109
+ it('!res.ok → lève AuraError(status)', async () => {
110
+ const { ai } = make(() => new Response(JSON.stringify({ error: { code: 'nope', message: 'x' } }), { status: 429 }))
111
+ await expect(async () => {
112
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
113
+ for await (const _ of ai.chatStream([{ role: 'user', content: 'hi' }])) { /* drain */ }
114
+ }).rejects.toSatisfy((e: unknown) => isAuraError(e) && (e as { status: number }).status === 429)
115
+ })
116
+
117
+ it('AbortSignal déjà avorté → fin d’itération propre (aucun delta, pas de throw)', async () => {
118
+ const ac = new AbortController()
119
+ ac.abort()
120
+ const { ai } = make((_url, init) =>
121
+ init.signal?.aborted
122
+ ? Promise.reject(Object.assign(new Error('aborted'), { name: 'AbortError' }))
123
+ : Promise.resolve(streamResponse([delta('a'), done()])),
124
+ )
125
+ const out: string[] = []
126
+ for await (const chunk of ai.chatStream([{ role: 'user', content: 'hi' }], { signal: ac.signal })) {
127
+ out.push(chunk.delta)
128
+ }
129
+ expect(out).toEqual([])
130
+ })
131
+
132
+ it('refresh proactif si token expiré (refresh AVANT ouverture du flux)', async () => {
133
+ const store = new BaseAuthStore()
134
+ store.save(expiredJwt(), 'rt', fakeUser())
135
+ const { ai, calls } = make((url) => {
136
+ if (url.includes('/v1/auth/')) return new Response(JSON.stringify({ access_token: validJwt(), refresh_token: 'rt2', token_type: 'bearer', expires_in: 3600, user: fakeUser() }), { status: 200, headers: { 'Content-Type': 'application/json' } })
137
+ return streamResponse([done()])
138
+ }, store)
139
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
140
+ for await (const _ of ai.chatStream([{ role: 'user', content: 'hi' }])) { /* drain */ }
141
+ expect(calls[0]!.url).toContain('/v1/auth/p/refresh')
142
+ expect(calls[1]!.url).toContain('/chat/stream')
143
+ })
144
+
145
+ it('chat() (JSON) reste inchangé', async () => {
146
+ const { ai } = make(() => new Response(JSON.stringify({ data: { model: 'm', content: 'hi' } }), { status: 200, headers: { 'Content-Type': 'application/json' } }))
147
+ const { data, error } = await ai.chat([{ role: 'user', content: 'hi' }])
148
+ expect(error).toBeNull()
149
+ expect(data).toEqual({ model: 'm', content: 'hi' })
150
+ })
151
+ })
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { AiService } from '../src/AiService'
3
+ import { BaseAuthStore } from '@aurabase/core'
4
+ import type { AuraClientLike } from '@aurabase/core'
5
+
6
+ function make(responder: (url: string, init: RequestInit) => Response) {
7
+ const calls: Array<{ url: string; method: string }> = []
8
+ const client: AuraClientLike = {
9
+ baseUrl: 'http://x', projectId: 'p', apiKey: 'k', authStore: new BaseAuthStore(),
10
+ fetchFn: (async (url: string, init: RequestInit) => {
11
+ calls.push({ url: String(url), method: String(init.method) })
12
+ return responder(String(url), init)
13
+ }) as unknown as typeof globalThis.fetch,
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ auth: { refreshSession: async () => ({ data: null, error: null }) as any },
16
+ }
17
+ return { ai: new AiService(client), calls }
18
+ }
19
+ const json = (o: unknown, s = 200) => new Response(JSON.stringify(o), { status: s, headers: { 'Content-Type': 'application/json' } })
20
+
21
+ describe('ai RAG — CRUD namespaces', () => {
22
+ it('listNamespaces → GET /rag/namespaces, dé-wrappe la liste', async () => {
23
+ const { ai, calls } = make(() => json({ data: ['ns_a', 'ns_b'] }))
24
+ const { data, error } = await ai.listNamespaces()
25
+ expect(error).toBeNull()
26
+ expect(data).toEqual(['ns_a', 'ns_b'])
27
+ expect(calls[0]).toEqual({ url: 'http://x/v1/ai/p/rag/namespaces', method: 'GET' })
28
+ })
29
+
30
+ it('deleteNamespace → DELETE /rag/{ns} (encodé), dé-wrappe { deleted }', async () => {
31
+ const { ai, calls } = make(() => json({ data: { deleted: 3 } }))
32
+ const { data, error } = await ai.deleteNamespace('ns test/x')
33
+ expect(error).toBeNull()
34
+ expect(data).toEqual({ deleted: 3 })
35
+ expect(calls[0]!.method).toBe('DELETE')
36
+ expect(calls[0]!.url).toBe('http://x/v1/ai/p/rag/ns%20test%2Fx')
37
+ })
38
+
39
+ it('reindexNamespace → POST /rag/{ns}/reindex (encodé), dé-wrappe ReindexResponse', async () => {
40
+ const { ai, calls } = make(() => json({ data: { reindexed: 42, embedding_model: 'text-embedding-3-small' } }))
41
+ const { data, error } = await ai.reindexNamespace('ns#1')
42
+ expect(error).toBeNull()
43
+ expect(data).toEqual({ reindexed: 42, embedding_model: 'text-embedding-3-small' })
44
+ expect(calls[0]!.method).toBe('POST')
45
+ expect(calls[0]!.url).toBe('http://x/v1/ai/p/rag/ns%231/reindex')
46
+ })
47
+
48
+ it('propage une erreur HTTP (status)', async () => {
49
+ const { ai } = make(() => json({ error: { code: 'not_found', message: 'x' } }, 404))
50
+ const { data, error } = await ai.deleteNamespace('missing')
51
+ expect(data).toBeNull()
52
+ expect(error?.status).toBe(404)
53
+ })
54
+ })