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