@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.
- package/package.json +4 -4
- package/packages/ai/node_modules/.bin/esbuild +0 -19
- package/packages/ai/node_modules/.bin/tsc +0 -22
- package/packages/ai/node_modules/.bin/tsserver +0 -22
- package/packages/ai/node_modules/.bin/tsup +0 -22
- package/packages/ai/node_modules/.bin/tsup-node +0 -22
- package/packages/ai/node_modules/.bin/vite +0 -22
- package/packages/ai/node_modules/.bin/vitest +0 -22
- package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/packages/ai/src/AiService.ts +0 -226
- package/packages/ai/src/index.ts +0 -18
- package/packages/ai/src/sse.ts +0 -141
- package/packages/ai/src/types.ts +0 -30
- package/packages/ai/tests/chat-stream.test.ts +0 -151
- package/packages/ai/tests/rag-namespaces.test.ts +0 -54
- package/packages/ai/tests/sse.test.ts +0 -69
- package/packages/ai/tsconfig.json +0 -5
- package/packages/ai/tsup.config.ts +0 -11
- package/packages/aurabase/node_modules/.bin/esbuild +0 -19
- package/packages/aurabase/node_modules/.bin/tsc +0 -22
- package/packages/aurabase/node_modules/.bin/tsserver +0 -22
- package/packages/aurabase/node_modules/.bin/tsup +0 -22
- package/packages/aurabase/node_modules/.bin/tsup-node +0 -22
- package/packages/aurabase/node_modules/.bin/vite +0 -22
- package/packages/aurabase/node_modules/.bin/vitest +0 -22
- package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/packages/aurabase/src/AuraClient.ts +0 -237
- package/packages/aurabase/src/index.ts +0 -152
- package/packages/aurabase/src/ssr.ts +0 -156
- package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +0 -82
- package/packages/aurabase/tests/ai.test.ts +0 -111
- package/packages/aurabase/tests/auth-admin.test.ts +0 -515
- package/packages/aurabase/tests/auth-e2e.test.ts +0 -135
- package/packages/aurabase/tests/auth-hardening.test.ts +0 -206
- package/packages/aurabase/tests/auth.test.ts +0 -947
- package/packages/aurabase/tests/database.test.ts +0 -230
- package/packages/aurabase/tests/db-types-e2e.test.ts +0 -217
- package/packages/aurabase/tests/dispose.test.ts +0 -23
- package/packages/aurabase/tests/foreign-token.e2e.test.ts +0 -129
- package/packages/aurabase/tests/functions.test.ts +0 -107
- package/packages/aurabase/tests/integration.test.ts +0 -413
- package/packages/aurabase/tests/live-production-validation.js +0 -241
- package/packages/aurabase/tests/live-sdk-production-validation.js +0 -293
- package/packages/aurabase/tests/mocks.ts +0 -139
- package/packages/aurabase/tests/notifications.test.ts +0 -314
- package/packages/aurabase/tests/observability-backup.test.ts +0 -186
- package/packages/aurabase/tests/observability-hook.test.ts +0 -130
- package/packages/aurabase/tests/production-robustness.test.ts +0 -268
- package/packages/aurabase/tests/public-api-surface.test.ts +0 -53
- package/packages/aurabase/tests/query-builder.test.ts +0 -329
- package/packages/aurabase/tests/readme-smoke-e2e.test.ts +0 -231
- package/packages/aurabase/tests/realtime-filter-e2e.test.ts +0 -344
- package/packages/aurabase/tests/refresh-locks.test.ts +0 -210
- package/packages/aurabase/tests/retry-option.test.ts +0 -17
- package/packages/aurabase/tests/rpc-e2e.test.ts +0 -254
- package/packages/aurabase/tests/rs256-local-e2e.test.ts +0 -114
- package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +0 -235
- package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +0 -124
- package/packages/aurabase/tests/service-role-guard.test.ts +0 -99
- package/packages/aurabase/tests/ssr.test.ts +0 -91
- package/packages/aurabase/tests/storage-backend-e2e.test.ts +0 -481
- package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +0 -104
- package/packages/aurabase/tests/storage-transform-e2e.test.ts +0 -684
- package/packages/aurabase/tests/storage.test.ts +0 -55
- package/packages/aurabase/tests/types/client-generic.test-d.ts +0 -79
- package/packages/aurabase/tests/types/docs-snippets.test-d.ts +0 -571
- package/packages/aurabase/tests/types/error-code.test-d.ts +0 -53
- package/packages/aurabase/tsconfig.json +0 -5
- package/packages/aurabase/tsconfig.test-d.json +0 -5
- package/packages/aurabase/tsup.config.ts +0 -11
- package/packages/aurabase/vitest.config.ts +0 -16
- package/packages/auth/node_modules/.bin/esbuild +0 -19
- package/packages/auth/node_modules/.bin/tsc +0 -22
- package/packages/auth/node_modules/.bin/tsserver +0 -22
- package/packages/auth/node_modules/.bin/tsup +0 -22
- package/packages/auth/node_modules/.bin/tsup-node +0 -22
- package/packages/auth/node_modules/.bin/vite +0 -22
- package/packages/auth/node_modules/.bin/vitest +0 -22
- package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/packages/auth/src/AuthAdminService.ts +0 -149
- package/packages/auth/src/AuthService.ts +0 -1166
- package/packages/auth/src/SessionManager.ts +0 -92
- package/packages/auth/src/index.ts +0 -17
- package/packages/auth/src/jwks.ts +0 -337
- package/packages/auth/src/pkce.ts +0 -27
- package/packages/auth/src/types.ts +0 -89
- package/packages/auth/tests/auth-lifecycle.test.ts +0 -149
- package/packages/auth/tests/detect-session.test.ts +0 -361
- package/packages/auth/tests/getclaims.test.ts +0 -780
- package/packages/auth/tests/is-mfa-challenge.test.ts +0 -27
- package/packages/auth/tests/password-recovery.test.ts +0 -124
- package/packages/auth/tests/pkce.test.ts +0 -192
- package/packages/auth/tests/session-manager.test.ts +0 -103
- package/packages/auth/tests/set-session.test.ts +0 -139
- package/packages/auth/tests/verify-otp.test.ts +0 -153
- package/packages/auth/tsconfig.json +0 -5
- package/packages/auth/tsup.config.ts +0 -11
- package/packages/core/node_modules/.bin/esbuild +0 -22
- package/packages/core/node_modules/.bin/tsc +0 -22
- package/packages/core/node_modules/.bin/tsserver +0 -22
- package/packages/core/node_modules/.bin/tsup +0 -22
- package/packages/core/node_modules/.bin/tsup-node +0 -22
- package/packages/core/node_modules/.bin/vite +0 -22
- package/packages/core/node_modules/.bin/vitest +0 -22
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/packages/core/src/AuraError.ts +0 -89
- package/packages/core/src/BaseService.ts +0 -44
- package/packages/core/src/client.ts +0 -34
- package/packages/core/src/cookie-utils.ts +0 -42
- package/packages/core/src/fetch.ts +0 -435
- package/packages/core/src/index.ts +0 -22
- package/packages/core/src/retry.ts +0 -63
- package/packages/core/src/serialize.ts +0 -6
- package/packages/core/src/stores/BaseAuthStore.ts +0 -164
- package/packages/core/src/stores/LocalAuthStore.ts +0 -199
- package/packages/core/src/types.ts +0 -1042
- package/packages/core/tests/auth-store.test.ts +0 -300
- package/packages/core/tests/error-code.test.ts +0 -61
- package/packages/core/tests/error.test.ts +0 -24
- package/packages/core/tests/fetch.test.ts +0 -286
- package/packages/core/tests/mocks.ts +0 -139
- package/packages/core/tests/observability.test.ts +0 -86
- package/packages/core/tests/refresh-hardening.test.ts +0 -164
- package/packages/core/tests/refresh-observability.test.ts +0 -111
- package/packages/core/tests/retry.test.ts +0 -106
- package/packages/core/tests/serialize.test.ts +0 -12
- package/packages/core/tests/timeout.test.ts +0 -79
- package/packages/core/tsconfig.json +0 -5
- package/packages/core/tsup.config.ts +0 -11
- package/packages/db/node_modules/.bin/esbuild +0 -19
- package/packages/db/node_modules/.bin/tsc +0 -22
- package/packages/db/node_modules/.bin/tsserver +0 -22
- package/packages/db/node_modules/.bin/tsup +0 -22
- package/packages/db/node_modules/.bin/tsup-node +0 -22
- package/packages/db/node_modules/.bin/vite +0 -22
- package/packages/db/node_modules/.bin/vitest +0 -22
- package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/packages/db/src/DatabaseService.ts +0 -170
- package/packages/db/src/QueryBuilder.ts +0 -830
- package/packages/db/src/index.ts +0 -2
- package/packages/db/src/pgrst-types/common/common.ts +0 -86
- package/packages/db/src/pgrst-types/common/rpc.ts +0 -151
- package/packages/db/src/pgrst-types/feature-flags.ts +0 -20
- package/packages/db/src/pgrst-types/types.ts +0 -134
- package/packages/db/src/select-query-parser/parser.ts +0 -479
- package/packages/db/src/select-query-parser/result.ts +0 -553
- package/packages/db/src/select-query-parser/types.ts +0 -132
- package/packages/db/src/select-query-parser/utils.ts +0 -707
- package/packages/db/tests/filters.test.ts +0 -81
- package/packages/db/tests/mutations-filtered.e2e.test.ts +0 -132
- package/packages/db/tests/mutations-filtered.test.ts +0 -188
- package/packages/db/tests/order.test.ts +0 -45
- package/packages/db/tests/rpc.test.ts +0 -41
- package/packages/db/tests/scale/build-large-schema.ts +0 -94
- package/packages/db/tests/scale/scale-compile.test.ts +0 -167
- package/packages/db/tests/schema.test.ts +0 -77
- package/packages/db/tests/single-accept.e2e.test.ts +0 -123
- package/packages/db/tests/single.test.ts +0 -211
- package/packages/db/tests/types/filters.test-d.ts +0 -52
- package/packages/db/tests/types/fixtures.ts +0 -40
- package/packages/db/tests/types/from-views.test-d.ts +0 -18
- package/packages/db/tests/types/mutations.test-d.ts +0 -23
- package/packages/db/tests/types/rpc.test-d.ts +0 -60
- package/packages/db/tests/types/schema-shape.test-d.ts +0 -8
- package/packages/db/tests/types/select-inference.test-d.ts +0 -38
- package/packages/db/tests/vendored/fixtures.ts +0 -83
- package/packages/db/tests/vendored/get-result.test-d.ts +0 -75
- package/packages/db/tsconfig.json +0 -5
- package/packages/db/tsconfig.test-d.json +0 -5
- package/packages/db/tsup.config.ts +0 -11
- package/packages/db/vitest.config.ts +0 -8
- package/packages/functions/node_modules/.bin/esbuild +0 -19
- package/packages/functions/node_modules/.bin/tsc +0 -22
- package/packages/functions/node_modules/.bin/tsserver +0 -22
- package/packages/functions/node_modules/.bin/tsup +0 -22
- package/packages/functions/node_modules/.bin/tsup-node +0 -22
- package/packages/functions/node_modules/.bin/vite +0 -22
- package/packages/functions/node_modules/.bin/vitest +0 -22
- package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/packages/functions/src/FunctionsService.ts +0 -188
- package/packages/functions/src/errors.ts +0 -70
- package/packages/functions/src/index.ts +0 -6
- package/packages/functions/tests/invoke.test.ts +0 -97
- package/packages/functions/tests/response-type-errors.test.ts +0 -135
- package/packages/functions/tsconfig.json +0 -5
- package/packages/functions/tsup.config.ts +0 -11
- package/packages/gen/node_modules/.bin/esbuild +0 -19
- package/packages/gen/node_modules/.bin/tsc +0 -22
- package/packages/gen/node_modules/.bin/tsserver +0 -22
- package/packages/gen/node_modules/.bin/tsup +0 -22
- package/packages/gen/node_modules/.bin/tsup-node +0 -22
- package/packages/gen/node_modules/.bin/vite +0 -22
- package/packages/gen/node_modules/.bin/vitest +0 -22
- package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/packages/gen/src/cli.ts +0 -67
- package/packages/gen/src/generate.ts +0 -0
- package/packages/gen/src/index.ts +0 -2
- package/packages/gen/tests/compile.test.ts +0 -213
- package/packages/gen/tests/e2e.test.ts +0 -130
- package/packages/gen/tests/generate.test.ts +0 -396
- package/packages/gen/tsconfig.json +0 -5
- package/packages/gen/tsup.config.ts +0 -11
- package/packages/notifications/node_modules/.bin/esbuild +0 -19
- package/packages/notifications/node_modules/.bin/tsc +0 -22
- package/packages/notifications/node_modules/.bin/tsserver +0 -22
- package/packages/notifications/node_modules/.bin/tsup +0 -22
- package/packages/notifications/node_modules/.bin/tsup-node +0 -22
- package/packages/notifications/node_modules/.bin/vite +0 -22
- package/packages/notifications/node_modules/.bin/vitest +0 -22
- package/packages/notifications/src/NotificationsService.ts +0 -230
- package/packages/notifications/src/index.ts +0 -19
- package/packages/notifications/tsconfig.json +0 -5
- package/packages/notifications/tsup.config.ts +0 -11
- package/packages/realtime/node_modules/.bin/esbuild +0 -19
- package/packages/realtime/node_modules/.bin/tsc +0 -22
- package/packages/realtime/node_modules/.bin/tsserver +0 -22
- package/packages/realtime/node_modules/.bin/tsup +0 -22
- package/packages/realtime/node_modules/.bin/tsup-node +0 -22
- package/packages/realtime/node_modules/.bin/vite +0 -22
- package/packages/realtime/node_modules/.bin/vitest +0 -22
- package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/packages/realtime/src/AuraChannel.ts +0 -485
- package/packages/realtime/src/HeartbeatManager.ts +0 -73
- package/packages/realtime/src/RealtimeService.ts +0 -505
- package/packages/realtime/src/columnFilter.ts +0 -301
- package/packages/realtime/src/index.ts +0 -4
- package/packages/realtime/tests/ack-routing.test.ts +0 -81
- package/packages/realtime/tests/column-filter.test.ts +0 -144
- package/packages/realtime/tests/dispatch.test.ts +0 -191
- package/packages/realtime/tests/heartbeat.test.ts +0 -67
- package/packages/realtime/tests/http-broadcast.test.ts +0 -68
- package/packages/realtime/tests/realtime-token.test.ts +0 -43
- package/packages/realtime/tests/reconnect-race.test.ts +0 -149
- package/packages/realtime/tests/ref-id.test.ts +0 -34
- package/packages/realtime/tests/set-auth.test.ts +0 -70
- package/packages/realtime/tsconfig.json +0 -5
- package/packages/realtime/tsup.config.ts +0 -11
- package/packages/storage/node_modules/.bin/esbuild +0 -19
- package/packages/storage/node_modules/.bin/tsc +0 -22
- package/packages/storage/node_modules/.bin/tsserver +0 -22
- package/packages/storage/node_modules/.bin/tsup +0 -22
- package/packages/storage/node_modules/.bin/tsup-node +0 -22
- package/packages/storage/node_modules/.bin/vite +0 -22
- package/packages/storage/node_modules/.bin/vitest +0 -22
- package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/packages/storage/src/DownloadBuilder.ts +0 -60
- package/packages/storage/src/StorageService.ts +0 -323
- package/packages/storage/src/index.ts +0 -2
- package/packages/storage/src/path.ts +0 -35
- package/packages/storage/src/transform.ts +0 -15
- package/packages/storage/tests/download.test.ts +0 -64
- package/packages/storage/tests/parity.test.ts +0 -154
- package/packages/storage/tests/path.test.ts +0 -87
- package/packages/storage/tests/transform.test.ts +0 -79
- package/packages/storage/tsconfig.json +0 -5
- package/packages/storage/tsup.config.ts +0 -11
- package/pnpm-workspace.yaml +0 -24
- 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,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
|
-
}
|