@aurabase/auth 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,8 +39,13 @@ await aura.auth.signOut()
39
39
  ### Session
40
40
 
41
41
  ```ts
42
- // getSession() n'est PAS un AuraResponse : il lit directement le store local (synchrone en pratique).
43
- const { accessToken, user } = await aura.auth.getSession()
42
+ // getSession() lit le store local (synchrone en pratique) :
43
+ const { data: { session } } = await aura.auth.getSession()
44
+ if (session) {
45
+ console.log('Session ID:', session.id)
46
+ console.log('Access token:', session.access_token)
47
+ console.log('User ID:', session.user.id)
48
+ }
44
49
 
45
50
  const refreshed = await aura.auth.refreshSession()
46
51
 
@@ -55,6 +60,32 @@ if (first) await aura.auth.revokeSession(first.id)
55
60
  await aura.auth.revokeAllSessions()
56
61
  ```
57
62
 
63
+ ### Configuration du stockage (SSR & Node.js)
64
+
65
+ ```ts
66
+ import { createClient, LocalAuthStore, BaseAuthStore } from '@aurabase/aurabase-js'
67
+
68
+ // Par défaut, le client choisit automatiquement :
69
+ // - Navigateur : `LocalAuthStore` avec clé scopée par projet (`aura-<slug>-auth`)
70
+ // - Node.js / SSR : `BaseAuthStore` en mémoire avec exportToCookie() / loadFromCookie()
71
+
72
+ // Pour personnaliser le stockage :
73
+ const aura = createClient('https://mon-projet.aurabase.cloud', 'aura_anon_xxx', {
74
+ auth: {
75
+ storage: new LocalAuthStore('custom-auth-key'),
76
+ autoRefreshToken: true,
77
+ persistSession: true,
78
+ },
79
+ })
80
+
81
+ // Sérialisation cookie (SSR) :
82
+ const setCookieHeader = aura.authStore.exportToCookie({
83
+ httpOnly: true,
84
+ secure: true,
85
+ sameSite: 'lax',
86
+ })
87
+ ```
88
+
58
89
  ### OAuth (implicite ou PKCE)
59
90
 
60
91
  `signInWithOAuth` ne redirige plus « en dur » : il renvoie toujours `{ url }` (et redirige en plus via `window.location.href` si un `window` existe).
@@ -288,14 +319,19 @@ await admin.authAdmin.listUsers({ limit: 50 })
288
319
  import { AuthAdminService } from '@aurabase/auth'
289
320
  ```
290
321
 
291
- Méthodes clés : `listUsers`, `getUser`, `createUser`, `updateUser`, `deleteUser`, `banUser`, `unbanUser`, `generateLink`, `getUserStats`, `listUserIdentities`, `listSessions`, `revokeSession`, `getAuditLog`, `getSettings`, `updateSettings`, `listEmailTemplates`/`getEmailTemplate`/`updateEmailTemplate`/`deleteEmailTemplate`, `testEmailTemplate`.
322
+ Méthodes clés : `listUsers`, `getUser`, `createUser`, `updateUser`, `deleteUser`, `banUser`, `unbanUser`, `generateLink`, `getUserStats`, `listUserIdentities`, `listSessions`, `revokeSession`, `impersonate` (RFC 8693 avec claim `act`), `importUsers` (import en masse avec conversion automatique bcrypt vers Argon2id), `listSigningKeys` (clés ES256 du projet), `getAuditLog`, `getSettings`, `updateSettings`, `listEmailTemplates`/`getEmailTemplate`/`updateEmailTemplate`/`deleteEmailTemplate`, `testEmailTemplate`.
323
+
324
+ ### Authentification Multi-Facteurs (MFA v0.7.0)
325
+
326
+ Aurabase supporte l'authentification multifacteur complète avec niveaux d'assurance (AAL1, AAL2) et claim standard `amr` :
327
+ - **TOTP** : `c.auth.mfa.enroll()`, `c.auth.mfa.verifySetup(token)`, `c.auth.mfa.verify({ mfa_token, code })`
328
+ - **Facteurs disponibles** : `c.auth.mfa.factors(mfa_token)` liste les méthodes éligibles (`totp`, `webauthn`, `sms`)
329
+ - **WebAuthn / Passkeys** : `c.auth.mfa.webauthn`
330
+ - **SMS OTP** : `c.auth.mfa.sms`
292
331
 
293
- ## Limitations
332
+ ### Vérification locale de jeton (JWKS / ES256)
294
333
 
295
- - Pas de `resend()` / `reauthenticate()` / `verifyOtp()` unifiés (style Supabase) : chaque canal (magic link, email OTP, SMS OTP) a sa propre paire `signInWith*`/`verify*`.
296
- - Pas de SSO/SAML.
297
- - MFA limité au TOTP : pas de `listFactors()`, pas de niveau d'authentification (AAL), pas de MFA par téléphone.
298
- - `getClaims()` en fallback (HS256 / JWKS vide) ne renvoie pas `exp`/`iat` — ne pas s'y fier après un fallback.
334
+ Chaque projet émet des jetons signés avec sa propre clé asymétrique **ES256**. Vous pouvez vérifier les jetons hors-ligne via n'importe quelle bibliothèque standard (`jose`, Go, Python) en récupérant la clé publique sur `https://<projet>.aurabase.cloud/v1/auth/.well-known/jwks.json`.
299
335
 
