@aurabase/aurabase-js 0.3.0 → 0.4.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.
- package/README.md +26 -795
- package/{packages/aurabase/dist/AuraClient-DAtDbfPB.d.ts → dist/AuraClient-BcFE8u1v.d.cts} +1 -1
- package/{packages/aurabase/dist/AuraClient-DAtDbfPB.d.cts → dist/AuraClient-BcFE8u1v.d.ts} +1 -1
- package/{packages/aurabase/dist → dist}/index.d.cts +1 -1
- package/{packages/aurabase/dist → dist}/index.d.ts +1 -1
- package/{packages/aurabase/dist → dist}/ssr.d.cts +1 -1
- package/{packages/aurabase/dist → dist}/ssr.d.ts +1 -1
- package/package.json +72 -82
- package/.changeset/config.json +0 -11
- package/CHANGELOG.md +0 -30
- package/packages/ai/LICENSE +0 -21
- package/packages/ai/README.md +0 -106
- package/packages/ai/dist/index.cjs +0 -229
- package/packages/ai/dist/index.d.cts +0 -139
- package/packages/ai/dist/index.d.ts +0 -139
- package/packages/ai/dist/index.js +0 -226
- 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/package.json +0 -61
- 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/LICENSE +0 -21
- package/packages/aurabase/README.md +0 -64
- 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/package.json +0 -80
- 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/aurabase/vitest.e2e.config.ts +0 -9
- package/packages/auth/LICENSE +0 -21
- package/packages/auth/README.md +0 -215
- package/packages/auth/dist/index.cjs +0 -1175
- package/packages/auth/dist/index.d.cts +0 -397
- package/packages/auth/dist/index.d.ts +0 -397
- package/packages/auth/dist/index.js +0 -1170
- 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/package.json +0 -61
- 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/LICENSE +0 -21
- package/packages/core/README.md +0 -132
- package/packages/core/dist/index.cjs +0 -695
- package/packages/core/dist/index.d.cts +0 -1137
- package/packages/core/dist/index.d.ts +0 -1137
- package/packages/core/dist/index.js +0 -673
- 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/package.json +0 -57
- 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/LICENSE +0 -21
- package/packages/db/README.md +0 -152
- package/packages/db/VENDORED.md +0 -93
- package/packages/db/dist/index.cjs +0 -544
- package/packages/db/dist/index.d.cts +0 -1111
- package/packages/db/dist/index.d.ts +0 -1111
- package/packages/db/dist/index.js +0 -541
- 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/package.json +0 -62
- 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/LICENSE +0 -21
- package/packages/functions/README.md +0 -96
- package/packages/functions/dist/index.cjs +0 -173
- package/packages/functions/dist/index.d.cts +0 -81
- package/packages/functions/dist/index.d.ts +0 -81
- package/packages/functions/dist/index.js +0 -168
- 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/package.json +0 -60
- 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/LICENSE +0 -21
- package/packages/gen/README.md +0 -84
- package/packages/gen/dist/cli.cjs +0 -334
- package/packages/gen/dist/cli.d.cts +0 -1
- package/packages/gen/dist/cli.d.ts +0 -1
- package/packages/gen/dist/cli.js +0 -332
- package/packages/gen/dist/index.cjs +0 -257
- package/packages/gen/dist/index.d.cts +0 -12
- package/packages/gen/dist/index.d.ts +0 -12
- package/packages/gen/dist/index.js +0 -255
- 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/package.json +0 -66
- 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/LICENSE +0 -21
- package/packages/notifications/README.md +0 -134
- package/packages/notifications/dist/index.cjs +0 -128
- package/packages/notifications/dist/index.d.cts +0 -81
- package/packages/notifications/dist/index.d.ts +0 -81
- package/packages/notifications/dist/index.js +0 -125
- 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/package.json +0 -60
- 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/LICENSE +0 -21
- package/packages/realtime/README.md +0 -183
- package/packages/realtime/dist/index.cjs +0 -977
- package/packages/realtime/dist/index.d.cts +0 -259
- package/packages/realtime/dist/index.d.ts +0 -259
- package/packages/realtime/dist/index.js +0 -973
- 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/package.json +0 -60
- 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/LICENSE +0 -21
- package/packages/storage/README.md +0 -141
- package/packages/storage/dist/index.cjs +0 -333
- package/packages/storage/dist/index.d.cts +0 -185
- package/packages/storage/dist/index.d.ts +0 -185
- package/packages/storage/dist/index.js +0 -329
- 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/package.json +0 -60
- 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
- /package/{packages/aurabase/dist → dist}/index.cjs +0 -0
- /package/{packages/aurabase/dist → dist}/index.js +0 -0
- /package/{packages/aurabase/dist → dist}/ssr.cjs +0 -0
- /package/{packages/aurabase/dist → dist}/ssr.js +0 -0
|
@@ -1,1166 +0,0 @@
|
|
|
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
|
-
}
|