@aurabase/aurabase-js 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/package.json +4 -4
  2. package/packages/ai/node_modules/.bin/esbuild +0 -19
  3. package/packages/ai/node_modules/.bin/tsc +0 -22
  4. package/packages/ai/node_modules/.bin/tsserver +0 -22
  5. package/packages/ai/node_modules/.bin/tsup +0 -22
  6. package/packages/ai/node_modules/.bin/tsup-node +0 -22
  7. package/packages/ai/node_modules/.bin/vite +0 -22
  8. package/packages/ai/node_modules/.bin/vitest +0 -22
  9. package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  10. package/packages/ai/src/AiService.ts +0 -226
  11. package/packages/ai/src/index.ts +0 -18
  12. package/packages/ai/src/sse.ts +0 -141
  13. package/packages/ai/src/types.ts +0 -30
  14. package/packages/ai/tests/chat-stream.test.ts +0 -151
  15. package/packages/ai/tests/rag-namespaces.test.ts +0 -54
  16. package/packages/ai/tests/sse.test.ts +0 -69
  17. package/packages/ai/tsconfig.json +0 -5
  18. package/packages/ai/tsup.config.ts +0 -11
  19. package/packages/aurabase/node_modules/.bin/esbuild +0 -19
  20. package/packages/aurabase/node_modules/.bin/tsc +0 -22
  21. package/packages/aurabase/node_modules/.bin/tsserver +0 -22
  22. package/packages/aurabase/node_modules/.bin/tsup +0 -22
  23. package/packages/aurabase/node_modules/.bin/tsup-node +0 -22
  24. package/packages/aurabase/node_modules/.bin/vite +0 -22
  25. package/packages/aurabase/node_modules/.bin/vitest +0 -22
  26. package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  27. package/packages/aurabase/src/AuraClient.ts +0 -237
  28. package/packages/aurabase/src/index.ts +0 -152
  29. package/packages/aurabase/src/ssr.ts +0 -156
  30. package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +0 -82
  31. package/packages/aurabase/tests/ai.test.ts +0 -111
  32. package/packages/aurabase/tests/auth-admin.test.ts +0 -515
  33. package/packages/aurabase/tests/auth-e2e.test.ts +0 -135
  34. package/packages/aurabase/tests/auth-hardening.test.ts +0 -206
  35. package/packages/aurabase/tests/auth.test.ts +0 -947
  36. package/packages/aurabase/tests/database.test.ts +0 -230
  37. package/packages/aurabase/tests/db-types-e2e.test.ts +0 -217
  38. package/packages/aurabase/tests/dispose.test.ts +0 -23
  39. package/packages/aurabase/tests/foreign-token.e2e.test.ts +0 -129
  40. package/packages/aurabase/tests/functions.test.ts +0 -107
  41. package/packages/aurabase/tests/integration.test.ts +0 -413
  42. package/packages/aurabase/tests/live-production-validation.js +0 -241
  43. package/packages/aurabase/tests/live-sdk-production-validation.js +0 -293
  44. package/packages/aurabase/tests/mocks.ts +0 -139
  45. package/packages/aurabase/tests/notifications.test.ts +0 -314
  46. package/packages/aurabase/tests/observability-backup.test.ts +0 -186
  47. package/packages/aurabase/tests/observability-hook.test.ts +0 -130
  48. package/packages/aurabase/tests/production-robustness.test.ts +0 -268
  49. package/packages/aurabase/tests/public-api-surface.test.ts +0 -53
  50. package/packages/aurabase/tests/query-builder.test.ts +0 -329
  51. package/packages/aurabase/tests/readme-smoke-e2e.test.ts +0 -231
  52. package/packages/aurabase/tests/realtime-filter-e2e.test.ts +0 -344
  53. package/packages/aurabase/tests/refresh-locks.test.ts +0 -210
  54. package/packages/aurabase/tests/retry-option.test.ts +0 -17
  55. package/packages/aurabase/tests/rpc-e2e.test.ts +0 -254
  56. package/packages/aurabase/tests/rs256-local-e2e.test.ts +0 -114
  57. package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +0 -235
  58. package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +0 -124
  59. package/packages/aurabase/tests/service-role-guard.test.ts +0 -99
  60. package/packages/aurabase/tests/ssr.test.ts +0 -91
  61. package/packages/aurabase/tests/storage-backend-e2e.test.ts +0 -481
  62. package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +0 -104
  63. package/packages/aurabase/tests/storage-transform-e2e.test.ts +0 -684
  64. package/packages/aurabase/tests/storage.test.ts +0 -55
  65. package/packages/aurabase/tests/types/client-generic.test-d.ts +0 -79
  66. package/packages/aurabase/tests/types/docs-snippets.test-d.ts +0 -571
  67. package/packages/aurabase/tests/types/error-code.test-d.ts +0 -53
  68. package/packages/aurabase/tsconfig.json +0 -5
  69. package/packages/aurabase/tsconfig.test-d.json +0 -5
  70. package/packages/aurabase/tsup.config.ts +0 -11
  71. package/packages/aurabase/vitest.config.ts +0 -16
  72. package/packages/auth/node_modules/.bin/esbuild +0 -19
  73. package/packages/auth/node_modules/.bin/tsc +0 -22
  74. package/packages/auth/node_modules/.bin/tsserver +0 -22
  75. package/packages/auth/node_modules/.bin/tsup +0 -22
  76. package/packages/auth/node_modules/.bin/tsup-node +0 -22
  77. package/packages/auth/node_modules/.bin/vite +0 -22
  78. package/packages/auth/node_modules/.bin/vitest +0 -22
  79. package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  80. package/packages/auth/src/AuthAdminService.ts +0 -149
  81. package/packages/auth/src/AuthService.ts +0 -1166
  82. package/packages/auth/src/SessionManager.ts +0 -92
  83. package/packages/auth/src/index.ts +0 -17
  84. package/packages/auth/src/jwks.ts +0 -337
  85. package/packages/auth/src/pkce.ts +0 -27
  86. package/packages/auth/src/types.ts +0 -89
  87. package/packages/auth/tests/auth-lifecycle.test.ts +0 -149
  88. package/packages/auth/tests/detect-session.test.ts +0 -361
  89. package/packages/auth/tests/getclaims.test.ts +0 -780
  90. package/packages/auth/tests/is-mfa-challenge.test.ts +0 -27
  91. package/packages/auth/tests/password-recovery.test.ts +0 -124
  92. package/packages/auth/tests/pkce.test.ts +0 -192
  93. package/packages/auth/tests/session-manager.test.ts +0 -103
  94. package/packages/auth/tests/set-session.test.ts +0 -139
  95. package/packages/auth/tests/verify-otp.test.ts +0 -153
  96. package/packages/auth/tsconfig.json +0 -5
  97. package/packages/auth/tsup.config.ts +0 -11
  98. package/packages/core/node_modules/.bin/esbuild +0 -22
  99. package/packages/core/node_modules/.bin/tsc +0 -22
  100. package/packages/core/node_modules/.bin/tsserver +0 -22
  101. package/packages/core/node_modules/.bin/tsup +0 -22
  102. package/packages/core/node_modules/.bin/tsup-node +0 -22
  103. package/packages/core/node_modules/.bin/vite +0 -22
  104. package/packages/core/node_modules/.bin/vitest +0 -22
  105. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  106. package/packages/core/src/AuraError.ts +0 -89
  107. package/packages/core/src/BaseService.ts +0 -44
  108. package/packages/core/src/client.ts +0 -34
  109. package/packages/core/src/cookie-utils.ts +0 -42
  110. package/packages/core/src/fetch.ts +0 -435
  111. package/packages/core/src/index.ts +0 -22
  112. package/packages/core/src/retry.ts +0 -63
  113. package/packages/core/src/serialize.ts +0 -6
  114. package/packages/core/src/stores/BaseAuthStore.ts +0 -164
  115. package/packages/core/src/stores/LocalAuthStore.ts +0 -199
  116. package/packages/core/src/types.ts +0 -1042
  117. package/packages/core/tests/auth-store.test.ts +0 -300
  118. package/packages/core/tests/error-code.test.ts +0 -61
  119. package/packages/core/tests/error.test.ts +0 -24
  120. package/packages/core/tests/fetch.test.ts +0 -286
  121. package/packages/core/tests/mocks.ts +0 -139
  122. package/packages/core/tests/observability.test.ts +0 -86
  123. package/packages/core/tests/refresh-hardening.test.ts +0 -164
  124. package/packages/core/tests/refresh-observability.test.ts +0 -111
  125. package/packages/core/tests/retry.test.ts +0 -106
  126. package/packages/core/tests/serialize.test.ts +0 -12
  127. package/packages/core/tests/timeout.test.ts +0 -79
  128. package/packages/core/tsconfig.json +0 -5
  129. package/packages/core/tsup.config.ts +0 -11
  130. package/packages/db/node_modules/.bin/esbuild +0 -19
  131. package/packages/db/node_modules/.bin/tsc +0 -22
  132. package/packages/db/node_modules/.bin/tsserver +0 -22
  133. package/packages/db/node_modules/.bin/tsup +0 -22
  134. package/packages/db/node_modules/.bin/tsup-node +0 -22
  135. package/packages/db/node_modules/.bin/vite +0 -22
  136. package/packages/db/node_modules/.bin/vitest +0 -22
  137. package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  138. package/packages/db/src/DatabaseService.ts +0 -170
  139. package/packages/db/src/QueryBuilder.ts +0 -830
  140. package/packages/db/src/index.ts +0 -2
  141. package/packages/db/src/pgrst-types/common/common.ts +0 -86
  142. package/packages/db/src/pgrst-types/common/rpc.ts +0 -151
  143. package/packages/db/src/pgrst-types/feature-flags.ts +0 -20
  144. package/packages/db/src/pgrst-types/types.ts +0 -134
  145. package/packages/db/src/select-query-parser/parser.ts +0 -479
  146. package/packages/db/src/select-query-parser/result.ts +0 -553
  147. package/packages/db/src/select-query-parser/types.ts +0 -132
  148. package/packages/db/src/select-query-parser/utils.ts +0 -707
  149. package/packages/db/tests/filters.test.ts +0 -81
  150. package/packages/db/tests/mutations-filtered.e2e.test.ts +0 -132
  151. package/packages/db/tests/mutations-filtered.test.ts +0 -188
  152. package/packages/db/tests/order.test.ts +0 -45
  153. package/packages/db/tests/rpc.test.ts +0 -41
  154. package/packages/db/tests/scale/build-large-schema.ts +0 -94
  155. package/packages/db/tests/scale/scale-compile.test.ts +0 -167
  156. package/packages/db/tests/schema.test.ts +0 -77
  157. package/packages/db/tests/single-accept.e2e.test.ts +0 -123
  158. package/packages/db/tests/single.test.ts +0 -211
  159. package/packages/db/tests/types/filters.test-d.ts +0 -52
  160. package/packages/db/tests/types/fixtures.ts +0 -40
  161. package/packages/db/tests/types/from-views.test-d.ts +0 -18
  162. package/packages/db/tests/types/mutations.test-d.ts +0 -23
  163. package/packages/db/tests/types/rpc.test-d.ts +0 -60
  164. package/packages/db/tests/types/schema-shape.test-d.ts +0 -8
  165. package/packages/db/tests/types/select-inference.test-d.ts +0 -38
  166. package/packages/db/tests/vendored/fixtures.ts +0 -83
  167. package/packages/db/tests/vendored/get-result.test-d.ts +0 -75
  168. package/packages/db/tsconfig.json +0 -5
  169. package/packages/db/tsconfig.test-d.json +0 -5
  170. package/packages/db/tsup.config.ts +0 -11
  171. package/packages/db/vitest.config.ts +0 -8
  172. package/packages/functions/node_modules/.bin/esbuild +0 -19
  173. package/packages/functions/node_modules/.bin/tsc +0 -22
  174. package/packages/functions/node_modules/.bin/tsserver +0 -22
  175. package/packages/functions/node_modules/.bin/tsup +0 -22
  176. package/packages/functions/node_modules/.bin/tsup-node +0 -22
  177. package/packages/functions/node_modules/.bin/vite +0 -22
  178. package/packages/functions/node_modules/.bin/vitest +0 -22
  179. package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  180. package/packages/functions/src/FunctionsService.ts +0 -188
  181. package/packages/functions/src/errors.ts +0 -70
  182. package/packages/functions/src/index.ts +0 -6
  183. package/packages/functions/tests/invoke.test.ts +0 -97
  184. package/packages/functions/tests/response-type-errors.test.ts +0 -135
  185. package/packages/functions/tsconfig.json +0 -5
  186. package/packages/functions/tsup.config.ts +0 -11
  187. package/packages/gen/node_modules/.bin/esbuild +0 -19
  188. package/packages/gen/node_modules/.bin/tsc +0 -22
  189. package/packages/gen/node_modules/.bin/tsserver +0 -22
  190. package/packages/gen/node_modules/.bin/tsup +0 -22
  191. package/packages/gen/node_modules/.bin/tsup-node +0 -22
  192. package/packages/gen/node_modules/.bin/vite +0 -22
  193. package/packages/gen/node_modules/.bin/vitest +0 -22
  194. package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  195. package/packages/gen/src/cli.ts +0 -67
  196. package/packages/gen/src/generate.ts +0 -0
  197. package/packages/gen/src/index.ts +0 -2
  198. package/packages/gen/tests/compile.test.ts +0 -213
  199. package/packages/gen/tests/e2e.test.ts +0 -130
  200. package/packages/gen/tests/generate.test.ts +0 -396
  201. package/packages/gen/tsconfig.json +0 -5
  202. package/packages/gen/tsup.config.ts +0 -11
  203. package/packages/notifications/node_modules/.bin/esbuild +0 -19
  204. package/packages/notifications/node_modules/.bin/tsc +0 -22
  205. package/packages/notifications/node_modules/.bin/tsserver +0 -22
  206. package/packages/notifications/node_modules/.bin/tsup +0 -22
  207. package/packages/notifications/node_modules/.bin/tsup-node +0 -22
  208. package/packages/notifications/node_modules/.bin/vite +0 -22
  209. package/packages/notifications/node_modules/.bin/vitest +0 -22
  210. package/packages/notifications/src/NotificationsService.ts +0 -230
  211. package/packages/notifications/src/index.ts +0 -19
  212. package/packages/notifications/tsconfig.json +0 -5
  213. package/packages/notifications/tsup.config.ts +0 -11
  214. package/packages/realtime/node_modules/.bin/esbuild +0 -19
  215. package/packages/realtime/node_modules/.bin/tsc +0 -22
  216. package/packages/realtime/node_modules/.bin/tsserver +0 -22
  217. package/packages/realtime/node_modules/.bin/tsup +0 -22
  218. package/packages/realtime/node_modules/.bin/tsup-node +0 -22
  219. package/packages/realtime/node_modules/.bin/vite +0 -22
  220. package/packages/realtime/node_modules/.bin/vitest +0 -22
  221. package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  222. package/packages/realtime/src/AuraChannel.ts +0 -485
  223. package/packages/realtime/src/HeartbeatManager.ts +0 -73
  224. package/packages/realtime/src/RealtimeService.ts +0 -505
  225. package/packages/realtime/src/columnFilter.ts +0 -301
  226. package/packages/realtime/src/index.ts +0 -4
  227. package/packages/realtime/tests/ack-routing.test.ts +0 -81
  228. package/packages/realtime/tests/column-filter.test.ts +0 -144
  229. package/packages/realtime/tests/dispatch.test.ts +0 -191
  230. package/packages/realtime/tests/heartbeat.test.ts +0 -67
  231. package/packages/realtime/tests/http-broadcast.test.ts +0 -68
  232. package/packages/realtime/tests/realtime-token.test.ts +0 -43
  233. package/packages/realtime/tests/reconnect-race.test.ts +0 -149
  234. package/packages/realtime/tests/ref-id.test.ts +0 -34
  235. package/packages/realtime/tests/set-auth.test.ts +0 -70
  236. package/packages/realtime/tsconfig.json +0 -5
  237. package/packages/realtime/tsup.config.ts +0 -11
  238. package/packages/storage/node_modules/.bin/esbuild +0 -19
  239. package/packages/storage/node_modules/.bin/tsc +0 -22
  240. package/packages/storage/node_modules/.bin/tsserver +0 -22
  241. package/packages/storage/node_modules/.bin/tsup +0 -22
  242. package/packages/storage/node_modules/.bin/tsup-node +0 -22
  243. package/packages/storage/node_modules/.bin/vite +0 -22
  244. package/packages/storage/node_modules/.bin/vitest +0 -22
  245. package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  246. package/packages/storage/src/DownloadBuilder.ts +0 -60
  247. package/packages/storage/src/StorageService.ts +0 -323
  248. package/packages/storage/src/index.ts +0 -2
  249. package/packages/storage/src/path.ts +0 -35
  250. package/packages/storage/src/transform.ts +0 -15
  251. package/packages/storage/tests/download.test.ts +0 -64
  252. package/packages/storage/tests/parity.test.ts +0 -154
  253. package/packages/storage/tests/path.test.ts +0 -87
  254. package/packages/storage/tests/transform.test.ts +0 -79
  255. package/packages/storage/tsconfig.json +0 -5
  256. package/packages/storage/tsup.config.ts +0 -11
  257. package/pnpm-workspace.yaml +0 -24
  258. package/tsconfig.base.json +0 -15
