@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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseService, isTokenExpired, clientScope, AuraError, refreshOnce, DEFAULT_REQUEST_TIMEOUT_MS, emitAuraEvent } from '@aurabase/core';
1
+ import { BaseService, AuraError, isTokenExpired, clientScope, refreshOnce, DEFAULT_REQUEST_TIMEOUT_MS, emitAuraEvent } from '@aurabase/core';
2
2
 
3
3
  // src/AuthAdminService.ts
4
4
  var AuthAdminService = class extends BaseService {
@@ -50,9 +50,81 @@ var AuthAdminService = class extends BaseService {
50
50
  }
51
51
  /**
52
52
  * Génère un lien à usage unique pour un utilisateur (`'magic'`, `'recovery'` ou `'invite'`) et renvoie `{ link, token }`.
53
+ * Accepte soit `generateLink(userId, type)` soit `generateLink({ userId, type })`.
53
54
  */
54
- async generateLink(userId, type) {
55
- return this.request("POST", `${this.prefix}/users/${userId}/generate-link`, { type });
55
+ async generateLink(userIdOrParams, type) {
56
+ let resolvedUserId;
57
+ let resolvedType;
58
+ if (typeof userIdOrParams === "object" && userIdOrParams !== null) {
59
+ resolvedUserId = userIdOrParams.userId ?? userIdOrParams.user_id;
60
+ resolvedType = userIdOrParams.type ?? type;
61
+ } else if (typeof userIdOrParams === "string") {
62
+ resolvedUserId = userIdOrParams;
63
+ resolvedType = type;
64
+ }
65
+ if (!resolvedUserId || typeof resolvedUserId !== "string" || resolvedUserId.trim() === "" || resolvedUserId.includes("[object")) {
66
+ return {
67
+ data: null,
68
+ error: new AuraError(400, {
69
+ error: {
70
+ code: "INVALID_ARGUMENT",
71
+ message: "userId must be a non-empty string (UUID). Usage: generateLink(userId, type) or generateLink({ userId, type })."
72
+ }
73
+ })
74
+ };
75
+ }
76
+ if (!resolvedType) {
77
+ return {
78
+ data: null,
79
+ error: new AuraError(400, {
80
+ error: {
81
+ code: "INVALID_ARGUMENT",
82
+ message: 'type is required for generateLink ("magic" | "recovery" | "invite").'
83
+ }
84
+ })
85
+ };
86
+ }
87
+ return this.request("POST", `${this.prefix}/users/${encodeURIComponent(resolvedUserId)}/generate-link`, { type: resolvedType });
88
+ }
89
+ /**
90
+ * Impersonation (AUTH-037) : jeton COURT (10 minutes au plus) pour agir en tant que l'utilisateur,
91
+ * avec le claim `act: { sub }`. Pas de refresh token, journalisé (`admin_impersonation`).
92
+ * Un compte `owner`/`admin` exige `allowPrivileged: true` ET une `reason`.
93
+ */
94
+ async impersonate(userId, options) {
95
+ return this.request("POST", `${this.prefix}/users/${userId}/impersonate`, {
96
+ reason: options?.reason,
97
+ ttl_seconds: options?.ttlSeconds,
98
+ allow_privileged: options?.allowPrivileged ?? false
99
+ });
100
+ }
101
+ /**
102
+ * Import en masse (AUTH-037) : jusqu'a 1000 utilisateurs par appel, avec hash bcrypt ou argon2id.
103
+ * Rapport ligne par ligne ; une ligne en erreur n'arrete pas les autres.
104
+ */
105
+ async importUsers(users) {
106
+ return this.request("POST", `${this.prefix}/users/import`, {
107
+ users: users.map((u) => ({
108
+ email: u.email,
109
+ email_verified: u.emailVerified ?? false,
110
+ password_hash: u.passwordHash,
111
+ name: u.name,
112
+ user_metadata: u.userMetadata,
113
+ app_metadata: u.appMetadata
114
+ }))
115
+ });
116
+ }
117
+ /** Cles de signature du projet (active + previous), URL du JWKS et emetteur (`iss`). */
118
+ async listSigningKeys() {
119
+ return this.request("GET", `${this.prefix}/signing-keys`);
120
+ }
121
+ /** Rotation : une nouvelle cle signe les nouveaux jetons, l'ancienne reste publiee 24 h. */
122
+ async rotateSigningKey() {
123
+ return this.request("POST", `${this.prefix}/signing-keys/rotate`, {});
124
+ }
125
+ /** Revoque une ancienne cle compromise (retiree du JWKS, jetons refuses). */
126
+ async revokeSigningKey(kid) {
127
+ return this.request("DELETE", `${this.prefix}/signing-keys/${encodeURIComponent(kid)}`);
56
128
  }
57
129
  /**
58
130
  * Statistiques des utilisateurs : total, vérifiés, bannis, avec MFA et nouveaux sur 7 jours.
@@ -351,6 +423,77 @@ async function codeChallengeS256(verifier) {
351
423
  return b64url(new Uint8Array(digest));
352
424
  }
353
425
 
426
+ // src/webauthn.ts
427
+ function base64urlToBytes(valeur) {
428
+ const b64 = valeur.replace(/-/g, "+").replace(/_/g, "/");
429
+ const bin = atob(b64 + "=".repeat((4 - b64.length % 4) % 4));
430
+ const octets = new Uint8Array(new ArrayBuffer(bin.length));
431
+ for (let i = 0; i < bin.length; i++) octets[i] = bin.charCodeAt(i);
432
+ return octets;
433
+ }
434
+ function bytesToBase64url(octets) {
435
+ const vue = octets instanceof ArrayBuffer ? new Uint8Array(octets) : new Uint8Array(octets.buffer, octets.byteOffset, octets.byteLength);
436
+ let bin = "";
437
+ for (const o of vue) bin += String.fromCharCode(o);
438
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
439
+ }
440
+ function creationOptionsFromJson(json) {
441
+ const pk = json.publicKey;
442
+ return {
443
+ publicKey: {
444
+ ...pk,
445
+ challenge: base64urlToBytes(pk.challenge),
446
+ user: { ...pk.user, id: base64urlToBytes(pk.user.id) },
447
+ excludeCredentials: (pk.excludeCredentials ?? []).map((c) => ({
448
+ ...c,
449
+ id: base64urlToBytes(c.id)
450
+ }))
451
+ }
452
+ };
453
+ }
454
+ function requestOptionsFromJson(json) {
455
+ const pk = json.publicKey;
456
+ return {
457
+ publicKey: {
458
+ ...pk,
459
+ challenge: base64urlToBytes(pk.challenge),
460
+ allowCredentials: (pk.allowCredentials ?? []).map((c) => ({
461
+ ...c,
462
+ id: base64urlToBytes(c.id)
463
+ }))
464
+ }
465
+ };
466
+ }
467
+ function attestationToJson(cred) {
468
+ const r = cred.response;
469
+ return {
470
+ id: cred.id,
471
+ rawId: bytesToBase64url(cred.rawId),
472
+ type: cred.type,
473
+ response: {
474
+ attestationObject: bytesToBase64url(r.attestationObject),
475
+ clientDataJSON: bytesToBase64url(r.clientDataJSON),
476
+ transports: typeof r.getTransports === "function" ? r.getTransports() : void 0
477
+ },
478
+ extensions: cred.getClientExtensionResults?.() ?? {}
479
+ };
480
+ }
481
+ function assertionToJson(cred) {
482
+ const r = cred.response;
483
+ return {
484
+ id: cred.id,
485
+ rawId: bytesToBase64url(cred.rawId),
486
+ type: cred.type,
487
+ response: {
488
+ authenticatorData: bytesToBase64url(r.authenticatorData),
489
+ clientDataJSON: bytesToBase64url(r.clientDataJSON),
490
+ signature: bytesToBase64url(r.signature),
491
+ userHandle: r.userHandle ? bytesToBase64url(r.userHandle) : null
492
+ },
493
+ extensions: cred.getClientExtensionResults?.() ?? {}
494
+ };
495
+ }
496
+
354
497
  // src/SessionManager.ts
355
498
  var SessionManager = class {
356
499
  opts;
@@ -423,6 +566,15 @@ var SessionManager = class {
423
566
  };
424
567
 
425
568
  // src/AuthService.ts
569
+ function extractSessionId(token) {
570
+ if (!token) return void 0;
571
+ try {
572
+ const payload = decodeJwt(token).payload;
573
+ return payload?.session_id ?? payload?.sid ?? void 0;
574
+ } catch {
575
+ return void 0;
576
+ }
577
+ }
426
578
  function isMfaChallenge(v) {
427
579
  return typeof v === "object" && v !== null && v.mfa_required === true;
428
580
  }
@@ -452,10 +604,16 @@ var AuthService = class extends BaseService {
452
604
  _stateListeners = /* @__PURE__ */ new Set();
453
605
  _sessionManager;
454
606
  _applyingRemote = false;
607
+ /** AUTH-011 — séquence des effacements de session et dernier effacement annoncé par SIGNED_OUT. */
608
+ _clearSeq = 0;
609
+ _signedOutSeq = 0;
455
610
  _unsubStore = null;
456
611
  _disposed = false;
612
+ /** AUTH-015 — flux OAuth par défaut du client. */
613
+ _defaultFlowType;
457
614
  constructor(client, options) {
458
615
  super(client);
616
+ this._defaultFlowType = options?.flowType;
459
617
  const browser = typeof window !== "undefined";
460
618
  const enabled = !!options?.forceEnabled || (options?.autoRefresh ?? true) && browser && (options?.persistSession ?? true);
461
619
  this._sessionManager = new SessionManager({
@@ -471,8 +629,18 @@ var AuthService = class extends BaseService {
471
629
  onRemoteMessage: (data) => this._applyRemoteState(data)
472
630
  });
473
631
  this._sessionManager.start();
632
+ let hadToken = !!this.client.authStore.token;
474
633
  this._unsubStore = this.client.authStore.onChange((token) => {
475
634
  this._sessionManager.setActive(!!token);
635
+ if (!token && hadToken && !this._applyingRemote) {
636
+ const seq = ++this._clearSeq;
637
+ queueMicrotask(() => {
638
+ if (this._signedOutSeq < seq && !this.client.authStore.token) {
639
+ this._emitEvent("SIGNED_OUT", null, null);
640
+ }
641
+ });
642
+ }
643
+ hadToken = !!token;
476
644
  if (!this._applyingRemote) {
477
645
  const state = this._broadcastState();
478
646
  if (!token || state !== null) {
@@ -685,10 +853,9 @@ var AuthService = class extends BaseService {
685
853
  queueMicrotask(() => {
686
854
  if (this._stateListeners.has(callback)) {
687
855
  const session = store.token && store.user ? {
856
+ id: extractSessionId(store.token),
688
857
  access_token: store.token,
689
858
  refresh_token: store.refreshToken ?? "",
690
- accessToken: store.token,
691
- refreshToken: store.refreshToken ?? "",
692
859
  token_type: "Bearer",
693
860
  expires_in: 900,
694
861
  user: store.user
@@ -699,11 +866,11 @@ var AuthService = class extends BaseService {
699
866
  return result;
700
867
  }
701
868
  _emitEvent(event, user, accessToken, extra) {
869
+ if (event === "SIGNED_OUT") this._signedOutSeq = this._clearSeq;
702
870
  const session = accessToken && user ? {
871
+ id: extractSessionId(accessToken),
703
872
  access_token: accessToken,
704
873
  refresh_token: this.client.authStore.refreshToken ?? "",
705
- accessToken,
706
- refreshToken: this.client.authStore.refreshToken ?? "",
707
874
  token_type: "Bearer",
708
875
  expires_in: 900,
709
876
  user
@@ -733,16 +900,18 @@ var AuthService = class extends BaseService {
733
900
  }
734
901
  if (req.options?.data) payload.data = req.options.data;
735
902
  if (req.options?.emailRedirectTo) payload.email_redirect_to = req.options.emailRedirectTo;
903
+ if (req.options?.preventEmailEnumeration) payload.prevent_email_enumeration = true;
736
904
  const res = await this.request("POST", `${this.prefix}/register`, payload);
905
+ if (res.data && !isMfaChallenge(res.data) && !res.data.access_token) {
906
+ res.data.session = null;
907
+ return res;
908
+ }
737
909
  if (res.data && !isMfaChallenge(res.data)) {
738
910
  const tp = res.data;
739
- tp.accessToken = tp.access_token;
740
- tp.refreshToken = tp.refresh_token;
741
911
  tp.session = {
912
+ id: extractSessionId(tp.access_token),
742
913
  access_token: tp.access_token,
743
914
  refresh_token: tp.refresh_token,
744
- accessToken: tp.access_token,
745
- refreshToken: tp.refresh_token,
746
915
  token_type: tp.token_type,
747
916
  expires_in: tp.expires_in,
748
917
  user: tp.user,
@@ -754,11 +923,6 @@ var AuthService = class extends BaseService {
754
923
  }
755
924
  if (res.data && isMfaChallenge(res.data)) {
756
925
  const mfa = res.data;
757
- mfa.mfaChallenge = {
758
- mfaRequired: mfa.mfa_required,
759
- mfaToken: mfa.mfa_token,
760
- mfaSetupRequired: mfa.mfa_setup_required
761
- };
762
926
  this._emitEvent("MFA_CHALLENGE", null, null, {
763
927
  mfaToken: mfa.mfa_token,
764
928
  mfaSetupRequired: mfa.mfa_setup_required
@@ -780,13 +944,10 @@ var AuthService = class extends BaseService {
780
944
  const res = await this.request("POST", `${this.prefix}/login`, payload);
781
945
  if (res.data && !isMfaChallenge(res.data)) {
782
946
  const tp = res.data;
783
- tp.accessToken = tp.access_token;
784
- tp.refreshToken = tp.refresh_token;
785
947
  tp.session = {
948
+ id: extractSessionId(tp.access_token),
786
949
  access_token: tp.access_token,
787
950
  refresh_token: tp.refresh_token,
788
- accessToken: tp.access_token,
789
- refreshToken: tp.refresh_token,
790
951
  token_type: tp.token_type,
791
952
  expires_in: tp.expires_in,
792
953
  user: tp.user,
@@ -798,11 +959,6 @@ var AuthService = class extends BaseService {
798
959
  }
799
960
  if (res.data && isMfaChallenge(res.data)) {
800
961
  const mfa = res.data;
801
- mfa.mfaChallenge = {
802
- mfaRequired: mfa.mfa_required,
803
- mfaToken: mfa.mfa_token,
804
- mfaSetupRequired: mfa.mfa_setup_required
805
- };
806
962
  this._emitEvent("MFA_CHALLENGE", null, null, {
807
963
  mfaToken: mfa.mfa_token,
808
964
  mfaSetupRequired: mfa.mfa_setup_required
@@ -831,7 +987,7 @@ var AuthService = class extends BaseService {
831
987
  error: new AuraError(0, {
832
988
  error: {
833
989
  code: "invalid_provider",
834
- message: "signInWithOAuth: le fournisseur doit \xEAtre une cha\xEEne non vide, par exemple signInWithOAuth('google', { redirectTo }) ou signInWithOAuth({ provider: 'google', redirectTo })"
990
+ message: "signInWithOAuth: the provider must be a non-empty string, e.g. signInWithOAuth('google', { redirectTo }) or signInWithOAuth({ provider: 'google', redirectTo })"
835
991
  }
836
992
  })
837
993
  };
@@ -845,22 +1001,22 @@ var AuthService = class extends BaseService {
845
1001
  params.set(k, v);
846
1002
  continue;
847
1003
  }
848
- 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(", ")}`;
1004
+ 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(", ")}`;
849
1005
  return {
850
1006
  data: null,
851
1007
  error: new AuraError(400, {
852
1008
  error: {
853
1009
  code: "invalid_oauth_query_param",
854
- message: `signInWithOAuth: queryParams.${k} refus\xE9 \u2014 ${raison}.`
1010
+ message: `signInWithOAuth: queryParams.${k} rejected \u2014 ${raison}.`
855
1011
  }
856
1012
  })
857
1013
  };
858
1014
  }
859
1015
  }
860
- if (options?.flowType === "pkce") {
1016
+ if ((options?.flowType ?? this._defaultFlowType) === "pkce") {
861
1017
  if (typeof window !== "undefined" && !this.client.authStore.persistent) {
862
1018
  console.warn(
863
- `[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.`
1019
+ '[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.'
864
1020
  );
865
1021
  }
866
1022
  try {
@@ -875,7 +1031,7 @@ var AuthService = class extends BaseService {
875
1031
  error: new AuraError(0, {
876
1032
  error: {
877
1033
  code: "pkce_generation_failed",
878
- message: "PKCE: g\xE9n\xE9ration du code_verifier/challenge impossible (WebCrypto indisponible)"
1034
+ message: "PKCE: cannot generate the code_verifier/challenge (WebCrypto unavailable)"
879
1035
  }
880
1036
  })
881
1037
  };
@@ -883,7 +1039,7 @@ var AuthService = class extends BaseService {
883
1039
  } else {
884
1040
  if (typeof window !== "undefined" && !this.client.authStore.persistent) {
885
1041
  console.warn(
886
- '[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).'
1042
+ '[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).'
887
1043
  );
888
1044
  }
889
1045
  try {
@@ -892,7 +1048,7 @@ var AuthService = class extends BaseService {
892
1048
  }
893
1049
  }
894
1050
  const url = `${this.client.baseUrl}/v1/auth/oauth/${encodeURIComponent(provider)}/start?${params.toString()}`;
895
- if (typeof window !== "undefined") {
1051
+ if (typeof window !== "undefined" && window.location) {
896
1052
  window.location.href = url;
897
1053
  }
898
1054
  return { data: { url }, error: null };
@@ -949,31 +1105,22 @@ var AuthService = class extends BaseService {
949
1105
  return res;
950
1106
  }
951
1107
  /**
952
- * Session courante lue depuis le store (synchrone en pratique, pas d'appel réseau).
953
- *
954
- * DOUBLE COMPATIBILITÉ DX (Parité Supabase & compatibilité existante Aurabase) :
955
- * - Permet `const { data: { session }, error } = await aura.auth.getSession()` (forme canonique)
956
- * - Permet `const { accessToken, user } = await aura.auth.getSession()` (forme plate historique)
1108
+ * Session courante lue depuis le store (pas d'appel réseau). Forme unique (AUTH-019) :
1109
+ * `const { data: { session } } = await aura.auth.getSession()`.
1110
+ * Côté serveur, cette session n'est PAS vérifiée : autorisez avec `getUser()` (AUTH-004).
957
1111
  */
958
1112
  getSession() {
959
1113
  const token = this.client.authStore.token;
960
1114
  const user = this.client.authStore.user;
961
1115
  const session = token && user ? {
1116
+ id: extractSessionId(token),
962
1117
  access_token: token,
963
1118
  refresh_token: this.client.authStore.refreshToken ?? "",
964
- accessToken: token,
965
- refreshToken: this.client.authStore.refreshToken ?? "",
966
1119
  token_type: "Bearer",
967
1120
  expires_in: 900,
968
1121
  user
969
1122
  } : null;
970
- return Promise.resolve({
971
- data: { session },
972
- error: null,
973
- accessToken: token,
974
- access_token: token,
975
- user
976
- });
1123
+ return Promise.resolve({ data: { session }, error: null });
977
1124
  }
978
1125
  /**
979
1126
  * 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).
@@ -1241,7 +1388,7 @@ var AuthService = class extends BaseService {
1241
1388
  error: new AuraError(0, {
1242
1389
  error: {
1243
1390
  code: "allow_expired_unsupported_on_fallback",
1244
- message: "getClaims: allowExpired ne peut pas \xEAtre honor\xE9 sur le chemin de validation serveur (fallback HS256/JWKS)"
1391
+ message: "getClaims: allowExpired cannot be honored on the server validation path (HS256/JWKS fallback)"
1245
1392
  }
1246
1393
  })
1247
1394
  };
@@ -1269,7 +1416,7 @@ var AuthService = class extends BaseService {
1269
1416
  error: new AuraError(0, {
1270
1417
  error: {
1271
1418
  code: "invalid_response",
1272
- message: "getClaims: la validation serveur du token (getUser) a r\xE9pondu sans contenu \u2014 claims ind\xE9terminables"
1419
+ message: "getClaims: the server token validation (getUser) answered with no content, so the claims cannot be determined"
1273
1420
  }
1274
1421
  })
1275
1422
  };
@@ -1285,6 +1432,17 @@ var AuthService = class extends BaseService {
1285
1432
  * Supprime le compte de l'utilisateur connecté après confirmation par son mot de passe, puis vide la session locale.
1286
1433
  */
1287
1434
  async deleteUser(password) {
1435
+ if (!password || typeof password !== "string" || password.trim() === "") {
1436
+ return {
1437
+ data: null,
1438
+ error: new AuraError(400, {
1439
+ error: {
1440
+ code: "PASSWORD_REQUIRED",
1441
+ message: "Password is required to delete account"
1442
+ }
1443
+ })
1444
+ };
1445
+ }
1288
1446
  const res = await this.request("DELETE", `${this.prefix}/user`, { password });
1289
1447
  if (!res.error) {
1290
1448
  this.client.authStore.clear();
@@ -1346,9 +1504,107 @@ var AuthService = class extends BaseService {
1346
1504
  regenerateRecoveryCodes: (codeOrParams) => {
1347
1505
  const code = typeof codeOrParams === "string" ? codeOrParams : codeOrParams.code;
1348
1506
  return this.regenerateRecoveryCodes(code);
1507
+ },
1508
+ factors: (mfaToken) => this.request("POST", `${this.prefix}/mfa/factors`, { mfa_token: mfaToken }),
1509
+ sms: {
1510
+ setup: (phone) => this.request("POST", `${this.prefix}/mfa/sms/setup`, { phone }),
1511
+ verifySetup: (code) => this.request("POST", `${this.prefix}/mfa/sms/setup/verify`, { code }),
1512
+ send: (mfaToken) => this.request("POST", `${this.prefix}/mfa/sms/send`, {
1513
+ mfa_token: mfaToken
1514
+ }),
1515
+ remove: (password) => this.request("DELETE", `${this.prefix}/mfa/sms`, { password })
1516
+ },
1517
+ webauthn: {
1518
+ register: (params) => this.webauthnRegister(params),
1519
+ verify: (params) => this.webauthnVerify(params),
1520
+ list: () => this.request("GET", `${this.prefix}/mfa/webauthn`),
1521
+ remove: (id, password) => this.request(
1522
+ "DELETE",
1523
+ `${this.prefix}/mfa/webauthn/${encodeURIComponent(id)}`,
1524
+ { password }
1525
+ )
1349
1526
  }
1350
1527
  };
1351
1528
  }
1529
+ webauthnIndisponible() {
1530
+ return {
1531
+ data: null,
1532
+ error: new AuraError(0, {
1533
+ error: {
1534
+ code: "webauthn_unavailable",
1535
+ message: "WebAuthn is not available here: navigator.credentials is missing. Use a browser in a secure context (https or localhost), or pass `credentials` explicitly."
1536
+ }
1537
+ })
1538
+ };
1539
+ }
1540
+ /**
1541
+ * Enrôle une passkey WebAuthn pour l'utilisateur connecté (cérémonie complète : options du serveur,
1542
+ * `navigator.credentials.create`, confirmation). Le premier facteur du compte active le MFA et
1543
+ * renvoie les codes de récupération (`recovery_codes`) — à afficher une seule fois.
1544
+ */
1545
+ async webauthnRegister(params) {
1546
+ const conteneur = params?.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
1547
+ if (!conteneur) return this.webauthnIndisponible();
1548
+ const debut = await this.request(
1549
+ "POST",
1550
+ `${this.prefix}/mfa/webauthn/register/start`,
1551
+ { friendly_name: params?.friendlyName }
1552
+ );
1553
+ if (!debut.data) return debut;
1554
+ let cred;
1555
+ try {
1556
+ cred = await conteneur.create(creationOptionsFromJson(debut.data.options));
1557
+ } catch (e) {
1558
+ return this.webauthnAnnule(e);
1559
+ }
1560
+ if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
1561
+ return this.request("POST", `${this.prefix}/mfa/webauthn/register/finish`, {
1562
+ credential: attestationToJson(cred),
1563
+ friendly_name: params?.friendlyName
1564
+ });
1565
+ }
1566
+ /**
1567
+ * Relève un défi MFA (`mfaToken`, reçu d'un `signIn`) avec une passkey : options du serveur,
1568
+ * `navigator.credentials.get`, vérification. Enregistre la session et émet `SIGNED_IN`.
1569
+ */
1570
+ async webauthnVerify(params) {
1571
+ const conteneur = params.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
1572
+ if (!conteneur) return this.webauthnIndisponible();
1573
+ const debut = await this.request(
1574
+ "POST",
1575
+ `${this.prefix}/mfa/webauthn/verify/start`,
1576
+ { mfa_token: params.mfaToken }
1577
+ );
1578
+ if (!debut.data) return debut;
1579
+ let cred;
1580
+ try {
1581
+ cred = await conteneur.get(requestOptionsFromJson(debut.data.options));
1582
+ } catch (e) {
1583
+ return this.webauthnAnnule(e);
1584
+ }
1585
+ if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
1586
+ const res = await this.request("POST", `${this.prefix}/mfa/webauthn/verify/finish`, {
1587
+ mfa_token: params.mfaToken,
1588
+ credential: assertionToJson(cred)
1589
+ });
1590
+ if (res.data) {
1591
+ this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
1592
+ this._emitEvent("SIGNED_IN", res.data.user, res.data.access_token);
1593
+ }
1594
+ return res;
1595
+ }
1596
+ webauthnAnnule(cause) {
1597
+ const nom = cause instanceof Error ? cause.name : "Error";
1598
+ return {
1599
+ data: null,
1600
+ error: new AuraError(0, {
1601
+ error: {
1602
+ code: nom === "NotAllowedError" ? "webauthn_cancelled" : "webauthn_failed",
1603
+ message: nom === "NotAllowedError" ? "The passkey prompt was cancelled or timed out." : `The passkey ceremony failed: ${cause instanceof Error ? cause.message : String(cause)}`
1604
+ }
1605
+ })
1606
+ };
1607
+ }
1352
1608
  /** MFA : démarre l'enrôlement TOTP et renvoie le `secret` et l'URL `otpauth_url` à afficher en QR code. */
1353
1609
  async mfaEnroll() {
1354
1610
  return this.request("POST", `${this.prefix}/mfa/setup`);
@@ -1361,10 +1617,31 @@ var AuthService = class extends BaseService {
1361
1617
  * MFA : termine une connexion qui a renvoyé un défi (`mfa_token` et code TOTP). Enregistre la session et émet `SIGNED_IN`.
1362
1618
  */
1363
1619
  async mfaVerify(req) {
1364
- const res = await this.request("POST", `${this.prefix}/mfa/verify-login`, req);
1620
+ const raw = req;
1621
+ const mfa_token = raw.mfa_token ?? raw.mfaToken ?? raw.challenge_id ?? raw.challengeId;
1622
+ const code = raw.code;
1623
+ const recovery_code = raw.recovery_code ?? raw.recoveryCode;
1624
+ const sms_code = raw.sms_code ?? raw.smsCode;
1625
+ const body = {};
1626
+ if (mfa_token) body.mfa_token = mfa_token;
1627
+ if (code !== void 0) body.code = code;
1628
+ if (recovery_code !== void 0) body.recovery_code = recovery_code;
1629
+ if (sms_code !== void 0) body.sms_code = sms_code;
1630
+ const res = await this.request("POST", `${this.prefix}/mfa/verify-login`, body);
1365
1631
  if (res.data) {
1366
- this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
1367
- this._emitEvent("SIGNED_IN", res.data.user, res.data.access_token);
1632
+ const tp = res.data;
1633
+ tp.session = {
1634
+ id: extractSessionId(tp.access_token),
1635
+ access_token: tp.access_token,
1636
+ refresh_token: tp.refresh_token,
1637
+ token_type: tp.token_type,
1638
+ expires_in: tp.expires_in,
1639
+ user: tp.user,
1640
+ provider_token: tp.provider_token,
1641
+ provider_refresh_token: tp.provider_refresh_token
1642
+ };
1643
+ this.client.authStore.save(tp.access_token, tp.refresh_token, tp.user);
1644
+ this._emitEvent("SIGNED_IN", tp.user, tp.access_token);
1368
1645
  }
1369
1646
  return res;
1370
1647
  }
@@ -1518,10 +1795,10 @@ var AuthService = class extends BaseService {
1518
1795
  async getSettings() {
1519
1796
  return this.request("GET", `${this.prefix}/settings`);
1520
1797
  }
1521
- /** Clé publiable (anon key) du projet — GET /anon-key (public, à l'image du modèle Supabase). */
1522
- async getAnonKey() {
1523
- return this.request("GET", `${this.prefix}/anon-key`);
1524
- }
1798
+ // AUTH-023 — `getAnonKey()` retirée : la route `/anon-key` n'est servie qu'aux projets qui
1799
+ // activent l'auth hébergée (garde C1, aucun BaaS n'expose la clé d'un projet à un anonyme), et
1800
+ // un client qui l'appelle possède déjà la clé anon (passée à createClient). La méthode
1801
+ // répondait donc 404 pour presque tous les projets.
1525
1802
  // -- Password recovery (token consumption) ----------------------------------
1526
1803
  /**
1527
1804
  * Consomme le token de récupération de mot de passe et applique le nouveau mot de passe.
@@ -1558,4 +1835,4 @@ var AuthService = class extends BaseService {
1558
1835
  }
1559
1836
  };
1560
1837
 
1561
- export { AuthAdminService, AuthService, SessionManager, isMfaChallenge };
1838
+ export { AuthAdminService, AuthService, SessionManager, assertionToJson, attestationToJson, base64urlToBytes, bytesToBase64url, creationOptionsFromJson, isMfaChallenge, requestOptionsFromJson };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurabase/auth",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "Service d'authentification du SDK Aurabase (sessions, OAuth/PKCE, MFA, magic link, OTP, admin)",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -46,7 +46,7 @@
46
46
  "authentication"
47
47
  ],
48
48
  "dependencies": {
49
- "@aurabase/core": "0.6.2"
49
+ "@aurabase/core": "0.7.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "20.14.15",