@aurabase/auth 0.4.0 → 0.5.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/dist/index.js CHANGED
@@ -1,75 +1,109 @@
1
- import { BaseService, isTokenExpired, AuraError, refreshOnce, emitAuraEvent } from '@aurabase/core';
1
+ import { BaseService, isTokenExpired, clientScope, AuraError, refreshOnce, DEFAULT_REQUEST_TIMEOUT_MS, emitAuraEvent } from '@aurabase/core';
2
2
 
3
- // src/AuthService.ts
4
-
5
- // src/SessionManager.ts
6
- var SessionManager = class {
7
- opts;
8
- _interval = null;
9
- _channel = null;
10
- _active = false;
11
- _visHandler = null;
12
- constructor(opts) {
13
- this.opts = { tickMs: 3e4, ...opts };
3
+ // src/AuthAdminService.ts
4
+ var AuthAdminService = class extends BaseService {
5
+ /**
6
+ * Préfixe des routes de ce service. Aucun identifiant de projet : il est injecté par
7
+ * la passerelle depuis le sous-domaine du projet (`inject_project_id`,
8
+ * `services/aura-gateway/src/middleware/project_subdomain.rs`).
9
+ */
10
+ get prefix() {
11
+ return "/v1/auth/admin";
14
12
  }
15
- get isActive() {
16
- return this._active;
13
+ // -- Users ----------------------------------------------------------------
14
+ async listUsers(params) {
15
+ const query = new URLSearchParams();
16
+ if (params?.limit !== void 0) query.set("limit", String(params.limit));
17
+ if (params?.offset !== void 0) query.set("offset", String(params.offset));
18
+ if (params?.search) query.set("search", params.search);
19
+ const qs = query.toString();
20
+ return this.request("GET", `${this.prefix}/users${qs ? "?" + qs : ""}`);
17
21
  }
18
- start() {
19
- if (!this.opts.enabled) return;
20
- if (typeof BroadcastChannel !== "undefined" && !this._channel) {
21
- this._channel = new BroadcastChannel(this.opts.channelName);
22
- this._channel.onmessage = (e) => this.opts.onRemoteMessage(e.data);
23
- }
24
- if (typeof document !== "undefined" && !this._visHandler) {
25
- this._visHandler = () => {
26
- if (document.hidden) {
27
- this._stopInterval();
28
- } else if (this._active) {
29
- this._startInterval();
30
- if (this.opts.isRefreshDue()) this.opts.onTick();
31
- }
32
- };
33
- document.addEventListener("visibilitychange", this._visHandler);
34
- }
22
+ async getUser(userId) {
23
+ return this.request("GET", `${this.prefix}/users/${userId}`);
35
24
  }
36
- setActive(active) {
37
- this._active = active;
38
- if (!this.opts.enabled) return;
39
- const hidden = typeof document !== "undefined" && document.hidden;
40
- if (active && !hidden) this._startInterval();
41
- else this._stopInterval();
25
+ async createUser(data) {
26
+ return this.request("POST", `${this.prefix}/users`, data);
42
27
  }
43
- broadcast(data) {
44
- this._channel?.postMessage(data);
28
+ async updateUser(userId, data) {
29
+ return this.request("PUT", `${this.prefix}/users/${userId}`, data);
45
30
  }
46
- stop() {
47
- this._stopInterval();
48
- this._active = false;
49
- if (this._channel) {
50
- try {
51
- this._channel.close();
52
- } catch {
53
- }
54
- this._channel = null;
55
- }
56
- if (this._visHandler && typeof document !== "undefined") {
57
- document.removeEventListener("visibilitychange", this._visHandler);
58
- this._visHandler = null;
59
- }
31
+ async deleteUser(userId) {
32
+ return this.request("DELETE", `${this.prefix}/users/${userId}`);
60
33
  }
61
- _startInterval() {
62
- if (this._interval) return;
63
- this._interval = setInterval(() => {
64
- if (this.opts.isRefreshDue()) this.opts.onTick();
65
- }, this.opts.tickMs);
66
- this._interval.unref?.();
34
+ async banUser(userId) {
35
+ return this.request("POST", `${this.prefix}/users/${userId}/ban`, {});
67
36
  }
68
- _stopInterval() {
69
- if (this._interval) {
70
- clearInterval(this._interval);
71
- this._interval = null;
72
- }
37
+ async unbanUser(userId) {
38
+ return this.request("POST", `${this.prefix}/users/${userId}/unban`, {});
39
+ }
40
+ async generateLink(userId, type) {
41
+ return this.request("POST", `${this.prefix}/users/${userId}/generate-link`, { type });
42
+ }
43
+ async getUserStats() {
44
+ return this.request("GET", `${this.prefix}/users/stats`);
45
+ }
46
+ async listUserIdentities(userId) {
47
+ return this.request("GET", `${this.prefix}/users/${userId}/identities`);
48
+ }
49
+ // -- Sessions ---------------------------------------------------------------
50
+ async listSessions(params) {
51
+ const query = new URLSearchParams();
52
+ if (params?.userId !== void 0) query.set("user_id", params.userId);
53
+ if (params?.limit !== void 0) query.set("limit", String(params.limit));
54
+ if (params?.offset !== void 0) query.set("offset", String(params.offset));
55
+ const qs = query.toString();
56
+ return this.request("GET", `${this.prefix}/sessions${qs ? "?" + qs : ""}`);
57
+ }
58
+ async revokeSession(sessionId) {
59
+ return this.request("DELETE", `${this.prefix}/sessions/${sessionId}`);
60
+ }
61
+ // -- Audit log ------------------------------------------------------------
62
+ async getAuditLog(params) {
63
+ const query = new URLSearchParams();
64
+ if (params?.userId !== void 0) query.set("user_id", params.userId);
65
+ if (params?.limit !== void 0) query.set("limit", String(params.limit));
66
+ if (params?.offset !== void 0) query.set("offset", String(params.offset));
67
+ const qs = query.toString();
68
+ return this.request("GET", `${this.prefix}/audit-log${qs ? "?" + qs : ""}`);
69
+ }
70
+ // -- Settings -------------------------------------------------------------
71
+ async getSettings() {
72
+ return this.request("GET", `${this.prefix}/settings`);
73
+ }
74
+ /**
75
+ * Met à jour les réglages auth du projet (fusion partielle : seuls les champs fournis
76
+ * sont écrits, `COALESCE` côté backend).
77
+ *
78
+ * Le corps est un {@link AuthSettingsUpdate}, PAS un `Partial<AuthSettings>` : la vue de
79
+ * lecture ne sait pas porter les secrets write-only (`smtp_password`, `webhook_secret`,
80
+ * `captcha_secret_key`, `sms_api_key`/`sms_api_secret`, `hook_secret`) ni les drapeaux
81
+ * d'effacement (`smtp_clear`, `sms_clear`, `oauth_credentials_clear`), et porte à l'inverse
82
+ * des projections (`smtp_password_set`, `hook_secret_set`, `updated_at`…) que le backend
83
+ * REFUSE.
84
+ *
85
+ * Le serveur refuse aussi bien un champ inconnu qu'un `null` explicite — `400` dans les deux
86
+ * cas, le champ fautif est nommé. Ce n'est plus seulement un contrôle TypeScript : ne
87
+ * renvoyez donc pas tel quel un objet lu par {@link getSettings}.
88
+ */
89
+ async updateSettings(data) {
90
+ return this.request("PUT", `${this.prefix}/settings`, data);
91
+ }
92
+ // -- Email templates ------------------------------------------------------
93
+ async listEmailTemplates() {
94
+ return this.request("GET", `${this.prefix}/email-templates`);
95
+ }
96
+ async getEmailTemplate(type) {
97
+ return this.request("GET", `${this.prefix}/email-templates/${type}`);
98
+ }
99
+ async updateEmailTemplate(type, data) {
100
+ return this.request("PUT", `${this.prefix}/email-templates/${type}`, data);
101
+ }
102
+ async deleteEmailTemplate(type) {
103
+ return this.request("DELETE", `${this.prefix}/email-templates/${type}`);
104
+ }
105
+ async testEmailTemplate(type, body) {
106
+ return this.request("POST", `${this.prefix}/email-templates/${type}/test`, body);
73
107
  }
74
108
  };
75
109
 
@@ -114,14 +148,30 @@ function parseMaxAge(cc) {
114
148
  const m = cc?.match(/max-age=(\d+)/);
115
149
  return m ? Number(m[1]) * 1e3 : null;
116
150
  }
117
- function fetchJwksDeduped(state, fetchFn, baseUrl) {
151
+ function fetchJwksDeduped(state, fetchFn, baseUrl, timeoutMs) {
118
152
  const existing = state.inflight.get(baseUrl);
119
153
  if (existing) return existing;
120
154
  const p = (async () => {
155
+ let timer;
156
+ let bodyTimer;
121
157
  try {
122
- const res = await fetchFn(`${baseUrl}/.well-known/jwks.json`);
158
+ let signal;
159
+ if (timeoutMs > 0 && typeof AbortController !== "undefined") {
160
+ const ctrl = new AbortController();
161
+ timer = setTimeout(() => ctrl.abort(), timeoutMs);
162
+ timer.unref?.();
163
+ signal = ctrl.signal;
164
+ }
165
+ const res = await fetchFn(`${baseUrl}/.well-known/jwks.json`, signal ? { signal } : {});
123
166
  if (!res.ok) throw new Error("jwks_fetch_failed");
124
- const body = await res.json();
167
+ const body = await Promise.race([
168
+ res.json(),
169
+ new Promise((_resolve, reject) => {
170
+ if (!(timeoutMs > 0)) return;
171
+ bodyTimer = setTimeout(() => reject(new Error("jwks_body_timeout")), timeoutMs);
172
+ bodyTimer.unref?.();
173
+ })
174
+ ]);
125
175
  const parsed = parseMaxAge(res.headers.get("Cache-Control")) ?? DEFAULT_MAX_AGE_MS;
126
176
  const maxAge = Math.min(parsed, MAX_AGE_CAP_MS);
127
177
  const keys = body.keys ?? [];
@@ -131,12 +181,15 @@ function fetchJwksDeduped(state, fetchFn, baseUrl) {
131
181
  } catch {
132
182
  state.lastFailedAt.set(baseUrl, Date.now());
133
183
  throw new Error("jwks_fetch_failed");
184
+ } finally {
185
+ if (timer) clearTimeout(timer);
186
+ if (bodyTimer) clearTimeout(bodyTimer);
134
187
  }
135
188
  })().finally(() => state.inflight.delete(baseUrl));
136
189
  state.inflight.set(baseUrl, p);
137
190
  return p;
138
191
  }
139
- async function loadJwks(state, fetchFn, baseUrl) {
192
+ async function loadJwks(state, fetchFn, baseUrl, timeoutMs) {
140
193
  const now = Date.now();
141
194
  const c = state.cache.get(baseUrl);
142
195
  if (c && now - c.fetchedAt < c.maxAgeMs) return c.keys;
@@ -144,9 +197,9 @@ async function loadJwks(state, fetchFn, baseUrl) {
144
197
  if (failedAt !== void 0 && now - failedAt < NEGATIVE_CACHE_TTL_MS) {
145
198
  throw new Error("jwks_fetch_failed");
146
199
  }
147
- return fetchJwksDeduped(state, fetchFn, baseUrl);
200
+ return fetchJwksDeduped(state, fetchFn, baseUrl, timeoutMs);
148
201
  }
149
- async function forcedRefetchJwks(state, fetchFn, baseUrl) {
202
+ async function forcedRefetchJwks(state, fetchFn, baseUrl, timeoutMs) {
150
203
  const now = Date.now();
151
204
  const last = state.lastForcedFetchAt.get(baseUrl);
152
205
  if (last !== void 0 && now - last < FORCED_REFETCH_COOLDOWN_MS) return null;
@@ -155,7 +208,7 @@ async function forcedRefetchJwks(state, fetchFn, baseUrl) {
155
208
  if (failedAt !== void 0 && now - failedAt < NEGATIVE_CACHE_TTL_MS) {
156
209
  throw new Error("jwks_fetch_failed");
157
210
  }
158
- return fetchJwksDeduped(state, fetchFn, baseUrl);
211
+ return fetchJwksDeduped(state, fetchFn, baseUrl, timeoutMs);
159
212
  }
160
213
  function algParams(alg) {
161
214
  if (alg === "RS256") {
@@ -182,11 +235,12 @@ async function verifyJwtLocal(scope, fetchFn, baseUrl, jwt, opts = {}) {
182
235
  const params = algParams(d.header.alg);
183
236
  if (!params) return { ok: false, code: "unsupported_alg" };
184
237
  const state = stateFor(scope);
238
+ const timeoutMs = opts.timeoutMs ?? 0;
185
239
  const matchesAlg = (k) => d.header.alg === "RS256" ? k.kty === "RSA" : k.kty === "EC";
186
240
  const candidateKeys = (keys2) => d.header.kid ? keys2.filter((k) => k.kid === d.header.kid && matchesAlg(k)) : keys2.filter((k) => matchesAlg(k));
187
241
  let keys;
188
242
  try {
189
- keys = await loadJwks(state, fetchFn, baseUrl);
243
+ keys = await loadJwks(state, fetchFn, baseUrl, timeoutMs);
190
244
  } catch {
191
245
  try {
192
246
  opts.onEvent?.(baseUrl);
@@ -198,7 +252,7 @@ async function verifyJwtLocal(scope, fetchFn, baseUrl, jwt, opts = {}) {
198
252
  if (candidates.length === 0 && d.header.kid) {
199
253
  let refetched;
200
254
  try {
201
- refetched = await forcedRefetchJwks(state, fetchFn, baseUrl);
255
+ refetched = await forcedRefetchJwks(state, fetchFn, baseUrl, timeoutMs);
202
256
  } catch {
203
257
  try {
204
258
  opts.onEvent?.(baseUrl);
@@ -264,10 +318,103 @@ async function codeChallengeS256(verifier) {
264
318
  return b64url(new Uint8Array(digest));
265
319
  }
266
320
 
321
+ // src/SessionManager.ts
322
+ var SessionManager = class {
323
+ opts;
324
+ _interval = null;
325
+ _channel = null;
326
+ _active = false;
327
+ _visHandler = null;
328
+ constructor(opts) {
329
+ this.opts = { tickMs: 3e4, ...opts };
330
+ }
331
+ get isActive() {
332
+ return this._active;
333
+ }
334
+ start() {
335
+ if (!this.opts.enabled) return;
336
+ if (typeof BroadcastChannel !== "undefined" && !this._channel) {
337
+ this._channel = new BroadcastChannel(this.opts.channelName);
338
+ this._channel.onmessage = (e) => this.opts.onRemoteMessage(e.data);
339
+ }
340
+ if (typeof document !== "undefined" && !this._visHandler) {
341
+ this._visHandler = () => {
342
+ if (document.hidden) {
343
+ this._stopInterval();
344
+ } else if (this._active) {
345
+ this._startInterval();
346
+ if (this.opts.isRefreshDue()) this.opts.onTick();
347
+ }
348
+ };
349
+ document.addEventListener("visibilitychange", this._visHandler);
350
+ }
351
+ }
352
+ setActive(active) {
353
+ this._active = active;
354
+ if (!this.opts.enabled) return;
355
+ const hidden = typeof document !== "undefined" && document.hidden;
356
+ if (active && !hidden) this._startInterval();
357
+ else this._stopInterval();
358
+ }
359
+ broadcast(data) {
360
+ this._channel?.postMessage(data);
361
+ }
362
+ stop() {
363
+ this._stopInterval();
364
+ this._active = false;
365
+ if (this._channel) {
366
+ try {
367
+ this._channel.close();
368
+ } catch {
369
+ }
370
+ this._channel = null;
371
+ }
372
+ if (this._visHandler && typeof document !== "undefined") {
373
+ document.removeEventListener("visibilitychange", this._visHandler);
374
+ this._visHandler = null;
375
+ }
376
+ }
377
+ _startInterval() {
378
+ if (this._interval) return;
379
+ this._interval = setInterval(() => {
380
+ if (this.opts.isRefreshDue()) this.opts.onTick();
381
+ }, this.opts.tickMs);
382
+ this._interval.unref?.();
383
+ }
384
+ _stopInterval() {
385
+ if (this._interval) {
386
+ clearInterval(this._interval);
387
+ this._interval = null;
388
+ }
389
+ }
390
+ };
391
+
267
392
  // src/AuthService.ts
268
393
  function isMfaChallenge(v) {
269
394
  return typeof v === "object" && v !== null && v.mfa_required === true;
270
395
  }
396
+ var OAUTH_FORWARDABLE_QUERY_PARAMS = /* @__PURE__ */ new Set([
397
+ "access_type",
398
+ "acr_values",
399
+ "claims_locales",
400
+ "display",
401
+ "hd",
402
+ "include_granted_scopes",
403
+ "login_hint",
404
+ "max_age",
405
+ "prompt",
406
+ "ui_locales"
407
+ ]);
408
+ var OAUTH_SDK_OWNED_QUERY_PARAMS = /* @__PURE__ */ new Set([
409
+ "project_id",
410
+ "redirect_uri",
411
+ // Alias de `redirect_uri` accepté par le backend (nom GoTrue, employé par les SDK
412
+ // Dart/Python/Rust) : il ne doit pas non plus arriver par `queryParams`.
413
+ "redirect_to",
414
+ "scopes",
415
+ "code_challenge",
416
+ "code_challenge_method"
417
+ ]);
271
418
  var AuthService = class extends BaseService {
272
419
  _stateListeners = /* @__PURE__ */ new Set();
273
420
  _sessionManager;
@@ -279,7 +426,7 @@ var AuthService = class extends BaseService {
279
426
  const browser = typeof window !== "undefined";
280
427
  const enabled = !!options?.forceEnabled || (options?.autoRefresh ?? true) && browser && (options?.persistSession ?? true);
281
428
  this._sessionManager = new SessionManager({
282
- channelName: `aura-${client.projectId}-auth`,
429
+ channelName: `aura-${clientScope(client.baseUrl)}-auth`,
283
430
  enabled,
284
431
  onTick: () => {
285
432
  void this.refreshSession();
@@ -310,6 +457,9 @@ var AuthService = class extends BaseService {
310
457
  * Détecte et consomme une session présente dans l'URL courante au boot (callback
311
458
  * OAuth), à l'image de `detectSessionInUrl` de supabase-js :
312
459
  * - fragment `#access_token=…&refresh_token=…` (flux implicit) → `setSession`.
460
+ * - fragment `#mfa_token=…` (atterrissage `{redirectTo}/oauth/mfa` d'un utilisateur MFA au
461
+ * retour d'OAuth) → émet `MFA_CHALLENGE` avec `mfaToken` (AUCUNE session posée : le défi
462
+ * reste à relever via `mfaVerify`).
313
463
  * - query `?token=…&type=recovery` (lien de récupération de mot de passe) → émet
314
464
  * `PASSWORD_RECOVERY` avec `recoveryToken` (AUCUNE session posée, PAS de `SIGNED_IN`).
315
465
  * - query `?code=…` (flux PKCE) → `exchangeCodeForSession` (relit le verifier).
@@ -338,6 +488,18 @@ var AuthService = class extends BaseService {
338
488
  }
339
489
  return;
340
490
  }
491
+ if (hash.includes("mfa_token")) {
492
+ const initiatedKey = this._oauthInitiatedKey;
493
+ if (!this.client.authStore.getItem(initiatedKey)) return;
494
+ this.client.authStore.removeItem(initiatedKey);
495
+ const frag = new URLSearchParams(hash.replace(/^#/, ""));
496
+ const mfaToken = frag.get("mfa_token");
497
+ if (mfaToken) {
498
+ this._emitEvent("MFA_CHALLENGE", null, null, { mfaToken });
499
+ this._cleanAuthParamsFromUrl();
500
+ }
501
+ return;
502
+ }
341
503
  {
342
504
  const qs = new URLSearchParams(search.replace(/^\?/, ""));
343
505
  const recoveryToken = qs.get("token");
@@ -426,12 +588,36 @@ var AuthService = class extends BaseService {
426
588
  this._unsubStore = null;
427
589
  this._stateListeners.clear();
428
590
  }
591
+ /**
592
+ * Préfixe des routes de ce service. Aucun identifiant de projet : il est injecté par
593
+ * la passerelle depuis le sous-domaine du projet (`inject_project_id`,
594
+ * `services/aura-gateway/src/middleware/project_subdomain.rs`).
595
+ */
429
596
  get prefix() {
430
- return `/v1/auth/${this.client.projectId}`;
597
+ return "/v1/auth";
598
+ }
599
+ /**
600
+ * `project_id` de la SESSION en cours, lu dans le token stocké — seule référence de projet
601
+ * dont dispose encore le client (voir SEC-F2 dans `getClaims`). `null` s'il n'y a pas de
602
+ * session, si le token est illisible, ou s'il ne porte pas ce claim.
603
+ */
604
+ _sessionProjectId() {
605
+ const token = this.client.authStore.token;
606
+ if (!token) return null;
607
+ try {
608
+ const pid = decodeJwt(token).payload.project_id;
609
+ return typeof pid === "string" && pid !== "" ? pid : null;
610
+ } catch {
611
+ return null;
612
+ }
431
613
  }
432
- /** Clé project-scoped du `code_verifier` PKCE (anti-collision multi-projets). */
614
+ /**
615
+ * Clé du `code_verifier` PKCE, scopée par la PORTÉE du client (`clientScope`, dérivée de
616
+ * l'URL du projet) : deux clients de deux projets instanciés dans la MÊME page partagent
617
+ * un seul `localStorage`, et ne doivent pas se marcher dessus.
618
+ */
433
619
  get _pkceVerifierKey() {
434
- return `aura-${this.client.projectId}-pkce-code-verifier`;
620
+ return `aura-${clientScope(this.client.baseUrl)}-pkce-code-verifier`;
435
621
  }
436
622
  /**
437
623
  * Clé project-scoped du MARQUEUR d'initiation du flux OAuth implicit (SEC-F3).
@@ -441,7 +627,7 @@ var AuthService = class extends BaseService {
441
627
  * sera jamais adopté.
442
628
  */
443
629
  get _oauthInitiatedKey() {
444
- return `aura-${this.client.projectId}-oauth-implicit-initiated`;
630
+ return `aura-${clientScope(this.client.baseUrl)}-oauth-implicit-initiated`;
445
631
  }
446
632
  // -- Auth state change ------------------------------------------------------
447
633
  onAuthStateChange(callback) {
@@ -501,14 +687,56 @@ var AuthService = class extends BaseService {
501
687
  }
502
688
  return res;
503
689
  }
690
+ /**
691
+ * Démarre un flux OAuth : renvoie toujours `{ url }` (et redirige en plus le navigateur
692
+ * si un `window` existe).
693
+ *
694
+ * ### `options.queryParams` — contrat (N22)
695
+ *
696
+ * Les clés listées par {@link OAUTH_FORWARDABLE_QUERY_PARAMS} — `prompt`, `login_hint`,
697
+ * `display`, `ui_locales`, `max_age`, `acr_values`, `claims_locales`, `access_type`,
698
+ * `include_granted_scopes`, `hd` — sont **réellement transmises au provider**, jusque
699
+ * dans l'URL d'autorisation.
700
+ *
701
+ * Toute autre clé est **refusée** : la méthode renvoie
702
+ * `error.code === 'invalid_oauth_query_param'` et n'amorce aucune redirection. Rien
703
+ * n'est jamais accepté puis jeté en silence. Sont notamment refusés :
704
+ * - les paramètres du protocole (`state`, `nonce`, `client_id`, `response_type`,
705
+ * `scope`, `code_challenge`…) — ils sont calculés par le serveur ;
706
+ * - les paramètres construits par cette méthode (`redirect_uri`, `scopes`) — passez par
707
+ * `redirectTo`, `scopes` ou `flowType` ;
708
+ * - `project_id` : le projet est porté par l'URL du client, jamais par un paramètre ;
709
+ * - les variantes de casse (`Prompt`, `State`) et les clés inconnues.
710
+ *
711
+ * L'autorité est le **backend** (`OAUTH_FORWARDABLE_PARAMS` de
712
+ * `services/aura-auth/src/handlers/oauth.rs`), qui répond `400` sur les mêmes règles :
713
+ * l'endpoint de démarrage est public et appelable sans SDK. Ce filtre-ci n'est que son
714
+ * miroir, pour échouer avant la redirection ; un test de parité interdit qu'ils divergent.
715
+ *
716
+ * Les **valeurs**, elles, ne sont validées que par le backend (non vide, sans caractère
717
+ * de contrôle, 512 caractères au plus, jamais deux fois la même clé) — un manquement
718
+ * donne un `400` au démarrage du flux, pas une erreur de cette méthode.
719
+ */
504
720
  async signInWithOAuth(provider, options) {
505
721
  const params = new URLSearchParams();
506
- params.set("project_id", this.client.projectId);
507
722
  if (options?.redirectTo) params.set("redirect_uri", options.redirectTo);
508
723
  if (options?.scopes) params.set("scopes", options.scopes);
509
724
  if (options?.queryParams) {
510
725
  for (const [k, v] of Object.entries(options.queryParams)) {
511
- params.set(k, v);
726
+ if (OAUTH_FORWARDABLE_QUERY_PARAMS.has(k)) {
727
+ params.set(k, v);
728
+ continue;
729
+ }
730
+ const raison = OAUTH_SDK_OWNED_QUERY_PARAMS.has(k) ? `il est construit par signInWithOAuth (utilisez l'option d\xE9di\xE9e : ${k === "redirect_uri" || k === "redirect_to" ? "redirectTo" : k === "scopes" ? "scopes" : k === "project_id" ? "l'URL du projet pass\xE9e \xE0 createClient()" : "flowType:'pkce'"})` : `il n'est pas transmis au provider. Seuls ces param\xE8tres d'exp\xE9rience de connexion le sont : ${[...OAUTH_FORWARDABLE_QUERY_PARAMS].join(", ")}`;
731
+ return {
732
+ data: null,
733
+ error: new AuraError(400, {
734
+ error: {
735
+ code: "invalid_oauth_query_param",
736
+ message: `signInWithOAuth: queryParams.${k} refus\xE9 \u2014 ${raison}.`
737
+ }
738
+ })
739
+ };
512
740
  }
513
741
  }
514
742
  if (options?.flowType === "pkce") {
@@ -569,9 +797,14 @@ var AuthService = class extends BaseService {
569
797
  this._emitEvent("SIGNED_OUT", null, null);
570
798
  return { data: null, error: null };
571
799
  }
572
- const res = await this.request("POST", `${this.prefix}/logout`, {
573
- refresh_token: refreshToken
574
- }, { skipAuthRefresh: true });
800
+ const res = await this.request(
801
+ "POST",
802
+ `${this.prefix}/logout`,
803
+ {
804
+ refresh_token: refreshToken
805
+ },
806
+ { skipAuthRefresh: true }
807
+ );
575
808
  this.client.authStore.clear();
576
809
  this._emitEvent("SIGNED_OUT", null, null);
577
810
  return res;
@@ -599,11 +832,11 @@ var AuthService = class extends BaseService {
599
832
  * `fetch` manuels côté app). Pour la session complète, lire `client.authStore`
600
833
  * directement (`.token`, `.refreshToken`, `.user`, `.isValid`).
601
834
  */
602
- async getSession() {
603
- return {
835
+ getSession() {
836
+ return Promise.resolve({
604
837
  accessToken: this.client.authStore.token,
605
838
  user: this.client.authStore.user
606
- };
839
+ });
607
840
  }
608
841
  async setSession(accessToken, refreshToken) {
609
842
  const store = this.client.authStore;
@@ -636,6 +869,16 @@ var AuthService = class extends BaseService {
636
869
  restore();
637
870
  return res;
638
871
  }
872
+ /**
873
+ * Échange le code d'autorisation PKCE contre une session.
874
+ *
875
+ * Renvoie un `LoginResponse`, PAS un `TokenPair` : si l'utilisateur a le MFA actif, le
876
+ * backend répond `{ mfa_required: true, mfa_token }` (gate « C1-bis » de
877
+ * `POST /oauth/pkce`) au lieu d'une paire de jetons. Dans ce cas AUCUNE session n'est
878
+ * enregistrée et `MFA_CHALLENGE` est émis avec le `mfaToken` : l'application demande le
879
+ * code TOTP puis appelle `mfaVerify({ mfa_token, code })`. Discriminez avec
880
+ * {@link isMfaChallenge}.
881
+ */
639
882
  async exchangeCodeForSession(authCode, codeVerifier) {
640
883
  return this._exchangeCode(authCode, codeVerifier, "SIGNED_IN");
641
884
  }
@@ -651,7 +894,10 @@ var AuthService = class extends BaseService {
651
894
  return {
652
895
  data: null,
653
896
  error: new AuraError(0, {
654
- error: { code: "no_code_verifier", message: "No PKCE code_verifier available (not provided and none stored)" }
897
+ error: {
898
+ code: "no_code_verifier",
899
+ message: "No PKCE code_verifier available (not provided and none stored)"
900
+ }
655
901
  })
656
902
  };
657
903
  }
@@ -660,9 +906,18 @@ var AuthService = class extends BaseService {
660
906
  auth_code: authCode,
661
907
  code_verifier: verifier
662
908
  });
909
+ if (res.data && isMfaChallenge(res.data)) {
910
+ const mfa = res.data;
911
+ this._emitEvent("MFA_CHALLENGE", null, null, {
912
+ mfaToken: mfa.mfa_token,
913
+ mfaSetupRequired: mfa.mfa_setup_required
914
+ });
915
+ return res;
916
+ }
663
917
  if (res.data) {
664
- this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
665
- this._emitEvent(successEvent, res.data.user, res.data.access_token);
918
+ const tp = res.data;
919
+ this.client.authStore.save(tp.access_token, tp.refresh_token, tp.user);
920
+ this._emitEvent(successEvent, tp.user, tp.access_token);
666
921
  }
667
922
  return res;
668
923
  } finally {
@@ -690,7 +945,9 @@ var AuthService = class extends BaseService {
690
945
  if (!rt) {
691
946
  return {
692
947
  data: null,
693
- error: new AuraError(0, { error: { code: "no_session", message: "No refresh token available" } })
948
+ error: new AuraError(0, {
949
+ error: { code: "no_session", message: "No refresh token available" }
950
+ })
694
951
  };
695
952
  }
696
953
  const res = await this.request(
@@ -764,7 +1021,14 @@ var AuthService = class extends BaseService {
764
1021
  allowExpired: options?.allowExpired,
765
1022
  // LOT 3 (I8) : relie le callback générique de jwks.ts au hook d'observabilité
766
1023
  // 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 })
1024
+ onEvent: (baseUrl) => emitAuraEvent(this.client.onEvent, this.client.debug, {
1025
+ kind: "jwks-fetch-failed",
1026
+ baseUrl
1027
+ }),
1028
+ // P2-1 : borne le fetch JWKS (et la lecture de son corps) sur le même
1029
+ // budget que les autres requêtes du client. Sans elle, un serveur JWKS
1030
+ // muet figeait `getClaims` définitivement (single-flight `inflight`).
1031
+ timeoutMs: this.client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
768
1032
  });
769
1033
  const checkIssAud = (c) => {
770
1034
  if (options?.issuer !== void 0 && c.iss !== options.issuer) {
@@ -788,7 +1052,8 @@ var AuthService = class extends BaseService {
788
1052
  const issAudErr = checkIssAud(claims);
789
1053
  if (issAudErr) return { data: null, error: issAudErr };
790
1054
  const claimProject = claims.project_id;
791
- if (typeof claimProject === "string" && claimProject !== this.client.projectId) {
1055
+ const sessionProject = this._sessionProjectId();
1056
+ if (typeof claimProject === "string" && sessionProject !== null && claimProject !== sessionProject) {
792
1057
  return {
793
1058
  data: null,
794
1059
  error: new AuraError(0, {
@@ -837,6 +1102,17 @@ var AuthService = class extends BaseService {
837
1102
  error: null
838
1103
  };
839
1104
  }
1105
+ if (u.error === null) {
1106
+ return {
1107
+ data: null,
1108
+ error: new AuraError(0, {
1109
+ error: {
1110
+ code: "invalid_response",
1111
+ message: "getClaims: la validation serveur du token (getUser) a r\xE9pondu sans contenu \u2014 claims ind\xE9terminables"
1112
+ }
1113
+ })
1114
+ };
1115
+ }
840
1116
  return { data: null, error: u.error };
841
1117
  }
842
1118
  return {
@@ -942,17 +1218,6 @@ var AuthService = class extends BaseService {
942
1218
  }
943
1219
  return res;
944
1220
  }
945
- case "invite":
946
- case "phone_change":
947
- return {
948
- data: null,
949
- error: new AuraError(0, {
950
- error: {
951
- code: "otp_type_unsupported",
952
- message: `verifyOtp: le type '${params.type}' n'est pas support\xE9 (aucun endpoint backend).`
953
- }
954
- })
955
- };
956
1221
  default:
957
1222
  return {
958
1223
  data: null,
@@ -975,10 +1240,6 @@ var AuthService = class extends BaseService {
975
1240
  return res;
976
1241
  }
977
1242
  // -- Email verification -----------------------------------------------------
978
- /** Rétrocompat : délègue à `verifyOtp({ type: 'signup' })` (POST /verify-email). */
979
- async verifyEmail(token) {
980
- return this.verifyOtp({ type: "signup", token });
981
- }
982
1243
  async resendVerificationEmail(email) {
983
1244
  const e = email ?? this.client.authStore.user?.email;
984
1245
  if (!e) {
@@ -993,18 +1254,10 @@ var AuthService = class extends BaseService {
993
1254
  async signInWithEmailOtp(email) {
994
1255
  return this.request("POST", `${this.prefix}/email-otp/send`, { email });
995
1256
  }
996
- /** Rétrocompat : délègue à `verifyOtp({ type: 'email' })` (POST /email-otp/verify). */
997
- async verifyEmailOtp(email, code) {
998
- return this.verifyOtp({ type: "email", email, token: code });
999
- }
1000
1257
  // -- SMS OTP (Phase 2) ----------------------------------------------------
1001
1258
  async signInWithSmsOtp(phone) {
1002
1259
  return this.request("POST", `${this.prefix}/sms/send`, { phone });
1003
1260
  }
1004
- /** Rétrocompat : délègue à `verifyOtp({ type: 'sms' })` (POST /sms/verify). */
1005
- async verifySmsOtp(phone, code) {
1006
- return this.verifyOtp({ type: "sms", phone, token: code });
1007
- }
1008
1261
  // -- Identity linking (Phase 2) -------------------------------------------
1009
1262
  async listIdentities() {
1010
1263
  return this.request("GET", `${this.prefix}/user/identities`);
@@ -1015,29 +1268,37 @@ var AuthService = class extends BaseService {
1015
1268
  async unlinkIdentity(identityId) {
1016
1269
  return this.request("DELETE", `${this.prefix}/user/identities/${identityId}`);
1017
1270
  }
1018
- // -- Email change (Phase 2) -----------------------------------------------
1019
- /**
1020
- * Rétrocompat : délègue à `verifyOtp({ type: 'email_change' })`.
1021
- * CHANGEMENT ASSUMÉ (parité) : émet désormais `USER_UPDATED` (auparavant aucun
1022
- * événement) — voir CHANGELOG.
1023
- */
1024
- async confirmEmailChange(token) {
1025
- return this.verifyOtp({ type: "email_change", token });
1026
- }
1027
1271
  // -- MFA recovery codes (Phase 2) -----------------------------------------
1028
1272
  async regenerateRecoveryCodes(code) {
1029
1273
  return this.request("POST", `${this.prefix}/mfa/recovery-codes/regenerate`, { code });
1030
1274
  }
1031
1275
  // -- Native mobile OAuth (Phase 2) ----------------------------------------
1276
+ /**
1277
+ * Échange un ID token de provider (Google/Apple, mobile natif) contre une session.
1278
+ *
1279
+ * Renvoie un `LoginResponse`, PAS un `TokenPair` : comme partout ailleurs, un utilisateur
1280
+ * avec MFA actif reçoit `{ mfa_required: true, mfa_token }` et AUCUNE session n'est
1281
+ * enregistrée — `MFA_CHALLENGE` est émis, à relever par `mfaVerify`. Discriminez avec
1282
+ * {@link isMfaChallenge}.
1283
+ */
1032
1284
  async signInWithIdToken(provider, idToken, accessToken) {
1033
1285
  const res = await this.request("POST", `${this.prefix}/token`, {
1034
1286
  provider,
1035
1287
  id_token: idToken,
1036
1288
  access_token: accessToken
1037
1289
  });
1290
+ if (res.data && isMfaChallenge(res.data)) {
1291
+ const mfa = res.data;
1292
+ this._emitEvent("MFA_CHALLENGE", null, null, {
1293
+ mfaToken: mfa.mfa_token,
1294
+ mfaSetupRequired: mfa.mfa_setup_required
1295
+ });
1296
+ return res;
1297
+ }
1038
1298
  if (res.data) {
1039
- this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
1040
- this._emitEvent("SIGNED_IN", res.data.user, res.data.access_token);
1299
+ const tp = res.data;
1300
+ this.client.authStore.save(tp.access_token, tp.refresh_token, tp.user);
1301
+ this._emitEvent("SIGNED_IN", tp.user, tp.access_token);
1041
1302
  }
1042
1303
  return res;
1043
1304
  }
@@ -1065,7 +1326,11 @@ var AuthService = class extends BaseService {
1065
1326
  * Body confirmé au handler (`PromoteAnonymousRequest`) : `{ email, password }`.
1066
1327
  */
1067
1328
  async promoteAnonymous(credentials) {
1068
- const res = await this.request("POST", `${this.prefix}/anonymous/promote`, credentials);
1329
+ const res = await this.request(
1330
+ "POST",
1331
+ `${this.prefix}/anonymous/promote`,
1332
+ credentials
1333
+ );
1069
1334
  if (res.data) {
1070
1335
  this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
1071
1336
  this._emitEvent("USER_UPDATED", res.data.user, res.data.access_token);
@@ -1081,90 +1346,5 @@ var AuthService = class extends BaseService {
1081
1346
  return this.verifyOtp({ type: "magiclink", token });
1082
1347
  }
1083
1348
  };
1084
- var AuthAdminService = class extends BaseService {
1085
- get prefix() {
1086
- return `/v1/auth/${this.client.projectId}/admin`;
1087
- }
1088
- // -- Users ----------------------------------------------------------------
1089
- async listUsers(params) {
1090
- const query = new URLSearchParams();
1091
- if (params?.limit !== void 0) query.set("limit", String(params.limit));
1092
- if (params?.offset !== void 0) query.set("offset", String(params.offset));
1093
- if (params?.search) query.set("search", params.search);
1094
- const qs = query.toString();
1095
- return this.request("GET", `${this.prefix}/users${qs ? "?" + qs : ""}`);
1096
- }
1097
- async getUser(userId) {
1098
- return this.request("GET", `${this.prefix}/users/${userId}`);
1099
- }
1100
- async createUser(data) {
1101
- return this.request("POST", `${this.prefix}/users`, data);
1102
- }
1103
- async updateUser(userId, data) {
1104
- return this.request("PUT", `${this.prefix}/users/${userId}`, data);
1105
- }
1106
- async deleteUser(userId) {
1107
- return this.request("DELETE", `${this.prefix}/users/${userId}`);
1108
- }
1109
- async banUser(userId) {
1110
- return this.request("POST", `${this.prefix}/users/${userId}/ban`, {});
1111
- }
1112
- async unbanUser(userId) {
1113
- return this.request("POST", `${this.prefix}/users/${userId}/unban`, {});
1114
- }
1115
- async generateLink(userId, type) {
1116
- return this.request("POST", `${this.prefix}/users/${userId}/generate-link`, { type });
1117
- }
1118
- async getUserStats() {
1119
- return this.request("GET", `${this.prefix}/users/stats`);
1120
- }
1121
- async listUserIdentities(userId) {
1122
- return this.request("GET", `${this.prefix}/users/${userId}/identities`);
1123
- }
1124
- // -- Sessions ---------------------------------------------------------------
1125
- async listSessions(params) {
1126
- const query = new URLSearchParams();
1127
- if (params?.userId !== void 0) query.set("user_id", params.userId);
1128
- if (params?.limit !== void 0) query.set("limit", String(params.limit));
1129
- if (params?.offset !== void 0) query.set("offset", String(params.offset));
1130
- const qs = query.toString();
1131
- return this.request("GET", `${this.prefix}/sessions${qs ? "?" + qs : ""}`);
1132
- }
1133
- async revokeSession(sessionId) {
1134
- return this.request("DELETE", `${this.prefix}/sessions/${sessionId}`);
1135
- }
1136
- // -- Audit log ------------------------------------------------------------
1137
- async getAuditLog(params) {
1138
- const query = new URLSearchParams();
1139
- if (params?.userId !== void 0) query.set("user_id", params.userId);
1140
- if (params?.limit !== void 0) query.set("limit", String(params.limit));
1141
- if (params?.offset !== void 0) query.set("offset", String(params.offset));
1142
- const qs = query.toString();
1143
- return this.request("GET", `${this.prefix}/audit-log${qs ? "?" + qs : ""}`);
1144
- }
1145
- // -- Settings -------------------------------------------------------------
1146
- async getSettings() {
1147
- return this.request("GET", `${this.prefix}/settings`);
1148
- }
1149
- async updateSettings(data) {
1150
- return this.request("PUT", `${this.prefix}/settings`, data);
1151
- }
1152
- // -- Email templates ------------------------------------------------------
1153
- async listEmailTemplates() {
1154
- return this.request("GET", `${this.prefix}/email-templates`);
1155
- }
1156
- async getEmailTemplate(type) {
1157
- return this.request("GET", `${this.prefix}/email-templates/${type}`);
1158
- }
1159
- async updateEmailTemplate(type, data) {
1160
- return this.request("PUT", `${this.prefix}/email-templates/${type}`, data);
1161
- }
1162
- async deleteEmailTemplate(type) {
1163
- return this.request("DELETE", `${this.prefix}/email-templates/${type}`);
1164
- }
1165
- async testEmailTemplate(type, body) {
1166
- return this.request("POST", `${this.prefix}/email-templates/${type}/test`, body);
1167
- }
1168
- };
1169
1349
 
1170
1350
  export { AuthAdminService, AuthService, SessionManager, isMfaChallenge };