@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,1166 @@
1
+ import { BaseService } from '@aurabase/core'
2
+ import type {
3
+ AuraResponse,
4
+ AuthChangePayload,
5
+ AuthEvent,
6
+ GetClaimsResult,
7
+ IdentityInfo,
8
+ JwtClaims,
9
+ LoginResponse,
10
+ MagicLinkRequest,
11
+ MfaChallenge,
12
+ MfaEnrollResponse,
13
+ MfaVerifyRequest,
14
+ OAuthSignInOptions,
15
+ PasswordResetRequest,
16
+ SessionInfo,
17
+ SignInRequest,
18
+ SignUpRequest,
19
+ TokenPair,
20
+ UpdateUserRequest,
21
+ User,
22
+ } from '@aurabase/core'
23
+ import type { AuraClientLike } from '@aurabase/core'
24
+ import { refreshOnce, isTokenExpired } from '@aurabase/core'
25
+ import { AuraError } from '@aurabase/core'
26
+ import { emitAuraEvent } from '@aurabase/core'
27
+ import type {
28
+ VerifyOtpParams,
29
+ VerifyEmailChangeOtpParams,
30
+ } from './types'
31
+ import { SessionManager } from './SessionManager'
32
+ import { verifyJwtLocal, decodeJwt } from './jwks'
33
+ import { generateCodeVerifier, codeChallengeS256 } from './pkce'
34
+
35
+ type AuthStateCallback = (payload: AuthChangePayload) => void
36
+
37
+ export interface AuthServiceOptions {
38
+ autoRefresh?: boolean
39
+ persistSession?: boolean
40
+ /** Interne/test : force l'activation du SessionManager même hors navigateur. */
41
+ forceEnabled?: boolean
42
+ /**
43
+ * Détecte/consomme une session dans l'URL au boot (callback OAuth). Défaut :
44
+ * `true` en navigateur, no-op hors navigateur. Voir `_detectSessionInUrl`.
45
+ */
46
+ detectSessionInUrl?: boolean
47
+ }
48
+
49
+ /**
50
+ * Garde de type discriminant `LoginResponse = TokenPair | MfaChallenge` (LOT 3, 3.4).
51
+ * Exportée (`@aurabase/auth` et re-export `aurabase`) pour permettre aux consommateurs
52
+ * de discriminer eux-mêmes le retour de `signUp`/`signInWithPassword` sans réimplémenter
53
+ * la même vérification structurelle (`mfa_required === true`).
54
+ */
55
+ export function isMfaChallenge(v: unknown): v is MfaChallenge {
56
+ return (
57
+ typeof v === 'object' &&
58
+ v !== null &&
59
+ (v as Record<string, unknown>).mfa_required === true
60
+ )
61
+ }
62
+
63
+ export class AuthService extends BaseService {
64
+ private _stateListeners = new Set<AuthStateCallback>()
65
+ private _sessionManager: SessionManager
66
+ private _applyingRemote = false
67
+ private _unsubStore: (() => void) | null = null
68
+ private _disposed = false
69
+
70
+ constructor(client: AuraClientLike, options?: AuthServiceOptions) {
71
+ super(client)
72
+ const browser = typeof window !== 'undefined'
73
+ const enabled =
74
+ !!options?.forceEnabled ||
75
+ ((options?.autoRefresh ?? true) && browser && (options?.persistSession ?? true))
76
+
77
+ this._sessionManager = new SessionManager({
78
+ channelName: `aura-${client.projectId}-auth`,
79
+ enabled,
80
+ onTick: () => {
81
+ void this.refreshSession()
82
+ },
83
+ isRefreshDue: () => {
84
+ const t = this.client.authStore.token
85
+ return !!t && !!this.client.authStore.refreshToken && isTokenExpired(t, 60)
86
+ },
87
+ onRemoteMessage: (data) => this._applyRemoteState(data),
88
+ })
89
+ this._sessionManager.start()
90
+
91
+ // Le ticker + le broadcast sont pilotés par les changements du store (aucune
92
+ // duplication dans les méthodes signIn/signOut : leurs save/clear déclenchent ceci).
93
+ this._unsubStore = this.client.authStore.onChange((token) => {
94
+ this._sessionManager.setActive(!!token)
95
+ if (!this._applyingRemote) {
96
+ const state = this._broadcastState()
97
+ // Broadcaste un vrai signout (!token → null) OU un état COMPLET ; jamais un
98
+ // état incomplet (ex. le placeholder user `{}` de setSession).
99
+ if (!token || state !== null) {
100
+ this._sessionManager.broadcast(state)
101
+ }
102
+ }
103
+ })
104
+
105
+ // Session déjà présente au démarrage (chargée du storage) → armer le ticker.
106
+ if (this.client.authStore.token) this._sessionManager.setActive(true)
107
+
108
+ // detectSessionInUrl : boucle le callback OAuth côté SDK au boot (défaut true en
109
+ // navigateur). Best-effort, non bloquant, ne throw jamais.
110
+ const detect = options?.detectSessionInUrl ?? browser
111
+ if (detect && browser) {
112
+ void this._detectSessionInUrl()
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Détecte et consomme une session présente dans l'URL courante au boot (callback
118
+ * OAuth), à l'image de `detectSessionInUrl` de supabase-js :
119
+ * - fragment `#access_token=…&refresh_token=…` (flux implicit) → `setSession`.
120
+ * - query `?token=…&type=recovery` (lien de récupération de mot de passe) → émet
121
+ * `PASSWORD_RECOVERY` avec `recoveryToken` (AUCUNE session posée, PAS de `SIGNED_IN`).
122
+ * - query `?code=…` (flux PKCE) → `exchangeCodeForSession` (relit le verifier).
123
+ * Puis nettoie l'URL via `history.replaceState` (retire tokens/code de la barre
124
+ * d'adresse et de l'historique). Idempotent, no-op si aucun token/code n'est
125
+ * présent, no-op hors navigateur. NE THROW JAMAIS (best-effort au boot).
126
+ */
127
+ async _detectSessionInUrl(): Promise<void> {
128
+ if (typeof window === 'undefined') return
129
+ try {
130
+ const loc = window.location
131
+ if (!loc) return
132
+ const hash = typeof loc.hash === 'string' ? loc.hash : ''
133
+ const search = typeof loc.search === 'string' ? loc.search : ''
134
+
135
+ // Flux implicit : tokens dans le fragment (jamais envoyés au serveur).
136
+ if (hash.includes('access_token')) {
137
+ // SEC-F3 (anti session-fixation / login-CSRF) : n'ADOPTER un fragment implicit
138
+ // QUE si CE SDK a lui-même initié le flux (marqueur posé par signInWithOAuth,
139
+ // survit au redirect via un store persistant). Sans preuve d'initiation, un
140
+ // `#access_token` peut avoir été forgé par un attaquant et injecté dans l'URL
141
+ // de la victime (`app/#access_token=<token_attaquant>`) → on ne le consomme
142
+ // JAMAIS. Le chemin PKCE (`?code=`) est déjà lié au `code_verifier` → intact.
143
+ const initiatedKey = this._oauthInitiatedKey
144
+ const initiated = this.client.authStore.getItem(initiatedKey)
145
+ if (!initiated) return // fragment NON sollicité → ignoré (aucun setSession)
146
+ // Marqueur à USAGE UNIQUE : purge immédiate (le flux ne doit pas être rejouable).
147
+ this.client.authStore.removeItem(initiatedKey)
148
+ const frag = new URLSearchParams(hash.replace(/^#/, ''))
149
+ const accessToken = frag.get('access_token')
150
+ const refreshToken = frag.get('refresh_token')
151
+ if (accessToken && refreshToken) {
152
+ await this.setSession(accessToken, refreshToken)
153
+ this._cleanAuthParamsFromUrl()
154
+ }
155
+ return
156
+ }
157
+
158
+ // Lien de RÉCUPÉRATION de mot de passe : `?token=<opaque>&type=recovery`
159
+ // (émis tel quel par le backend `forgot_password`, PAS de flux PKCE — un lien
160
+ // email froid n'a jamais initié de flux côté SDK, donc pas de code_verifier).
161
+ // On N'ÉCHANGE AUCUNE session : on se contente d'émettre `PASSWORD_RECOVERY` en
162
+ // exposant le `recoveryToken` opaque, que l'app repasse ensuite à
163
+ // `resetPassword(token, newPassword)`. AUCUN `SIGNED_IN`, aucune session posée
164
+ // (l'utilisateur n'est authentifié qu'après avoir choisi un nouveau mot de passe).
165
+ {
166
+ const qs = new URLSearchParams(search.replace(/^\?/, ''))
167
+ const recoveryToken = qs.get('token')
168
+ if (qs.get('type') === 'recovery' && recoveryToken) {
169
+ this._emitEvent('PASSWORD_RECOVERY', null, null, { recoveryToken })
170
+ this._cleanAuthParamsFromUrl()
171
+ return
172
+ }
173
+ }
174
+
175
+ // Flux PKCE : code d'autorisation dans la query (callback OAuth normal).
176
+ if (/[?&]code=/.test(search)) {
177
+ const qs = new URLSearchParams(search.replace(/^\?/, ''))
178
+ const code = qs.get('code')
179
+ if (code) {
180
+ await this.exchangeCodeForSession(code)
181
+ this._cleanAuthParamsFromUrl()
182
+ }
183
+ return
184
+ }
185
+ } catch {
186
+ // best-effort au boot : ne jamais throw ni rejeter.
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Retire de l'URL courante les paramètres d'auth du callback OAuth : le fragment
192
+ * entier (flux implicit) et les query params `code`/`state` (flux PKCE), tout en
193
+ * préservant les autres query params. Via `history.replaceState` (pas de nouvelle
194
+ * entrée d'historique). Best-effort, ne throw jamais.
195
+ */
196
+ private _cleanAuthParamsFromUrl(): void {
197
+ try {
198
+ if (typeof window === 'undefined') return
199
+ const h = window.history
200
+ const loc = window.location
201
+ if (!h || typeof h.replaceState !== 'function' || !loc) return
202
+ const qs = new URLSearchParams(
203
+ typeof loc.search === 'string' ? loc.search.replace(/^\?/, '') : '',
204
+ )
205
+ qs.delete('code')
206
+ qs.delete('state')
207
+ // Paramètres du lien de récupération de mot de passe (`?token=…&project_id=…&type=recovery`).
208
+ qs.delete('token')
209
+ qs.delete('project_id')
210
+ qs.delete('type')
211
+ const remaining = qs.toString()
212
+ const path = loc.pathname ?? '/'
213
+ const clean = path + (remaining ? `?${remaining}` : '')
214
+ h.replaceState(null, '', clean || '/')
215
+ } catch {
216
+ // ignore
217
+ }
218
+ }
219
+
220
+ private _broadcastState(): { token: string; refreshToken: string; user: User } | null {
221
+ const s = this.client.authStore
222
+ // État COMPLET uniquement : refuse le placeholder user `{}` (sans id) posé temporairement
223
+ // par setSession, pour ne pas diffuser d'utilisateur vide aux autres onglets.
224
+ if (
225
+ !s.token ||
226
+ !s.refreshToken ||
227
+ !s.user ||
228
+ typeof s.user !== 'object' ||
229
+ !('id' in s.user) ||
230
+ !s.user.id
231
+ ) {
232
+ return null
233
+ }
234
+ return { token: s.token, refreshToken: s.refreshToken, user: s.user }
235
+ }
236
+
237
+ private _applyRemoteState(data: unknown): void {
238
+ // Valide la FORME du message distant avant d'appliquer (anti-corruption / SDK tiers / message malformé).
239
+ if (data !== null) {
240
+ const d = data as Record<string, unknown>
241
+ if (
242
+ typeof d.token !== 'string' ||
243
+ typeof d.refreshToken !== 'string' ||
244
+ !d.user ||
245
+ typeof d.user !== 'object'
246
+ ) {
247
+ return // message invalide → ignoré
248
+ }
249
+ }
250
+ this._applyingRemote = true
251
+ try {
252
+ const store = this.client.authStore
253
+ const prevToken = store.token
254
+ if (!data) {
255
+ store.clear()
256
+ this._emitEvent('SIGNED_OUT', null, null)
257
+ } else {
258
+ const s = data as { token: string; refreshToken: string; user: User }
259
+ store.save(s.token, s.refreshToken, s.user)
260
+ // Inférence fidèle de l'événement : login / refresh de token / mise à jour de profil.
261
+ const event: AuthEvent = !prevToken
262
+ ? 'SIGNED_IN'
263
+ : s.token !== prevToken
264
+ ? 'TOKEN_REFRESHED'
265
+ : 'USER_UPDATED'
266
+ this._emitEvent(event, s.user, s.token)
267
+ }
268
+ } finally {
269
+ this._applyingRemote = false
270
+ }
271
+ }
272
+
273
+ /** Démonte le cycle de vie proactif : ticker, visibilité, BroadcastChannel, listeners. */
274
+ dispose(): void {
275
+ if (this._disposed) return
276
+ this._disposed = true
277
+ this._sessionManager.stop()
278
+ this._unsubStore?.()
279
+ this._unsubStore = null
280
+ this._stateListeners.clear()
281
+ }
282
+
283
+ private get prefix(): string {
284
+ return `/v1/auth/${this.client.projectId}`
285
+ }
286
+
287
+ /** Clé project-scoped du `code_verifier` PKCE (anti-collision multi-projets). */
288
+ private get _pkceVerifierKey(): string {
289
+ return `aura-${this.client.projectId}-pkce-code-verifier`
290
+ }
291
+
292
+ /**
293
+ * Clé project-scoped du MARQUEUR d'initiation du flux OAuth implicit (SEC-F3).
294
+ * Posé par `signInWithOAuth` (flux implicit) avant le redirect ; vérifié + purgé
295
+ * par `_detectSessionInUrl` au retour. Un fragment `#access_token` forgé/injecté
296
+ * par un attaquant (login-CSRF / session fixation) n'aura PAS ce marqueur → il ne
297
+ * sera jamais adopté.
298
+ */
299
+ private get _oauthInitiatedKey(): string {
300
+ return `aura-${this.client.projectId}-oauth-implicit-initiated`
301
+ }
302
+
303
+ // -- Auth state change ------------------------------------------------------
304
+
305
+ onAuthStateChange(callback: AuthStateCallback): { unsubscribe: () => void } {
306
+ // Après dispose(), le client est mort : ne rien émettre (pas même INITIAL_SESSION).
307
+ if (this._disposed) return { unsubscribe: () => {} }
308
+ // E20 : bus d'événements UNIQUE (`_stateListeners`). On ne s'abonne plus aussi à
309
+ // `authStore.onChange` (qui émettait un `SIGNED_IN`/`SIGNED_OUT` en doublon, mal
310
+ // étiqueté, à chaque save/clear — y compris lors d'un simple refresh). Le store
311
+ // reste un détail de persistance ; AuthService est le seul émetteur d'événements.
312
+ this._stateListeners.add(callback)
313
+ // Emit INITIAL_SESSION on subscribe (Supabase parity)
314
+ const store = this.client.authStore
315
+ queueMicrotask(() => {
316
+ // Se taire si l'abonnement a été retiré (unsubscribe ou dispose avant le tick).
317
+ if (this._stateListeners.has(callback)) {
318
+ callback({ event: 'INITIAL_SESSION', user: store.user, accessToken: store.token })
319
+ }
320
+ })
321
+ return {
322
+ unsubscribe: () => {
323
+ this._stateListeners.delete(callback)
324
+ },
325
+ }
326
+ }
327
+
328
+ private _emitEvent(
329
+ event: AuthEvent,
330
+ user: User | null,
331
+ accessToken: string | null,
332
+ extra?: { mfaToken?: string; mfaSetupRequired?: boolean; recoveryToken?: string },
333
+ ): void {
334
+ for (const cb of this._stateListeners) {
335
+ try {
336
+ cb({ event, user, accessToken, ...extra })
337
+ } catch {
338
+ // subscriber errors must not break the service
339
+ }
340
+ }
341
+ }
342
+
343
+ // -- Sign up / sign in / sign out -------------------------------------------
344
+
345
+ async signUp(req: SignUpRequest): Promise<AuraResponse<LoginResponse>> {
346
+ const res = await this.request<LoginResponse>('POST', `${this.prefix}/register`, req)
347
+ if (res.data && !isMfaChallenge(res.data)) {
348
+ const tp = res.data as TokenPair
349
+ this.client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
350
+ this._emitEvent('SIGNED_IN', tp.user, tp.access_token)
351
+ }
352
+ if (res.data && isMfaChallenge(res.data)) {
353
+ const mfa = res.data as MfaChallenge
354
+ this._emitEvent('MFA_CHALLENGE', null, null, {
355
+ mfaToken: mfa.mfa_token,
356
+ mfaSetupRequired: mfa.mfa_setup_required,
357
+ })
358
+ }
359
+ return res
360
+ }
361
+
362
+ async signInWithPassword(req: SignInRequest): Promise<AuraResponse<LoginResponse>> {
363
+ const res = await this.request<LoginResponse>('POST', `${this.prefix}/login`, req)
364
+ if (res.data && !isMfaChallenge(res.data)) {
365
+ const tp = res.data as TokenPair
366
+ this.client.authStore.save(tp.access_token, tp.refresh_token, tp.user)
367
+ this._emitEvent('SIGNED_IN', tp.user, tp.access_token)
368
+ }
369
+ if (res.data && isMfaChallenge(res.data)) {
370
+ const mfa = res.data as MfaChallenge
371
+ this._emitEvent('MFA_CHALLENGE', null, null, {
372
+ mfaToken: mfa.mfa_token,
373
+ mfaSetupRequired: mfa.mfa_setup_required,
374
+ })
375
+ }
376
+ return res
377
+ }
378
+
379
+ async signInWithOAuth(
380
+ provider: string,
381
+ options?: OAuthSignInOptions,
382
+ ): Promise<AuraResponse<{ url: string }>> {
383
+ const params = new URLSearchParams()
384
+ params.set('project_id', this.client.projectId)
385
+ // Le backend lit `redirect_uri`, PAS `redirect_to` (vérifié empiriquement).
386
+ if (options?.redirectTo) params.set('redirect_uri', options.redirectTo)
387
+ if (options?.scopes) params.set('scopes', options.scopes)
388
+ if (options?.queryParams) {
389
+ for (const [k, v] of Object.entries(options.queryParams)) {
390
+ params.set(k, v)
391
+ }
392
+ }
393
+ // PKCE opt-in (flowType: 'pkce') : ne modifie PAS le comportement par défaut
394
+ // (implicite) des consommateurs existants. Génère + stocke le `code_verifier`
395
+ // et ajoute code_challenge/code_challenge_method attendus par le backend
396
+ // (oauth.rs OAuthStartQuery, méthode S256 uniquement).
397
+ if (options?.flowType === 'pkce') {
398
+ // DX : PKCE exige un store persistant pour que le `code_verifier` survive au
399
+ // redirect pleine page vers le provider OAuth et retour. Un store mémoire le
400
+ // perd → l'échange échouera au retour. On avertit sans bloquer.
401
+ if (typeof window !== 'undefined' && !this.client.authStore.persistent) {
402
+ console.warn(
403
+ '[aurabase] PKCE (flowType:"pkce") requiert un store persistant (LocalAuthStore) pour que le code_verifier survive au redirect OAuth ; le store courant est en mémoire — l\'échange échouera au retour.',
404
+ )
405
+ }
406
+ // WebCrypto peut être indisponible (certains runtimes SSR / edge) →
407
+ // codeChallengeS256 throw. Le contrat est AuraResponse<{url}> : on échoue
408
+ // fermé plutôt que de rejeter la promesse.
409
+ try {
410
+ const verifier = generateCodeVerifier()
411
+ const challenge = await codeChallengeS256(verifier)
412
+ this.client.authStore.setItem(this._pkceVerifierKey, verifier)
413
+ params.set('code_challenge', challenge)
414
+ params.set('code_challenge_method', 'S256')
415
+ } catch {
416
+ return {
417
+ data: null,
418
+ error: new AuraError(0, {
419
+ error: {
420
+ code: 'pkce_generation_failed',
421
+ message: 'PKCE: génération du code_verifier/challenge impossible (WebCrypto indisponible)',
422
+ },
423
+ }),
424
+ }
425
+ }
426
+ } else {
427
+ // SEC-F3 : flux IMPLICIT — pose le MARQUEUR d'initiation. Au retour du provider,
428
+ // `_detectSessionInUrl` n'adoptera le fragment `#access_token` que si ce marqueur
429
+ // est présent (preuve que CE SDK a initié le flux). Un fragment forgé par un
430
+ // attaquant ne l'aura pas. Le marqueur doit survivre au redirect pleine page →
431
+ // exige un store persistant (LocalAuthStore) ; en store mémoire il ne survit pas
432
+ // (le fragment ne sera alors pas adopté — fail-closed, plus sûr). On recommande
433
+ // FORTEMENT le flux PKCE (`flowType:'pkce'`), déjà lié cryptographiquement au
434
+ // `code_verifier` et non sujet à cette classe d'attaque.
435
+ if (typeof window !== 'undefined' && !this.client.authStore.persistent) {
436
+ console.warn(
437
+ '[aurabase] SÉCURITÉ : signInWithOAuth en flux implicit avec un store NON persistant ' +
438
+ '(mémoire) — le marqueur anti session-fixation ne survivra pas au redirect OAuth et la ' +
439
+ 'session ne sera pas adoptée au retour. Utilisez un LocalAuthStore, et préférez ' +
440
+ 'flowType:"pkce" (recommandé, immunisé contre la session fixation).',
441
+ )
442
+ }
443
+ try {
444
+ this.client.authStore.setItem(this._oauthInitiatedKey, '1')
445
+ } catch {
446
+ // best-effort : ne bloque jamais le démarrage du flux OAuth.
447
+ }
448
+ }
449
+ // OAuth start is a global route: /v1/auth/oauth/{provider}/start
450
+ const url = `${this.client.baseUrl}/v1/auth/oauth/${encodeURIComponent(provider)}/start?${params.toString()}`
451
+ if (typeof window !== 'undefined') {
452
+ window.location.href = url
453
+ }
454
+ // Toujours renvoyer l'URL (pas de no-op silencieux en SSR / hors navigateur).
455
+ return { data: { url }, error: null }
456
+ }
457
+
458
+ async signInWithMagicLink(req: MagicLinkRequest): Promise<AuraResponse<{ message: string }>> {
459
+ return this.request('POST', `${this.prefix}/magic-link`, req)
460
+ }
461
+
462
+ async signInAnonymously(): Promise<AuraResponse<TokenPair>> {
463
+ const res = await this.request<TokenPair>('POST', `${this.prefix}/anonymous`)
464
+ if (res.data) {
465
+ this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user)
466
+ this._emitEvent('SIGNED_IN', res.data.user, res.data.access_token)
467
+ }
468
+ return res
469
+ }
470
+
471
+ async signOut(): Promise<AuraResponse<null>> {
472
+ const refreshToken = this.client.authStore.refreshToken
473
+ if (!refreshToken) {
474
+ this.client.authStore.clear()
475
+ this._emitEvent('SIGNED_OUT', null, null)
476
+ return { data: null, error: null }
477
+ }
478
+ const res = await this.request<null>('POST', `${this.prefix}/logout`, {
479
+ refresh_token: refreshToken,
480
+ }, { skipAuthRefresh: true })
481
+ this.client.authStore.clear()
482
+ this._emitEvent('SIGNED_OUT', null, null)
483
+ return res
484
+ }
485
+
486
+ // -- Session management -----------------------------------------------------
487
+
488
+ async refreshSession(): Promise<AuraResponse<TokenPair>> {
489
+ // E17 : passe par le single-flight partagé `refreshOnce` (le même mutex que le
490
+ // refresh proactif et le retry 401) — évite deux refresh concurrents avec le même
491
+ // refresh token, qui sur backend à rotation stricte déconnectaient l'utilisateur.
492
+ const res = await refreshOnce(this.client)
493
+ if (res.data) {
494
+ this._emitEvent('TOKEN_REFRESHED', res.data.user, res.data.access_token)
495
+ }
496
+ return res
497
+ }
498
+
499
+ /**
500
+ * Session courante lue depuis le store (synchrone en pratique, pas d'appel réseau).
501
+ *
502
+ * ÉCART DE FORME AVEC SUPABASE (LOT 3, 3.5, documenté — pas de breaking change) :
503
+ * `supabase-js` renvoie `AuraResponse`-like `{data:{session: Session|null}, error}`
504
+ * où `Session` regroupe `access_token`/`refresh_token`/`expires_at`/`user`. Ici,
505
+ * `getSession()` renvoie directement `{accessToken, user}` (pas d'enveloppe
506
+ * `{data, error}`, pas de `refreshToken`/`expiresAt`). Ce choix est délibéré :
507
+ * cette méthode ne fait aucune requête réseau et ne peut donc jamais échouer
508
+ * (`error` serait toujours `null`) — l'envelopper aurait été un contrat menteur.
509
+ * Le `refreshToken` n'est pas exposé pour éviter qu'un consommateur le manipule
510
+ * hors du store (contrairement à `accessToken`, souvent nécessaire pour des appels
511
+ * `fetch` manuels côté app). Pour la session complète, lire `client.authStore`
512
+ * directement (`.token`, `.refreshToken`, `.user`, `.isValid`).
513
+ */
514
+ async getSession(): Promise<{ accessToken: string | null; user: User | null }> {
515
+ return {
516
+ accessToken: this.client.authStore.token,
517
+ user: this.client.authStore.user,
518
+ }
519
+ }
520
+
521
+ async setSession(accessToken: string, refreshToken: string): Promise<AuraResponse<User>> {
522
+ const store = this.client.authStore
523
+ // Snapshot pour rollback si la validation échoue : sans lui, un token invalide
524
+ // laissait le store pollué (token présent + user placeholder `{}`) alors que
525
+ // `isValid` ne teste que l'expiration du JWT — l'app croyait alors à une
526
+ // session valide et partait avec un mauvais token.
527
+ const prev = { token: store.token, refreshToken: store.refreshToken, user: store.user }
528
+ const hadValidPrev = !!(
529
+ prev.token &&
530
+ prev.refreshToken &&
531
+ prev.user &&
532
+ (prev.user as { id?: string }).id
533
+ )
534
+ const restore = (): void => {
535
+ if (hadValidPrev) {
536
+ store.save(prev.token as string, prev.refreshToken as string, prev.user as User)
537
+ } else {
538
+ store.clear()
539
+ }
540
+ }
541
+
542
+ // Parité Supabase : si l'access token fourni est DÉJÀ expiré, on le rafraîchit
543
+ // via le refresh token plutôt que d'appeler `/user` avec un token périmé
544
+ // (qui serait rejeté 401 par le gateway).
545
+ if (isTokenExpired(accessToken, 0)) {
546
+ store.save(accessToken, refreshToken, {} as User)
547
+ const refreshed = await refreshOnce(this.client)
548
+ if (refreshed.data) {
549
+ this._emitEvent('SIGNED_IN', refreshed.data.user, refreshed.data.access_token)
550
+ return { data: refreshed.data.user, error: null }
551
+ }
552
+ restore()
553
+ return { data: null, error: refreshed.error }
554
+ }
555
+
556
+ store.save(accessToken, refreshToken, {} as User)
557
+ const res = await this.request<User>('GET', `${this.prefix}/user`)
558
+ if (res.data) {
559
+ store.save(accessToken, refreshToken, res.data)
560
+ this._emitEvent('SIGNED_IN', res.data, accessToken)
561
+ return res
562
+ }
563
+ // Échec de validation : on ne laisse pas le store pollué.
564
+ restore()
565
+ return res
566
+ }
567
+
568
+ async exchangeCodeForSession(authCode: string, codeVerifier?: string): Promise<AuraResponse<TokenPair>> {
569
+ return this._exchangeCode(authCode, codeVerifier, 'SIGNED_IN')
570
+ }
571
+
572
+ /**
573
+ * Cœur de l'échange PKCE. `successEvent` détermine l'événement émis en cas de
574
+ * succès : `SIGNED_IN` (connexion normale) ou `PASSWORD_RECOVERY` (callback de
575
+ * récupération de mot de passe, `?code=…&type=recovery` — voir `_detectSessionInUrl`).
576
+ */
577
+ private async _exchangeCode(
578
+ authCode: string,
579
+ codeVerifier: string | undefined,
580
+ successEvent: AuthEvent,
581
+ ): Promise<AuraResponse<TokenPair>> {
582
+ const verifierKey = this._pkceVerifierKey
583
+ const verifier = codeVerifier ?? this.client.authStore.getItem(verifierKey)
584
+ if (!verifier) {
585
+ return {
586
+ data: null,
587
+ error: new AuraError(0, {
588
+ error: { code: 'no_code_verifier', message: 'No PKCE code_verifier available (not provided and none stored)' },
589
+ }),
590
+ }
591
+ }
592
+ try {
593
+ const res = await this.request<TokenPair>('POST', `${this.prefix}/oauth/pkce`, {
594
+ auth_code: authCode,
595
+ code_verifier: verifier,
596
+ })
597
+ if (res.data) {
598
+ this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user)
599
+ this._emitEvent(successEvent, res.data.user, res.data.access_token)
600
+ }
601
+ return res
602
+ } finally {
603
+ // Purge INCONDITIONNELLE du `code_verifier` (succès OU échec) : un code PKCE
604
+ // est à usage unique — après une tentative d'échange, le verifier ne doit
605
+ // plus jamais être réutilisable (parité supabase-js `_exchangeCodeForSession`).
606
+ this.client.authStore.removeItem(verifierKey)
607
+ }
608
+ }
609
+
610
+ async listSessions(): Promise<AuraResponse<SessionInfo[]>> {
611
+ const store = this.client.authStore
612
+ if (store.token && store.refreshToken && !store.isValid) {
613
+ const refreshed = await this.refreshSession()
614
+ if (refreshed.error) {
615
+ return { data: null, error: refreshed.error }
616
+ }
617
+ }
618
+ const refreshToken = store.refreshToken
619
+ return this.request('GET', `${this.prefix}/sessions`, undefined, {
620
+ headers: refreshToken ? { 'x-refresh-token': refreshToken } : undefined,
621
+ })
622
+ }
623
+
624
+ async revokeSession(sessionId: string): Promise<AuraResponse<null>> {
625
+ return this.request('DELETE', `${this.prefix}/sessions/${sessionId}`)
626
+ }
627
+
628
+ async revokeAllSessions(): Promise<AuraResponse<{ message: string }>> {
629
+ const rt = this.client.authStore.refreshToken
630
+ if (!rt) {
631
+ return {
632
+ data: null,
633
+ error: new AuraError(0, { error: { code: 'no_session', message: 'No refresh token available' } }),
634
+ }
635
+ }
636
+ const res = await this.request<{ message: string }>(
637
+ 'POST',
638
+ `${this.prefix}/logout`,
639
+ { refresh_token: rt, scope: 'global' },
640
+ { skipAuthRefresh: true },
641
+ )
642
+ if (!res.error) {
643
+ this.client.authStore.clear()
644
+ this._emitEvent('SIGNED_OUT', null, null)
645
+ }
646
+ return res
647
+ }
648
+
649
+ // -- User management --------------------------------------------------------
650
+
651
+ /**
652
+ * Récupère l'utilisateur validé côté serveur.
653
+ *
654
+ * Sans argument : valide le token de la SESSION courante (`store.token`),
655
+ * avec refresh proactif — comportement historique.
656
+ *
657
+ * Avec `jwt` (LOT D) : valide le token ARBITRAIRE passé, via l'override
658
+ * `authToken` (`Authorization: Bearer <jwt>`), SANS toucher la session
659
+ * (`skipAuthRefresh`, aucun refresh ni retry-401 sur le store). Le backend
660
+ * applique l'isolation projet native : un token d'un AUTRE projet → 403.
661
+ */
662
+ async getUser(jwt?: string): Promise<AuraResponse<User>> {
663
+ if (jwt !== undefined) {
664
+ return this.request('GET', `${this.prefix}/user`, undefined, {
665
+ authToken: jwt,
666
+ skipAuthRefresh: true,
667
+ })
668
+ }
669
+ return this.request('GET', `${this.prefix}/user`)
670
+ }
671
+
672
+ /**
673
+ * Vérifie et décode les claims d'un JWT.
674
+ *
675
+ * Chemin nominal : vérification LOCALE via JWKS + WebCrypto, uniquement pour
676
+ * `alg ∈ {RS256, ES256}` (anti-alg-confusion : jamais de vérif locale HS256).
677
+ * Chemin fallback : `getUser()` (validation serveur) quand la vérif locale
678
+ * n'est pas applicable — `alg` non supporté (HS256), JWKS vide, aucune clé
679
+ * correspondante, ou fetch JWKS en échec. C'est le chemin nominal sur les
680
+ * déploiements HS256 (JWKS `{"keys":[]}`).
681
+ *
682
+ * FOREIGN-TOKEN (LOT D) : le fallback getUser valide le token RÉELLEMENT visé.
683
+ * Pour le token de session (no-arg, ou `jwt === authStore.token`), c'est
684
+ * `getUser()` (validation du token du store). Pour un `jwt` ÉTRANGER, c'est
685
+ * `getUser(jwt)` qui présente CE token en `Authorization` (override `authToken`,
686
+ * jamais celui du store) : le backend le valide et renvoie SON identité. Aucune
687
+ * misattribution possible — on présente exactement le token à vérifier — et
688
+ * l'isolation projet est native côté gateway (token d'un autre projet → 403,
689
+ * remonté en AuraError, jamais fabriqué).
690
+ *
691
+ * NOTE (fallback) : les claims renvoyées par le chemin fallback sont dérivées
692
+ * de `getUser()` (identité validée serveur) et NE contiennent PAS `exp`/`iat`
693
+ * (ni les autres champs temporels du JWT) — un consommateur ne doit pas se
694
+ * fier à `claims.exp` / `claims.iat` après un fallback. Seul le chemin de
695
+ * vérification locale RS256/ES256 renvoie les claims JWT complètes.
696
+ */
697
+ async getClaims(
698
+ jwt?: string,
699
+ options?: { allowExpired?: boolean; issuer?: string; audience?: string },
700
+ ): Promise<AuraResponse<GetClaimsResult>> {
701
+ const token = jwt ?? this.client.authStore.token
702
+ // Le fallback getUser valide le token du store, pas l'argument `jwt`. On ne
703
+ // peut donc l'autoriser que si le token vérifié est bien celui de la session.
704
+ const usingStoreToken = jwt === undefined || jwt === this.client.authStore.token
705
+ if (!token) {
706
+ return {
707
+ data: null,
708
+ error: new AuraError(0, { error: { code: 'no_session', message: 'No JWT available' } }),
709
+ }
710
+ }
711
+
712
+ // SEC-F4 : `this.client` sert de clé d'isolation du cache JWKS (WeakMap
713
+ // client-scoped dans jwks.ts) — deux clients distincts ne partagent aucun cache.
714
+ const r = await verifyJwtLocal(this.client, this.client.fetchFn, this.client.baseUrl, token, {
715
+ allowExpired: options?.allowExpired,
716
+ // LOT 3 (I8) : relie le callback générique de jwks.ts au hook d'observabilité
717
+ // du client (onEvent/debug). jwks.ts reste sans dépendance vers @aurabase/core.
718
+ onEvent: (baseUrl) =>
719
+ emitAuraEvent(this.client.onEvent, this.client.debug, { kind: 'jwks-fetch-failed', baseUrl }),
720
+ })
721
+ // Validation optionnelle iss/aud (parité `jose` : égalité simple, `iss` en
722
+ // string, `aud` en string OU array via `includes`). N'impose rien par défaut :
723
+ // contrôlé seulement si l'option est fournie. SEC-F1 : appliquée AUSSI au chemin
724
+ // fallback (voir plus bas) — plus d'options mortes en prod HS256.
725
+ const checkIssAud = (c: {
726
+ iss?: unknown
727
+ aud?: unknown
728
+ }): AuraError | null => {
729
+ if (options?.issuer !== undefined && c.iss !== options.issuer) {
730
+ return new AuraError(0, {
731
+ error: { code: 'invalid_issuer', message: `getClaims: issuer mismatch` },
732
+ })
733
+ }
734
+ if (options?.audience !== undefined) {
735
+ const aud = c.aud
736
+ const audList = Array.isArray(aud) ? aud : aud !== undefined ? [aud] : []
737
+ if (!audList.includes(options.audience)) {
738
+ return new AuraError(0, {
739
+ error: { code: 'invalid_audience', message: `getClaims: audience mismatch` },
740
+ })
741
+ }
742
+ }
743
+ return null
744
+ }
745
+
746
+ if (r.ok) {
747
+ const claims = r.claims as JwtClaims
748
+ const issAudErr = checkIssAud(claims)
749
+ if (issAudErr) return { data: null, error: issAudErr }
750
+ // SEC-F2 : ISOLATION PROJET sur le chemin de vérif LOCALE. Les clés JWKS sont
751
+ // propres au DÉPLOIEMENT (partagées par tous ses projets) — un token émis pour
752
+ // un AUTRE projet vérifie donc sa signature et passerait sans ce garde-fou. Le
753
+ // backend porte l'isolation dans le claim `project_id` (libs/aura-core
754
+ // AuraClaims.project_id). On refuse par défaut tout token dont le `project_id`
755
+ // ne correspond PAS au projet du client (le chemin fallback getUser applique
756
+ // déjà cette isolation nativement via le gateway → 403). On ne rejette que si
757
+ // le claim est PRÉSENT et divergent (un token tiers sans `project_id` — ex.
758
+ // Google — n'est pas concerné par l'isolation multi-tenant Aurabase).
759
+ const claimProject = claims.project_id
760
+ if (typeof claimProject === 'string' && claimProject !== this.client.projectId) {
761
+ return {
762
+ data: null,
763
+ error: new AuraError(0, {
764
+ error: {
765
+ code: 'project_isolation_violation',
766
+ message: `getClaims: token project mismatch (isolation)`,
767
+ },
768
+ }),
769
+ }
770
+ }
771
+ return { data: { claims, header: r.header }, error: null }
772
+ }
773
+
774
+ // Fallback getUser : HS256 (unsupported_alg), JWKS vide / clé absente
775
+ // (no_matching_key), ou fetch JWKS en échec — le token peut rester valide
776
+ // côté serveur (cas déploiement HS256).
777
+ if (
778
+ r.code === 'unsupported_alg' ||
779
+ r.code === 'no_matching_key' ||
780
+ r.code === 'jwks_fetch_failed'
781
+ ) {
782
+ // SEC-F1 : les options `issuer`/`audience`/`allowExpired` ne s'appliquaient
783
+ // qu'au chemin local et étaient MORTES sur le fallback (= la prod HS256). On
784
+ // décode les claims LOCALEMENT (payload base64url, SANS vérif de signature —
785
+ // uniquement pour lire iss/aud/exp) et on tranche AVANT tout appel serveur.
786
+ let localPayload: { iss?: unknown; aud?: unknown; exp?: unknown } = {}
787
+ try {
788
+ localPayload = decodeJwt(token).payload
789
+ } catch {
790
+ // payload indéchiffrable → on laisse le fallback getUser trancher.
791
+ }
792
+ const issAudErr = checkIssAud(localPayload)
793
+ if (issAudErr) return { data: null, error: issAudErr }
794
+ // `allowExpired` ne PEUT PAS être honoré par le fallback serveur : `getUser`
795
+ // rejette un token expiré (401). Plutôt qu'un 401 trompeur qui contredit
796
+ // l'intention de l'appelant, on remonte un code explicite et cohérent quand
797
+ // le token est réellement expiré ET que allowExpired était demandé.
798
+ if (
799
+ options?.allowExpired &&
800
+ typeof localPayload.exp === 'number' &&
801
+ localPayload.exp < Math.floor(Date.now() / 1000)
802
+ ) {
803
+ return {
804
+ data: null,
805
+ error: new AuraError(0, {
806
+ error: {
807
+ code: 'allow_expired_unsupported_on_fallback',
808
+ message:
809
+ 'getClaims: allowExpired ne peut pas être honoré sur le chemin de validation serveur (fallback HS256/JWKS)',
810
+ },
811
+ }),
812
+ }
813
+ }
814
+ // LOT D — plus de confused-deputy : on valide le token RÉELLEMENT visé.
815
+ // - `usingStoreToken` : `getUser()` no-arg valide le token de session
816
+ // (refresh proactif conservé) — chemin historique INCHANGÉ.
817
+ // - token étranger (`!usingStoreToken`) : `getUser(token)` envoie CE token
818
+ // en `Authorization` (override `authToken`), pas celui du store. Le
819
+ // backend le valide et renvoie SON identité, avec isolation projet native
820
+ // (token d'un autre projet → 403 remonté en AuraError, jamais fabriqué).
821
+ // Auparavant fail-closed (`unverifiable_token`) car le SDK ne POUVAIT PAS
822
+ // cibler le token étranger (Authorization toujours écrasé par store.token).
823
+ const u = usingStoreToken ? await this.getUser() : await this.getUser(token)
824
+ if (u.data) {
825
+ // Alg RÉEL du token (décodé) plutôt qu'un `HS256` en dur mensonger : sur un
826
+ // vrai token RS256 tombé en fallback (`jwks_fetch_failed`), le header doit
827
+ // refléter `RS256`. `unknown` si le header est indéchiffrable.
828
+ let realAlg = 'unknown'
829
+ try {
830
+ realAlg = decodeJwt(token).header.alg ?? 'unknown'
831
+ } catch {
832
+ // header indéchiffrable → 'unknown'
833
+ }
834
+ return {
835
+ data: {
836
+ // Claims dérivées de getUser : PAS de `exp`/`iat` (voir JwtClaims/JSDoc).
837
+ // `exp`/`iat` étant optionnels, plus de `as unknown as` masquant l'absence.
838
+ claims: { ...u.data, sub: u.data.id } as JwtClaims,
839
+ header: { alg: realAlg },
840
+ },
841
+ error: null,
842
+ }
843
+ }
844
+ return { data: null, error: u.error }
845
+ }
846
+
847
+ // Échecs de sécurité réels (invalid_jwt, signature_invalid, token_expired,
848
+ // token_not_yet_valid) : jamais de fallback silencieux.
849
+ return {
850
+ data: null,
851
+ error: new AuraError(0, { error: { code: r.code, message: `getClaims: ${r.code}` } }),
852
+ }
853
+ }
854
+
855
+ async deleteUser(password: string): Promise<AuraResponse<null>> {
856
+ const res = await this.request<null>('DELETE', `${this.prefix}/user`, { password })
857
+ if (!res.error) {
858
+ this.client.authStore.clear()
859
+ this._emitEvent('SIGNED_OUT', null, null)
860
+ }
861
+ return res
862
+ }
863
+
864
+ async updateUser(req: UpdateUserRequest): Promise<AuraResponse<User>> {
865
+ const res = await this.request<User>('PUT', `${this.prefix}/user`, req)
866
+ if (res.data) {
867
+ const store = this.client.authStore
868
+ if (store.token && store.refreshToken) {
869
+ store.save(store.token, store.refreshToken, res.data)
870
+ }
871
+ this._emitEvent('USER_UPDATED', res.data, this.client.authStore.token)
872
+ }
873
+ return res
874
+ }
875
+
876
+ // -- Password recovery ------------------------------------------------------
877
+
878
+ async resetPasswordForEmail(req: PasswordResetRequest): Promise<AuraResponse<{ message: string }>> {
879
+ return this.request('POST', `${this.prefix}/forgot-password`, req)
880
+ }
881
+
882
+ async updatePassword(newPassword: string, currentPassword?: string): Promise<AuraResponse<User>> {
883
+ const res = await this.request<User>('PUT', `${this.prefix}/user`, {
884
+ password: newPassword,
885
+ current_password: currentPassword,
886
+ })
887
+ if (!res.error) {
888
+ // Le backend révoque TOUTES les sessions au changement de mot de passe et ne renvoie pas
889
+ // de nouvelle TokenPair → les tokens en store sont morts. On déconnecte proprement.
890
+ this.client.authStore.clear()
891
+ this._emitEvent('SIGNED_OUT', null, null)
892
+ }
893
+ return res
894
+ }
895
+
896
+ // -- MFA --------------------------------------------------------------------
897
+
898
+ async mfaEnroll(): Promise<AuraResponse<MfaEnrollResponse>> {
899
+ return this.request('POST', `${this.prefix}/mfa/setup`)
900
+ }
901
+
902
+ async mfaVerifySetup(code: string): Promise<AuraResponse<{ recovery_codes: string[] }>> {
903
+ return this.request('POST', `${this.prefix}/mfa/setup/verify`, { code })
904
+ }
905
+
906
+ async mfaVerify(req: MfaVerifyRequest): Promise<AuraResponse<TokenPair>> {
907
+ const res = await this.request<TokenPair>('POST', `${this.prefix}/mfa/verify-login`, req)
908
+ if (res.data) {
909
+ this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user)
910
+ this._emitEvent('SIGNED_IN', res.data.user, res.data.access_token)
911
+ }
912
+ return res
913
+ }
914
+
915
+ async mfaUnenroll(password: string): Promise<AuraResponse<{ message: string }>> {
916
+ return this.request('POST', `${this.prefix}/mfa/disable`, { password })
917
+ }
918
+
919
+ // -- OTP verification (façade unifiée, parité @supabase/auth-js) -------------
920
+
921
+ /**
922
+ * Façade unifiée de vérification OTP (parité `verifyOtp` de `@supabase/auth-js`).
923
+ * Route vers l'endpoint historique correct selon `params.type`, en conservant
924
+ * l'effet de bord (sauvegarde de session + émission d'événement). SOURCE DE
925
+ * VÉRITÉ UNIQUE : les méthodes historiques (`verifyEmail`, `verifySmsOtp`, …)
926
+ * délèguent désormais ici.
927
+ *
928
+ * Mapping :
929
+ * - `signup` → POST /verify-email `{ token }` → SIGNED_IN
930
+ * - `magiclink` → POST /magic-link/verify `{ token }` → SIGNED_IN
931
+ * - `email` → POST /email-otp/verify `{ email, code }` → SIGNED_IN
932
+ * - `sms` → POST /sms/verify `{ phone, code }` → SIGNED_IN
933
+ * - `recovery` → POST /reset-password `{ token, password }` → SIGNED_IN
934
+ * - `email_change` → POST /user/confirm-email-change `{ token }` → USER_UPDATED
935
+ *
936
+ * Divergences documentées :
937
+ * - `recovery` EXIGE `password` (le backend fusionne consommation du token +
938
+ * changement de mot de passe) → `AuraError('missing_password')` sans appel réseau sinon.
939
+ * - `invite`/`phone_change` n'ont aucun endpoint backend → `AuraError('otp_type_unsupported')`.
940
+ * - `email_change` émet dorénavant `USER_UPDATED` (auparavant aucun événement) —
941
+ * amélioration assumée (parité), pas une régression.
942
+ */
943
+ async verifyOtp(params: VerifyEmailChangeOtpParams): Promise<AuraResponse<User>>
944
+ async verifyOtp(params: Exclude<VerifyOtpParams, VerifyEmailChangeOtpParams>): Promise<AuraResponse<TokenPair>>
945
+ async verifyOtp(params: VerifyOtpParams): Promise<AuraResponse<TokenPair | User>> {
946
+ switch (params.type) {
947
+ case 'signup':
948
+ return this._verifyToTokenPair(`${this.prefix}/verify-email`, { token: params.token })
949
+ case 'magiclink':
950
+ return this._verifyToTokenPair(`${this.prefix}/magic-link/verify`, { token: params.token })
951
+ case 'email':
952
+ return this._verifyToTokenPair(`${this.prefix}/email-otp/verify`, {
953
+ email: params.email,
954
+ code: params.token,
955
+ })
956
+ case 'sms':
957
+ return this._verifyToTokenPair(`${this.prefix}/sms/verify`, {
958
+ phone: params.phone,
959
+ code: params.token,
960
+ })
961
+ case 'recovery': {
962
+ if (!params.password) {
963
+ return {
964
+ data: null,
965
+ error: new AuraError(0, {
966
+ error: {
967
+ code: 'missing_password',
968
+ message:
969
+ "verifyOtp({ type: 'recovery' }) exige `password` : le backend fusionne consommation du token et changement de mot de passe.",
970
+ },
971
+ }),
972
+ }
973
+ }
974
+ return this._verifyToTokenPair(`${this.prefix}/reset-password`, {
975
+ token: params.token,
976
+ password: params.password,
977
+ })
978
+ }
979
+ case 'email_change': {
980
+ const res = await this.request<User>('POST', `${this.prefix}/user/confirm-email-change`, {
981
+ token: params.token,
982
+ })
983
+ if (res.data) {
984
+ const store = this.client.authStore
985
+ if (store.token && store.refreshToken) {
986
+ store.save(store.token, store.refreshToken, res.data)
987
+ }
988
+ this._emitEvent('USER_UPDATED', res.data, this.client.authStore.token)
989
+ }
990
+ return res
991
+ }
992
+ case 'invite':
993
+ case 'phone_change':
994
+ return {
995
+ data: null,
996
+ error: new AuraError(0, {
997
+ error: {
998
+ code: 'otp_type_unsupported',
999
+ message: `verifyOtp: le type '${params.type}' n'est pas supporté (aucun endpoint backend).`,
1000
+ },
1001
+ }),
1002
+ }
1003
+ default:
1004
+ return {
1005
+ data: null,
1006
+ error: new AuraError(0, {
1007
+ error: { code: 'otp_type_unsupported', message: 'verifyOtp: type OTP inconnu.' },
1008
+ }),
1009
+ }
1010
+ }
1011
+ }
1012
+
1013
+ /**
1014
+ * Helper commun aux vérifications OTP renvoyant une `TokenPair` : appelle
1015
+ * l'endpoint, puis (succès) sauvegarde la session et émet `SIGNED_IN`.
1016
+ */
1017
+ private async _verifyToTokenPair(path: string, body: unknown): Promise<AuraResponse<TokenPair>> {
1018
+ const res = await this.request<TokenPair>('POST', path, body)
1019
+ if (res.data) {
1020
+ this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user)
1021
+ this._emitEvent('SIGNED_IN', res.data.user, res.data.access_token)
1022
+ }
1023
+ return res
1024
+ }
1025
+
1026
+ // -- Email verification -----------------------------------------------------
1027
+
1028
+ /** Rétrocompat : délègue à `verifyOtp({ type: 'signup' })` (POST /verify-email). */
1029
+ async verifyEmail(token: string): Promise<AuraResponse<TokenPair>> {
1030
+ return this.verifyOtp({ type: 'signup', token })
1031
+ }
1032
+
1033
+ async resendVerificationEmail(email?: string): Promise<AuraResponse<{ message: string }>> {
1034
+ const e = email ?? this.client.authStore.user?.email
1035
+ if (!e) {
1036
+ return {
1037
+ data: null,
1038
+ error: new AuraError(0, { error: { code: 'missing_email', message: 'Email is required' } }),
1039
+ }
1040
+ }
1041
+ return this.request('POST', `${this.prefix}/resend-verification`, { email: e })
1042
+ }
1043
+
1044
+ // -- Email OTP (Phase 2) --------------------------------------------------
1045
+
1046
+ async signInWithEmailOtp(email: string): Promise<AuraResponse<{ message: string }>> {
1047
+ return this.request('POST', `${this.prefix}/email-otp/send`, { email })
1048
+ }
1049
+
1050
+ /** Rétrocompat : délègue à `verifyOtp({ type: 'email' })` (POST /email-otp/verify). */
1051
+ async verifyEmailOtp(email: string, code: string): Promise<AuraResponse<TokenPair>> {
1052
+ return this.verifyOtp({ type: 'email', email, token: code })
1053
+ }
1054
+
1055
+ // -- SMS OTP (Phase 2) ----------------------------------------------------
1056
+
1057
+ async signInWithSmsOtp(phone: string): Promise<AuraResponse<{ message: string }>> {
1058
+ return this.request('POST', `${this.prefix}/sms/send`, { phone })
1059
+ }
1060
+
1061
+ /** Rétrocompat : délègue à `verifyOtp({ type: 'sms' })` (POST /sms/verify). */
1062
+ async verifySmsOtp(phone: string, code: string): Promise<AuraResponse<TokenPair>> {
1063
+ return this.verifyOtp({ type: 'sms', phone, token: code })
1064
+ }
1065
+
1066
+ // -- Identity linking (Phase 2) -------------------------------------------
1067
+
1068
+ async listIdentities(): Promise<AuraResponse<IdentityInfo[]>> {
1069
+ return this.request('GET', `${this.prefix}/user/identities`)
1070
+ }
1071
+
1072
+ async linkIdentity(provider: string): Promise<AuraResponse<{ url: string; provider: string }>> {
1073
+ return this.request('POST', `${this.prefix}/user/identities/link/${provider}`)
1074
+ }
1075
+
1076
+ async unlinkIdentity(identityId: string): Promise<AuraResponse<{ message: string }>> {
1077
+ return this.request('DELETE', `${this.prefix}/user/identities/${identityId}`)
1078
+ }
1079
+
1080
+ // -- Email change (Phase 2) -----------------------------------------------
1081
+
1082
+ /**
1083
+ * Rétrocompat : délègue à `verifyOtp({ type: 'email_change' })`.
1084
+ * CHANGEMENT ASSUMÉ (parité) : émet désormais `USER_UPDATED` (auparavant aucun
1085
+ * événement) — voir CHANGELOG.
1086
+ */
1087
+ async confirmEmailChange(token: string): Promise<AuraResponse<User>> {
1088
+ return this.verifyOtp({ type: 'email_change', token })
1089
+ }
1090
+
1091
+ // -- MFA recovery codes (Phase 2) -----------------------------------------
1092
+
1093
+ async regenerateRecoveryCodes(code: string): Promise<AuraResponse<{ recovery_codes: string[] }>> {
1094
+ return this.request('POST', `${this.prefix}/mfa/recovery-codes/regenerate`, { code })
1095
+ }
1096
+
1097
+ // -- Native mobile OAuth (Phase 2) ----------------------------------------
1098
+
1099
+ async signInWithIdToken(provider: string, idToken: string, accessToken?: string): Promise<AuraResponse<TokenPair>> {
1100
+ const res = await this.request<TokenPair>('POST', `${this.prefix}/token`, {
1101
+ provider,
1102
+ id_token: idToken,
1103
+ access_token: accessToken,
1104
+ })
1105
+ if (res.data) {
1106
+ this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user)
1107
+ this._emitEvent('SIGNED_IN', res.data.user, res.data.access_token)
1108
+ }
1109
+ return res
1110
+ }
1111
+
1112
+ // -- Project settings / public config --------------------------------------
1113
+
1114
+ /** GoTrue parity : GET /settings — config publique du projet (pas d'auth requise). */
1115
+ async getSettings(): Promise<
1116
+ AuraResponse<{
1117
+ disable_signup: boolean
1118
+ external: Record<string, boolean>
1119
+ mailer_autoconfirm: boolean
1120
+ mfa_enabled: boolean
1121
+ }>
1122
+ > {
1123
+ return this.request('GET', `${this.prefix}/settings`)
1124
+ }
1125
+
1126
+ /** Clé publiable (anon key) du projet — GET /anon-key (public, à l'image du modèle Supabase). */
1127
+ async getAnonKey(): Promise<AuraResponse<{ anon_key: string }>> {
1128
+ return this.request('GET', `${this.prefix}/anon-key`)
1129
+ }
1130
+
1131
+ // -- Password recovery (token consumption) ----------------------------------
1132
+
1133
+ /**
1134
+ * Consomme le token de récupération de mot de passe et applique le nouveau mot de passe.
1135
+ * Body confirmé au handler (`ResetPasswordRequest` — services/aura-auth/src/handlers/auth.rs) :
1136
+ * `{ token, password }`. Révoque les anciennes sessions et renvoie une nouvelle TokenPair.
1137
+ */
1138
+ async resetPassword(token: string, newPassword: string): Promise<AuraResponse<TokenPair>> {
1139
+ return this.verifyOtp({ type: 'recovery', token, password: newPassword })
1140
+ }
1141
+
1142
+ // -- Anonymous promotion -----------------------------------------------------
1143
+
1144
+ /**
1145
+ * Convertit l'utilisateur anonyme courant (JWT requis) en compte régulier email+password.
1146
+ * Body confirmé au handler (`PromoteAnonymousRequest`) : `{ email, password }`.
1147
+ */
1148
+ async promoteAnonymous(credentials: { email: string; password: string }): Promise<AuraResponse<TokenPair>> {
1149
+ const res = await this.request<TokenPair>('POST', `${this.prefix}/anonymous/promote`, credentials)
1150
+ if (res.data) {
1151
+ this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user)
1152
+ this._emitEvent('USER_UPDATED', res.data.user, res.data.access_token)
1153
+ }
1154
+ return res
1155
+ }
1156
+
1157
+ // -- Magic link verification -------------------------------------------------
1158
+
1159
+ /**
1160
+ * Consomme le token de magic link et renvoie une TokenPair.
1161
+ * Body confirmé au handler (`VerifyMagicLinkRequest`) : `{ token }`.
1162
+ */
1163
+ async verifyMagicLink(token: string): Promise<AuraResponse<TokenPair>> {
1164
+ return this.verifyOtp({ type: 'magiclink', token })
1165
+ }
1166
+ }