@aurabase/auth 0.4.1 → 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, DEFAULT_REQUEST_TIMEOUT_MS, 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
 
@@ -284,10 +318,103 @@ async function codeChallengeS256(verifier) {
284
318
  return b64url(new Uint8Array(digest));
285
319
  }
286
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
+
287
392
  // src/AuthService.ts
288
393
  function isMfaChallenge(v) {
289
394
  return typeof v === "object" && v !== null && v.mfa_required === true;
290
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
+ ]);
291
418
  var AuthService = class extends BaseService {
292
419
  _stateListeners = /* @__PURE__ */ new Set();
293
420
  _sessionManager;
@@ -299,7 +426,7 @@ var AuthService = class extends BaseService {
299
426
  const browser = typeof window !== "undefined";
300
427
  const enabled = !!options?.forceEnabled || (options?.autoRefresh ?? true) && browser && (options?.persistSession ?? true);
301
428
  this._sessionManager = new SessionManager({
302
- channelName: `aura-${client.projectId}-auth`,
429
+ channelName: `aura-${clientScope(client.baseUrl)}-auth`,
303
430
  enabled,
304
431
  onTick: () => {
305
432
  void this.refreshSession();
@@ -330,6 +457,9 @@ var AuthService = class extends BaseService {
330
457
  * Détecte et consomme une session présente dans l'URL courante au boot (callback
331
458
  * OAuth), à l'image de `detectSessionInUrl` de supabase-js :
332
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`).
333
463
  * - query `?token=…&type=recovery` (lien de récupération de mot de passe) → émet
334
464
  * `PASSWORD_RECOVERY` avec `recoveryToken` (AUCUNE session posée, PAS de `SIGNED_IN`).
335
465
  * - query `?code=…` (flux PKCE) → `exchangeCodeForSession` (relit le verifier).
@@ -358,6 +488,18 @@ var AuthService = class extends BaseService {
358
488
  }
359
489
  return;
360
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
+ }
361
503
  {
362
504
  const qs = new URLSearchParams(search.replace(/^\?/, ""));
363
505
  const recoveryToken = qs.get("token");
@@ -446,12 +588,36 @@ var AuthService = class extends BaseService {
446
588
  this._unsubStore = null;
447
589
  this._stateListeners.clear();
448
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
+ */
449
596
  get prefix() {
450
- 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
+ }
451
613
  }
452
- /** 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
+ */
453
619
  get _pkceVerifierKey() {
454
- return `aura-${this.client.projectId}-pkce-code-verifier`;
620
+ return `aura-${clientScope(this.client.baseUrl)}-pkce-code-verifier`;
455
621
  }
456
622
  /**
457
623
  * Clé project-scoped du MARQUEUR d'initiation du flux OAuth implicit (SEC-F3).
@@ -461,7 +627,7 @@ var AuthService = class extends BaseService {
461
627
  * sera jamais adopté.
462
628
  */
463
629
  get _oauthInitiatedKey() {
464
- return `aura-${this.client.projectId}-oauth-implicit-initiated`;
630
+ return `aura-${clientScope(this.client.baseUrl)}-oauth-implicit-initiated`;
465
631
  }
466
632
  // -- Auth state change ------------------------------------------------------
467
633
  onAuthStateChange(callback) {
@@ -521,14 +687,56 @@ var AuthService = class extends BaseService {
521
687
  }
522
688
  return res;
523
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
+ */
524
720
  async signInWithOAuth(provider, options) {
525
721
  const params = new URLSearchParams();
526
- params.set("project_id", this.client.projectId);
527
722
  if (options?.redirectTo) params.set("redirect_uri", options.redirectTo);
528
723
  if (options?.scopes) params.set("scopes", options.scopes);
529
724
  if (options?.queryParams) {
530
725
  for (const [k, v] of Object.entries(options.queryParams)) {
531
- 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
+ };
532
740
  }
533
741
  }
534
742
  if (options?.flowType === "pkce") {
@@ -589,9 +797,14 @@ var AuthService = class extends BaseService {
589
797
  this._emitEvent("SIGNED_OUT", null, null);
590
798
  return { data: null, error: null };
591
799
  }
592
- const res = await this.request("POST", `${this.prefix}/logout`, {
593
- refresh_token: refreshToken
594
- }, { skipAuthRefresh: true });
800
+ const res = await this.request(
801
+ "POST",
802
+ `${this.prefix}/logout`,
803
+ {
804
+ refresh_token: refreshToken
805
+ },
806
+ { skipAuthRefresh: true }
807
+ );
595
808
  this.client.authStore.clear();
596
809
  this._emitEvent("SIGNED_OUT", null, null);
597
810
  return res;
@@ -619,11 +832,11 @@ var AuthService = class extends BaseService {
619
832
  * `fetch` manuels côté app). Pour la session complète, lire `client.authStore`
620
833
  * directement (`.token`, `.refreshToken`, `.user`, `.isValid`).
621
834
  */
622
- async getSession() {
623
- return {
835
+ getSession() {
836
+ return Promise.resolve({
624
837
  accessToken: this.client.authStore.token,
625
838
  user: this.client.authStore.user
626
- };
839
+ });
627
840
  }
628
841
  async setSession(accessToken, refreshToken) {
629
842
  const store = this.client.authStore;
@@ -656,6 +869,16 @@ var AuthService = class extends BaseService {
656
869
  restore();
657
870
  return res;
658
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
+ */
659
882
  async exchangeCodeForSession(authCode, codeVerifier) {
660
883
  return this._exchangeCode(authCode, codeVerifier, "SIGNED_IN");
661
884
  }
@@ -671,7 +894,10 @@ var AuthService = class extends BaseService {
671
894
  return {
672
895
  data: null,
673
896
  error: new AuraError(0, {
674
- 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
+ }
675
901
  })
676
902
  };
677
903
  }
@@ -680,9 +906,18 @@ var AuthService = class extends BaseService {
680
906
  auth_code: authCode,
681
907
  code_verifier: verifier
682
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
+ }
683
917
  if (res.data) {
684
- this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
685
- 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);
686
921
  }
687
922
  return res;
688
923
  } finally {
@@ -710,7 +945,9 @@ var AuthService = class extends BaseService {
710
945
  if (!rt) {
711
946
  return {
712
947
  data: null,
713
- 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
+ })
714
951
  };
715
952
  }
716
953
  const res = await this.request(
@@ -784,7 +1021,10 @@ var AuthService = class extends BaseService {
784
1021
  allowExpired: options?.allowExpired,
785
1022
  // LOT 3 (I8) : relie le callback générique de jwks.ts au hook d'observabilité
786
1023
  // du client (onEvent/debug). jwks.ts reste sans dépendance vers @aurabase/core.
787
- 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
+ }),
788
1028
  // P2-1 : borne le fetch JWKS (et la lecture de son corps) sur le même
789
1029
  // budget que les autres requêtes du client. Sans elle, un serveur JWKS
790
1030
  // muet figeait `getClaims` définitivement (single-flight `inflight`).
@@ -812,7 +1052,8 @@ var AuthService = class extends BaseService {
812
1052
  const issAudErr = checkIssAud(claims);
813
1053
  if (issAudErr) return { data: null, error: issAudErr };
814
1054
  const claimProject = claims.project_id;
815
- if (typeof claimProject === "string" && claimProject !== this.client.projectId) {
1055
+ const sessionProject = this._sessionProjectId();
1056
+ if (typeof claimProject === "string" && sessionProject !== null && claimProject !== sessionProject) {
816
1057
  return {
817
1058
  data: null,
818
1059
  error: new AuraError(0, {
@@ -977,17 +1218,6 @@ var AuthService = class extends BaseService {
977
1218
  }
978
1219
  return res;
979
1220
  }
980
- case "invite":
981
- case "phone_change":
982
- return {
983
- data: null,
984
- error: new AuraError(0, {
985
- error: {
986
- code: "otp_type_unsupported",
987
- message: `verifyOtp: le type '${params.type}' n'est pas support\xE9 (aucun endpoint backend).`
988
- }
989
- })
990
- };
991
1221
  default:
992
1222
  return {
993
1223
  data: null,
@@ -1010,10 +1240,6 @@ var AuthService = class extends BaseService {
1010
1240
  return res;
1011
1241
  }
1012
1242
  // -- Email verification -----------------------------------------------------
1013
- /** Rétrocompat : délègue à `verifyOtp({ type: 'signup' })` (POST /verify-email). */
1014
- async verifyEmail(token) {
1015
- return this.verifyOtp({ type: "signup", token });
1016
- }
1017
1243
  async resendVerificationEmail(email) {
1018
1244
  const e = email ?? this.client.authStore.user?.email;
1019
1245
  if (!e) {
@@ -1028,18 +1254,10 @@ var AuthService = class extends BaseService {
1028
1254
  async signInWithEmailOtp(email) {
1029
1255
  return this.request("POST", `${this.prefix}/email-otp/send`, { email });
1030
1256
  }
1031
- /** Rétrocompat : délègue à `verifyOtp({ type: 'email' })` (POST /email-otp/verify). */
1032
- async verifyEmailOtp(email, code) {
1033
- return this.verifyOtp({ type: "email", email, token: code });
1034
- }
1035
1257
  // -- SMS OTP (Phase 2) ----------------------------------------------------
1036
1258
  async signInWithSmsOtp(phone) {
1037
1259
  return this.request("POST", `${this.prefix}/sms/send`, { phone });
1038
1260
  }
1039
- /** Rétrocompat : délègue à `verifyOtp({ type: 'sms' })` (POST /sms/verify). */
1040
- async verifySmsOtp(phone, code) {
1041
- return this.verifyOtp({ type: "sms", phone, token: code });
1042
- }
1043
1261
  // -- Identity linking (Phase 2) -------------------------------------------
1044
1262
  async listIdentities() {
1045
1263
  return this.request("GET", `${this.prefix}/user/identities`);
@@ -1050,29 +1268,37 @@ var AuthService = class extends BaseService {
1050
1268
  async unlinkIdentity(identityId) {
1051
1269
  return this.request("DELETE", `${this.prefix}/user/identities/${identityId}`);
1052
1270
  }
1053
- // -- Email change (Phase 2) -----------------------------------------------
1054
- /**
1055
- * Rétrocompat : délègue à `verifyOtp({ type: 'email_change' })`.
1056
- * CHANGEMENT ASSUMÉ (parité) : émet désormais `USER_UPDATED` (auparavant aucun
1057
- * événement) — voir CHANGELOG.
1058
- */
1059
- async confirmEmailChange(token) {
1060
- return this.verifyOtp({ type: "email_change", token });
1061
- }
1062
1271
  // -- MFA recovery codes (Phase 2) -----------------------------------------
1063
1272
  async regenerateRecoveryCodes(code) {
1064
1273
  return this.request("POST", `${this.prefix}/mfa/recovery-codes/regenerate`, { code });
1065
1274
  }
1066
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
+ */
1067
1284
  async signInWithIdToken(provider, idToken, accessToken) {
1068
1285
  const res = await this.request("POST", `${this.prefix}/token`, {
1069
1286
  provider,
1070
1287
  id_token: idToken,
1071
1288
  access_token: accessToken
1072
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
+ }
1073
1298
  if (res.data) {
1074
- this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
1075
- 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);
1076
1302
  }
1077
1303
  return res;
1078
1304
  }
@@ -1100,7 +1326,11 @@ var AuthService = class extends BaseService {
1100
1326
  * Body confirmé au handler (`PromoteAnonymousRequest`) : `{ email, password }`.
1101
1327
  */
1102
1328
  async promoteAnonymous(credentials) {
1103
- 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
+ );
1104
1334
  if (res.data) {
1105
1335
  this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
1106
1336
  this._emitEvent("USER_UPDATED", res.data.user, res.data.access_token);
@@ -1116,90 +1346,5 @@ var AuthService = class extends BaseService {
1116
1346
  return this.verifyOtp({ type: "magiclink", token });
1117
1347
  }
1118
1348
  };
1119
- var AuthAdminService = class extends BaseService {
1120
- get prefix() {
1121
- return `/v1/auth/${this.client.projectId}/admin`;
1122
- }
1123
- // -- Users ----------------------------------------------------------------
1124
- async listUsers(params) {
1125
- const query = new URLSearchParams();
1126
- if (params?.limit !== void 0) query.set("limit", String(params.limit));
1127
- if (params?.offset !== void 0) query.set("offset", String(params.offset));
1128
- if (params?.search) query.set("search", params.search);
1129
- const qs = query.toString();
1130
- return this.request("GET", `${this.prefix}/users${qs ? "?" + qs : ""}`);
1131
- }
1132
- async getUser(userId) {
1133
- return this.request("GET", `${this.prefix}/users/${userId}`);
1134
- }
1135
- async createUser(data) {
1136
- return this.request("POST", `${this.prefix}/users`, data);
1137
- }
1138
- async updateUser(userId, data) {
1139
- return this.request("PUT", `${this.prefix}/users/${userId}`, data);
1140
- }
1141
- async deleteUser(userId) {
1142
- return this.request("DELETE", `${this.prefix}/users/${userId}`);
1143
- }
1144
- async banUser(userId) {
1145
- return this.request("POST", `${this.prefix}/users/${userId}/ban`, {});
1146
- }
1147
- async unbanUser(userId) {
1148
- return this.request("POST", `${this.prefix}/users/${userId}/unban`, {});
1149
- }
1150
- async generateLink(userId, type) {
1151
- return this.request("POST", `${this.prefix}/users/${userId}/generate-link`, { type });
1152
- }
1153
- async getUserStats() {
1154
- return this.request("GET", `${this.prefix}/users/stats`);
1155
- }
1156
- async listUserIdentities(userId) {
1157
- return this.request("GET", `${this.prefix}/users/${userId}/identities`);
1158
- }
1159
- // -- Sessions ---------------------------------------------------------------
1160
- async listSessions(params) {
1161
- const query = new URLSearchParams();
1162
- if (params?.userId !== void 0) query.set("user_id", params.userId);
1163
- if (params?.limit !== void 0) query.set("limit", String(params.limit));
1164
- if (params?.offset !== void 0) query.set("offset", String(params.offset));
1165
- const qs = query.toString();
1166
- return this.request("GET", `${this.prefix}/sessions${qs ? "?" + qs : ""}`);
1167
- }
1168
- async revokeSession(sessionId) {
1169
- return this.request("DELETE", `${this.prefix}/sessions/${sessionId}`);
1170
- }
1171
- // -- Audit log ------------------------------------------------------------
1172
- async getAuditLog(params) {
1173
- const query = new URLSearchParams();
1174
- if (params?.userId !== void 0) query.set("user_id", params.userId);
1175
- if (params?.limit !== void 0) query.set("limit", String(params.limit));
1176
- if (params?.offset !== void 0) query.set("offset", String(params.offset));
1177
- const qs = query.toString();
1178
- return this.request("GET", `${this.prefix}/audit-log${qs ? "?" + qs : ""}`);
1179
- }
1180
- // -- Settings -------------------------------------------------------------
1181
- async getSettings() {
1182
- return this.request("GET", `${this.prefix}/settings`);
1183
- }
1184
- async updateSettings(data) {
1185
- return this.request("PUT", `${this.prefix}/settings`, data);
1186
- }
1187
- // -- Email templates ------------------------------------------------------
1188
- async listEmailTemplates() {
1189
- return this.request("GET", `${this.prefix}/email-templates`);
1190
- }
1191
- async getEmailTemplate(type) {
1192
- return this.request("GET", `${this.prefix}/email-templates/${type}`);
1193
- }
1194
- async updateEmailTemplate(type, data) {
1195
- return this.request("PUT", `${this.prefix}/email-templates/${type}`, data);
1196
- }
1197
- async deleteEmailTemplate(type) {
1198
- return this.request("DELETE", `${this.prefix}/email-templates/${type}`);
1199
- }
1200
- async testEmailTemplate(type, body) {
1201
- return this.request("POST", `${this.prefix}/email-templates/${type}/test`, body);
1202
- }
1203
- };
1204
1349
 
1205
1350
  export { AuthAdminService, AuthService, SessionManager, isMfaChallenge };