@aurabase/auth 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/index.cjs +45 -10
- package/dist/index.js +46 -11
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -48,7 +48,10 @@ const refreshed = await aura.auth.refreshSession()
|
|
|
48
48
|
await aura.auth.setSession('stored-access-token', 'stored-refresh-token')
|
|
49
49
|
|
|
50
50
|
const sessions = await aura.auth.listSessions()
|
|
51
|
-
|
|
51
|
+
// `data` peut être `null` même en succès (204 / corps vide) : on le teste plutôt
|
|
52
|
+
// que `error`, ce qui évite l'assertion `!` et fait narrower TypeScript.
|
|
53
|
+
const first = sessions.data?.[0]
|
|
54
|
+
if (first) await aura.auth.revokeSession(first.id)
|
|
52
55
|
await aura.auth.revokeAllSessions()
|
|
53
56
|
```
|
|
54
57
|
|
|
@@ -64,9 +67,10 @@ const { data, error } = await aura.auth.signInWithOAuth('github', {
|
|
|
64
67
|
})
|
|
65
68
|
|
|
66
69
|
if (error) throw error
|
|
70
|
+
if (!data) throw new Error("aucune URL de redirection renvoyée")
|
|
67
71
|
|
|
68
|
-
// Côté serveur / SSR : pas de redirection automatique, il faut rediriger manuellement vers data
|
|
69
|
-
console.log(data
|
|
72
|
+
// Côté serveur / SSR : pas de redirection automatique, il faut rediriger manuellement vers data.url
|
|
73
|
+
console.log(data.url)
|
|
70
74
|
```
|
|
71
75
|
|
|
72
76
|
Retour du provider (flux PKCE) :
|
package/dist/index.cjs
CHANGED
|
@@ -116,14 +116,30 @@ function parseMaxAge(cc) {
|
|
|
116
116
|
const m = cc?.match(/max-age=(\d+)/);
|
|
117
117
|
return m ? Number(m[1]) * 1e3 : null;
|
|
118
118
|
}
|
|
119
|
-
function fetchJwksDeduped(state, fetchFn, baseUrl) {
|
|
119
|
+
function fetchJwksDeduped(state, fetchFn, baseUrl, timeoutMs) {
|
|
120
120
|
const existing = state.inflight.get(baseUrl);
|
|
121
121
|
if (existing) return existing;
|
|
122
122
|
const p = (async () => {
|
|
123
|
+
let timer;
|
|
124
|
+
let bodyTimer;
|
|
123
125
|
try {
|
|
124
|
-
|
|
126
|
+
let signal;
|
|
127
|
+
if (timeoutMs > 0 && typeof AbortController !== "undefined") {
|
|
128
|
+
const ctrl = new AbortController();
|
|
129
|
+
timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
130
|
+
timer.unref?.();
|
|
131
|
+
signal = ctrl.signal;
|
|
132
|
+
}
|
|
133
|
+
const res = await fetchFn(`${baseUrl}/.well-known/jwks.json`, signal ? { signal } : {});
|
|
125
134
|
if (!res.ok) throw new Error("jwks_fetch_failed");
|
|
126
|
-
const body = await
|
|
135
|
+
const body = await Promise.race([
|
|
136
|
+
res.json(),
|
|
137
|
+
new Promise((_resolve, reject) => {
|
|
138
|
+
if (!(timeoutMs > 0)) return;
|
|
139
|
+
bodyTimer = setTimeout(() => reject(new Error("jwks_body_timeout")), timeoutMs);
|
|
140
|
+
bodyTimer.unref?.();
|
|
141
|
+
})
|
|
142
|
+
]);
|
|
127
143
|
const parsed = parseMaxAge(res.headers.get("Cache-Control")) ?? DEFAULT_MAX_AGE_MS;
|
|
128
144
|
const maxAge = Math.min(parsed, MAX_AGE_CAP_MS);
|
|
129
145
|
const keys = body.keys ?? [];
|
|
@@ -133,12 +149,15 @@ function fetchJwksDeduped(state, fetchFn, baseUrl) {
|
|
|
133
149
|
} catch {
|
|
134
150
|
state.lastFailedAt.set(baseUrl, Date.now());
|
|
135
151
|
throw new Error("jwks_fetch_failed");
|
|
152
|
+
} finally {
|
|
153
|
+
if (timer) clearTimeout(timer);
|
|
154
|
+
if (bodyTimer) clearTimeout(bodyTimer);
|
|
136
155
|
}
|
|
137
156
|
})().finally(() => state.inflight.delete(baseUrl));
|
|
138
157
|
state.inflight.set(baseUrl, p);
|
|
139
158
|
return p;
|
|
140
159
|
}
|
|
141
|
-
async function loadJwks(state, fetchFn, baseUrl) {
|
|
160
|
+
async function loadJwks(state, fetchFn, baseUrl, timeoutMs) {
|
|
142
161
|
const now = Date.now();
|
|
143
162
|
const c = state.cache.get(baseUrl);
|
|
144
163
|
if (c && now - c.fetchedAt < c.maxAgeMs) return c.keys;
|
|
@@ -146,9 +165,9 @@ async function loadJwks(state, fetchFn, baseUrl) {
|
|
|
146
165
|
if (failedAt !== void 0 && now - failedAt < NEGATIVE_CACHE_TTL_MS) {
|
|
147
166
|
throw new Error("jwks_fetch_failed");
|
|
148
167
|
}
|
|
149
|
-
return fetchJwksDeduped(state, fetchFn, baseUrl);
|
|
168
|
+
return fetchJwksDeduped(state, fetchFn, baseUrl, timeoutMs);
|
|
150
169
|
}
|
|
151
|
-
async function forcedRefetchJwks(state, fetchFn, baseUrl) {
|
|
170
|
+
async function forcedRefetchJwks(state, fetchFn, baseUrl, timeoutMs) {
|
|
152
171
|
const now = Date.now();
|
|
153
172
|
const last = state.lastForcedFetchAt.get(baseUrl);
|
|
154
173
|
if (last !== void 0 && now - last < FORCED_REFETCH_COOLDOWN_MS) return null;
|
|
@@ -157,7 +176,7 @@ async function forcedRefetchJwks(state, fetchFn, baseUrl) {
|
|
|
157
176
|
if (failedAt !== void 0 && now - failedAt < NEGATIVE_CACHE_TTL_MS) {
|
|
158
177
|
throw new Error("jwks_fetch_failed");
|
|
159
178
|
}
|
|
160
|
-
return fetchJwksDeduped(state, fetchFn, baseUrl);
|
|
179
|
+
return fetchJwksDeduped(state, fetchFn, baseUrl, timeoutMs);
|
|
161
180
|
}
|
|
162
181
|
function algParams(alg) {
|
|
163
182
|
if (alg === "RS256") {
|
|
@@ -184,11 +203,12 @@ async function verifyJwtLocal(scope, fetchFn, baseUrl, jwt, opts = {}) {
|
|
|
184
203
|
const params = algParams(d.header.alg);
|
|
185
204
|
if (!params) return { ok: false, code: "unsupported_alg" };
|
|
186
205
|
const state = stateFor(scope);
|
|
206
|
+
const timeoutMs = opts.timeoutMs ?? 0;
|
|
187
207
|
const matchesAlg = (k) => d.header.alg === "RS256" ? k.kty === "RSA" : k.kty === "EC";
|
|
188
208
|
const candidateKeys = (keys2) => d.header.kid ? keys2.filter((k) => k.kid === d.header.kid && matchesAlg(k)) : keys2.filter((k) => matchesAlg(k));
|
|
189
209
|
let keys;
|
|
190
210
|
try {
|
|
191
|
-
keys = await loadJwks(state, fetchFn, baseUrl);
|
|
211
|
+
keys = await loadJwks(state, fetchFn, baseUrl, timeoutMs);
|
|
192
212
|
} catch {
|
|
193
213
|
try {
|
|
194
214
|
opts.onEvent?.(baseUrl);
|
|
@@ -200,7 +220,7 @@ async function verifyJwtLocal(scope, fetchFn, baseUrl, jwt, opts = {}) {
|
|
|
200
220
|
if (candidates.length === 0 && d.header.kid) {
|
|
201
221
|
let refetched;
|
|
202
222
|
try {
|
|
203
|
-
refetched = await forcedRefetchJwks(state, fetchFn, baseUrl);
|
|
223
|
+
refetched = await forcedRefetchJwks(state, fetchFn, baseUrl, timeoutMs);
|
|
204
224
|
} catch {
|
|
205
225
|
try {
|
|
206
226
|
opts.onEvent?.(baseUrl);
|
|
@@ -766,7 +786,11 @@ var AuthService = class extends core.BaseService {
|
|
|
766
786
|
allowExpired: options?.allowExpired,
|
|
767
787
|
// LOT 3 (I8) : relie le callback générique de jwks.ts au hook d'observabilité
|
|
768
788
|
// du client (onEvent/debug). jwks.ts reste sans dépendance vers @aurabase/core.
|
|
769
|
-
onEvent: (baseUrl) => core.emitAuraEvent(this.client.onEvent, this.client.debug, { kind: "jwks-fetch-failed", baseUrl })
|
|
789
|
+
onEvent: (baseUrl) => core.emitAuraEvent(this.client.onEvent, this.client.debug, { kind: "jwks-fetch-failed", baseUrl }),
|
|
790
|
+
// P2-1 : borne le fetch JWKS (et la lecture de son corps) sur le même
|
|
791
|
+
// budget que les autres requêtes du client. Sans elle, un serveur JWKS
|
|
792
|
+
// muet figeait `getClaims` définitivement (single-flight `inflight`).
|
|
793
|
+
timeoutMs: this.client.requestTimeoutMs ?? core.DEFAULT_REQUEST_TIMEOUT_MS
|
|
770
794
|
});
|
|
771
795
|
const checkIssAud = (c) => {
|
|
772
796
|
if (options?.issuer !== void 0 && c.iss !== options.issuer) {
|
|
@@ -839,6 +863,17 @@ var AuthService = class extends core.BaseService {
|
|
|
839
863
|
error: null
|
|
840
864
|
};
|
|
841
865
|
}
|
|
866
|
+
if (u.error === null) {
|
|
867
|
+
return {
|
|
868
|
+
data: null,
|
|
869
|
+
error: new core.AuraError(0, {
|
|
870
|
+
error: {
|
|
871
|
+
code: "invalid_response",
|
|
872
|
+
message: "getClaims: la validation serveur du token (getUser) a r\xE9pondu sans contenu \u2014 claims ind\xE9terminables"
|
|
873
|
+
}
|
|
874
|
+
})
|
|
875
|
+
};
|
|
876
|
+
}
|
|
842
877
|
return { data: null, error: u.error };
|
|
843
878
|
}
|
|
844
879
|
return {
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseService, isTokenExpired, AuraError, refreshOnce, emitAuraEvent } from '@aurabase/core';
|
|
1
|
+
import { BaseService, isTokenExpired, AuraError, refreshOnce, DEFAULT_REQUEST_TIMEOUT_MS, emitAuraEvent } from '@aurabase/core';
|
|
2
2
|
|
|
3
3
|
// src/AuthService.ts
|
|
4
4
|
|
|
@@ -114,14 +114,30 @@ function parseMaxAge(cc) {
|
|
|
114
114
|
const m = cc?.match(/max-age=(\d+)/);
|
|
115
115
|
return m ? Number(m[1]) * 1e3 : null;
|
|
116
116
|
}
|
|
117
|
-
function fetchJwksDeduped(state, fetchFn, baseUrl) {
|
|
117
|
+
function fetchJwksDeduped(state, fetchFn, baseUrl, timeoutMs) {
|
|
118
118
|
const existing = state.inflight.get(baseUrl);
|
|
119
119
|
if (existing) return existing;
|
|
120
120
|
const p = (async () => {
|
|
121
|
+
let timer;
|
|
122
|
+
let bodyTimer;
|
|
121
123
|
try {
|
|
122
|
-
|
|
124
|
+
let signal;
|
|
125
|
+
if (timeoutMs > 0 && typeof AbortController !== "undefined") {
|
|
126
|
+
const ctrl = new AbortController();
|
|
127
|
+
timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
128
|
+
timer.unref?.();
|
|
129
|
+
signal = ctrl.signal;
|
|
130
|
+
}
|
|
131
|
+
const res = await fetchFn(`${baseUrl}/.well-known/jwks.json`, signal ? { signal } : {});
|
|
123
132
|
if (!res.ok) throw new Error("jwks_fetch_failed");
|
|
124
|
-
const body = await
|
|
133
|
+
const body = await Promise.race([
|
|
134
|
+
res.json(),
|
|
135
|
+
new Promise((_resolve, reject) => {
|
|
136
|
+
if (!(timeoutMs > 0)) return;
|
|
137
|
+
bodyTimer = setTimeout(() => reject(new Error("jwks_body_timeout")), timeoutMs);
|
|
138
|
+
bodyTimer.unref?.();
|
|
139
|
+
})
|
|
140
|
+
]);
|
|
125
141
|
const parsed = parseMaxAge(res.headers.get("Cache-Control")) ?? DEFAULT_MAX_AGE_MS;
|
|
126
142
|
const maxAge = Math.min(parsed, MAX_AGE_CAP_MS);
|
|
127
143
|
const keys = body.keys ?? [];
|
|
@@ -131,12 +147,15 @@ function fetchJwksDeduped(state, fetchFn, baseUrl) {
|
|
|
131
147
|
} catch {
|
|
132
148
|
state.lastFailedAt.set(baseUrl, Date.now());
|
|
133
149
|
throw new Error("jwks_fetch_failed");
|
|
150
|
+
} finally {
|
|
151
|
+
if (timer) clearTimeout(timer);
|
|
152
|
+
if (bodyTimer) clearTimeout(bodyTimer);
|
|
134
153
|
}
|
|
135
154
|
})().finally(() => state.inflight.delete(baseUrl));
|
|
136
155
|
state.inflight.set(baseUrl, p);
|
|
137
156
|
return p;
|
|
138
157
|
}
|
|
139
|
-
async function loadJwks(state, fetchFn, baseUrl) {
|
|
158
|
+
async function loadJwks(state, fetchFn, baseUrl, timeoutMs) {
|
|
140
159
|
const now = Date.now();
|
|
141
160
|
const c = state.cache.get(baseUrl);
|
|
142
161
|
if (c && now - c.fetchedAt < c.maxAgeMs) return c.keys;
|
|
@@ -144,9 +163,9 @@ async function loadJwks(state, fetchFn, baseUrl) {
|
|
|
144
163
|
if (failedAt !== void 0 && now - failedAt < NEGATIVE_CACHE_TTL_MS) {
|
|
145
164
|
throw new Error("jwks_fetch_failed");
|
|
146
165
|
}
|
|
147
|
-
return fetchJwksDeduped(state, fetchFn, baseUrl);
|
|
166
|
+
return fetchJwksDeduped(state, fetchFn, baseUrl, timeoutMs);
|
|
148
167
|
}
|
|
149
|
-
async function forcedRefetchJwks(state, fetchFn, baseUrl) {
|
|
168
|
+
async function forcedRefetchJwks(state, fetchFn, baseUrl, timeoutMs) {
|
|
150
169
|
const now = Date.now();
|
|
151
170
|
const last = state.lastForcedFetchAt.get(baseUrl);
|
|
152
171
|
if (last !== void 0 && now - last < FORCED_REFETCH_COOLDOWN_MS) return null;
|
|
@@ -155,7 +174,7 @@ async function forcedRefetchJwks(state, fetchFn, baseUrl) {
|
|
|
155
174
|
if (failedAt !== void 0 && now - failedAt < NEGATIVE_CACHE_TTL_MS) {
|
|
156
175
|
throw new Error("jwks_fetch_failed");
|
|
157
176
|
}
|
|
158
|
-
return fetchJwksDeduped(state, fetchFn, baseUrl);
|
|
177
|
+
return fetchJwksDeduped(state, fetchFn, baseUrl, timeoutMs);
|
|
159
178
|
}
|
|
160
179
|
function algParams(alg) {
|
|
161
180
|
if (alg === "RS256") {
|
|
@@ -182,11 +201,12 @@ async function verifyJwtLocal(scope, fetchFn, baseUrl, jwt, opts = {}) {
|
|
|
182
201
|
const params = algParams(d.header.alg);
|
|
183
202
|
if (!params) return { ok: false, code: "unsupported_alg" };
|
|
184
203
|
const state = stateFor(scope);
|
|
204
|
+
const timeoutMs = opts.timeoutMs ?? 0;
|
|
185
205
|
const matchesAlg = (k) => d.header.alg === "RS256" ? k.kty === "RSA" : k.kty === "EC";
|
|
186
206
|
const candidateKeys = (keys2) => d.header.kid ? keys2.filter((k) => k.kid === d.header.kid && matchesAlg(k)) : keys2.filter((k) => matchesAlg(k));
|
|
187
207
|
let keys;
|
|
188
208
|
try {
|
|
189
|
-
keys = await loadJwks(state, fetchFn, baseUrl);
|
|
209
|
+
keys = await loadJwks(state, fetchFn, baseUrl, timeoutMs);
|
|
190
210
|
} catch {
|
|
191
211
|
try {
|
|
192
212
|
opts.onEvent?.(baseUrl);
|
|
@@ -198,7 +218,7 @@ async function verifyJwtLocal(scope, fetchFn, baseUrl, jwt, opts = {}) {
|
|
|
198
218
|
if (candidates.length === 0 && d.header.kid) {
|
|
199
219
|
let refetched;
|
|
200
220
|
try {
|
|
201
|
-
refetched = await forcedRefetchJwks(state, fetchFn, baseUrl);
|
|
221
|
+
refetched = await forcedRefetchJwks(state, fetchFn, baseUrl, timeoutMs);
|
|
202
222
|
} catch {
|
|
203
223
|
try {
|
|
204
224
|
opts.onEvent?.(baseUrl);
|
|
@@ -764,7 +784,11 @@ var AuthService = class extends BaseService {
|
|
|
764
784
|
allowExpired: options?.allowExpired,
|
|
765
785
|
// LOT 3 (I8) : relie le callback générique de jwks.ts au hook d'observabilité
|
|
766
786
|
// du client (onEvent/debug). jwks.ts reste sans dépendance vers @aurabase/core.
|
|
767
|
-
onEvent: (baseUrl) => emitAuraEvent(this.client.onEvent, this.client.debug, { kind: "jwks-fetch-failed", baseUrl })
|
|
787
|
+
onEvent: (baseUrl) => emitAuraEvent(this.client.onEvent, this.client.debug, { kind: "jwks-fetch-failed", baseUrl }),
|
|
788
|
+
// P2-1 : borne le fetch JWKS (et la lecture de son corps) sur le même
|
|
789
|
+
// budget que les autres requêtes du client. Sans elle, un serveur JWKS
|
|
790
|
+
// muet figeait `getClaims` définitivement (single-flight `inflight`).
|
|
791
|
+
timeoutMs: this.client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
768
792
|
});
|
|
769
793
|
const checkIssAud = (c) => {
|
|
770
794
|
if (options?.issuer !== void 0 && c.iss !== options.issuer) {
|
|
@@ -837,6 +861,17 @@ var AuthService = class extends BaseService {
|
|
|
837
861
|
error: null
|
|
838
862
|
};
|
|
839
863
|
}
|
|
864
|
+
if (u.error === null) {
|
|
865
|
+
return {
|
|
866
|
+
data: null,
|
|
867
|
+
error: new AuraError(0, {
|
|
868
|
+
error: {
|
|
869
|
+
code: "invalid_response",
|
|
870
|
+
message: "getClaims: la validation serveur du token (getUser) a r\xE9pondu sans contenu \u2014 claims ind\xE9terminables"
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
};
|
|
874
|
+
}
|
|
840
875
|
return { data: null, error: u.error };
|
|
841
876
|
}
|
|
842
877
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurabase/auth",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Service d'authentification du SDK Aurabase (sessions, OAuth/PKCE, MFA, magic link, OTP, admin)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -46,9 +46,10 @@
|
|
|
46
46
|
"authentication"
|
|
47
47
|
],
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@aurabase/core": "0.4.
|
|
49
|
+
"@aurabase/core": "0.4.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@types/node": "20.14.15",
|
|
52
53
|
"tsup": "^8.0.0",
|
|
53
54
|
"typescript": "^5.5.0",
|
|
54
55
|
"vitest": "^4.1.6"
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"scripts": {
|
|
57
58
|
"build": "tsup",
|
|
58
59
|
"test": "vitest run --passWithNoTests",
|
|
59
|
-
"typecheck": "tsc --noEmit"
|
|
60
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
61
|
+
"typecheck:tests": "tsc --noEmit -p tsconfig.test.json"
|
|
60
62
|
}
|
|
61
63
|
}
|