@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,780 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
|
-
import { AuthService } from '../src/AuthService'
|
|
3
|
-
import { __resetJwksCache } from '../src/jwks'
|
|
4
|
-
import { BaseAuthStore } from '@aurabase/core'
|
|
5
|
-
import type { AuraClientLike, User, AuraObservabilityEvent } from '@aurabase/core'
|
|
6
|
-
|
|
7
|
-
// --- helpers -----------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
function b64url(obj: unknown): string {
|
|
10
|
-
return btoa(JSON.stringify(obj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function b64urlBytes(bytes: Uint8Array): string {
|
|
14
|
-
let bin = ''
|
|
15
|
-
for (const b of bytes) bin += String.fromCharCode(b)
|
|
16
|
-
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Génère une paire RS256 et exporte la clé publique en JWK (avec kid). */
|
|
20
|
-
async function genRs256(kid?: string): Promise<{ pair: CryptoKeyPair; jwk: JsonWebKey }> {
|
|
21
|
-
const pair = await crypto.subtle.generateKey(
|
|
22
|
-
{ name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
|
|
23
|
-
true,
|
|
24
|
-
['sign', 'verify'],
|
|
25
|
-
)
|
|
26
|
-
const jwk = await crypto.subtle.exportKey('jwk', pair.publicKey)
|
|
27
|
-
if (kid) (jwk as { kid?: string }).kid = kid
|
|
28
|
-
return { pair, jwk }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Signe un JWT RS256 avec la clé privée fournie. */
|
|
32
|
-
async function signRs256(
|
|
33
|
-
payload: Record<string, unknown>,
|
|
34
|
-
priv: CryptoKey,
|
|
35
|
-
kid?: string,
|
|
36
|
-
): Promise<string> {
|
|
37
|
-
const header = { alg: 'RS256', typ: 'JWT', ...(kid ? { kid } : {}) }
|
|
38
|
-
const signingInput = `${b64url(header)}.${b64url(payload)}`
|
|
39
|
-
const sig = await crypto.subtle.sign(
|
|
40
|
-
{ name: 'RSASSA-PKCS1-v1_5' },
|
|
41
|
-
priv,
|
|
42
|
-
new TextEncoder().encode(signingInput),
|
|
43
|
-
)
|
|
44
|
-
return `${signingInput}.${b64urlBytes(new Uint8Array(sig))}`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** JWT HS256 factice (signature non vérifiable localement → fallback getUser). */
|
|
48
|
-
function hs256Jwt(sub = 'u1', ttlSec = 3600): string {
|
|
49
|
-
const exp = Math.floor(Date.now() / 1000) + ttlSec
|
|
50
|
-
return `${b64url({ alg: 'HS256', typ: 'JWT' })}.${b64url({ sub, exp })}.sig`
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** JWT HS256 factice avec payload arbitraire (pour tester iss/aud/exp au fallback). */
|
|
54
|
-
function hs256WithClaims(payload: Record<string, unknown>): string {
|
|
55
|
-
return `${b64url({ alg: 'HS256', typ: 'JWT' })}.${b64url(payload)}.sig`
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function fakeUser(id = 'u1'): User {
|
|
59
|
-
return {
|
|
60
|
-
id,
|
|
61
|
-
email: 't@e.com',
|
|
62
|
-
role: 'authenticated',
|
|
63
|
-
email_verified: true,
|
|
64
|
-
is_banned: false,
|
|
65
|
-
is_anonymous: false,
|
|
66
|
-
mfa_enabled: false,
|
|
67
|
-
user_metadata: {},
|
|
68
|
-
app_metadata: {},
|
|
69
|
-
created_at: '2025-01-01T00:00:00Z',
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
class FakeBC {
|
|
74
|
-
static instances: FakeBC[] = []
|
|
75
|
-
onmessage: ((e: { data: unknown }) => void) | null = null
|
|
76
|
-
posted: unknown[] = []
|
|
77
|
-
closed = false
|
|
78
|
-
constructor(public name: string) {
|
|
79
|
-
FakeBC.instances.push(this)
|
|
80
|
-
}
|
|
81
|
-
postMessage(d: unknown) {
|
|
82
|
-
this.posted.push(d)
|
|
83
|
-
}
|
|
84
|
-
close() {
|
|
85
|
-
this.closed = true
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
FakeBC.instances = []
|
|
91
|
-
;(globalThis as unknown as { BroadcastChannel: unknown }).BroadcastChannel = FakeBC
|
|
92
|
-
// Le cache JWKS est process-global (limite connue, documentée dans jwks.ts) :
|
|
93
|
-
// on l'isole entre tests plutôt que de multiplier les baseUrls distincts.
|
|
94
|
-
__resetJwksCache()
|
|
95
|
-
})
|
|
96
|
-
afterEach(() => {
|
|
97
|
-
vi.useRealTimers()
|
|
98
|
-
vi.restoreAllMocks()
|
|
99
|
-
delete (globalThis as unknown as { BroadcastChannel?: unknown }).BroadcastChannel
|
|
100
|
-
__resetJwksCache()
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
function makeAuth(
|
|
104
|
-
baseUrl: string,
|
|
105
|
-
fetchFn: typeof globalThis.fetch,
|
|
106
|
-
store = new BaseAuthStore(),
|
|
107
|
-
onEvent?: (e: AuraObservabilityEvent) => void,
|
|
108
|
-
) {
|
|
109
|
-
const client: AuraClientLike = {
|
|
110
|
-
baseUrl,
|
|
111
|
-
projectId: 'p',
|
|
112
|
-
apiKey: 'k',
|
|
113
|
-
fetchFn,
|
|
114
|
-
authStore: store,
|
|
115
|
-
auth: null as unknown as AuraClientLike['auth'],
|
|
116
|
-
...(onEvent ? { onEvent } : {}),
|
|
117
|
-
}
|
|
118
|
-
const svc = new AuthService(client, { autoRefresh: false, persistSession: false })
|
|
119
|
-
;(client as { auth: unknown }).auth = svc
|
|
120
|
-
return { svc, client, store }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** fetchFn servant un JWKS + l'endpoint /user, avec compteur de fetch JWKS. */
|
|
124
|
-
function makeFetch(
|
|
125
|
-
jwks: () => { keys: JsonWebKey[] },
|
|
126
|
-
user: User = fakeUser(),
|
|
127
|
-
opts: { jwksStatus?: number; cacheControl?: string; delayMs?: number } = {},
|
|
128
|
-
) {
|
|
129
|
-
const counts = { jwks: 0, user: 0 }
|
|
130
|
-
const fetchFn = (async (url: string | URL | Request) => {
|
|
131
|
-
const u = String(url)
|
|
132
|
-
if (u.includes('/.well-known/jwks.json')) {
|
|
133
|
-
counts.jwks++
|
|
134
|
-
if (opts.delayMs) await new Promise((r) => setTimeout(r, opts.delayMs))
|
|
135
|
-
const status = opts.jwksStatus ?? 200
|
|
136
|
-
if (status >= 400) return new Response('fail', { status })
|
|
137
|
-
return new Response(JSON.stringify(jwks()), {
|
|
138
|
-
status,
|
|
139
|
-
headers: {
|
|
140
|
-
'Content-Type': 'application/json',
|
|
141
|
-
'Cache-Control': opts.cacheControl ?? 'max-age=600',
|
|
142
|
-
},
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
if (u.endsWith('/user')) {
|
|
146
|
-
counts.user++
|
|
147
|
-
return new Response(JSON.stringify(user), {
|
|
148
|
-
status: 200,
|
|
149
|
-
headers: { 'Content-Type': 'application/json' },
|
|
150
|
-
})
|
|
151
|
-
}
|
|
152
|
-
return new Response('{}', { status: 200 })
|
|
153
|
-
}) as unknown as typeof globalThis.fetch
|
|
154
|
-
return { fetchFn, counts }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// --- tests -------------------------------------------------------------------
|
|
158
|
-
|
|
159
|
-
describe('AuthService.getClaims — vérif JWT locale via JWKS', () => {
|
|
160
|
-
it('vérifie un JWT RS256 valide localement → claims', async () => {
|
|
161
|
-
const { pair, jwk } = await genRs256('k1')
|
|
162
|
-
const token = await signRs256(
|
|
163
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600, role: 'authenticated' },
|
|
164
|
-
pair.privateKey,
|
|
165
|
-
'k1',
|
|
166
|
-
)
|
|
167
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [jwk] }))
|
|
168
|
-
const { svc } = makeAuth('http://rs256-ok.local', fetchFn)
|
|
169
|
-
|
|
170
|
-
const { data, error } = await svc.getClaims(token)
|
|
171
|
-
|
|
172
|
-
expect(error).toBeNull()
|
|
173
|
-
expect(data?.claims.sub).toBe('u1')
|
|
174
|
-
expect(data?.header.alg).toBe('RS256')
|
|
175
|
-
expect(counts.jwks).toBe(1)
|
|
176
|
-
expect(counts.user).toBe(0) // pas de fallback réseau
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('signature falsifiée → error signature_invalid', async () => {
|
|
180
|
-
const { pair, jwk } = await genRs256('k1')
|
|
181
|
-
const token = await signRs256(
|
|
182
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
183
|
-
pair.privateKey,
|
|
184
|
-
'k1',
|
|
185
|
-
)
|
|
186
|
-
// Altère le PREMIER caractère de la signature (le dernier caractère base64url
|
|
187
|
-
// ne porte que des bits de padding inutilisés → décoderait les mêmes octets).
|
|
188
|
-
const parts = token.split('.')
|
|
189
|
-
parts[2] = (parts[2][0] === 'A' ? 'B' : 'A') + parts[2].slice(1)
|
|
190
|
-
const tampered = parts.join('.')
|
|
191
|
-
const { fetchFn } = makeFetch(() => ({ keys: [jwk] }))
|
|
192
|
-
const { svc } = makeAuth('http://rs256-tampered.local', fetchFn)
|
|
193
|
-
|
|
194
|
-
const { data, error } = await svc.getClaims(tampered)
|
|
195
|
-
|
|
196
|
-
expect(data).toBeNull()
|
|
197
|
-
expect((error as { code?: string })?.code).toBe('signature_invalid')
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('token expiré → error token_expired', async () => {
|
|
201
|
-
const { pair, jwk } = await genRs256('k1')
|
|
202
|
-
const token = await signRs256(
|
|
203
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) - 60 },
|
|
204
|
-
pair.privateKey,
|
|
205
|
-
'k1',
|
|
206
|
-
)
|
|
207
|
-
const { fetchFn } = makeFetch(() => ({ keys: [jwk] }))
|
|
208
|
-
const { svc } = makeAuth('http://rs256-expired.local', fetchFn)
|
|
209
|
-
|
|
210
|
-
const { data, error } = await svc.getClaims(token)
|
|
211
|
-
|
|
212
|
-
expect(data).toBeNull()
|
|
213
|
-
expect((error as { code?: string })?.code).toBe('token_expired')
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
it('alg HS256 (token de session) → fallback getUser (mocké)', async () => {
|
|
217
|
-
// Chemin nominal CLIENT : le token HS256 EST celui de la session (store).
|
|
218
|
-
// getClaims() no-arg → usingStoreToken → fallback getUser autorisé.
|
|
219
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [] }), fakeUser('u1'))
|
|
220
|
-
const store = new BaseAuthStore()
|
|
221
|
-
store.save(hs256Jwt('u1'), 'refresh', fakeUser('u1'))
|
|
222
|
-
const { svc } = makeAuth('http://hs256.local', fetchFn, store)
|
|
223
|
-
|
|
224
|
-
const { data, error } = await svc.getClaims()
|
|
225
|
-
|
|
226
|
-
expect(error).toBeNull()
|
|
227
|
-
expect(data?.claims.sub).toBe('u1')
|
|
228
|
-
// alg non supporté → aucune tentative de fetch JWKS, fallback direct getUser.
|
|
229
|
-
expect(counts.jwks).toBe(0)
|
|
230
|
-
expect(counts.user).toBe(1)
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
it('LOT D : token HS256 ÉTRANGER (≠ session) → getUser(foreignToken) valide le token ÉTRANGER (pas le store)', async () => {
|
|
234
|
-
// Le store porte u_store ; on demande les claims d'un token étranger u_foreign
|
|
235
|
-
// (même projet). Avant LOT D : le SDK ne pouvait PAS cibler le token étranger
|
|
236
|
-
// (Authorization toujours écrasé par store.token) → fail-closed unverifiable_token.
|
|
237
|
-
// Après LOT D : getUser(foreignToken) envoie le token ÉTRANGER en Authorization,
|
|
238
|
-
// le backend le valide (isolation projet native) et renvoie SON identité.
|
|
239
|
-
// Preuve anti-misattribution : /user renvoie le sub du token RÉELLEMENT présenté.
|
|
240
|
-
const fetchFn = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
241
|
-
const u = String(url)
|
|
242
|
-
if (u.includes('/.well-known/jwks.json')) {
|
|
243
|
-
return new Response(JSON.stringify({ keys: [] }), {
|
|
244
|
-
status: 200,
|
|
245
|
-
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=600' },
|
|
246
|
-
})
|
|
247
|
-
}
|
|
248
|
-
if (u.endsWith('/user')) {
|
|
249
|
-
// Décode le sub du JWT présenté dans Authorization (le backend validerait
|
|
250
|
-
// réellement la signature ; ici on prouve QUEL token est présenté).
|
|
251
|
-
const auth = (init?.headers as Record<string, string> | undefined)?.['Authorization'] ?? ''
|
|
252
|
-
const jwt = auth.replace(/^Bearer /, '')
|
|
253
|
-
const payload = jwt.split('.')[1] ?? ''
|
|
254
|
-
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(payload.length / 4) * 4, '=')
|
|
255
|
-
const sub = (JSON.parse(atob(b64)) as { sub?: string }).sub ?? 'unknown'
|
|
256
|
-
return new Response(JSON.stringify(fakeUser(sub)), {
|
|
257
|
-
status: 200,
|
|
258
|
-
headers: { 'Content-Type': 'application/json' },
|
|
259
|
-
})
|
|
260
|
-
}
|
|
261
|
-
return new Response('{}', { status: 200 })
|
|
262
|
-
}) as unknown as typeof globalThis.fetch
|
|
263
|
-
const store = new BaseAuthStore()
|
|
264
|
-
store.save(hs256Jwt('u_store'), 'refresh', fakeUser('u_store'))
|
|
265
|
-
const { svc } = makeAuth('http://foreign-token.local', fetchFn, store)
|
|
266
|
-
|
|
267
|
-
const { data, error } = await svc.getClaims(hs256Jwt('u_foreign'))
|
|
268
|
-
|
|
269
|
-
expect(error).toBeNull()
|
|
270
|
-
// Les claims proviennent du token ÉTRANGER validé serveur, PAS du store.
|
|
271
|
-
expect(data?.claims.sub).toBe('u_foreign')
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
it('LOT D : token étranger refusé par le backend (403 autre projet / invalide) → error, pas de fabrication', async () => {
|
|
275
|
-
// Isolation projet native : un token d'un AUTRE projet → 403 côté gateway.
|
|
276
|
-
// getClaims doit remonter l'AuraError, jamais fabriquer des claims.
|
|
277
|
-
const fetchFn = (async (url: string | URL | Request) => {
|
|
278
|
-
const u = String(url)
|
|
279
|
-
if (u.includes('/.well-known/jwks.json')) {
|
|
280
|
-
return new Response(JSON.stringify({ keys: [] }), {
|
|
281
|
-
status: 200,
|
|
282
|
-
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=600' },
|
|
283
|
-
})
|
|
284
|
-
}
|
|
285
|
-
if (u.endsWith('/user')) {
|
|
286
|
-
return new Response(JSON.stringify({ error: { code: 'forbidden', message: 'wrong project' } }), {
|
|
287
|
-
status: 403,
|
|
288
|
-
headers: { 'Content-Type': 'application/json' },
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
return new Response('{}', { status: 200 })
|
|
292
|
-
}) as unknown as typeof globalThis.fetch
|
|
293
|
-
const store = new BaseAuthStore()
|
|
294
|
-
store.save(hs256Jwt('u_store'), 'refresh', fakeUser('u_store'))
|
|
295
|
-
const { svc } = makeAuth('http://foreign-403.local', fetchFn, store)
|
|
296
|
-
|
|
297
|
-
const { data, error } = await svc.getClaims(hs256Jwt('u_foreign'))
|
|
298
|
-
|
|
299
|
-
expect(data).toBeNull()
|
|
300
|
-
expect((error as { status?: number })?.status).toBe(403)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it('LOT D : getUser(jwt) valide le token passé, pas celui du store', async () => {
|
|
304
|
-
const seen: string[] = []
|
|
305
|
-
const fetchFn = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
306
|
-
const u = String(url)
|
|
307
|
-
if (u.endsWith('/user')) {
|
|
308
|
-
const auth = (init?.headers as Record<string, string> | undefined)?.['Authorization'] ?? ''
|
|
309
|
-
seen.push(auth)
|
|
310
|
-
return new Response(JSON.stringify(fakeUser('u_foreign')), {
|
|
311
|
-
status: 200,
|
|
312
|
-
headers: { 'Content-Type': 'application/json' },
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
return new Response('{}', { status: 200 })
|
|
316
|
-
}) as unknown as typeof globalThis.fetch
|
|
317
|
-
const store = new BaseAuthStore()
|
|
318
|
-
store.save(hs256Jwt('u_store'), 'refresh', fakeUser('u_store'))
|
|
319
|
-
const { svc } = makeAuth('http://getuser-jwt.local', fetchFn, store)
|
|
320
|
-
|
|
321
|
-
const { data, error } = await svc.getUser('the-foreign-jwt')
|
|
322
|
-
|
|
323
|
-
expect(error).toBeNull()
|
|
324
|
-
expect(data?.id).toBe('u_foreign')
|
|
325
|
-
expect(seen).toContain('Bearer the-foreign-jwt') // le token PASSÉ, pas le store
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
it('JWKS vide (token de session) → fallback getUser', async () => {
|
|
329
|
-
const { pair } = await genRs256() // token RS256 SANS kid
|
|
330
|
-
const token = await signRs256(
|
|
331
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
332
|
-
pair.privateKey,
|
|
333
|
-
)
|
|
334
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [] }), fakeUser('u1'))
|
|
335
|
-
const store = new BaseAuthStore()
|
|
336
|
-
store.save(token, 'r', fakeUser('u1')) // token de session → fallback autorisé
|
|
337
|
-
const { svc } = makeAuth('http://jwks-empty.local', fetchFn, store)
|
|
338
|
-
|
|
339
|
-
const { data, error } = await svc.getClaims() // no-arg
|
|
340
|
-
|
|
341
|
-
expect(error).toBeNull()
|
|
342
|
-
expect(data?.claims.sub).toBe('u1')
|
|
343
|
-
expect(counts.jwks).toBe(1) // pas de kid → pas de refetch
|
|
344
|
-
expect(counts.user).toBe(1)
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
it('JWK malformé (kty compatible) → error signature_invalid, sans rejet ni fallback', async () => {
|
|
348
|
-
// Token RS256 valide, mais le JWKS sert une clé RSA corrompue (n/e invalides).
|
|
349
|
-
// Le match par kty la sélectionne, puis crypto.subtle.importKey *jette*.
|
|
350
|
-
const { pair } = await genRs256('k1')
|
|
351
|
-
const token = await signRs256(
|
|
352
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
353
|
-
pair.privateKey,
|
|
354
|
-
'k1',
|
|
355
|
-
)
|
|
356
|
-
const badJwk: JsonWebKey = { kty: 'RSA', kid: 'k1', n: '!!!not-base64url!!!', e: 'AQAB', alg: 'RS256' }
|
|
357
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [badJwk] }), fakeUser('u1'))
|
|
358
|
-
const { svc } = makeAuth('http://jwk-malformed.local', fetchFn)
|
|
359
|
-
|
|
360
|
-
// Ne doit jamais rejeter la promesse : le contrat est AuraResponse { data, error }.
|
|
361
|
-
const { data, error } = await svc.getClaims(token)
|
|
362
|
-
|
|
363
|
-
expect(data).toBeNull()
|
|
364
|
-
expect((error as { code?: string })?.code).toBe('signature_invalid')
|
|
365
|
-
expect(counts.user).toBe(0) // fail-closed : pas de fallback getUser sur JWKS corrompu
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
it('kid inconnu → refetch JWKS une fois, puis fallback si toujours absent', async () => {
|
|
369
|
-
const { pair } = await genRs256('unknown-kid')
|
|
370
|
-
const token = await signRs256(
|
|
371
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
372
|
-
pair.privateKey,
|
|
373
|
-
'unknown-kid',
|
|
374
|
-
)
|
|
375
|
-
// JWKS toujours vide → aucune clé ne matchera, ni par kid ni par kty.
|
|
376
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [] }), fakeUser('u1'))
|
|
377
|
-
const store = new BaseAuthStore()
|
|
378
|
-
store.save(token, 'r', fakeUser('u1')) // token de session → fallback autorisé
|
|
379
|
-
const { svc } = makeAuth('http://kid-unknown.local', fetchFn, store)
|
|
380
|
-
|
|
381
|
-
const { data, error } = await svc.getClaims() // no-arg
|
|
382
|
-
|
|
383
|
-
expect(error).toBeNull()
|
|
384
|
-
expect(data?.claims.sub).toBe('u1')
|
|
385
|
-
expect(counts.jwks).toBe(2) // 1 initial + 1 refetch forcé (rotation)
|
|
386
|
-
expect(counts.user).toBe(1) // puis fallback getUser
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
it('rotation RS256 : kid absent mais autre clé RSA présente → fallback (jamais signature_invalid)', async () => {
|
|
390
|
-
// Token signé avec kid='k-old' ; le JWKS ne sert qu'une AUTRE clé RSA valide
|
|
391
|
-
// (kid='k-new'). selectKey borné au kid ne doit PAS sélectionner k-new (sinon
|
|
392
|
-
// verify échoue → signature_invalid SANS fallback). On veut no_matching_key →
|
|
393
|
-
// fallback getUser (le token est le token de session).
|
|
394
|
-
const { pair } = await genRs256('k-old') // clé de signature (k-old)
|
|
395
|
-
const { jwk: jwkNew } = await genRs256('k-new') // 2e paire, clé publique servie
|
|
396
|
-
const token = await signRs256(
|
|
397
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
398
|
-
pair.privateKey,
|
|
399
|
-
'k-old',
|
|
400
|
-
)
|
|
401
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [jwkNew] }), fakeUser('u1'))
|
|
402
|
-
const store = new BaseAuthStore()
|
|
403
|
-
store.save(token, 'r', fakeUser('u1')) // token de session → fallback autorisé
|
|
404
|
-
const { svc } = makeAuth('http://rs256-rotation.local', fetchFn, store)
|
|
405
|
-
|
|
406
|
-
const { data, error } = await svc.getClaims() // no-arg
|
|
407
|
-
|
|
408
|
-
expect(error).toBeNull()
|
|
409
|
-
expect(data?.claims.sub).toBe('u1')
|
|
410
|
-
expect(counts.user).toBe(1) // fallback getUser, PAS signature_invalid
|
|
411
|
-
expect(counts.jwks).toBe(2) // initial + refetch forcé (kid présent introuvable)
|
|
412
|
-
})
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
describe('AuthService.getClaims — robustesse JWKS (LOT 1)', () => {
|
|
416
|
-
it('#1 dédup in-flight : 2 getClaims concurrents cache froid → 1 seul fetch JWKS', async () => {
|
|
417
|
-
const { pair, jwk } = await genRs256('k1')
|
|
418
|
-
const token = await signRs256(
|
|
419
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
420
|
-
pair.privateKey,
|
|
421
|
-
'k1',
|
|
422
|
-
)
|
|
423
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [jwk] }), fakeUser(), { delayMs: 5 })
|
|
424
|
-
const { svc } = makeAuth('http://jwks-dedup.local', fetchFn)
|
|
425
|
-
|
|
426
|
-
const [a, b] = await Promise.all([svc.getClaims(token), svc.getClaims(token)])
|
|
427
|
-
|
|
428
|
-
expect(a.error).toBeNull()
|
|
429
|
-
expect(b.error).toBeNull()
|
|
430
|
-
expect(counts.jwks).toBe(1) // un SEUL appel réseau malgré 2 vérifs concurrentes
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
it('#2 cooldown : 2 tokens à kid inconnu rapprochés → 1 seul refetch forcé', async () => {
|
|
434
|
-
const { pair } = await genRs256('unknown-kid')
|
|
435
|
-
const token = await signRs256(
|
|
436
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
437
|
-
pair.privateKey,
|
|
438
|
-
'unknown-kid',
|
|
439
|
-
)
|
|
440
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [] }), fakeUser('u1'))
|
|
441
|
-
const store = new BaseAuthStore()
|
|
442
|
-
store.save(token, 'r', fakeUser('u1'))
|
|
443
|
-
const { svc } = makeAuth('http://jwks-cooldown.local', fetchFn, store)
|
|
444
|
-
|
|
445
|
-
await svc.getClaims() // 1 initial + 1 refetch forcé = 2
|
|
446
|
-
await svc.getClaims() // initial servi du cache ; refetch forcé bloqué par cooldown
|
|
447
|
-
|
|
448
|
-
// Sans cooldown : 3 (le 2e appel referait un refetch forcé). Avec : borné à 2.
|
|
449
|
-
expect(counts.jwks).toBe(2)
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
it('#3 negative-cache : fetch KO puis re-appel < TTL → pas de 2e réseau', async () => {
|
|
453
|
-
const { pair } = await genRs256('k1')
|
|
454
|
-
const rsToken = await signRs256(
|
|
455
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
456
|
-
pair.privateKey,
|
|
457
|
-
'k1',
|
|
458
|
-
)
|
|
459
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [] }), fakeUser('u1'), { jwksStatus: 500 })
|
|
460
|
-
const store = new BaseAuthStore()
|
|
461
|
-
store.save(rsToken, 'r', fakeUser('u1'))
|
|
462
|
-
const { svc } = makeAuth('http://jwks-negcache.local', fetchFn, store)
|
|
463
|
-
|
|
464
|
-
await svc.getClaims() // fetch réseau → 500 → jwks_fetch_failed → fallback getUser
|
|
465
|
-
await svc.getClaims() // < TTL → negative-cache → PAS de 2e réseau
|
|
466
|
-
|
|
467
|
-
expect(counts.jwks).toBe(1)
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
it('#4 cap max-age : max-age=999999999 plafonné (refetch après la borne haute)', async () => {
|
|
471
|
-
vi.useFakeTimers()
|
|
472
|
-
const { pair, jwk } = await genRs256('k1')
|
|
473
|
-
const token = await signRs256(
|
|
474
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 10_000_000 },
|
|
475
|
-
pair.privateKey,
|
|
476
|
-
'k1',
|
|
477
|
-
)
|
|
478
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [jwk] }), fakeUser(), {
|
|
479
|
-
cacheControl: 'max-age=999999999',
|
|
480
|
-
})
|
|
481
|
-
const { svc } = makeAuth('http://jwks-cap.local', fetchFn)
|
|
482
|
-
|
|
483
|
-
const r1 = await svc.getClaims(token)
|
|
484
|
-
expect(r1.error).toBeNull()
|
|
485
|
-
expect(counts.jwks).toBe(1)
|
|
486
|
-
|
|
487
|
-
// Avance de 25h : au-delà du cap 24h mais très en deçà de 999999999s.
|
|
488
|
-
vi.advanceTimersByTime(25 * 60 * 60 * 1000)
|
|
489
|
-
|
|
490
|
-
const r2 = await svc.getClaims(token)
|
|
491
|
-
expect(r2.error).toBeNull()
|
|
492
|
-
// Sans cap : toujours en cache → 1. Avec cap 24h : cache périmé → refetch → 2.
|
|
493
|
-
expect(counts.jwks).toBe(2)
|
|
494
|
-
})
|
|
495
|
-
|
|
496
|
-
it('#6 sélection no-kid : 2 clés RSA sans kid, token signé par la 2e → succès', async () => {
|
|
497
|
-
const { jwk: jwk1 } = await genRs256() // clé leurre (ne signe pas)
|
|
498
|
-
const { pair: pair2, jwk: jwk2 } = await genRs256() // clé de signature
|
|
499
|
-
const token = await signRs256(
|
|
500
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
501
|
-
pair2.privateKey,
|
|
502
|
-
) // pas de kid
|
|
503
|
-
const { fetchFn } = makeFetch(() => ({ keys: [jwk1, jwk2] }))
|
|
504
|
-
const { svc } = makeAuth('http://jwks-nokid.local', fetchFn)
|
|
505
|
-
|
|
506
|
-
const { data, error } = await svc.getClaims(token)
|
|
507
|
-
|
|
508
|
-
expect(error).toBeNull()
|
|
509
|
-
expect(data?.claims.sub).toBe('u1')
|
|
510
|
-
expect(data?.header.alg).toBe('RS256')
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
it('#7 fallback : alg RÉEL du token (RS256), pas HS256 en dur ; exp non fabriqué', async () => {
|
|
514
|
-
// Vrai token RS256 en session, mais fetch JWKS KO → fallback getUser.
|
|
515
|
-
const { pair } = await genRs256('k1')
|
|
516
|
-
const token = await signRs256(
|
|
517
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
518
|
-
pair.privateKey,
|
|
519
|
-
'k1',
|
|
520
|
-
)
|
|
521
|
-
const { fetchFn } = makeFetch(() => ({ keys: [] }), fakeUser('u1'), { jwksStatus: 503 })
|
|
522
|
-
const store = new BaseAuthStore()
|
|
523
|
-
store.save(token, 'r', fakeUser('u1'))
|
|
524
|
-
const { svc } = makeAuth('http://jwks-fallback-alg.local', fetchFn, store)
|
|
525
|
-
|
|
526
|
-
const { data, error } = await svc.getClaims()
|
|
527
|
-
|
|
528
|
-
expect(error).toBeNull()
|
|
529
|
-
expect(data?.header.alg).toBe('RS256') // alg réel décodé, PAS 'HS256' en dur
|
|
530
|
-
// Honnêteté de type : le fallback ne fabrique pas d'exp/iat.
|
|
531
|
-
expect(data?.claims.exp).toBeUndefined()
|
|
532
|
-
expect(data?.claims.iat).toBeUndefined()
|
|
533
|
-
})
|
|
534
|
-
|
|
535
|
-
it('#7b local RS256 : exp présent (number) sur le chemin de vérif locale', async () => {
|
|
536
|
-
const { pair, jwk } = await genRs256('k1')
|
|
537
|
-
const exp = Math.floor(Date.now() / 1000) + 3600
|
|
538
|
-
const token = await signRs256({ sub: 'u1', exp }, pair.privateKey, 'k1')
|
|
539
|
-
const { fetchFn } = makeFetch(() => ({ keys: [jwk] }))
|
|
540
|
-
const { svc } = makeAuth('http://jwks-local-exp.local', fetchFn)
|
|
541
|
-
|
|
542
|
-
const { data } = await svc.getClaims(token)
|
|
543
|
-
|
|
544
|
-
expect(typeof data?.claims.exp).toBe('number')
|
|
545
|
-
expect(data?.claims.exp).toBe(exp)
|
|
546
|
-
})
|
|
547
|
-
|
|
548
|
-
it('#8 issuer/audience : validés sur le chemin local (RS256)', async () => {
|
|
549
|
-
const { pair, jwk } = await genRs256('k1')
|
|
550
|
-
const token = await signRs256(
|
|
551
|
-
{
|
|
552
|
-
sub: 'u1',
|
|
553
|
-
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
554
|
-
iss: 'https://issuer.a',
|
|
555
|
-
aud: 'aud-1',
|
|
556
|
-
},
|
|
557
|
-
pair.privateKey,
|
|
558
|
-
'k1',
|
|
559
|
-
)
|
|
560
|
-
const { fetchFn } = makeFetch(() => ({ keys: [jwk] }))
|
|
561
|
-
const { svc } = makeAuth('http://jwks-issaud.local', fetchFn)
|
|
562
|
-
|
|
563
|
-
const ok = await svc.getClaims(token, { issuer: 'https://issuer.a', audience: 'aud-1' })
|
|
564
|
-
expect(ok.error).toBeNull()
|
|
565
|
-
expect(ok.data?.claims.sub).toBe('u1')
|
|
566
|
-
|
|
567
|
-
const badIss = await svc.getClaims(token, { issuer: 'https://issuer.b' })
|
|
568
|
-
expect(badIss.data).toBeNull()
|
|
569
|
-
expect((badIss.error as { code?: string })?.code).toBe('invalid_issuer')
|
|
570
|
-
|
|
571
|
-
const badAud = await svc.getClaims(token, { audience: 'aud-2' })
|
|
572
|
-
expect(badAud.data).toBeNull()
|
|
573
|
-
expect((badAud.error as { code?: string })?.code).toBe('invalid_audience')
|
|
574
|
-
})
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
describe('AuthService.getClaims — observabilité jwks-fetch-failed (LOT 3, I8)', () => {
|
|
578
|
-
it('fetch JWKS en échec (500) → onEvent reçoit {kind:"jwks-fetch-failed", baseUrl}', async () => {
|
|
579
|
-
const { pair } = await genRs256('k1')
|
|
580
|
-
const token = await signRs256(
|
|
581
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
582
|
-
pair.privateKey,
|
|
583
|
-
'k1',
|
|
584
|
-
)
|
|
585
|
-
const { fetchFn } = makeFetch(() => ({ keys: [] }), fakeUser('u1'), { jwksStatus: 500 })
|
|
586
|
-
const store = new BaseAuthStore()
|
|
587
|
-
store.save(token, 'r', fakeUser('u1'))
|
|
588
|
-
const events: AuraObservabilityEvent[] = []
|
|
589
|
-
const { svc } = makeAuth('http://jwks-obs-fail.local', fetchFn, store, (e) => events.push(e))
|
|
590
|
-
|
|
591
|
-
const { error } = await svc.getClaims()
|
|
592
|
-
|
|
593
|
-
expect(error).toBeNull() // fallback getUser réussit malgré l'échec JWKS
|
|
594
|
-
expect(events).toContainEqual({
|
|
595
|
-
kind: 'jwks-fetch-failed',
|
|
596
|
-
baseUrl: 'http://jwks-obs-fail.local',
|
|
597
|
-
})
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
it('kid inconnu → refetch forcé qui échoue → onEvent reçoit aussi jwks-fetch-failed', async () => {
|
|
601
|
-
const { pair } = await genRs256('unknown-kid')
|
|
602
|
-
const token = await signRs256(
|
|
603
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
604
|
-
pair.privateKey,
|
|
605
|
-
'unknown-kid',
|
|
606
|
-
)
|
|
607
|
-
// 1er fetch JWKS OK (cache froid, vide) ; le refetch forcé (kid inconnu) échoue.
|
|
608
|
-
let call = 0
|
|
609
|
-
const fetchFn = (async (url: string | URL | Request) => {
|
|
610
|
-
const u = String(url)
|
|
611
|
-
if (u.includes('/.well-known/jwks.json')) {
|
|
612
|
-
call++
|
|
613
|
-
if (call === 1) {
|
|
614
|
-
return new Response(JSON.stringify({ keys: [] }), {
|
|
615
|
-
status: 200,
|
|
616
|
-
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=0' },
|
|
617
|
-
})
|
|
618
|
-
}
|
|
619
|
-
return new Response('fail', { status: 500 })
|
|
620
|
-
}
|
|
621
|
-
if (u.endsWith('/user')) {
|
|
622
|
-
return new Response(JSON.stringify(fakeUser('u1')), {
|
|
623
|
-
status: 200,
|
|
624
|
-
headers: { 'Content-Type': 'application/json' },
|
|
625
|
-
})
|
|
626
|
-
}
|
|
627
|
-
return new Response('{}', { status: 200 })
|
|
628
|
-
}) as unknown as typeof globalThis.fetch
|
|
629
|
-
const store = new BaseAuthStore()
|
|
630
|
-
store.save(token, 'r', fakeUser('u1'))
|
|
631
|
-
const events: AuraObservabilityEvent[] = []
|
|
632
|
-
const { svc } = makeAuth('http://jwks-obs-refetch-fail.local', fetchFn, store, (e) => events.push(e))
|
|
633
|
-
|
|
634
|
-
const { error } = await svc.getClaims()
|
|
635
|
-
|
|
636
|
-
expect(error).toBeNull()
|
|
637
|
-
expect(events).toContainEqual({
|
|
638
|
-
kind: 'jwks-fetch-failed',
|
|
639
|
-
baseUrl: 'http://jwks-obs-refetch-fail.local',
|
|
640
|
-
})
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
it('onEvent absent : aucun throw, comportement getClaims inchangé', async () => {
|
|
644
|
-
const { pair } = await genRs256('k1')
|
|
645
|
-
const token = await signRs256(
|
|
646
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
647
|
-
pair.privateKey,
|
|
648
|
-
'k1',
|
|
649
|
-
)
|
|
650
|
-
const { fetchFn } = makeFetch(() => ({ keys: [] }), fakeUser('u1'), { jwksStatus: 500 })
|
|
651
|
-
const store = new BaseAuthStore()
|
|
652
|
-
store.save(token, 'r', fakeUser('u1'))
|
|
653
|
-
const { svc } = makeAuth('http://jwks-obs-noop.local', fetchFn, store) // pas d'onEvent
|
|
654
|
-
|
|
655
|
-
const { error, data } = await svc.getClaims()
|
|
656
|
-
expect(error).toBeNull()
|
|
657
|
-
expect(data?.claims.sub).toBe('u1')
|
|
658
|
-
})
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
describe('AuthService.getClaims — SEC-F1 : options iss/aud/allowExpired appliquées AU FALLBACK', () => {
|
|
662
|
-
it('audience ≠ aud sur token HS256 (fallback) → invalid_audience (pas un no-op), AVANT tout appel serveur', async () => {
|
|
663
|
-
const now = Math.floor(Date.now() / 1000)
|
|
664
|
-
const token = hs256WithClaims({ sub: 'u1', exp: now + 3600, aud: 'real-aud' })
|
|
665
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [] }), fakeUser('u1'))
|
|
666
|
-
const store = new BaseAuthStore()
|
|
667
|
-
store.save(token, 'r', fakeUser('u1')) // token de session → fallback autorisé
|
|
668
|
-
const { svc } = makeAuth('http://f1-aud.local', fetchFn, store)
|
|
669
|
-
|
|
670
|
-
const { data, error } = await svc.getClaims(token, { audience: 'x' })
|
|
671
|
-
|
|
672
|
-
expect(data).toBeNull()
|
|
673
|
-
expect((error as { code?: string })?.code).toBe('invalid_audience')
|
|
674
|
-
expect(counts.user).toBe(0) // rejeté localement, aucun fallback réseau
|
|
675
|
-
})
|
|
676
|
-
|
|
677
|
-
it('issuer ≠ iss sur token HS256 (fallback) → invalid_issuer', async () => {
|
|
678
|
-
const now = Math.floor(Date.now() / 1000)
|
|
679
|
-
const token = hs256WithClaims({ sub: 'u1', exp: now + 3600, iss: 'https://issuer.a' })
|
|
680
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [] }), fakeUser('u1'))
|
|
681
|
-
const store = new BaseAuthStore()
|
|
682
|
-
store.save(token, 'r', fakeUser('u1'))
|
|
683
|
-
const { svc } = makeAuth('http://f1-iss.local', fetchFn, store)
|
|
684
|
-
|
|
685
|
-
const { data, error } = await svc.getClaims(token, { issuer: 'https://issuer.b' })
|
|
686
|
-
|
|
687
|
-
expect(data).toBeNull()
|
|
688
|
-
expect((error as { code?: string })?.code).toBe('invalid_issuer')
|
|
689
|
-
expect(counts.user).toBe(0)
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
it('iss/aud conformes sur token HS256 (fallback) → fallback getUser OK', async () => {
|
|
693
|
-
const now = Math.floor(Date.now() / 1000)
|
|
694
|
-
const token = hs256WithClaims({ sub: 'u1', exp: now + 3600, iss: 'https://issuer.a', aud: 'aud-1' })
|
|
695
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [] }), fakeUser('u1'))
|
|
696
|
-
const store = new BaseAuthStore()
|
|
697
|
-
store.save(token, 'r', fakeUser('u1'))
|
|
698
|
-
const { svc } = makeAuth('http://f1-ok.local', fetchFn, store)
|
|
699
|
-
|
|
700
|
-
const { data, error } = await svc.getClaims(token, { issuer: 'https://issuer.a', audience: 'aud-1' })
|
|
701
|
-
|
|
702
|
-
expect(error).toBeNull()
|
|
703
|
-
expect(data?.claims.sub).toBe('u1')
|
|
704
|
-
expect(counts.user).toBe(1) // options conformes → le fallback s'exécute
|
|
705
|
-
})
|
|
706
|
-
|
|
707
|
-
it('allowExpired sur token HS256 EXPIRÉ (fallback) → code cohérent allow_expired_unsupported_on_fallback', async () => {
|
|
708
|
-
const now = Math.floor(Date.now() / 1000)
|
|
709
|
-
const token = hs256WithClaims({ sub: 'u1', exp: now - 60 }) // expiré
|
|
710
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [] }), fakeUser('u1'))
|
|
711
|
-
const store = new BaseAuthStore()
|
|
712
|
-
store.save(token, 'r', fakeUser('u1'))
|
|
713
|
-
const { svc } = makeAuth('http://f1-exp.local', fetchFn, store)
|
|
714
|
-
|
|
715
|
-
const { data, error } = await svc.getClaims(token, { allowExpired: true })
|
|
716
|
-
|
|
717
|
-
expect(data).toBeNull()
|
|
718
|
-
expect((error as { code?: string })?.code).toBe('allow_expired_unsupported_on_fallback')
|
|
719
|
-
expect(counts.user).toBe(0) // pas d'appel serveur voué à un 401 trompeur
|
|
720
|
-
})
|
|
721
|
-
})
|
|
722
|
-
|
|
723
|
-
describe('AuthService.getClaims — SEC-F2 : isolation projet sur le chemin LOCAL (RS256)', () => {
|
|
724
|
-
it('token RS256 valide d’un AUTRE projet → rejeté project_isolation_violation', async () => {
|
|
725
|
-
const { pair, jwk } = await genRs256('k1')
|
|
726
|
-
const token = await signRs256(
|
|
727
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600, project_id: 'other-project' },
|
|
728
|
-
pair.privateKey,
|
|
729
|
-
'k1',
|
|
730
|
-
)
|
|
731
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [jwk] }))
|
|
732
|
-
// makeAuth : projectId = 'p'.
|
|
733
|
-
const { svc } = makeAuth('http://f2-iso.local', fetchFn)
|
|
734
|
-
|
|
735
|
-
const { data, error } = await svc.getClaims(token)
|
|
736
|
-
|
|
737
|
-
expect(data).toBeNull()
|
|
738
|
-
expect((error as { code?: string })?.code).toBe('project_isolation_violation')
|
|
739
|
-
expect(counts.user).toBe(0) // rejeté localement, pas de fabrication ni fallback
|
|
740
|
-
})
|
|
741
|
-
|
|
742
|
-
it('token RS256 du BON projet (project_id === client.projectId) → accepté', async () => {
|
|
743
|
-
const { pair, jwk } = await genRs256('k1')
|
|
744
|
-
const token = await signRs256(
|
|
745
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600, project_id: 'p' },
|
|
746
|
-
pair.privateKey,
|
|
747
|
-
'k1',
|
|
748
|
-
)
|
|
749
|
-
const { fetchFn } = makeFetch(() => ({ keys: [jwk] }))
|
|
750
|
-
const { svc } = makeAuth('http://f2-ok.local', fetchFn)
|
|
751
|
-
|
|
752
|
-
const { data, error } = await svc.getClaims(token)
|
|
753
|
-
|
|
754
|
-
expect(error).toBeNull()
|
|
755
|
-
expect(data?.claims.project_id).toBe('p')
|
|
756
|
-
})
|
|
757
|
-
})
|
|
758
|
-
|
|
759
|
-
describe('AuthService.getClaims — SEC-F4 : cache JWKS CLIENT-SCOPED (WeakMap)', () => {
|
|
760
|
-
it('deux clients distincts (même baseUrl) → caches JWKS ISOLÉS (chacun fetch le sien)', async () => {
|
|
761
|
-
const { pair, jwk } = await genRs256('k1')
|
|
762
|
-
const token = await signRs256(
|
|
763
|
-
{ sub: 'u1', exp: Math.floor(Date.now() / 1000) + 3600 },
|
|
764
|
-
pair.privateKey,
|
|
765
|
-
'k1',
|
|
766
|
-
)
|
|
767
|
-
// Un SEUL fetchFn/baseUrl partagé, mais DEUX clients (services) distincts.
|
|
768
|
-
const { fetchFn, counts } = makeFetch(() => ({ keys: [jwk] }))
|
|
769
|
-
const { svc: a } = makeAuth('http://f4-shared.local', fetchFn, new BaseAuthStore())
|
|
770
|
-
const { svc: b } = makeAuth('http://f4-shared.local', fetchFn, new BaseAuthStore())
|
|
771
|
-
|
|
772
|
-
const ra = await a.getClaims(token)
|
|
773
|
-
const rb = await b.getClaims(token)
|
|
774
|
-
|
|
775
|
-
expect(ra.error).toBeNull()
|
|
776
|
-
expect(rb.error).toBeNull()
|
|
777
|
-
// Cache global → 1 seul fetch. Client-scoped → chaque client fetch le sien = 2.
|
|
778
|
-
expect(counts.jwks).toBe(2)
|
|
779
|
-
})
|
|
780
|
-
})
|