@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 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
- await aura.auth.revokeSession(sessions.data![0].id)
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!.url
69
- console.log(data!.url)
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
- const res = await fetchFn(`${baseUrl}/.well-known/jwks.json`);
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 res.json();
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
- const res = await fetchFn(`${baseUrl}/.well-known/jwks.json`);
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 res.json();
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.0",
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.0"
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
  }