@aurabase/aurabase-js 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +11 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +833 -0
- package/package.json +90 -0
- package/packages/ai/LICENSE +21 -0
- package/packages/ai/README.md +106 -0
- package/packages/ai/dist/index.cjs +229 -0
- package/packages/ai/dist/index.d.cts +139 -0
- package/packages/ai/dist/index.d.ts +139 -0
- package/packages/ai/dist/index.js +226 -0
- package/packages/ai/node_modules/.bin/esbuild +19 -0
- package/packages/ai/node_modules/.bin/tsc +22 -0
- package/packages/ai/node_modules/.bin/tsserver +22 -0
- package/packages/ai/node_modules/.bin/tsup +22 -0
- package/packages/ai/node_modules/.bin/tsup-node +22 -0
- package/packages/ai/node_modules/.bin/vite +22 -0
- package/packages/ai/node_modules/.bin/vitest +22 -0
- package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/ai/package.json +61 -0
- package/packages/ai/src/AiService.ts +226 -0
- package/packages/ai/src/index.ts +18 -0
- package/packages/ai/src/sse.ts +141 -0
- package/packages/ai/src/types.ts +30 -0
- package/packages/ai/tests/chat-stream.test.ts +151 -0
- package/packages/ai/tests/rag-namespaces.test.ts +54 -0
- package/packages/ai/tests/sse.test.ts +69 -0
- package/packages/ai/tsconfig.json +5 -0
- package/packages/ai/tsup.config.ts +11 -0
- package/packages/aurabase/LICENSE +21 -0
- package/packages/aurabase/README.md +64 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.cts +77 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.ts +77 -0
- package/packages/aurabase/dist/index.cjs +237 -0
- package/packages/aurabase/dist/index.d.cts +46 -0
- package/packages/aurabase/dist/index.d.ts +46 -0
- package/packages/aurabase/dist/index.js +177 -0
- package/packages/aurabase/dist/ssr.cjs +224 -0
- package/packages/aurabase/dist/ssr.d.cts +65 -0
- package/packages/aurabase/dist/ssr.d.ts +65 -0
- package/packages/aurabase/dist/ssr.js +214 -0
- package/packages/aurabase/node_modules/.bin/esbuild +19 -0
- package/packages/aurabase/node_modules/.bin/tsc +22 -0
- package/packages/aurabase/node_modules/.bin/tsserver +22 -0
- package/packages/aurabase/node_modules/.bin/tsup +22 -0
- package/packages/aurabase/node_modules/.bin/tsup-node +22 -0
- package/packages/aurabase/node_modules/.bin/vite +22 -0
- package/packages/aurabase/node_modules/.bin/vitest +22 -0
- package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/aurabase/package.json +80 -0
- package/packages/aurabase/src/AuraClient.ts +237 -0
- package/packages/aurabase/src/index.ts +152 -0
- package/packages/aurabase/src/ssr.ts +156 -0
- package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +82 -0
- package/packages/aurabase/tests/ai.test.ts +111 -0
- package/packages/aurabase/tests/auth-admin.test.ts +515 -0
- package/packages/aurabase/tests/auth-e2e.test.ts +135 -0
- package/packages/aurabase/tests/auth-hardening.test.ts +206 -0
- package/packages/aurabase/tests/auth.test.ts +947 -0
- package/packages/aurabase/tests/database.test.ts +230 -0
- package/packages/aurabase/tests/db-types-e2e.test.ts +217 -0
- package/packages/aurabase/tests/dispose.test.ts +23 -0
- package/packages/aurabase/tests/foreign-token.e2e.test.ts +129 -0
- package/packages/aurabase/tests/functions.test.ts +107 -0
- package/packages/aurabase/tests/integration.test.ts +413 -0
- package/packages/aurabase/tests/live-production-validation.js +241 -0
- package/packages/aurabase/tests/live-sdk-production-validation.js +293 -0
- package/packages/aurabase/tests/mocks.ts +139 -0
- package/packages/aurabase/tests/notifications.test.ts +314 -0
- package/packages/aurabase/tests/observability-backup.test.ts +186 -0
- package/packages/aurabase/tests/observability-hook.test.ts +130 -0
- package/packages/aurabase/tests/production-robustness.test.ts +268 -0
- package/packages/aurabase/tests/public-api-surface.test.ts +53 -0
- package/packages/aurabase/tests/query-builder.test.ts +329 -0
- package/packages/aurabase/tests/readme-smoke-e2e.test.ts +231 -0
- package/packages/aurabase/tests/realtime-filter-e2e.test.ts +344 -0
- package/packages/aurabase/tests/refresh-locks.test.ts +210 -0
- package/packages/aurabase/tests/retry-option.test.ts +17 -0
- package/packages/aurabase/tests/rpc-e2e.test.ts +254 -0
- package/packages/aurabase/tests/rs256-local-e2e.test.ts +114 -0
- package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +235 -0
- package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +124 -0
- package/packages/aurabase/tests/service-role-guard.test.ts +99 -0
- package/packages/aurabase/tests/ssr.test.ts +91 -0
- package/packages/aurabase/tests/storage-backend-e2e.test.ts +481 -0
- package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +104 -0
- package/packages/aurabase/tests/storage-transform-e2e.test.ts +684 -0
- package/packages/aurabase/tests/storage.test.ts +55 -0
- package/packages/aurabase/tests/types/client-generic.test-d.ts +79 -0
- package/packages/aurabase/tests/types/docs-snippets.test-d.ts +571 -0
- package/packages/aurabase/tests/types/error-code.test-d.ts +53 -0
- package/packages/aurabase/tsconfig.json +5 -0
- package/packages/aurabase/tsconfig.test-d.json +5 -0
- package/packages/aurabase/tsup.config.ts +11 -0
- package/packages/aurabase/vitest.config.ts +16 -0
- package/packages/aurabase/vitest.e2e.config.ts +9 -0
- package/packages/auth/LICENSE +21 -0
- package/packages/auth/README.md +215 -0
- package/packages/auth/dist/index.cjs +1175 -0
- package/packages/auth/dist/index.d.cts +397 -0
- package/packages/auth/dist/index.d.ts +397 -0
- package/packages/auth/dist/index.js +1170 -0
- package/packages/auth/node_modules/.bin/esbuild +19 -0
- package/packages/auth/node_modules/.bin/tsc +22 -0
- package/packages/auth/node_modules/.bin/tsserver +22 -0
- package/packages/auth/node_modules/.bin/tsup +22 -0
- package/packages/auth/node_modules/.bin/tsup-node +22 -0
- package/packages/auth/node_modules/.bin/vite +22 -0
- package/packages/auth/node_modules/.bin/vitest +22 -0
- package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/auth/package.json +61 -0
- package/packages/auth/src/AuthAdminService.ts +149 -0
- package/packages/auth/src/AuthService.ts +1166 -0
- package/packages/auth/src/SessionManager.ts +92 -0
- package/packages/auth/src/index.ts +17 -0
- package/packages/auth/src/jwks.ts +337 -0
- package/packages/auth/src/pkce.ts +27 -0
- package/packages/auth/src/types.ts +89 -0
- package/packages/auth/tests/auth-lifecycle.test.ts +149 -0
- package/packages/auth/tests/detect-session.test.ts +361 -0
- package/packages/auth/tests/getclaims.test.ts +780 -0
- package/packages/auth/tests/is-mfa-challenge.test.ts +27 -0
- package/packages/auth/tests/password-recovery.test.ts +124 -0
- package/packages/auth/tests/pkce.test.ts +192 -0
- package/packages/auth/tests/session-manager.test.ts +103 -0
- package/packages/auth/tests/set-session.test.ts +139 -0
- package/packages/auth/tests/verify-otp.test.ts +153 -0
- package/packages/auth/tsconfig.json +5 -0
- package/packages/auth/tsup.config.ts +11 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +132 -0
- package/packages/core/dist/index.cjs +695 -0
- package/packages/core/dist/index.d.cts +1137 -0
- package/packages/core/dist/index.d.ts +1137 -0
- package/packages/core/dist/index.js +673 -0
- package/packages/core/node_modules/.bin/esbuild +22 -0
- package/packages/core/node_modules/.bin/tsc +22 -0
- package/packages/core/node_modules/.bin/tsserver +22 -0
- package/packages/core/node_modules/.bin/tsup +22 -0
- package/packages/core/node_modules/.bin/tsup-node +22 -0
- package/packages/core/node_modules/.bin/vite +22 -0
- package/packages/core/node_modules/.bin/vitest +22 -0
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/core/package.json +57 -0
- package/packages/core/src/AuraError.ts +89 -0
- package/packages/core/src/BaseService.ts +44 -0
- package/packages/core/src/client.ts +34 -0
- package/packages/core/src/cookie-utils.ts +42 -0
- package/packages/core/src/fetch.ts +435 -0
- package/packages/core/src/index.ts +22 -0
- package/packages/core/src/retry.ts +63 -0
- package/packages/core/src/serialize.ts +6 -0
- package/packages/core/src/stores/BaseAuthStore.ts +164 -0
- package/packages/core/src/stores/LocalAuthStore.ts +199 -0
- package/packages/core/src/types.ts +1042 -0
- package/packages/core/tests/auth-store.test.ts +300 -0
- package/packages/core/tests/error-code.test.ts +61 -0
- package/packages/core/tests/error.test.ts +24 -0
- package/packages/core/tests/fetch.test.ts +286 -0
- package/packages/core/tests/mocks.ts +139 -0
- package/packages/core/tests/observability.test.ts +86 -0
- package/packages/core/tests/refresh-hardening.test.ts +164 -0
- package/packages/core/tests/refresh-observability.test.ts +111 -0
- package/packages/core/tests/retry.test.ts +106 -0
- package/packages/core/tests/serialize.test.ts +12 -0
- package/packages/core/tests/timeout.test.ts +79 -0
- package/packages/core/tsconfig.json +5 -0
- package/packages/core/tsup.config.ts +11 -0
- package/packages/db/LICENSE +21 -0
- package/packages/db/README.md +152 -0
- package/packages/db/VENDORED.md +93 -0
- package/packages/db/dist/index.cjs +544 -0
- package/packages/db/dist/index.d.cts +1111 -0
- package/packages/db/dist/index.d.ts +1111 -0
- package/packages/db/dist/index.js +541 -0
- package/packages/db/node_modules/.bin/esbuild +19 -0
- package/packages/db/node_modules/.bin/tsc +22 -0
- package/packages/db/node_modules/.bin/tsserver +22 -0
- package/packages/db/node_modules/.bin/tsup +22 -0
- package/packages/db/node_modules/.bin/tsup-node +22 -0
- package/packages/db/node_modules/.bin/vite +22 -0
- package/packages/db/node_modules/.bin/vitest +22 -0
- package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/db/package.json +62 -0
- package/packages/db/src/DatabaseService.ts +170 -0
- package/packages/db/src/QueryBuilder.ts +830 -0
- package/packages/db/src/index.ts +2 -0
- package/packages/db/src/pgrst-types/common/common.ts +86 -0
- package/packages/db/src/pgrst-types/common/rpc.ts +151 -0
- package/packages/db/src/pgrst-types/feature-flags.ts +20 -0
- package/packages/db/src/pgrst-types/types.ts +134 -0
- package/packages/db/src/select-query-parser/parser.ts +479 -0
- package/packages/db/src/select-query-parser/result.ts +553 -0
- package/packages/db/src/select-query-parser/types.ts +132 -0
- package/packages/db/src/select-query-parser/utils.ts +707 -0
- package/packages/db/tests/filters.test.ts +81 -0
- package/packages/db/tests/mutations-filtered.e2e.test.ts +132 -0
- package/packages/db/tests/mutations-filtered.test.ts +188 -0
- package/packages/db/tests/order.test.ts +45 -0
- package/packages/db/tests/rpc.test.ts +41 -0
- package/packages/db/tests/scale/build-large-schema.ts +94 -0
- package/packages/db/tests/scale/scale-compile.test.ts +167 -0
- package/packages/db/tests/schema.test.ts +77 -0
- package/packages/db/tests/single-accept.e2e.test.ts +123 -0
- package/packages/db/tests/single.test.ts +211 -0
- package/packages/db/tests/types/filters.test-d.ts +52 -0
- package/packages/db/tests/types/fixtures.ts +40 -0
- package/packages/db/tests/types/from-views.test-d.ts +18 -0
- package/packages/db/tests/types/mutations.test-d.ts +23 -0
- package/packages/db/tests/types/rpc.test-d.ts +60 -0
- package/packages/db/tests/types/schema-shape.test-d.ts +8 -0
- package/packages/db/tests/types/select-inference.test-d.ts +38 -0
- package/packages/db/tests/vendored/fixtures.ts +83 -0
- package/packages/db/tests/vendored/get-result.test-d.ts +75 -0
- package/packages/db/tsconfig.json +5 -0
- package/packages/db/tsconfig.test-d.json +5 -0
- package/packages/db/tsup.config.ts +11 -0
- package/packages/db/vitest.config.ts +8 -0
- package/packages/functions/LICENSE +21 -0
- package/packages/functions/README.md +96 -0
- package/packages/functions/dist/index.cjs +173 -0
- package/packages/functions/dist/index.d.cts +81 -0
- package/packages/functions/dist/index.d.ts +81 -0
- package/packages/functions/dist/index.js +168 -0
- package/packages/functions/node_modules/.bin/esbuild +19 -0
- package/packages/functions/node_modules/.bin/tsc +22 -0
- package/packages/functions/node_modules/.bin/tsserver +22 -0
- package/packages/functions/node_modules/.bin/tsup +22 -0
- package/packages/functions/node_modules/.bin/tsup-node +22 -0
- package/packages/functions/node_modules/.bin/vite +22 -0
- package/packages/functions/node_modules/.bin/vitest +22 -0
- package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/functions/package.json +60 -0
- package/packages/functions/src/FunctionsService.ts +188 -0
- package/packages/functions/src/errors.ts +70 -0
- package/packages/functions/src/index.ts +6 -0
- package/packages/functions/tests/invoke.test.ts +97 -0
- package/packages/functions/tests/response-type-errors.test.ts +135 -0
- package/packages/functions/tsconfig.json +5 -0
- package/packages/functions/tsup.config.ts +11 -0
- package/packages/gen/LICENSE +21 -0
- package/packages/gen/README.md +84 -0
- package/packages/gen/dist/cli.cjs +334 -0
- package/packages/gen/dist/cli.d.cts +1 -0
- package/packages/gen/dist/cli.d.ts +1 -0
- package/packages/gen/dist/cli.js +332 -0
- package/packages/gen/dist/index.cjs +257 -0
- package/packages/gen/dist/index.d.cts +12 -0
- package/packages/gen/dist/index.d.ts +12 -0
- package/packages/gen/dist/index.js +255 -0
- package/packages/gen/node_modules/.bin/esbuild +19 -0
- package/packages/gen/node_modules/.bin/tsc +22 -0
- package/packages/gen/node_modules/.bin/tsserver +22 -0
- package/packages/gen/node_modules/.bin/tsup +22 -0
- package/packages/gen/node_modules/.bin/tsup-node +22 -0
- package/packages/gen/node_modules/.bin/vite +22 -0
- package/packages/gen/node_modules/.bin/vitest +22 -0
- package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/gen/package.json +66 -0
- package/packages/gen/src/cli.ts +67 -0
- package/packages/gen/src/generate.ts +0 -0
- package/packages/gen/src/index.ts +2 -0
- package/packages/gen/tests/compile.test.ts +213 -0
- package/packages/gen/tests/e2e.test.ts +130 -0
- package/packages/gen/tests/generate.test.ts +396 -0
- package/packages/gen/tsconfig.json +5 -0
- package/packages/gen/tsup.config.ts +11 -0
- package/packages/notifications/LICENSE +21 -0
- package/packages/notifications/README.md +134 -0
- package/packages/notifications/dist/index.cjs +128 -0
- package/packages/notifications/dist/index.d.cts +81 -0
- package/packages/notifications/dist/index.d.ts +81 -0
- package/packages/notifications/dist/index.js +125 -0
- package/packages/notifications/node_modules/.bin/esbuild +19 -0
- package/packages/notifications/node_modules/.bin/tsc +22 -0
- package/packages/notifications/node_modules/.bin/tsserver +22 -0
- package/packages/notifications/node_modules/.bin/tsup +22 -0
- package/packages/notifications/node_modules/.bin/tsup-node +22 -0
- package/packages/notifications/node_modules/.bin/vite +22 -0
- package/packages/notifications/node_modules/.bin/vitest +22 -0
- package/packages/notifications/package.json +60 -0
- package/packages/notifications/src/NotificationsService.ts +230 -0
- package/packages/notifications/src/index.ts +19 -0
- package/packages/notifications/tsconfig.json +5 -0
- package/packages/notifications/tsup.config.ts +11 -0
- package/packages/realtime/LICENSE +21 -0
- package/packages/realtime/README.md +183 -0
- package/packages/realtime/dist/index.cjs +977 -0
- package/packages/realtime/dist/index.d.cts +259 -0
- package/packages/realtime/dist/index.d.ts +259 -0
- package/packages/realtime/dist/index.js +973 -0
- package/packages/realtime/node_modules/.bin/esbuild +19 -0
- package/packages/realtime/node_modules/.bin/tsc +22 -0
- package/packages/realtime/node_modules/.bin/tsserver +22 -0
- package/packages/realtime/node_modules/.bin/tsup +22 -0
- package/packages/realtime/node_modules/.bin/tsup-node +22 -0
- package/packages/realtime/node_modules/.bin/vite +22 -0
- package/packages/realtime/node_modules/.bin/vitest +22 -0
- package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/realtime/package.json +60 -0
- package/packages/realtime/src/AuraChannel.ts +485 -0
- package/packages/realtime/src/HeartbeatManager.ts +73 -0
- package/packages/realtime/src/RealtimeService.ts +505 -0
- package/packages/realtime/src/columnFilter.ts +301 -0
- package/packages/realtime/src/index.ts +4 -0
- package/packages/realtime/tests/ack-routing.test.ts +81 -0
- package/packages/realtime/tests/column-filter.test.ts +144 -0
- package/packages/realtime/tests/dispatch.test.ts +191 -0
- package/packages/realtime/tests/heartbeat.test.ts +67 -0
- package/packages/realtime/tests/http-broadcast.test.ts +68 -0
- package/packages/realtime/tests/realtime-token.test.ts +43 -0
- package/packages/realtime/tests/reconnect-race.test.ts +149 -0
- package/packages/realtime/tests/ref-id.test.ts +34 -0
- package/packages/realtime/tests/set-auth.test.ts +70 -0
- package/packages/realtime/tsconfig.json +5 -0
- package/packages/realtime/tsup.config.ts +11 -0
- package/packages/storage/LICENSE +21 -0
- package/packages/storage/README.md +141 -0
- package/packages/storage/dist/index.cjs +333 -0
- package/packages/storage/dist/index.d.cts +185 -0
- package/packages/storage/dist/index.d.ts +185 -0
- package/packages/storage/dist/index.js +329 -0
- package/packages/storage/node_modules/.bin/esbuild +19 -0
- package/packages/storage/node_modules/.bin/tsc +22 -0
- package/packages/storage/node_modules/.bin/tsserver +22 -0
- package/packages/storage/node_modules/.bin/tsup +22 -0
- package/packages/storage/node_modules/.bin/tsup-node +22 -0
- package/packages/storage/node_modules/.bin/vite +22 -0
- package/packages/storage/node_modules/.bin/vitest +22 -0
- package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/storage/package.json +60 -0
- package/packages/storage/src/DownloadBuilder.ts +60 -0
- package/packages/storage/src/StorageService.ts +323 -0
- package/packages/storage/src/index.ts +2 -0
- package/packages/storage/src/path.ts +35 -0
- package/packages/storage/src/transform.ts +15 -0
- package/packages/storage/tests/download.test.ts +64 -0
- package/packages/storage/tests/parity.test.ts +154 -0
- package/packages/storage/tests/path.test.ts +87 -0
- package/packages/storage/tests/transform.test.ts +79 -0
- package/packages/storage/tsconfig.json +5 -0
- package/packages/storage/tsup.config.ts +11 -0
- package/pnpm-workspace.yaml +24 -0
- package/tsconfig.base.json +15 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// isMfaChallenge (LOT 3, 3.4) : garde de type exportée pour discriminer
|
|
3
|
+
// `LoginResponse = TokenPair | MfaChallenge` côté consommateur.
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest'
|
|
7
|
+
import { isMfaChallenge } from '../src/AuthService'
|
|
8
|
+
|
|
9
|
+
describe('isMfaChallenge', () => {
|
|
10
|
+
it('reconnaît un MfaChallenge (mfa_required: true)', () => {
|
|
11
|
+
expect(isMfaChallenge({ mfa_required: true, mfa_token: 'tok' })).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('rejette une TokenPair normale', () => {
|
|
15
|
+
expect(
|
|
16
|
+
isMfaChallenge({ access_token: 'a', refresh_token: 'b', token_type: 'bearer', expires_in: 3600 }),
|
|
17
|
+
).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('rejette les valeurs non-objet / null / mfa_required falsy', () => {
|
|
21
|
+
expect(isMfaChallenge(null)).toBe(false)
|
|
22
|
+
expect(isMfaChallenge(undefined)).toBe(false)
|
|
23
|
+
expect(isMfaChallenge('x')).toBe(false)
|
|
24
|
+
expect(isMfaChallenge({})).toBe(false)
|
|
25
|
+
expect(isMfaChallenge({ mfa_required: false })).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
2
|
+
import { AuthService } from '../src/AuthService'
|
|
3
|
+
import { BaseAuthStore } from '@aurabase/core'
|
|
4
|
+
import type { AuraClientLike, TokenPair, User, AuthEvent, AuthChangePayload } from '@aurabase/core'
|
|
5
|
+
|
|
6
|
+
function fakeUser(id = 'u1'): User {
|
|
7
|
+
return { id, email: 't@e.com', phone: null, role: 'authenticated', email_verified: true, is_banned: false, is_anonymous: false, mfa_enabled: false, user_metadata: {}, app_metadata: {}, created_at: '2025-01-01T00:00:00Z' }
|
|
8
|
+
}
|
|
9
|
+
function tokenPair(): TokenPair {
|
|
10
|
+
return { access_token: 'AT', refresh_token: 'RT', token_type: 'bearer', expires_in: 3600, user: fakeUser() }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const VERIFIER_KEY = 'aura-proj-1-pkce-code-verifier'
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.restoreAllMocks()
|
|
17
|
+
delete (globalThis as unknown as { window?: unknown }).window
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function makeAuth(store: BaseAuthStore) {
|
|
21
|
+
const fetchFn = (async (url: string) => {
|
|
22
|
+
if (String(url).includes('/oauth/pkce')) {
|
|
23
|
+
return new Response(JSON.stringify({ data: tokenPair() }), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
|
24
|
+
}
|
|
25
|
+
return new Response('{}', { status: 200 })
|
|
26
|
+
}) as unknown as typeof globalThis.fetch
|
|
27
|
+
const client: AuraClientLike = {
|
|
28
|
+
baseUrl: 'http://x', projectId: 'proj-1', apiKey: 'k', authStore: store,
|
|
29
|
+
fetchFn, auth: null as unknown as AuraClientLike['auth'],
|
|
30
|
+
}
|
|
31
|
+
const svc = new AuthService(client, { autoRefresh: false, persistSession: false })
|
|
32
|
+
;(client as { auth: unknown }).auth = svc
|
|
33
|
+
const payloads: AuthChangePayload[] = []
|
|
34
|
+
const events: AuthEvent[] = []
|
|
35
|
+
svc.onAuthStateChange((p) => {
|
|
36
|
+
payloads.push(p)
|
|
37
|
+
events.push(p.event)
|
|
38
|
+
})
|
|
39
|
+
return { svc, client, events, payloads }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setWindow(opts: { search?: string; pathname?: string }) {
|
|
43
|
+
const replaceState = vi.fn()
|
|
44
|
+
;(globalThis as unknown as { window: unknown }).window = {
|
|
45
|
+
location: { hash: '', search: opts.search ?? '', pathname: opts.pathname ?? '/app' },
|
|
46
|
+
history: { replaceState },
|
|
47
|
+
}
|
|
48
|
+
return replaceState
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('AuthService — PASSWORD_RECOVERY (lien email ?token=…&type=recovery)', () => {
|
|
52
|
+
it('?token=X&type=recovery → PASSWORD_RECOVERY émis une fois avec recoveryToken=X, PAS SIGNED_IN, PAS de session, URL nettoyée', async () => {
|
|
53
|
+
const store = new BaseAuthStore()
|
|
54
|
+
const { svc, events, payloads } = makeAuth(store)
|
|
55
|
+
const replaceState = setWindow({
|
|
56
|
+
search: '?token=RCVTOKEN&project_id=proj-1&type=recovery',
|
|
57
|
+
pathname: '/reset-password',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
await svc._detectSessionInUrl()
|
|
61
|
+
// Laisser filer d'éventuels microtasks.
|
|
62
|
+
await Promise.resolve()
|
|
63
|
+
|
|
64
|
+
const recoveryPayloads = payloads.filter((p) => p.event === 'PASSWORD_RECOVERY')
|
|
65
|
+
expect(recoveryPayloads).toHaveLength(1)
|
|
66
|
+
expect(recoveryPayloads[0]!.recoveryToken).toBe('RCVTOKEN')
|
|
67
|
+
expect(recoveryPayloads[0]!.user).toBeNull()
|
|
68
|
+
expect(recoveryPayloads[0]!.accessToken).toBeNull()
|
|
69
|
+
|
|
70
|
+
// AUCUNE connexion, AUCUNE session posée.
|
|
71
|
+
expect(events).not.toContain('SIGNED_IN')
|
|
72
|
+
expect(store.token).toBeNull()
|
|
73
|
+
|
|
74
|
+
// URL nettoyée (token/project_id/type retirés).
|
|
75
|
+
expect(replaceState).toHaveBeenCalledTimes(1)
|
|
76
|
+
const cleaned = String(replaceState.mock.calls[0]![2])
|
|
77
|
+
expect(cleaned).not.toContain('token=')
|
|
78
|
+
expect(cleaned).not.toContain('project_id=')
|
|
79
|
+
expect(cleaned).not.toContain('type=')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('?code=… normal (sans type=recovery) → SIGNED_IN, PAS PASSWORD_RECOVERY (non-régression)', async () => {
|
|
83
|
+
const store = new BaseAuthStore()
|
|
84
|
+
store.setItem(VERIFIER_KEY, 'verifier-value')
|
|
85
|
+
const { svc, events } = makeAuth(store)
|
|
86
|
+
setWindow({ search: '?code=NORMALCODE', pathname: '/cb' })
|
|
87
|
+
|
|
88
|
+
await svc._detectSessionInUrl()
|
|
89
|
+
await Promise.resolve()
|
|
90
|
+
|
|
91
|
+
expect(events).toContain('SIGNED_IN')
|
|
92
|
+
expect(events).not.toContain('PASSWORD_RECOVERY')
|
|
93
|
+
expect(store.token).toBe('AT')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('URL sans token ni code → aucun événement, no-op', async () => {
|
|
97
|
+
const store = new BaseAuthStore()
|
|
98
|
+
const { svc, events } = makeAuth(store)
|
|
99
|
+
setWindow({ search: '?foo=bar' })
|
|
100
|
+
|
|
101
|
+
await svc._detectSessionInUrl()
|
|
102
|
+
await Promise.resolve()
|
|
103
|
+
|
|
104
|
+
expect(events).not.toContain('PASSWORD_RECOVERY')
|
|
105
|
+
expect(events).not.toContain('SIGNED_IN')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('type=recovery sans token → ignoré (no-op, pas de PASSWORD_RECOVERY)', async () => {
|
|
109
|
+
const store = new BaseAuthStore()
|
|
110
|
+
const { svc, events } = makeAuth(store)
|
|
111
|
+
setWindow({ search: '?type=recovery' })
|
|
112
|
+
|
|
113
|
+
await svc._detectSessionInUrl()
|
|
114
|
+
await Promise.resolve()
|
|
115
|
+
|
|
116
|
+
expect(events).not.toContain('PASSWORD_RECOVERY')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('hors navigateur → no-op, ne throw pas', async () => {
|
|
120
|
+
const store = new BaseAuthStore()
|
|
121
|
+
const { svc } = makeAuth(store)
|
|
122
|
+
await expect(svc._detectSessionInUrl()).resolves.toBeUndefined()
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
+
import { AuthService } from '../src/AuthService'
|
|
3
|
+
import { generateCodeVerifier, codeChallengeS256 } from '../src/pkce'
|
|
4
|
+
import { BaseAuthStore } from '@aurabase/core'
|
|
5
|
+
import type { AuraClientLike, User, TokenPair } from '@aurabase/core'
|
|
6
|
+
|
|
7
|
+
// --- helpers -----------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function fakeUser(id = 'u1'): User {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
email: 't@e.com',
|
|
13
|
+
role: 'authenticated',
|
|
14
|
+
email_verified: true,
|
|
15
|
+
is_banned: false,
|
|
16
|
+
is_anonymous: false,
|
|
17
|
+
mfa_enabled: false,
|
|
18
|
+
user_metadata: {},
|
|
19
|
+
app_metadata: {},
|
|
20
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class FakeBC {
|
|
25
|
+
static instances: FakeBC[] = []
|
|
26
|
+
onmessage: ((e: { data: unknown }) => void) | null = null
|
|
27
|
+
posted: unknown[] = []
|
|
28
|
+
closed = false
|
|
29
|
+
constructor(public name: string) {
|
|
30
|
+
FakeBC.instances.push(this)
|
|
31
|
+
}
|
|
32
|
+
postMessage(d: unknown) {
|
|
33
|
+
this.posted.push(d)
|
|
34
|
+
}
|
|
35
|
+
close() {
|
|
36
|
+
this.closed = true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
FakeBC.instances = []
|
|
42
|
+
;(globalThis as unknown as { BroadcastChannel: unknown }).BroadcastChannel = FakeBC
|
|
43
|
+
})
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
vi.useRealTimers()
|
|
46
|
+
vi.restoreAllMocks()
|
|
47
|
+
delete (globalThis as unknown as { BroadcastChannel?: unknown }).BroadcastChannel
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
function makeAuth(
|
|
51
|
+
fetchFn: typeof globalThis.fetch = (async () =>
|
|
52
|
+
new Response('{}', { status: 200 })) as unknown as typeof globalThis.fetch,
|
|
53
|
+
store = new BaseAuthStore(),
|
|
54
|
+
) {
|
|
55
|
+
const client: AuraClientLike = {
|
|
56
|
+
baseUrl: 'http://pkce.local',
|
|
57
|
+
projectId: 'proj-1',
|
|
58
|
+
apiKey: 'k',
|
|
59
|
+
fetchFn,
|
|
60
|
+
authStore: store,
|
|
61
|
+
auth: null as unknown as AuraClientLike['auth'],
|
|
62
|
+
}
|
|
63
|
+
const svc = new AuthService(client, { autoRefresh: false, persistSession: false })
|
|
64
|
+
;(client as { auth: unknown }).auth = svc
|
|
65
|
+
return { svc, client, store }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const VERIFIER_KEY = 'aura-proj-1-pkce-code-verifier'
|
|
69
|
+
|
|
70
|
+
// --- pkce.ts -------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
describe('generateCodeVerifier', () => {
|
|
73
|
+
it('longueur dans [43, 128] et charset base64url', () => {
|
|
74
|
+
const v = generateCodeVerifier()
|
|
75
|
+
expect(v.length).toBeGreaterThanOrEqual(43)
|
|
76
|
+
expect(v.length).toBeLessThanOrEqual(128)
|
|
77
|
+
expect(v).toMatch(/^[A-Za-z0-9\-_]+$/)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('deux appels produisent des valeurs différentes (aléatoire)', () => {
|
|
81
|
+
const a = generateCodeVerifier()
|
|
82
|
+
const b = generateCodeVerifier()
|
|
83
|
+
expect(a).not.toBe(b)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('codeChallengeS256', () => {
|
|
88
|
+
it('vecteur connu RFC 7636 Appendix B', async () => {
|
|
89
|
+
const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
|
|
90
|
+
const expected = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'
|
|
91
|
+
await expect(codeChallengeS256(verifier)).resolves.toBe(expected)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// --- Store setItem/getItem/removeItem -------------------------------------------
|
|
96
|
+
|
|
97
|
+
describe('BaseAuthStore — stockage clé/valeur générique', () => {
|
|
98
|
+
it('setItem/getItem/removeItem round-trip', () => {
|
|
99
|
+
const store = new BaseAuthStore()
|
|
100
|
+
store.setItem('k', 'v')
|
|
101
|
+
expect(store.getItem('k')).toBe('v')
|
|
102
|
+
store.removeItem('k')
|
|
103
|
+
expect(store.getItem('k')).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("getItem d'une clé absente renvoie null", () => {
|
|
107
|
+
const store = new BaseAuthStore()
|
|
108
|
+
expect(store.getItem('absent')).toBeNull()
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// --- signInWithOAuth --------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
describe('AuthService.signInWithOAuth — flux PKCE opt-in', () => {
|
|
115
|
+
it("flowType: 'pkce' → URL contient code_challenge + S256, store contient le verifier, cohérence verifier↔challenge", async () => {
|
|
116
|
+
const { svc, store } = makeAuth()
|
|
117
|
+
// Pas de `window` dans l'environnement de test par défaut (node) → pas de redirect.
|
|
118
|
+
const { data, error } = await svc.signInWithOAuth('github', { flowType: 'pkce' })
|
|
119
|
+
|
|
120
|
+
expect(error).toBeNull()
|
|
121
|
+
expect(data?.url).toContain('code_challenge=')
|
|
122
|
+
expect(data?.url).toContain('code_challenge_method=S256')
|
|
123
|
+
|
|
124
|
+
const storedVerifier = store.getItem(VERIFIER_KEY)
|
|
125
|
+
expect(storedVerifier).toBeTruthy()
|
|
126
|
+
|
|
127
|
+
const url = new URL(data!.url)
|
|
128
|
+
const challengeInUrl = url.searchParams.get('code_challenge')
|
|
129
|
+
const expectedChallenge = await codeChallengeS256(storedVerifier!)
|
|
130
|
+
expect(challengeInUrl).toBe(expectedChallenge)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('sans flowType (implicite) → aucun paramètre PKCE, store vide', async () => {
|
|
134
|
+
const { svc, store } = makeAuth()
|
|
135
|
+
|
|
136
|
+
const { data, error } = await svc.signInWithOAuth('github')
|
|
137
|
+
|
|
138
|
+
expect(error).toBeNull()
|
|
139
|
+
expect(data?.url).not.toContain('code_challenge')
|
|
140
|
+
expect(store.getItem(VERIFIER_KEY)).toBeNull()
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// --- exchangeCodeForSession --------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
describe('AuthService.exchangeCodeForSession — lecture/purge du verifier stocké', () => {
|
|
147
|
+
it('sans verifier explicite → lit le verifier stocké, POST /oauth/pkce avec ce code_verifier, purge à succès', async () => {
|
|
148
|
+
const store = new BaseAuthStore()
|
|
149
|
+
store.setItem(VERIFIER_KEY, 'stored-verifier-value')
|
|
150
|
+
const tokenPair: TokenPair = {
|
|
151
|
+
access_token: 'at',
|
|
152
|
+
refresh_token: 'rt',
|
|
153
|
+
user: fakeUser(),
|
|
154
|
+
}
|
|
155
|
+
let capturedBody: unknown = null
|
|
156
|
+
const fetchFn = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
157
|
+
const u = String(url)
|
|
158
|
+
if (u.includes('/oauth/pkce')) {
|
|
159
|
+
capturedBody = init?.body ? JSON.parse(String(init.body)) : null
|
|
160
|
+
return new Response(JSON.stringify({ data: tokenPair }), {
|
|
161
|
+
status: 200,
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
return new Response('{}', { status: 200 })
|
|
166
|
+
}) as unknown as typeof globalThis.fetch
|
|
167
|
+
const { svc } = makeAuth(fetchFn, store)
|
|
168
|
+
|
|
169
|
+
const { data, error } = await svc.exchangeCodeForSession('auth-code-1')
|
|
170
|
+
|
|
171
|
+
expect(error).toBeNull()
|
|
172
|
+
expect(data?.access_token).toBe('at')
|
|
173
|
+
expect(capturedBody).toEqual({ auth_code: 'auth-code-1', code_verifier: 'stored-verifier-value' })
|
|
174
|
+
expect(store.getItem(VERIFIER_KEY)).toBeNull()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('sans verifier stocké ni fourni → error no_code_verifier, pas de POST', async () => {
|
|
178
|
+
const store = new BaseAuthStore()
|
|
179
|
+
let called = false
|
|
180
|
+
const fetchFn = (async () => {
|
|
181
|
+
called = true
|
|
182
|
+
return new Response('{}', { status: 200 })
|
|
183
|
+
}) as unknown as typeof globalThis.fetch
|
|
184
|
+
const { svc } = makeAuth(fetchFn, store)
|
|
185
|
+
|
|
186
|
+
const { data, error } = await svc.exchangeCodeForSession('auth-code-2')
|
|
187
|
+
|
|
188
|
+
expect(data).toBeNull()
|
|
189
|
+
expect((error as { code?: string })?.code).toBe('no_code_verifier')
|
|
190
|
+
expect(called).toBe(false)
|
|
191
|
+
})
|
|
192
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
2
|
+
import { SessionManager, type SessionManagerOptions } from '../src/SessionManager'
|
|
3
|
+
|
|
4
|
+
// Mock minimal de BroadcastChannel
|
|
5
|
+
class FakeBC {
|
|
6
|
+
static instances: FakeBC[] = []
|
|
7
|
+
onmessage: ((e: { data: unknown }) => void) | null = null
|
|
8
|
+
posted: unknown[] = []
|
|
9
|
+
closed = false
|
|
10
|
+
constructor(public name: string) {
|
|
11
|
+
FakeBC.instances.push(this)
|
|
12
|
+
}
|
|
13
|
+
postMessage(d: unknown) {
|
|
14
|
+
this.posted.push(d)
|
|
15
|
+
}
|
|
16
|
+
close() {
|
|
17
|
+
this.closed = true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
FakeBC.instances = []
|
|
23
|
+
;(globalThis as unknown as { BroadcastChannel: unknown }).BroadcastChannel = FakeBC
|
|
24
|
+
})
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.useRealTimers()
|
|
27
|
+
delete (globalThis as unknown as { BroadcastChannel?: unknown }).BroadcastChannel
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
function make(overrides: Partial<SessionManagerOptions> = {}) {
|
|
31
|
+
const onTick = vi.fn()
|
|
32
|
+
let due = true
|
|
33
|
+
const onRemoteMessage = vi.fn()
|
|
34
|
+
const sm = new SessionManager({
|
|
35
|
+
channelName: 'aura-p-auth',
|
|
36
|
+
enabled: true,
|
|
37
|
+
onTick,
|
|
38
|
+
isRefreshDue: () => due,
|
|
39
|
+
onRemoteMessage,
|
|
40
|
+
tickMs: 1000,
|
|
41
|
+
...overrides,
|
|
42
|
+
})
|
|
43
|
+
return { sm, onTick, onRemoteMessage, setDue: (v: boolean) => { due = v } }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('SessionManager', () => {
|
|
47
|
+
it('tick appelle onTick quand refresh dû, pas sinon', () => {
|
|
48
|
+
vi.useFakeTimers()
|
|
49
|
+
const { sm, onTick, setDue } = make()
|
|
50
|
+
sm.start()
|
|
51
|
+
sm.setActive(true)
|
|
52
|
+
vi.advanceTimersByTime(1000)
|
|
53
|
+
expect(onTick).toHaveBeenCalledTimes(1)
|
|
54
|
+
setDue(false)
|
|
55
|
+
vi.advanceTimersByTime(1000)
|
|
56
|
+
expect(onTick).toHaveBeenCalledTimes(1)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('inactif → aucun tick', () => {
|
|
60
|
+
vi.useFakeTimers()
|
|
61
|
+
const { sm, onTick } = make()
|
|
62
|
+
sm.start()
|
|
63
|
+
sm.setActive(false)
|
|
64
|
+
vi.advanceTimersByTime(5000)
|
|
65
|
+
expect(onTick).not.toHaveBeenCalled()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('broadcast poste sur le canal', () => {
|
|
69
|
+
const { sm } = make()
|
|
70
|
+
sm.start()
|
|
71
|
+
sm.broadcast({ hello: 1 })
|
|
72
|
+
expect(FakeBC.instances[0].posted).toEqual([{ hello: 1 }])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('réception → onRemoteMessage', () => {
|
|
76
|
+
const { sm, onRemoteMessage } = make()
|
|
77
|
+
sm.start()
|
|
78
|
+
FakeBC.instances[0].onmessage!({ data: { x: 2 } })
|
|
79
|
+
expect(onRemoteMessage).toHaveBeenCalledWith({ x: 2 })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('stop() coupe tout (canal fermé, plus de tick)', () => {
|
|
83
|
+
vi.useFakeTimers()
|
|
84
|
+
const { sm, onTick } = make()
|
|
85
|
+
sm.start()
|
|
86
|
+
sm.setActive(true)
|
|
87
|
+
sm.stop()
|
|
88
|
+
vi.advanceTimersByTime(5000)
|
|
89
|
+
expect(onTick).not.toHaveBeenCalled()
|
|
90
|
+
expect(FakeBC.instances[0].closed).toBe(true)
|
|
91
|
+
expect(sm.isActive).toBe(false)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('enabled=false → pas de canal, pas de tick', () => {
|
|
95
|
+
vi.useFakeTimers()
|
|
96
|
+
const { sm, onTick } = make({ enabled: false })
|
|
97
|
+
sm.start()
|
|
98
|
+
sm.setActive(true)
|
|
99
|
+
vi.advanceTimersByTime(5000)
|
|
100
|
+
expect(onTick).not.toHaveBeenCalled()
|
|
101
|
+
expect(FakeBC.instances.length).toBe(0)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
2
|
+
import { AuthService } from '../src/AuthService'
|
|
3
|
+
import { BaseAuthStore, AuraError } from '@aurabase/core'
|
|
4
|
+
import type { AuraClientLike, User } from '@aurabase/core'
|
|
5
|
+
|
|
6
|
+
// --- helpers -----------------------------------------------------------------
|
|
7
|
+
function b64url(obj: unknown): string {
|
|
8
|
+
return btoa(JSON.stringify(obj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
9
|
+
}
|
|
10
|
+
/** JWT non expiré (paramétrable pour obtenir des tokens distincts). */
|
|
11
|
+
function jwt(sub = 'u1', ttlSec = 3600): string {
|
|
12
|
+
const exp = Math.floor(Date.now() / 1000) + ttlSec
|
|
13
|
+
return `${b64url({ alg: 'HS256', typ: 'JWT' })}.${b64url({ sub, exp })}.sig`
|
|
14
|
+
}
|
|
15
|
+
/** JWT déjà expiré. */
|
|
16
|
+
function expiredJwt(sub = 'u1'): string {
|
|
17
|
+
const exp = Math.floor(Date.now() / 1000) - 60
|
|
18
|
+
return `${b64url({ alg: 'HS256', typ: 'JWT' })}.${b64url({ sub, exp })}.sig`
|
|
19
|
+
}
|
|
20
|
+
function fakeUser(id = 'u1'): User {
|
|
21
|
+
return {
|
|
22
|
+
id, email: 't@e.com', role: 'authenticated', email_verified: true,
|
|
23
|
+
is_banned: false, is_anonymous: false, mfa_enabled: false,
|
|
24
|
+
user_metadata: {}, app_metadata: {}, created_at: '2025-01-01T00:00:00Z',
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class FakeBC {
|
|
29
|
+
static instances: FakeBC[] = []
|
|
30
|
+
onmessage: ((e: { data: unknown }) => void) | null = null
|
|
31
|
+
posted: unknown[] = []
|
|
32
|
+
closed = false
|
|
33
|
+
constructor(public name: string) { FakeBC.instances.push(this) }
|
|
34
|
+
postMessage(d: unknown) { this.posted.push(d) }
|
|
35
|
+
close() { this.closed = true }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
FakeBC.instances = []
|
|
40
|
+
;(globalThis as unknown as { BroadcastChannel: unknown }).BroadcastChannel = FakeBC
|
|
41
|
+
})
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.useRealTimers()
|
|
44
|
+
vi.restoreAllMocks()
|
|
45
|
+
delete (globalThis as unknown as { BroadcastChannel?: unknown }).BroadcastChannel
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
function newAuth(store: BaseAuthStore, fetchFn?: typeof globalThis.fetch) {
|
|
49
|
+
const client: AuraClientLike = {
|
|
50
|
+
baseUrl: 'http://x', projectId: 'p', apiKey: 'k',
|
|
51
|
+
fetchFn: (fetchFn ?? (async () => new Response('{}', { status: 200 }))) as unknown as typeof globalThis.fetch,
|
|
52
|
+
authStore: store,
|
|
53
|
+
auth: null as unknown as AuraClientLike['auth'],
|
|
54
|
+
}
|
|
55
|
+
const svc = new AuthService(client, { autoRefresh: true, persistSession: true, forceEnabled: true })
|
|
56
|
+
;(client as { auth: unknown }).auth = svc
|
|
57
|
+
return { svc, client }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('AuthService.setSession — rollback + validation', () => {
|
|
61
|
+
it("échec de validation (/user KO) et aucune session préalable ⇒ store PURGÉ (pas de pollution)", async () => {
|
|
62
|
+
const store = new BaseAuthStore()
|
|
63
|
+
const { svc } = newAuth(store)
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
65
|
+
vi.spyOn(svc as any, 'request').mockResolvedValue({
|
|
66
|
+
data: null,
|
|
67
|
+
error: new AuraError(401, { error: { code: 'unauthorized', message: 'bad token' } }),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const res = await svc.setSession(jwt(), 'rt-bad')
|
|
71
|
+
|
|
72
|
+
expect(res.error).toBeTruthy()
|
|
73
|
+
expect(res.data).toBeNull()
|
|
74
|
+
// Le bug : sans rollback, token+refreshToken restaient posés avec user {}.
|
|
75
|
+
expect(store.token).toBeNull()
|
|
76
|
+
expect(store.refreshToken).toBeNull()
|
|
77
|
+
expect(store.user).toBeNull()
|
|
78
|
+
expect(store.isValid).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('validation OK ⇒ session complète + SIGNED_IN', async () => {
|
|
82
|
+
const store = new BaseAuthStore()
|
|
83
|
+
const { svc } = newAuth(store)
|
|
84
|
+
const token = jwt()
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
vi.spyOn(svc as any, 'request').mockResolvedValue({ data: fakeUser(), error: null })
|
|
87
|
+
const events: string[] = []
|
|
88
|
+
svc.onAuthStateChange(({ event }) => { if (event !== 'INITIAL_SESSION') events.push(event) })
|
|
89
|
+
|
|
90
|
+
const res = await svc.setSession(token, 'rt')
|
|
91
|
+
|
|
92
|
+
expect(res.data?.id).toBe('u1')
|
|
93
|
+
expect(store.token).toBe(token)
|
|
94
|
+
expect(store.user?.id).toBe('u1')
|
|
95
|
+
expect(events).toContain('SIGNED_IN')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('échec de validation avec session préalable VALIDE ⇒ restauration de l’ancienne session', async () => {
|
|
99
|
+
const store = new BaseAuthStore()
|
|
100
|
+
const oldToken = jwt('old')
|
|
101
|
+
store.save(oldToken, 'rt-old', fakeUser('old'))
|
|
102
|
+
const { svc } = newAuth(store)
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
vi.spyOn(svc as any, 'request').mockResolvedValue({
|
|
105
|
+
data: null,
|
|
106
|
+
error: new AuraError(404, { error: { code: 'not_found', message: 'user gone' } }),
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const res = await svc.setSession(jwt('new'), 'rt-new')
|
|
110
|
+
|
|
111
|
+
expect(res.error).toBeTruthy()
|
|
112
|
+
// L'ancienne session valide doit être intacte (pas écrasée par le token invalide).
|
|
113
|
+
expect(store.token).toBe(oldToken)
|
|
114
|
+
expect(store.refreshToken).toBe('rt-old')
|
|
115
|
+
expect(store.user?.id).toBe('old')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('access token DÉJÀ expiré ⇒ refresh via le refresh token (parité Supabase)', async () => {
|
|
119
|
+
const store = new BaseAuthStore()
|
|
120
|
+
const tokenPair = { access_token: jwt('u1'), refresh_token: 'rt-fresh', user: fakeUser() }
|
|
121
|
+
const fetchFn = (async (url: string) => {
|
|
122
|
+
if (String(url).endsWith('/refresh')) {
|
|
123
|
+
return new Response(JSON.stringify({ data: tokenPair }), { status: 200 })
|
|
124
|
+
}
|
|
125
|
+
return new Response('{}', { status: 200 })
|
|
126
|
+
}) as unknown as typeof globalThis.fetch
|
|
127
|
+
const { svc } = newAuth(store, fetchFn)
|
|
128
|
+
const events: string[] = []
|
|
129
|
+
svc.onAuthStateChange(({ event }) => { if (event !== 'INITIAL_SESSION') events.push(event) })
|
|
130
|
+
|
|
131
|
+
const res = await svc.setSession(expiredJwt(), 'rt-provided')
|
|
132
|
+
|
|
133
|
+
expect(res.data?.id).toBe('u1')
|
|
134
|
+
// Le store porte les tokens RAFRAÎCHIS, pas le token expiré fourni.
|
|
135
|
+
expect(store.token).toBe(tokenPair.access_token)
|
|
136
|
+
expect(store.refreshToken).toBe('rt-fresh')
|
|
137
|
+
expect(events).toContain('SIGNED_IN')
|
|
138
|
+
})
|
|
139
|
+
})
|