300
336
  ## Documentation
301
337
 
package/dist/index.cjs CHANGED
@@ -52,9 +52,81 @@ var AuthAdminService = class extends core.BaseService {
52
52
  }
53
53
  /**
54
54
  * Génère un lien à usage unique pour un utilisateur (`'magic'`, `'recovery'` ou `'invite'`) et renvoie `{ link, token }`.
55
+ * Accepte soit `generateLink(userId, type)` soit `generateLink({ userId, type })`.
55
56
  */
56
- async generateLink(userId, type) {
57
- return this.request("POST", `${this.prefix}/users/${userId}/generate-link`, { type });
57
+ async generateLink(userIdOrParams, type) {
58
+ let resolvedUserId;
59
+ let resolvedType;
60
+ if (typeof userIdOrParams === "object" && userIdOrParams !== null) {
61
+ resolvedUserId = userIdOrParams.userId ?? userIdOrParams.user_id;
62
+ resolvedType = userIdOrParams.type ?? type;
63
+ } else if (typeof userIdOrParams === "string") {
64
+ resolvedUserId = userIdOrParams;
65
+ resolvedType = type;
66
+ }
67
+ if (!resolvedUserId || typeof resolvedUserId !== "string" || resolvedUserId.trim() === "" || resolvedUserId.includes("[object")) {
68
+ return {
69
+ data: null,
70
+ error: new core.AuraError(400, {
71
+ error: {
72
+ code: "INVALID_ARGUMENT",
73
+ message: "userId must be a non-empty string (UUID). Usage: generateLink(userId, type) or generateLink({ userId, type })."
74
+ }
75
+ })
76
+ };
77
+ }
78
+ if (!resolvedType) {
79
+ return {
80
+ data: null,
81
+ error: new core.AuraError(400, {
82
+ error: {
83
+ code: "INVALID_ARGUMENT",
84
+ message: 'type is required for generateLink ("magic" | "recovery" | "invite").'
85
+ }
86
+ })
87
+ };
88
+ }
89
+ return this.request("POST", `${this.prefix}/users/${encodeURIComponent(resolvedUserId)}/generate-link`, { type: resolvedType });
90
+ }
91
+ /**
92
+ * Impersonation (AUTH-037) : jeton COURT (10 minutes au plus) pour agir en tant que l'utilisateur,
93
+ * avec le claim `act: { sub }`. Pas de refresh token, journalisé (`admin_impersonation`).
94
+ * Un compte `owner`/`admin` exige `allowPrivileged: true` ET une `reason`.
95
+ */
96
+ async impersonate(userId, options) {
97
+ return this.request("POST", `${this.prefix}/users/${userId}/impersonate`, {
98
+ reason: options?.reason,
99
+ ttl_seconds: options?.ttlSeconds,
100
+ allow_privileged: options?.allowPrivileged ?? false
101
+ });
102
+ }
103
+ /**
104
+ * Import en masse (AUTH-037) : jusqu'a 1000 utilisateurs par appel, avec hash bcrypt ou argon2id.
105
+ * Rapport ligne par ligne ; une ligne en erreur n'arrete pas les autres.
106
+ */
107
+ async importUsers(users) {
108
+ return this.request("POST", `${this.prefix}/users/import`, {
109
+ users: users.map((u) => ({
110
+ email: u.email,
111
+ email_verified: u.emailVerified ?? false,
112
+ password_hash: u.passwordHash,
113
+ name: u.name,
114
+ user_metadata: u.userMetadata,
115
+ app_metadata: u.appMetadata
116
+ }))
117
+ });
118
+ }
119
+ /** Cles de signature du projet (active + previous), URL du JWKS et emetteur (`iss`). */
120
+ async listSigningKeys() {
121
+ return this.request("GET", `${this.prefix}/signing-keys`);
122
+ }
123
+ /** Rotation : une nouvelle cle signe les nouveaux jetons, l'ancienne reste publiee 24 h. */
124
+ async rotateSigningKey() {
125
+ return this.request("POST", `${this.prefix}/signing-keys/rotate`, {});
126
+ }
127
+ /** Revoque une ancienne cle compromise (retiree du JWKS, jetons refuses). */
128
+ async revokeSigningKey(kid) {
129
+ return this.request("DELETE", `${this.prefix}/signing-keys/${encodeURIComponent(kid)}`);
58
130
  }
59
131
  /**
60
132
  * Statistiques des utilisateurs : total, vérifiés, bannis, avec MFA et nouveaux sur 7 jours.
@@ -353,6 +425,77 @@ async function codeChallengeS256(verifier) {
353
425
  return b64url(new Uint8Array(digest));
354
426
  }
355
427
 
428
+ // src/webauthn.ts
429
+ function base64urlToBytes(valeur) {
430
+ const b64 = valeur.replace(/-/g, "+").replace(/_/g, "/");
431
+ const bin = atob(b64 + "=".repeat((4 - b64.length % 4) % 4));
432
+ const octets = new Uint8Array(new ArrayBuffer(bin.length));
433
+ for (let i = 0; i < bin.length; i++) octets[i] = bin.charCodeAt(i);
434
+ return octets;
435
+ }
436
+ function bytesToBase64url(octets) {
437
+ const vue = octets instanceof ArrayBuffer ? new Uint8Array(octets) : new Uint8Array(octets.buffer, octets.byteOffset, octets.byteLength);
438
+ let bin = "";
439
+ for (const o of vue) bin += String.fromCharCode(o);
440
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
441
+ }
442
+ function creationOptionsFromJson(json) {
443
+ const pk = json.publicKey;
444
+ return {
445
+ publicKey: {
446
+ ...pk,
447
+ challenge: base64urlToBytes(pk.challenge),
448
+ user: { ...pk.user, id: base64urlToBytes(pk.user.id) },
449
+ excludeCredentials: (pk.excludeCredentials ?? []).map((c) => ({
450
+ ...c,
451
+ id: base64urlToBytes(c.id)
452
+ }))
453
+ }
454
+ };
455
+ }
456
+ function requestOptionsFromJson(json) {
457
+ const pk = json.publicKey;
458
+ return {
459
+ publicKey: {
460
+ ...pk,
461
+ challenge: base64urlToBytes(pk.challenge),
462
+ allowCredentials: (pk.allowCredentials ?? []).map((c) => ({
463
+ ...c,
464
+ id: base64urlToBytes(c.id)
465
+ }))
466
+ }
467
+ };
468
+ }
469
+ function attestationToJson(cred) {
470
+ const r = cred.response;
471
+ return {
472
+ id: cred.id,
473
+ rawId: bytesToBase64url(cred.rawId),
474
+ type: cred.type,
475
+ response: {
476
+ attestationObject: bytesToBase64url(r.attestationObject),
477
+ clientDataJSON: bytesToBase64url(r.clientDataJSON),
478
+ transports: typeof r.getTransports === "function" ? r.getTransports() : void 0
479
+ },
480
+ extensions: cred.getClientExtensionResults?.() ?? {}
481
+ };
482
+ }
483
+ function assertionToJson(cred) {
484
+ const r = cred.response;
485
+ return {
486
+ id: cred.id,
487
+ rawId: bytesToBase64url(cred.rawId),
488
+ type: cred.type,
489
+ response: {
490
+ authenticatorData: bytesToBase64url(r.authenticatorData),
491
+ clientDataJSON: bytesToBase64url(r.clientDataJSON),
492
+ signature: bytesToBase64url(r.signature),
493
+ userHandle: r.userHandle ? bytesToBase64url(r.userHandle) : null
494
+ },
495
+ extensions: cred.getClientExtensionResults?.() ?? {}
496
+ };
497
+ }
498
+
356
499
  // src/SessionManager.ts
357
500
  var SessionManager = class {
358
501
  opts;
@@ -425,6 +568,15 @@ var SessionManager = class {
425
568
  };
426
569
 
427
570
  // src/AuthService.ts
571
+ function extractSessionId(token) {
572
+ if (!token) return void 0;
573
+ try {
574
+ const payload = decodeJwt(token).payload;
575
+ return payload?.session_id ?? payload?.sid ?? void 0;
576
+ } catch {
577
+ return void 0;
578
+ }
579
+ }
428
580
  function isMfaChallenge(v) {
429
581
  return typeof v === "object" && v !== null && v.mfa_required === true;
430
582
  }
@@ -454,10 +606,16 @@ var AuthService = class extends core.BaseService {
454
606
  _stateListeners = /* @__PURE__ */ new Set();
455
607
  _sessionManager;
456
608
  _applyingRemote = false;
609
+ /** AUTH-011 — séquence des effacements de session et dernier effacement annoncé par SIGNED_OUT. */
610
+ _clearSeq = 0;
611
+ _signedOutSeq = 0;
457
612
  _unsubStore = null;
458
613
  _disposed = false;
614
+ /** AUTH-015 — flux OAuth par défaut du client. */
615
+ _defaultFlowType;
459
616
  constructor(client, options) {
460
617
  super(client);
618
+ this._defaultFlowType = options?.flowType;
461
619
  const browser = typeof window !== "undefined";
462
620
  const enabled = !!options?.forceEnabled || (options?.autoRefresh ?? true) && browser && (options?.persistSession ?? true);
463
621
  this._sessionManager = new SessionManager({
@@ -473,8 +631,18 @@ var AuthService = class extends core.BaseService {
473
631
  onRemoteMessage: (data) => this._applyRemoteState(data)
474
632
  });
475
633
  this._sessionManager.start();
634
+ let hadToken = !!this.client.authStore.token;
476
635
  this._unsubStore = this.client.authStore.onChange((token) => {
477
636
  this._sessionManager.setActive(!!token);
637
+ if (!token && hadToken && !this._applyingRemote) {
638
+ const seq = ++this._clearSeq;
639
+ queueMicrotask(() => {
640
+ if (this._signedOutSeq < seq && !this.client.authStore.token) {
641
+ this._emitEvent("SIGNED_OUT", null, null);
642
+ }
643
+ });
644
+ }
645
+ hadToken = !!token;
478
646
  if (!this._applyingRemote) {
479
647
  const state = this._broadcastState();
480
648
  if (!token || state !== null) {
@@ -687,10 +855,9 @@ var AuthService = class extends core.BaseService {
687
855
  queueMicrotask(() => {
688
856
  if (this._stateListeners.has(callback)) {
689
857
  const session = store.token && store.user ? {
858
+ id: extractSessionId(store.token),
690
859
  access_token: store.token,
691
860
  refresh_token: store.refreshToken ?? "",
692
- accessToken: store.token,
693
- refreshToken: store.refreshToken ?? "",
694
861
  token_type: "Bearer",
695
862
  expires_in: 900,
696
863
  user: store.user
@@ -701,11 +868,11 @@ var AuthService = class extends core.BaseService {
701
868
  return result;
702
869
  }
703
870
  _emitEvent(event, user, accessToken, extra) {
871
+ if (event === "SIGNED_OUT") this._signedOutSeq = this._clearSeq;
704
872
  const session = accessToken && user ? {
873
+ id: extractSessionId(accessToken),
705
874
  access_token: accessToken,
706
875
  refresh_token: this.client.authStore.refreshToken ?? "",
707
- accessToken,
708
- refreshToken: this.client.authStore.refreshToken ?? "",
709
876
  token_type: "Bearer",
710
877
  expires_in: 900,
711
878
  user
@@ -735,16 +902,18 @@ var AuthService = class extends core.BaseService {
735
902
  }
736
903
  if (req.options?.data) payload.data = req.options.data;
737
904
  if (req.options?.emailRedirectTo) payload.email_redirect_to = req.options.emailRedirectTo;
905
+ if (req.options?.preventEmailEnumeration) payload.prevent_email_enumeration = true;
738
906
  const res = await this.request("POST", `${this.prefix}/register`, payload);
907
+ if (res.data && !isMfaChallenge(res.data) && !res.data.access_token) {
908
+ res.data.session = null;
909
+ return res;
910
+ }
739
911
  if (res.data && !isMfaChallenge(res.data)) {
740
912
  const tp = res.data;
741
- tp.accessToken = tp.access_token;
742
- tp.refreshToken = tp.refresh_token;
743
913
  tp.session = {
914
+ id: extractSessionId(tp.access_token),
744
915
  access_token: tp.access_token,
745
916
  refresh_token: tp.refresh_token,
746
- accessToken: tp.access_token,
747
- refreshToken: tp.refresh_token,
748
917
  token_type: tp.token_type,
749
918
  expires_in: tp.expires_in,
750
919
  user: tp.user,
@@ -756,11 +925,6 @@ var AuthService = class extends core.BaseService {
756
925
  }
757
926
  if (res.data && isMfaChallenge(res.data)) {
758
927
  const mfa = res.data;
759
- mfa.mfaChallenge = {
760
- mfaRequired: mfa.mfa_required,
761
- mfaToken: mfa.mfa_token,
762
- mfaSetupRequired: mfa.mfa_setup_required
763
- };
764
928
  this._emitEvent("MFA_CHALLENGE", null, null, {
765
929
  mfaToken: mfa.mfa_token,
766
930
  mfaSetupRequired: mfa.mfa_setup_required
@@ -782,13 +946,10 @@ var AuthService = class extends core.BaseService {
782
946
  const res = await this.request("POST", `${this.prefix}/login`, payload);
783
947
  if (res.data && !isMfaChallenge(res.data)) {
784
948
  const tp = res.data;
785
- tp.accessToken = tp.access_token;
786
- tp.refreshToken = tp.refresh_token;
787
949
  tp.session = {
950
+ id: extractSessionId(tp.access_token),
788
951
  access_token: tp.access_token,
789
952
  refresh_token: tp.refresh_token,
790
- accessToken: tp.access_token,
791
- refreshToken: tp.refresh_token,
792
953
  token_type: tp.token_type,
793
954
  expires_in: tp.expires_in,
794
955
  user: tp.user,
@@ -800,11 +961,6 @@ var AuthService = class extends core.BaseService {
800
961
  }
801
962
  if (res.data && isMfaChallenge(res.data)) {
802
963
  const mfa = res.data;
803
- mfa.mfaChallenge = {
804
- mfaRequired: mfa.mfa_required,
805
- mfaToken: mfa.mfa_token,
806
- mfaSetupRequired: mfa.mfa_setup_required
807
- };
808
964
  this._emitEvent("MFA_CHALLENGE", null, null, {
809
965
  mfaToken: mfa.mfa_token,
810
966
  mfaSetupRequired: mfa.mfa_setup_required
@@ -833,7 +989,7 @@ var AuthService = class extends core.BaseService {
833
989
  error: new core.AuraError(0, {
834
990
  error: {
835
991
  code: "invalid_provider",
836
- message: "signInWithOAuth: le fournisseur doit \xEAtre une cha\xEEne non vide, par exemple signInWithOAuth('google', { redirectTo }) ou signInWithOAuth({ provider: 'google', redirectTo })"
992
+ message: "signInWithOAuth: the provider must be a non-empty string, e.g. signInWithOAuth('google', { redirectTo }) or signInWithOAuth({ provider: 'google', redirectTo })"
837
993
  }
838
994
  })
839
995
  };
@@ -847,22 +1003,22 @@ var AuthService = class extends core.BaseService {
847
1003
  params.set(k, v);
848
1004
  continue;
849
1005
  }
850
- 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(", ")}`;
1006
+ const raison = OAUTH_SDK_OWNED_QUERY_PARAMS.has(k) ? `it is built by signInWithOAuth (use the dedicated option: ${k === "redirect_uri" || k === "redirect_to" ? "redirectTo" : k === "scopes" ? "scopes" : k === "project_id" ? "the project URL passed to createClient()" : "flowType:'pkce'"})` : `it is not forwarded to the provider. Only these sign-in experience parameters are: ${[...OAUTH_FORWARDABLE_QUERY_PARAMS].join(", ")}`;
851
1007
  return {
852
1008
  data: null,
853
1009
  error: new core.AuraError(400, {
854
1010
  error: {
855
1011
  code: "invalid_oauth_query_param",
856
- message: `signInWithOAuth: queryParams.${k} refus\xE9 \u2014 ${raison}.`
1012
+ message: `signInWithOAuth: queryParams.${k} rejected \u2014 ${raison}.`
857
1013
  }
858
1014
  })
859
1015
  };
860
1016
  }
861
1017
  }
862
- if (options?.flowType === "pkce") {
1018
+ if ((options?.flowType ?? this._defaultFlowType) === "pkce") {
863
1019
  if (typeof window !== "undefined" && !this.client.authStore.persistent) {
864
1020
  console.warn(
865
- `[aurabase] PKCE (flowType:"pkce") requiert un store persistant (LocalAuthStore) pour que le code_verifier survive au redirect OAuth ; le store courant est en m\xE9moire \u2014 l'\xE9change \xE9chouera au retour.`
1021
+ '[aurabase] PKCE (flowType:"pkce") requires a persistent store (LocalAuthStore) so that the code_verifier survives the OAuth redirect; the current store is in-memory, so the exchange will fail on return.'
866
1022
  );
867
1023
  }
868
1024
  try {
@@ -877,7 +1033,7 @@ var AuthService = class extends core.BaseService {
877
1033
  error: new core.AuraError(0, {
878
1034
  error: {
879
1035
  code: "pkce_generation_failed",
880
- message: "PKCE: g\xE9n\xE9ration du code_verifier/challenge impossible (WebCrypto indisponible)"
1036
+ message: "PKCE: cannot generate the code_verifier/challenge (WebCrypto unavailable)"
881
1037
  }
882
1038
  })
883
1039
  };
@@ -885,7 +1041,7 @@ var AuthService = class extends core.BaseService {
885
1041
  } else {
886
1042
  if (typeof window !== "undefined" && !this.client.authStore.persistent) {
887
1043
  console.warn(
888
- '[aurabase] S\xC9CURIT\xC9 : signInWithOAuth en flux implicit avec un store NON persistant (m\xE9moire) \u2014 le marqueur anti session-fixation ne survivra pas au redirect OAuth et la session ne sera pas adopt\xE9e au retour. Utilisez un LocalAuthStore, et pr\xE9f\xE9rez flowType:"pkce" (recommand\xE9, immunis\xE9 contre la session fixation).'
1044
+ '[aurabase] SECURITY: signInWithOAuth in implicit flow with a NON-persistent store (in-memory): the anti session-fixation marker will not survive the OAuth redirect and the session will not be adopted on return. Use a LocalAuthStore, and prefer flowType:"pkce" (recommended, immune to session fixation).'
889
1045
  );
890
1046
  }
891
1047
  try {
@@ -894,7 +1050,7 @@ var AuthService = class extends core.BaseService {
894
1050
  }
895
1051
  }
896
1052
  const url = `${this.client.baseUrl}/v1/auth/oauth/${encodeURIComponent(provider)}/start?${params.toString()}`;
897
- if (typeof window !== "undefined") {
1053
+ if (typeof window !== "undefined" && window.location) {
898
1054
  window.location.href = url;
899
1055
  }
900
1056
  return { data: { url }, error: null };
@@ -951,31 +1107,22 @@ var AuthService = class extends core.BaseService {
951
1107
  return res;
952
1108
  }
953
1109
  /**
954
- * Session courante lue depuis le store (synchrone en pratique, pas d'appel réseau).
955
- *
956
- * DOUBLE COMPATIBILITÉ DX (Parité Supabase & compatibilité existante Aurabase) :
957
- * - Permet `const { data: { session }, error } = await aura.auth.getSession()` (forme canonique)
958
- * - Permet `const { accessToken, user } = await aura.auth.getSession()` (forme plate historique)
1110
+ * Session courante lue depuis le store (pas d'appel réseau). Forme unique (AUTH-019) :
1111
+ * `const { data: { session } } = await aura.auth.getSession()`.
1112
+ * Côté serveur, cette session n'est PAS vérifiée : autorisez avec `getUser()` (AUTH-004).
959
1113
  */
960
1114
  getSession() {
961
1115
  const token = this.client.authStore.token;
962
1116
  const user = this.client.authStore.user;
963
1117
  const session = token && user ? {
1118
+ id: extractSessionId(token),
964
1119
  access_token: token,
965
1120
  refresh_token: this.client.authStore.refreshToken ?? "",
966
- accessToken: token,
967
- refreshToken: this.client.authStore.refreshToken ?? "",
968
1121
  token_type: "Bearer",
969
1122
  expires_in: 900,
970
1123
  user
971
1124
  } : null;
972
- return Promise.resolve({
973
- data: { session },
974
- error: null,
975
- accessToken: token,
976
- access_token: token,
977
- user
978
- });
1125
+ return Promise.resolve({ data: { session }, error: null });
979
1126
  }
980
1127
  /**
981
1128
  * Installe une session à partir d'un jeton d'accès et d'un jeton de rafraîchissement existants (par exemple reçus d'un serveur).
@@ -1243,7 +1390,7 @@ var AuthService = class extends core.BaseService {
1243
1390
  error: new core.AuraError(0, {
1244
1391
  error: {
1245
1392
  code: "allow_expired_unsupported_on_fallback",
1246
- message: "getClaims: allowExpired ne peut pas \xEAtre honor\xE9 sur le chemin de validation serveur (fallback HS256/JWKS)"
1393
+ message: "getClaims: allowExpired cannot be honored on the server validation path (HS256/JWKS fallback)"
1247
1394
  }
1248
1395
  })
1249
1396
  };
@@ -1271,7 +1418,7 @@ var AuthService = class extends core.BaseService {
1271
1418
  error: new core.AuraError(0, {
1272
1419
  error: {
1273
1420
  code: "invalid_response",
1274
- message: "getClaims: la validation serveur du token (getUser) a r\xE9pondu sans contenu \u2014 claims ind\xE9terminables"
1421
+ message: "getClaims: the server token validation (getUser) answered with no content, so the claims cannot be determined"
1275
1422
  }
1276
1423
  })
1277
1424
  };
@@ -1287,6 +1434,17 @@ var AuthService = class extends core.BaseService {
1287
1434
  * Supprime le compte de l'utilisateur connecté après confirmation par son mot de passe, puis vide la session locale.
1288
1435
  */
1289
1436
  async deleteUser(password) {
1437
+ if (!password || typeof password !== "string" || password.trim() === "") {
1438
+ return {
1439
+ data: null,
1440
+ error: new core.AuraError(400, {
1441
+ error: {
1442
+ code: "PASSWORD_REQUIRED",
1443
+ message: "Password is required to delete account"
1444
+ }
1445
+ })
1446
+ };
1447
+ }
1290
1448
  const res = await this.request("DELETE", `${this.prefix}/user`, { password });
1291
1449
  if (!res.error) {
1292
1450
  this.client.authStore.clear();
@@ -1348,9 +1506,107 @@ var AuthService = class extends core.BaseService {
1348
1506
  regenerateRecoveryCodes: (codeOrParams) => {
1349
1507
  const code = typeof codeOrParams === "string" ? codeOrParams : codeOrParams.code;
1350
1508
  return this.regenerateRecoveryCodes(code);
1509
+ },
1510
+ factors: (mfaToken) => this.request("POST", `${this.prefix}/mfa/factors`, { mfa_token: mfaToken }),
1511
+ sms: {
1512
+ setup: (phone) => this.request("POST", `${this.prefix}/mfa/sms/setup`, { phone }),
1513
+ verifySetup: (code) => this.request("POST", `${this.prefix}/mfa/sms/setup/verify`, { code }),
1514
+ send: (mfaToken) => this.request("POST", `${this.prefix}/mfa/sms/send`, {
1515
+ mfa_token: mfaToken
1516
+ }),
1517
+ remove: (password) => this.request("DELETE", `${this.prefix}/mfa/sms`, { password })
1518
+ },
1519
+ webauthn: {
1520
+ register: (params) => this.webauthnRegister(params),
1521
+ verify: (params) => this.webauthnVerify(params),
1522
+ list: () => this.request("GET", `${this.prefix}/mfa/webauthn`),
1523
+ remove: (id, password) => this.request(
1524
+ "DELETE",
1525
+ `${this.prefix}/mfa/webauthn/${encodeURIComponent(id)}`,
1526
+ { password }
1527
+ )
1351
1528
  }
1352
1529
  };
1353
1530
  }
1531
+ webauthnIndisponible() {
1532
+ return {
1533
+ data: null,
1534
+ error: new core.AuraError(0, {
1535
+ error: {
1536
+ code: "webauthn_unavailable",
1537
+ message: "WebAuthn is not available here: navigator.credentials is missing. Use a browser in a secure context (https or localhost), or pass `credentials` explicitly."
1538
+ }
1539
+ })
1540
+ };
1541
+ }
1542
+ /**
1543
+ * Enrôle une passkey WebAuthn pour l'utilisateur connecté (cérémonie complète : options du serveur,
1544
+ * `navigator.credentials.create`, confirmation). Le premier facteur du compte active le MFA et
1545
+ * renvoie les codes de récupération (`recovery_codes`) — à afficher une seule fois.
1546
+ */
1547
+ async webauthnRegister(params) {
1548
+ const conteneur = params?.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
1549
+ if (!conteneur) return this.webauthnIndisponible();
1550
+ const debut = await this.request(
1551
+ "POST",
1552
+ `${this.prefix}/mfa/webauthn/register/start`,
1553
+ { friendly_name: params?.friendlyName }
1554
+ );
1555
+ if (!debut.data) return debut;
1556
+ let cred;
1557
+ try {
1558
+ cred = await conteneur.create(creationOptionsFromJson(debut.data.options));
1559
+ } catch (e) {
1560
+ return this.webauthnAnnule(e);
1561
+ }
1562
+ if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
1563
+ return this.request("POST", `${this.prefix}/mfa/webauthn/register/finish`, {
1564
+ credential: attestationToJson(cred),
1565
+ friendly_name: params?.friendlyName
1566
+ });
1567
+ }
1568
+ /**
1569
+ * Relève un défi MFA (`mfaToken`, reçu d'un `signIn`) avec une passkey : options du serveur,
1570
+ * `navigator.credentials.get`, vérification. Enregistre la session et émet `SIGNED_IN`.
1571
+ */
1572
+ async webauthnVerify(params) {
1573
+ const conteneur = params.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
1574
+ if (!conteneur) return this.webauthnIndisponible();
1575
+ const debut = await this.request(
1576
+ "POST",
1577
+ `${this.prefix}/mfa/webauthn/verify/start`,
1578
+ { mfa_token: params.mfaToken }
1579
+ );
1580
+ if (!debut.data) return debut;
1581
+ let cred;
1582
+ try {
1583
+ cred = await conteneur.get(requestOptionsFromJson(debut.data.options));
1584
+ } catch (e) {
1585
+ return this.webauthnAnnule(e);
1586
+ }
1587
+ if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
1588
+ const res = await this.request("POST", `${this.prefix}/mfa/webauthn/verify/finish`, {
1589
+ mfa_token: params.mfaToken,
1590
+ credential: assertionToJson(cred)
1591
+ });
1592
+ if (res.data) {
1593
+ this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
1594
+ this._emitEvent("SIGNED_IN", res.data.user, res.data.access_token);
1595
+ }
1596
+ return res;
1597
+ }
1598
+ webauthnAnnule(cause) {
1599
+ const nom = cause instanceof Error ? cause.name : "Error";
1600
+ return {
1601
+ data: null,
1602
+ error: new core.AuraError(0, {
1603
+ error: {
1604
+ code: nom === "NotAllowedError" ? "webauthn_cancelled" : "webauthn_failed",
1605
+ message: nom === "NotAllowedError" ? "The passkey prompt was cancelled or timed out." : `The passkey ceremony failed: ${cause instanceof Error ? cause.message : String(cause)}`
1606
+ }
1607
+ })
1608
+ };
1609
+ }
1354
1610
  /** MFA : démarre l'enrôlement TOTP et renvoie le `secret` et l'URL `otpauth_url` à afficher en QR code. */
1355
1611
  async mfaEnroll() {
1356
1612
  return this.request("POST", `${this.prefix}/mfa/setup`);
@@ -1363,10 +1619,31 @@ var AuthService = class extends core.BaseService {
1363
1619
  * MFA : termine une connexion qui a renvoyé un défi (`mfa_token` et code TOTP). Enregistre la session et émet `SIGNED_IN`.
1364
1620
  */
1365
1621
  async mfaVerify(req) {
1366
- const res = await this.request("POST", `${this.prefix}/mfa/verify-login`, req);
1622
+ const raw = req;
1623
+ const mfa_token = raw.mfa_token ?? raw.mfaToken ?? raw.challenge_id ?? raw.challengeId;
1624
+ const code = raw.code;
1625
+ const recovery_code = raw.recovery_code ?? raw.recoveryCode;
1626
+ const sms_code = raw.sms_code ?? raw.smsCode;
1627
+ const body = {};
1628
+ if (mfa_token) body.mfa_token = mfa_token;
1629
+ if (code !== void 0) body.code = code;
1630
+ if (recovery_code !== void 0) body.recovery_code = recovery_code;
1631
+ if (sms_code !== void 0) body.sms_code = sms_code;
1632
+ const res = await this.request("POST", `${this.prefix}/mfa/verify-login`, body);
1367
1633
  if (res.data) {
1368
- this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
1369
- this._emitEvent("SIGNED_IN", res.data.user, res.data.access_token);
1634
+ const tp = res.data;
1635
+ tp.session = {
1636
+ id: extractSessionId(tp.access_token),
1637
+ access_token: tp.access_token,
1638
+ refresh_token: tp.refresh_token,
1639
+ token_type: tp.token_type,
1640
+ expires_in: tp.expires_in,
1641
+ user: tp.user,
1642
+ provider_token: tp.provider_token,
1643
+ provider_refresh_token: tp.provider_refresh_token
1644
+ };
1645
+ this.client.authStore.save(tp.access_token, tp.refresh_token, tp.user);
1646
+ this._emitEvent("SIGNED_IN", tp.user, tp.access_token);
1370
1647
  }
1371
1648
  return res;
1372
1649
  }
@@ -1520,10 +1797,10 @@ var AuthService = class extends core.BaseService {
1520
1797
  async getSettings() {
1521
1798
  return this.request("GET", `${this.prefix}/settings`);
1522
1799
  }
1523
- /** Clé publiable (anon key) du projet — GET /anon-key (public, à l'image du modèle Supabase). */
1524
- async getAnonKey() {
1525
- return this.request("GET", `${this.prefix}/anon-key`);
1526
- }
1800
+ // AUTH-023 — `getAnonKey()` retirée : la route `/anon-key` n'est servie qu'aux projets qui
1801
+ // activent l'auth hébergée (garde C1, aucun BaaS n'expose la clé d'un projet à un anonyme), et
1802
+ // un client qui l'appelle possède déjà la clé anon (passée à createClient). La méthode
1803
+ // répondait donc 404 pour presque tous les projets.
1527
1804
  // -- Password recovery (token consumption) ----------------------------------
1528
1805
  /**
1529
1806
  * Consomme le token de récupération de mot de passe et applique le nouveau mot de passe.
@@ -1563,4 +1840,10 @@ var AuthService = class extends core.BaseService {
1563
1840
  exports.AuthAdminService = AuthAdminService;
1564
1841
  exports.AuthService = AuthService;
1565
1842
  exports.SessionManager = SessionManager;
1843
+ exports.assertionToJson = assertionToJson;
1844
+ exports.attestationToJson = attestationToJson;
1845
+ exports.base64urlToBytes = base64urlToBytes;
1846
+ exports.bytesToBase64url = bytesToBase64url;
1847
+ exports.creationOptionsFromJson = creationOptionsFromJson;
1566
1848
  exports.isMfaChallenge = isMfaChallenge;
1849
+ exports.requestOptionsFromJson = requestOptionsFromJson;