@@ -1,435 +0,0 @@
1
- import { AuraError } from './AuraError'
2
- import type { AuraResponse, TokenPair, Meta } from './types'
3
- import { emitAuraEvent } from './types'
4
- import type { AuraClientLike } from './client'
5
- import { isTokenExpired } from './stores/BaseAuthStore'
6
- import {
7
- isRetryableMethod,
8
- isRetryableStatus,
9
- computeBackoff,
10
- parseRetryAfter,
11
- abortableSleep,
12
- isAbortError,
13
- DEFAULT_MAX_RETRIES,
14
- } from './retry'
15
- import { serializeBody } from './serialize'
16
-
17
- const REFRESH_MARGIN_SEC = 30
18
-
19
- /** Plafond du délai `Retry-After` (borne un serveur renvoyant une valeur énorme). */
20
- const RETRY_AFTER_CAP_MS = 60_000
21
-
22
- /** Timeout par défaut d'une requête HTTP (borne un serveur qui « hang »). */
23
- export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
24
-
25
- /**
26
- * Déballe l'enveloppe backend `{data, meta}` de façon SÛRE (minor 6b).
27
- * `'data' in body` JETTE un TypeError si `body` n'est pas un objet (corps JSON
28
- * primitif ou `null` : `42`, `"x"`, `null`). On garde alors le corps tel quel comme
29
- * `data` (pas de throw, pas de perte de valeur). `null` explicite dans une enveloppe
30
- * (`{data:null,meta:null}`) reste distingué de « absent » (I5).
31
- */
32
- function unwrapEnvelope(body: unknown): unknown {
33
- if (body !== null && typeof body === 'object' && 'data' in body) {
34
- return (body as { data: unknown }).data
35
- }
36
- return body
37
- }
38
-
39
- /**
40
- * Single-flight de rafraîchissement (E17).
41
- * Une seule promesse de refresh en vol par client : `maybeRefresh` (proactif),
42
- * `AuthService.refreshSession` (manuel) et le retry 401 (E19) passent TOUS par
43
- * `refreshOnce`, ce qui évite deux refresh concurrents avec le même refresh token
44
- * (qui, sur un backend à rotation stricte, provoquaient une déconnexion aléatoire).
45
- */
46
- const inflightRefreshes = new WeakMap<AuraClientLike, Promise<AuraResponse<TokenPair>>>()
47
-
48
- /** Effectue le refresh (sans dédup) — appelé uniquement via `refreshOnce`. */
49
- async function doRefresh(client: AuraClientLike): Promise<AuraResponse<TokenPair>> {
50
- const store = client.authStore
51
- const rt = store.refreshToken
52
- if (!rt) {
53
- return {
54
- data: null,
55
- error: new AuraError(0, { error: { code: 'no_session', message: 'No refresh token available' } }),
56
- }
57
- }
58
- // Commit-guard (B) : snapshot (epoch + refresh token) avant le fetch. Si un clear()/signOut
59
- // OU un save()/signIn concurrent (ou une rotation par un autre onglet) s'intercale pendant
60
- // l'await, la session courante a changé → on ne doit NI la ressusciter (succès) NI l'effacer
61
- // (échec sur l'ancien token). On compare epoch ET l'identité du refresh token.
62
- const epoch = store.removalEpoch
63
- let res: Response
64
- try {
65
- res = await client.fetchFn(`${client.baseUrl}/v1/auth/${client.projectId}/refresh`, {
66
- method: 'POST',
67
- headers: { 'Content-Type': 'application/json' },
68
- body: serializeBody({ refresh_token: rt }),
69
- })
70
- } catch (err) {
71
- // Erreur réseau : on garde les tokens existants (peut-être encore valides).
72
- return {
73
- data: null,
74
- error: new AuraError(0, {
75
- error: { code: 'network_error', message: err instanceof Error ? err.message : 'Network request failed' },
76
- }),
77
- }
78
- }
79
- if (!res.ok) {
80
- let body: { error?: { code?: string; message?: string }; message?: string } | null = null
81
- try {
82
- body = await res.json()
83
- } catch {
84
- // corps non-JSON
85
- }
86
- // Clear sélectif (B, proactive-preserve) : ne détruire la session que sur rejet DÉFINITIF
87
- // du refresh token (invalide/révoqué/expiré). Sur transitoire (5xx/429/408/réseau), on
88
- // préserve la session. Commit-guard : ne PAS effacer une session déjà changée pendant
89
- // l'await (signIn/rotation concurrente) — le 401 porte alors sur un ANCIEN token.
90
- if (res.status === 400 || res.status === 401 || res.status === 403) {
91
- if (store.removalEpoch === epoch && store.refreshToken === rt) {
92
- store.clear()
93
- }
94
- }
95
- return { data: null, error: new AuraError(res.status, body) }
96
- }
97
- // Chemin succès : le corps DOIT être du JSON. Un 200 non-JSON (proxy mal
98
- // configuré, page HTML d'erreur renvoyée en 200…) ne doit pas faire rejeter
99
- // la promesse `refreshOnce` (les appelants attendent un AuraResponse, jamais
100
- // un throw) — on renvoie un AuraError structuré, cohérent avec le chemin erreur.
101
- let body: { data?: TokenPair } & Partial<TokenPair>
102
- try {
103
- body = await res.json()
104
- } catch {
105
- return {
106
- data: null,
107
- error: new AuraError(0, { error: { code: 'invalid_response', message: 'Invalid refresh response (non-JSON body)' } }),
108
- }
109
- }
110
- // I5 : `'data' in body` (pas `body.data ?? body`) — un `data` EXPLICITEMENT `null`
111
- // dans l'enveloppe backend (`ApiResponse::empty()` → `{"data":null,"meta":null}`)
112
- // ne doit jamais retomber sur l'enveloppe entière (nullish coalescing traite `null`
113
- // comme absent). Ici la réponse de refresh a toujours un `data` enveloppé ; ce
114
- // garde-fou reste cohérent avec le même correctif dans `fetchWithAuth`.
115
- const data = unwrapEnvelope(body) as TokenPair
116
- if (data?.access_token && data?.refresh_token && data?.user) {
117
- // Commit-guard : un clear()/signOut OU un save()/signIn concurrent s'est-il intercalé ?
118
- // On compare l'epoch de suppression ET l'identité du refresh token utilisé.
119
- if (store.removalEpoch !== epoch || store.refreshToken !== rt) {
120
- emitAuraEvent(client.onEvent, client.debug, {
121
- kind: 'refresh-discarded',
122
- reason: 'Session changed during refresh (concurrent signIn/signOut/rotation)',
123
- })
124
- return {
125
- data: null,
126
- error: new AuraError(409, { error: { code: 'refresh_discarded', message: 'Session changed during refresh' } }),
127
- }
128
- }
129
- store.save(data.access_token, data.refresh_token, data.user)
130
- emitAuraEvent(client.onEvent, client.debug, { kind: 'refresh-success' })
131
- return { data, error: null }
132
- }
133
- return {
134
- data: null,
135
- error: new AuraError(0, { error: { code: 'invalid_response', message: 'Invalid refresh response' } }),
136
- }
137
- }
138
-
139
- /**
140
- * Coordination CROSS-ONGLETS du refresh via l'API Web Locks (I2). Le single-flight
141
- * `inflightRefreshes` ne dédup que DANS un onglet ; deux onglets pouvaient encore
142
- * lancer deux refresh concurrents avec le même refresh token → churn de rotation
143
- * (le commit-guard évitait la déconnexion mais pas la course). On enveloppe donc le
144
- * refresh dans `navigator.locks.request(lockName)` EN NAVIGATEUR uniquement ; hors
145
- * navigateur (SSR/Node) fallback au single-flight in-process seul (aucun lock).
146
- *
147
- * Anti-interblocage : l'ATTENTE d'acquisition est bornée par un `AbortSignal`
148
- * (timeout = requestTimeoutMs). Si un autre onglet tient le lock trop longtemps
149
- * (refresh qui « hang »), on n'attend pas indéfiniment — on abandonne le lock et on
150
- * exécute le refresh SANS coordination (fallback). Une fois le lock acquis, le timer
151
- * est désarmé (le signal n'a plus d'effet côté spec) : pas de double exécution.
152
- */
153
- function withRefreshLock(
154
- client: AuraClientLike,
155
- fn: () => Promise<AuraResponse<TokenPair>>,
156
- ): Promise<AuraResponse<TokenPair>> {
157
- // Web Locks = coordination cross-ONGLETS : purement navigateur. On gate sur
158
- // `window` : sur un serveur Node (SSR), `navigator.locks` existe (Node 21+) mais
159
- // est PROCESS-GLOBAL — deux requêtes d'utilisateurs différents partageant le même
160
- // `projectId` verraient leurs refresh sérialisés par `aura-refresh-{projectId}`.
161
- // En dehors d'un navigateur, on retombe sur le single-flight in-process seul.
162
- const browser = typeof window !== 'undefined'
163
- const nav = (globalThis as { navigator?: { locks?: { request?: unknown } } }).navigator
164
- const locks = nav?.locks
165
- if (!browser || !locks || typeof locks.request !== 'function') {
166
- return fn()
167
- }
168
- const request = locks.request as (
169
- name: string,
170
- options: { signal?: AbortSignal },
171
- cb: () => Promise<AuraResponse<TokenPair>>,
172
- ) => Promise<AuraResponse<TokenPair>>
173
-
174
- const lockName = `aura-refresh-${client.projectId}`
175
- const timeoutMs =
176
- (client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS) || DEFAULT_REQUEST_TIMEOUT_MS
177
-
178
- let timer: ReturnType<typeof setTimeout> | undefined
179
- const options: { signal?: AbortSignal } = {}
180
- if (typeof AbortController !== 'undefined') {
181
- const ctrl = new AbortController()
182
- timer = setTimeout(() => ctrl.abort(), timeoutMs)
183
- ;(timer as unknown as { unref?: () => void }).unref?.()
184
- options.signal = ctrl.signal
185
- }
186
- const clearTimer = (): void => {
187
- if (timer) {
188
- clearTimeout(timer)
189
- timer = undefined
190
- }
191
- }
192
-
193
- try {
194
- return Promise.resolve(
195
- request(lockName, options, () => {
196
- // Lock acquis : désarmer le timer d'attente avant d'exécuter le refresh.
197
- clearTimer()
198
- return fn()
199
- }),
200
- ).catch(() => {
201
- // Acquisition abandonnée (signal abort → AbortError) ou API en erreur
202
- // (SecurityError…) : fallback SANS lock — jamais de blocage permanent.
203
- clearTimer()
204
- return fn()
205
- })
206
- } catch {
207
- clearTimer()
208
- return fn()
209
- }
210
- }
211
-
212
- /** Point d'entrée unique du refresh, dédupliqué par client (single-flight). */
213
- export function refreshOnce(client: AuraClientLike): Promise<AuraResponse<TokenPair>> {
214
- const existing = inflightRefreshes.get(client)
215
- if (existing) return existing
216
- // Le lock cross-onglets enveloppe le refresh ; le single-flight in-process (ci-dessus)
217
- // reste la 1re barrière (une seule acquisition de lock par onglet à la fois).
218
- const p = withRefreshLock(client, () => doRefresh(client)).finally(() =>
219
- inflightRefreshes.delete(client),
220
- )
221
- inflightRefreshes.set(client, p)
222
- return p
223
- }
224
-
225
- /** Refresh proactif : seulement si le token est (presque) expiré. */
226
- async function maybeRefresh(client: AuraClientLike): Promise<void> {
227
- const store = client.authStore
228
- if (!store.token || !store.refreshToken) return
229
- if (!isTokenExpired(store.token, REFRESH_MARGIN_SEC)) return
230
- await refreshOnce(client)
231
- }
232
-
233
- export interface FetchInit {
234
- method?: string
235
- headers?: Record<string, string>
236
- body?: unknown
237
- signal?: AbortSignal
238
- skipAuthRefresh?: boolean
239
- /**
240
- * Override explicite du `Authorization: Bearer` (LOT D). S'il est fourni, il
241
- * GAGNE sur `store.token` — permet de valider un token ARBITRAIRE (foreign)
242
- * plutôt que celui de la session courante (ex. `getUser(jwt)`). Sans lui, le
243
- * comportement historique s'applique (`Authorization = Bearer store.token`).
244
- */
245
- authToken?: string
246
- /** Désactive le retry transitoire pour cette requête (override). */
247
- skipRetry?: boolean
248
- /** Interne (E19) : marque une requête déjà rejouée après refresh pour borner la récursion. */
249
- _retried?: boolean
250
- }
251
-
252
- /**
253
- * Core fetch wrapper that handles:
254
- * - Lazy token refresh before expiry
255
- * - Authorization / API key injection
256
- * - Content-Type defaulting
257
- * - Structured AuraResponse parsing
258
- */
259
- export async function fetchWithAuth<T>(
260
- url: string,
261
- init: FetchInit,
262
- client: AuraClientLike,
263
- ): Promise<AuraResponse<T>> {
264
- // Attempt lazy token refresh
265
- if (!init.skipAuthRefresh) {
266
- await maybeRefresh(client)
267
- }
268
-
269
- const method = (init.method ?? 'GET').toUpperCase()
270
- const retryEnabled =
271
- !init.skipRetry &&
272
- (client.retryConfig?.enabled ?? true) &&
273
- isRetryableMethod(method)
274
- const maxRetries = client.retryConfig?.maxRetries ?? DEFAULT_MAX_RETRIES
275
-
276
- const store = client.authStore
277
- const isFormData = typeof FormData !== 'undefined' && init.body instanceof FormData
278
-
279
- // Timeout par défaut : borne une requête vers un serveur qui « hang ». Appliqué
280
- // seulement si l'appelant n'a pas fourni son propre `signal` (invoke, downloads…
281
- // gèrent leur annulation) et si le corps n'est pas un FormData (uploads longs).
282
- const timeoutMs = client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
283
- const applyTimeout = timeoutMs > 0 && init.signal === undefined && !isFormData
284
-
285
- let attempt = 0
286
- for (;;) {
287
- const headers: Record<string, string> = { ...init.headers }
288
-
289
- // Inject auth headers (Supabase-compatible: apikey ALWAYS sent, JWT when authenticated)
290
- if (client.apiKey) {
291
- headers['apikey'] = client.apiKey
292
- }
293
- // LOT D : override `authToken` prioritaire sur `store.token`. Sans override,
294
- // comportement historique (JWT de session injecté quand authentifié).
295
- if (init.authToken) {
296
- headers['Authorization'] = `Bearer ${init.authToken}`
297
- } else if (store.token) {
298
- headers['Authorization'] = `Bearer ${store.token}`
299
- }
300
- // Set Content-Type unless body is FormData
301
- if (!isFormData && init.body !== undefined) {
302
- headers['Content-Type'] = headers['Content-Type'] ?? 'application/json'
303
- }
304
- if (attempt > 0) {
305
- headers['X-Retry-Count'] = String(attempt)
306
- }
307
-
308
- // Timer de timeout PAR TENTATIVE (chaque rejeu repart avec un timeout neuf).
309
- // On utilise un AbortController maison (plutôt que AbortSignal.timeout) pour
310
- // pouvoir clearTimeout dès la résolution — évite tout timer fantôme en test.
311
- let timedOut = false
312
- let timeoutTimer: ReturnType<typeof setTimeout> | undefined
313
- let attemptSignal = init.signal
314
- if (applyTimeout) {
315
- const ctrl = new AbortController()
316
- timeoutTimer = setTimeout(() => { timedOut = true; ctrl.abort() }, timeoutMs)
317
- // `unref` (Node) pour ne pas bloquer la sortie de process ; no-op en navigateur.
318
- ;(timeoutTimer as unknown as { unref?: () => void }).unref?.()
319
- attemptSignal = ctrl.signal
320
- }
321
- const clearTimeoutTimer = () => { if (timeoutTimer) clearTimeout(timeoutTimer) }
322
-
323
- const requestInit: RequestInit = {
324
- method,
325
- headers,
326
- signal: attemptSignal,
327
- }
328
- if (init.body !== undefined) {
329
- requestInit.body = isFormData ? (init.body as FormData) : serializeBody(init.body)
330
- }
331
-
332
- let res: Response
333
- try {
334
- res = await client.fetchFn(url, requestInit)
335
- } catch (err) {
336
- clearTimeoutTimer()
337
- // Erreur réseau transitoire : rejeu (méthodes sûres uniquement, jamais sur AbortError).
338
- // Un timeout produit un AbortError → non rejoué (échec borné et rapide).
339
- if (retryEnabled && attempt < maxRetries && !isAbortError(err)) {
340
- const nextAttempt = attempt + 1
341
- emitAuraEvent(client.onEvent, client.debug, { kind: 'retry', attempt: nextAttempt, url })
342
- await abortableSleep(computeBackoff(attempt), init.signal)
343
- attempt = nextAttempt
344
- continue
345
- }
346
- if (timedOut) {
347
- emitAuraEvent(client.onEvent, client.debug, { kind: 'timeout', url })
348
- }
349
- // E18 : vraie instance AuraError (status 0 = erreur côté client).
350
- return {
351
- data: null,
352
- error: new AuraError(0, {
353
- error: timedOut
354
- ? { code: 'timeout', message: `Request timed out after ${timeoutMs}ms` }
355
- : { code: 'network_error', message: err instanceof Error ? err.message : 'Network request failed' },
356
- }),
357
- }
358
- }
359
- clearTimeoutTimer()
360
-
361
- // Retry transitoire (GET/HEAD uniquement, statuts 408/429/502/503/504).
362
- if (retryEnabled && attempt < maxRetries && isRetryableStatus(res.status)) {
363
- // Retry-After prioritaire mais PLAFONNÉ (un serveur mal configuré/hostile pourrait
364
- // renvoyer 86400 → sommeil de 24h, ou overflow setTimeout Node → retry immédiat).
365
- const delay = Math.min(
366
- parseRetryAfter(res.headers.get('Retry-After')) ?? computeBackoff(attempt),
367
- RETRY_AFTER_CAP_MS,
368
- )
369
- // Consommer le corps non lu avant de rejouer (sinon fuite de socket sous undici/Node).
370
- await res.body?.cancel().catch(() => {})
371
- const nextAttempt = attempt + 1
372
- emitAuraEvent(client.onEvent, client.debug, { kind: 'retry', attempt: nextAttempt, status: res.status, url })
373
- await abortableSleep(delay, init.signal)
374
- attempt = nextAttempt
375
- continue
376
- }
377
-
378
- // E19 : retry réactif borné sur 401 (inchangé, orthogonal au retry transitoire).
379
- if (
380
- res.status === 401 &&
381
- !init.skipAuthRefresh &&
382
- !init._retried &&
383
- client.authStore.refreshToken
384
- ) {
385
- const refreshed = await refreshOnce(client)
386
- if (refreshed.data) {
387
- // Consommer le corps 401 non lu avant de rejouer la requête.
388
- await res.body?.cancel().catch(() => {})
389
- return fetchWithAuth<T>(url, { ...init, _retried: true }, client)
390
- }
391
- }
392
-
393
- // 204 No Content
394
- if (res.status === 204) {
395
- return { data: null, error: null }
396
- }
397
-
398
- // Error responses
399
- if (!res.ok) {
400
- let body: { error?: { code?: string; message?: string }; message?: string } | null = null
401
- try {
402
- body = await res.json()
403
- } catch {
404
- // non-JSON error body
405
- }
406
- // E18 : retourner la vraie instance (status, isUnauthorized, isRateLimited…).
407
- return { data: null, error: new AuraError(res.status, body) }
408
- }
409
-
410
- // Successful JSON response
411
- try {
412
- const body = await res.json()
413
- // I5 : `'data' in body` (pas `body.data ?? body`). `??` traite un `data`
414
- // EXPLICITEMENT `null` comme absent et retombait sur l'enveloppe entière
415
- // (`{data:null,meta:null}`) au lieu de `null` — ce qui rendait menteur le
416
- // type `AuraResponse<null>` des méthodes void (`signOut`, `deleteUser`,
417
- // `banUser`/`unbanUser`, `revokeSession`, `testEmailTemplate`…) : ces
418
- // handlers renvoient réellement `ApiResponse::<()>::empty()` côté backend
419
- // (`{"data":null,"meta":null}`, vérifié `services/aura-auth/src/handlers/
420
- // {auth,admin,sessions,email_templates}.rs`) — donc `null` est la valeur
421
- // honnête, pas l'enveloppe. `'data' in body` distingue « absent » (corps
422
- // non enveloppé, legacy) de « explicitement null » (enveloppe vide).
423
- return {
424
- data: unwrapEnvelope(body) as T,
425
- error: null,
426
- meta:
427
- body !== null && typeof body === 'object' && 'meta' in body
428
- ? (body as { meta?: Meta }).meta
429
- : undefined,
430
- }
431
- } catch {
432
- return { data: null, error: null }
433
- }
434
- }
435
- }
@@ -1,22 +0,0 @@
1
- export { AuraError, isAuraError } from './AuraError'
2
- export type { AuraClientLike } from './client'
3
- export { fetchWithAuth, refreshOnce } from './fetch'
4
- export type { FetchInit } from './fetch'
5
- export { BaseService } from './BaseService'
6
- export { BaseAuthStore, isTokenExpired } from './stores/BaseAuthStore'
7
- export { LocalAuthStore } from './stores/LocalAuthStore'
8
- export { serializeBody } from './serialize'
9
- export { parseCookieHeader, serializeCookieHeader } from './cookie-utils'
10
- export type { SerializeCookieOptions } from './cookie-utils'
11
- export {
12
- RETRYABLE_STATUS,
13
- RETRYABLE_METHODS,
14
- DEFAULT_MAX_RETRIES,
15
- isRetryableStatus,
16
- isRetryableMethod,
17
- computeBackoff,
18
- parseRetryAfter,
19
- abortableSleep,
20
- isAbortError,
21
- } from './retry'
22
- export * from './types'
@@ -1,63 +0,0 @@
1
- /**
2
- * Primitives de résilience réseau (module pur, sans I/O).
3
- * `Math.random`/`Date.now` sont injectables pour des tests déterministes.
4
- */
5
-
6
- export const RETRYABLE_STATUS = new Set([408, 429, 502, 503, 504])
7
- export const RETRYABLE_METHODS = new Set(['GET', 'HEAD'])
8
- export const DEFAULT_MAX_RETRIES = 3
9
-
10
- const BASE_MS = 500
11
- const CAP_MS = 10_000
12
-
13
- export function isRetryableStatus(status: number): boolean {
14
- return RETRYABLE_STATUS.has(status)
15
- }
16
-
17
- export function isRetryableMethod(method: string): boolean {
18
- return RETRYABLE_METHODS.has(method.toUpperCase())
19
- }
20
-
21
- /** Full jitter : random(0, min(BASE·2^attempt, CAP)). `attempt` commence à 0. */
22
- export function computeBackoff(attempt: number, rand: () => number = Math.random): number {
23
- const bound = Math.min(BASE_MS * 2 ** attempt, CAP_MS)
24
- return Math.floor(rand() * bound)
25
- }
26
-
27
- /** Parse un header `Retry-After` (secondes OU HTTP-date) → délai en ms, ou null. */
28
- export function parseRetryAfter(header: string | null, now: number = Date.now()): number | null {
29
- if (header === null) return null
30
- const trimmed = header.trim()
31
- if (/^\d+$/.test(trimmed)) return parseInt(trimmed, 10) * 1000
32
- const dateMs = Date.parse(trimmed)
33
- if (Number.isNaN(dateMs)) return null
34
- return Math.max(0, dateMs - now)
35
- }
36
-
37
- /** `sleep(ms)` qui résout tôt (sans rejeter) si `signal` s'annule. */
38
- export function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
39
- return new Promise((resolve) => {
40
- if (signal?.aborted) {
41
- resolve()
42
- return
43
- }
44
- const onAbort = () => {
45
- clearTimeout(timer)
46
- resolve()
47
- }
48
- const timer = setTimeout(() => {
49
- signal?.removeEventListener('abort', onAbort)
50
- resolve()
51
- }, ms)
52
- signal?.addEventListener('abort', onAbort, { once: true })
53
- })
54
- }
55
-
56
- /** True si l'erreur `fetch` est une annulation utilisateur (jamais rejouée). */
57
- export function isAbortError(err: unknown): boolean {
58
- return (
59
- typeof err === 'object' &&
60
- err !== null &&
61
- (err as { name?: unknown }).name === 'AbortError'
62
- )
63
- }
@@ -1,6 +0,0 @@
1
- /** JSON.stringify avec support des bigint (sérialisés en string). */
2
- export function serializeBody(body: unknown): string {
3
- return JSON.stringify(body, (_key, value) =>
4
- typeof value === 'bigint' ? value.toString() : value,
5
- )
6
- }
@@ -1,164 +0,0 @@
1
- import type { User, AuraObservabilityEvent } from '../types'
2
- import { emitAuraEvent } from '../types'
3
-
4
- type AuthChangeCallback = (token: string | null, user: User | null) => void
5
-
6
- /**
7
- * Decode the payload of a JWT without verifying the signature.
8
- * Works in both browser and Node (uses base64url decoding).
9
- */
10
- function decodeJwtPayload(token: string): Record<string, unknown> | null {
11
- try {
12
- const parts = token.split('.')
13
- if (parts.length !== 3) return null
14
- // base64url → base64
15
- let payload = parts[1]!.replace(/-/g, '+').replace(/_/g, '/')
16
- // pad to multiple of 4
17
- while (payload.length % 4 !== 0) payload += '='
18
- // atob is available in all target environments (browsers + Node >= 16)
19
- const json = atob(payload)
20
- return JSON.parse(json)
21
- } catch {
22
- return null
23
- }
24
- }
25
-
26
- /**
27
- * Check whether a JWT has expired (or will expire within `marginSec` seconds).
28
- */
29
- export function isTokenExpired(token: string, marginSec = 0): boolean {
30
- const payload = decodeJwtPayload(token)
31
- if (!payload || typeof payload.exp !== 'number') return true
32
- const nowSec = Math.floor(Date.now() / 1000)
33
- return payload.exp - marginSec <= nowSec
34
- }
35
-
36
- /**
37
- * In-memory auth store with an event-emitter pattern.
38
- * Can be extended (see LocalAuthStore) for persistence.
39
- */
40
- export class BaseAuthStore {
41
- /**
42
- * True si le store survit à un redirect pleine page (persistance disque, ex.
43
- * localStorage). Le store de base est en mémoire → non persistant. Exploité par
44
- * le flux PKCE : le `code_verifier` doit survivre au redirect OAuth.
45
- */
46
- readonly persistent: boolean = false
47
- private _token: string | null = null
48
- private _refreshToken: string | null = null
49
- private _user: User | null = null
50
- private _listeners: Set<AuthChangeCallback> = new Set()
51
- private _removalEpoch = 0
52
- /** Stockage générique clé/valeur (ex: PKCE `code_verifier`). En mémoire par défaut. */
53
- private _kv: Map<string, string> = new Map()
54
- /** Hook d'observabilité (LOT 3) — câblé par `AuraClient` via `setObservabilityHook`. */
55
- private _onEvent?: (event: AuraObservabilityEvent) => void
56
- private _debug = false
57
-
58
- // -- Getters ----------------------------------------------------------------
59
-
60
- /** Compteur incrémenté à chaque `clear()` — sert au commit-guard anti-race du refresh. */
61
- get removalEpoch(): number {
62
- return this._removalEpoch
63
- }
64
-
65
- get token(): string | null {
66
- return this._token
67
- }
68
-
69
- get refreshToken(): string | null {
70
- return this._refreshToken
71
- }
72
-
73
- get user(): User | null {
74
- return this._user
75
- }
76
-
77
- /** True if a non-expired access token is present. */
78
- get isValid(): boolean {
79
- return this._token !== null && !isTokenExpired(this._token)
80
- }
81
-
82
- // -- Mutators ---------------------------------------------------------------
83
-
84
- save(token: string, refreshToken: string, user: User): void {
85
- this._token = token
86
- this._refreshToken = refreshToken
87
- this._user = user
88
- this._triggerChange()
89
- }
90
-
91
- clear(): void {
92
- this._removalEpoch++
93
- this._token = null
94
- this._refreshToken = null
95
- this._user = null
96
- // Purge le stockage clé/valeur (dont le `code_verifier` PKCE) : un `signOut`/
97
- // `clear()` ne doit laisser aucun résidu d'auth réutilisable en mémoire.
98
- this._kv.clear()
99
- this._triggerChange()
100
- }
101
-
102
- // -- Generic key/value storage ------------------------------------------------
103
- // Utilisé par le flux PKCE (`code_verifier`) et tout autre besoin de stockage
104
- // arbitraire lié à l'auth. Impl de base : en mémoire (ne survit pas à un
105
- // redirect pleine page — voir `LocalAuthStore` pour la persistance).
106
-
107
- setItem(key: string, value: string): void {
108
- this._kv.set(key, value)
109
- }
110
-
111
- getItem(key: string): string | null {
112
- return this._kv.get(key) ?? null
113
- }
114
-
115
- removeItem(key: string): void {
116
- this._kv.delete(key)
117
- }
118
-
119
- // -- Observability (LOT 3) ----------------------------------------------------
120
-
121
- /**
122
- * Câble le hook d'observabilité (appelé par `AuraClient` à la construction).
123
- * Permet à `LocalAuthStore` d'émettre `storage-blocked` quand `localStorage` est
124
- * indisponible (navigation privée Safari, quota dépassé, storage désactivé…).
125
- * Sans effet sur `BaseAuthStore` elle-même (en mémoire, jamais "bloquée").
126
- */
127
- setObservabilityHook(
128
- onEvent: ((event: AuraObservabilityEvent) => void) | undefined,
129
- debug: boolean | undefined,
130
- ): void {
131
- this._onEvent = onEvent
132
- this._debug = !!debug
133
- }
134
-
135
- /** Émet `{kind:'storage-blocked', op}` — ne throw jamais (voir `emitAuraEvent`). */
136
- protected _emitStorageBlocked(op: string): void {
137
- emitAuraEvent(this._onEvent, this._debug, { kind: 'storage-blocked', op })
138
- }
139
-
140
- // -- Event emitter ----------------------------------------------------------
141
-
142
- /**
143
- * Register a callback that fires whenever the auth state changes.
144
- * Returns an unsubscribe function.
145
- */
146
- onChange(callback: AuthChangeCallback): () => void {
147
- this._listeners.add(callback)
148
- return () => {
149
- this._listeners.delete(callback)
150
- }
151
- }
152
-
153
- // -- Internal ---------------------------------------------------------------
154
-
155
- protected _triggerChange(): void {
156
- for (const cb of this._listeners) {
157
- try {
158
- cb(this._token, this._user)
159
- } catch {
160
- // subscriber errors must not break the store
161
- }
162
- }
163
- }
164
- }