@aurabase/auth 0.6.2 → 0.7.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/README.md CHANGED
@@ -288,14 +288,19 @@ await admin.authAdmin.listUsers({ limit: 50 })
288
288
  import { AuthAdminService } from '@aurabase/auth'
289
289
  ```
290
290
 
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`.
291
+ 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`.
292
292
 
293
- ## Limitations
293
+ ### Authentification Multi-Facteurs (MFA v0.7.0)
294
294
 
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.
295
+ Aurabase supporte l'authentification multifacteur complète avec niveaux d'assurance (AAL1, AAL2) et claim standard `amr` :
296
+ - **TOTP** : `c.auth.mfa.enroll()`, `c.auth.mfa.verifySetup(token)`, `c.auth.mfa.verify({ mfa_token, code })`
297
+ - **Facteurs disponibles** : `c.auth.mfa.factors(mfa_token)` liste les méthodes éligibles (`totp`, `webauthn`, `sms`)
298
+ - **WebAuthn / Passkeys** : `c.auth.mfa.webauthn`
299
+ - **SMS OTP** : `c.auth.mfa.sms`
300
+
301
+ ### Vérification locale de jeton (JWKS / ES256)
302
+
303
+ 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
304
 
300
305
  ## Documentation
301
306
 
package/dist/index.cjs CHANGED
@@ -56,6 +56,46 @@ var AuthAdminService = class extends core.BaseService {
56
56
  async generateLink(userId, type) {
57
57
  return this.request("POST", `${this.prefix}/users/${userId}/generate-link`, { type });
58
58
  }
59
+ /**
60
+ * Impersonation (AUTH-037) : jeton COURT (10 minutes au plus) pour agir en tant que l'utilisateur,
61
+ * avec le claim `act: { sub }`. Pas de refresh token, journalisé (`admin_impersonation`).
62
+ * Un compte `owner`/`admin` exige `allowPrivileged: true` ET une `reason`.
63
+ */
64
+ async impersonate(userId, options) {
65
+ return this.request("POST", `${this.prefix}/users/${userId}/impersonate`, {
66
+ reason: options?.reason,
67
+ ttl_seconds: options?.ttlSeconds,
68
+ allow_privileged: options?.allowPrivileged ?? false
69
+ });
70
+ }
71
+ /**
72
+ * Import en masse (AUTH-037) : jusqu'a 1000 utilisateurs par appel, avec hash bcrypt ou argon2id.
73
+ * Rapport ligne par ligne ; une ligne en erreur n'arrete pas les autres.
74
+ */
75
+ async importUsers(users) {
76
+ return this.request("POST", `${this.prefix}/users/import`, {
77
+ users: users.map((u) => ({
78
+ email: u.email,
79
+ email_verified: u.emailVerified ?? false,
80
+ password_hash: u.passwordHash,
81
+ name: u.name,
82
+ user_metadata: u.userMetadata,
83
+ app_metadata: u.appMetadata
84
+ }))
85
+ });
86
+ }
87
+ /** Cles de signature du projet (active + previous), URL du JWKS et emetteur (`iss`). */
88
+ async listSigningKeys() {
89
+ return this.request("GET", `${this.prefix}/signing-keys`);
90
+ }
91
+ /** Rotation : une nouvelle cle signe les nouveaux jetons, l'ancienne reste publiee 24 h. */
92
+ async rotateSigningKey() {
93
+ return this.request("POST", `${this.prefix}/signing-keys/rotate`, {});
94
+ }
95
+ /** Revoque une ancienne cle compromise (retiree du JWKS, jetons refuses). */
96
+ async revokeSigningKey(kid) {
97
+ return this.request("DELETE", `${this.prefix}/signing-keys/${encodeURIComponent(kid)}`);
98
+ }
59
99
  /**
60
100
  * Statistiques des utilisateurs : total, vérifiés, bannis, avec MFA et nouveaux sur 7 jours.
61
101
  */
@@ -353,6 +393,77 @@ async function codeChallengeS256(verifier) {
353
393
  return b64url(new Uint8Array(digest));
354
394
  }
355
395
 
396
+ // src/webauthn.ts
397
+ function base64urlToBytes(valeur) {
398
+ const b64 = valeur.replace(/-/g, "+").replace(/_/g, "/");
399
+ const bin = atob(b64 + "=".repeat((4 - b64.length % 4) % 4));
400
+ const octets = new Uint8Array(new ArrayBuffer(bin.length));
401
+ for (let i = 0; i < bin.length; i++) octets[i] = bin.charCodeAt(i);
402
+ return octets;
403
+ }
404
+ function bytesToBase64url(octets) {
405
+ const vue = octets instanceof ArrayBuffer ? new Uint8Array(octets) : new Uint8Array(octets.buffer, octets.byteOffset, octets.byteLength);
406
+ let bin = "";
407
+ for (const o of vue) bin += String.fromCharCode(o);
408
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
409
+ }
410
+ function creationOptionsFromJson(json) {
411
+ const pk = json.publicKey;
412
+ return {
413
+ publicKey: {
414
+ ...pk,
415
+ challenge: base64urlToBytes(pk.challenge),
416
+ user: { ...pk.user, id: base64urlToBytes(pk.user.id) },
417
+ excludeCredentials: (pk.excludeCredentials ?? []).map((c) => ({
418
+ ...c,
419
+ id: base64urlToBytes(c.id)
420
+ }))
421
+ }
422
+ };
423
+ }
424
+ function requestOptionsFromJson(json) {
425
+ const pk = json.publicKey;
426
+ return {
427
+ publicKey: {
428
+ ...pk,
429
+ challenge: base64urlToBytes(pk.challenge),
430
+ allowCredentials: (pk.allowCredentials ?? []).map((c) => ({
431
+ ...c,
432
+ id: base64urlToBytes(c.id)
433
+ }))
434
+ }
435
+ };
436
+ }
437
+ function attestationToJson(cred) {
438
+ const r = cred.response;
439
+ return {
440
+ id: cred.id,
441
+ rawId: bytesToBase64url(cred.rawId),
442
+ type: cred.type,
443
+ response: {
444
+ attestationObject: bytesToBase64url(r.attestationObject),
445
+ clientDataJSON: bytesToBase64url(r.clientDataJSON),
446
+ transports: typeof r.getTransports === "function" ? r.getTransports() : void 0
447
+ },
448
+ extensions: cred.getClientExtensionResults?.() ?? {}
449
+ };
450
+ }
451
+ function assertionToJson(cred) {
452
+ const r = cred.response;
453
+ return {
454
+ id: cred.id,
455
+ rawId: bytesToBase64url(cred.rawId),
456
+ type: cred.type,
457
+ response: {
458
+ authenticatorData: bytesToBase64url(r.authenticatorData),
459
+ clientDataJSON: bytesToBase64url(r.clientDataJSON),
460
+ signature: bytesToBase64url(r.signature),
461
+ userHandle: r.userHandle ? bytesToBase64url(r.userHandle) : null
462
+ },
463
+ extensions: cred.getClientExtensionResults?.() ?? {}
464
+ };
465
+ }
466
+
356
467
  // src/SessionManager.ts
357
468
  var SessionManager = class {
358
469
  opts;
@@ -454,10 +565,16 @@ var AuthService = class extends core.BaseService {
454
565
  _stateListeners = /* @__PURE__ */ new Set();
455
566
  _sessionManager;
456
567
  _applyingRemote = false;
568
+ /** AUTH-011 — séquence des effacements de session et dernier effacement annoncé par SIGNED_OUT. */
569
+ _clearSeq = 0;
570
+ _signedOutSeq = 0;
457
571
  _unsubStore = null;
458
572
  _disposed = false;
573
+ /** AUTH-015 — flux OAuth par défaut du client. */
574
+ _defaultFlowType;
459
575
  constructor(client, options) {
460
576
  super(client);
577
+ this._defaultFlowType = options?.flowType;
461
578
  const browser = typeof window !== "undefined";
462
579
  const enabled = !!options?.forceEnabled || (options?.autoRefresh ?? true) && browser && (options?.persistSession ?? true);
463
580
  this._sessionManager = new SessionManager({
@@ -473,8 +590,18 @@ var AuthService = class extends core.BaseService {
473
590
  onRemoteMessage: (data) => this._applyRemoteState(data)
474
591
  });
475
592
  this._sessionManager.start();
593
+ let hadToken = !!this.client.authStore.token;
476
594
  this._unsubStore = this.client.authStore.onChange((token) => {
477
595
  this._sessionManager.setActive(!!token);
596
+ if (!token && hadToken && !this._applyingRemote) {
597
+ const seq = ++this._clearSeq;
598
+ queueMicrotask(() => {
599
+ if (this._signedOutSeq < seq && !this.client.authStore.token) {
600
+ this._emitEvent("SIGNED_OUT", null, null);
601
+ }
602
+ });
603
+ }
604
+ hadToken = !!token;
478
605
  if (!this._applyingRemote) {
479
606
  const state = this._broadcastState();
480
607
  if (!token || state !== null) {
@@ -689,8 +816,6 @@ var AuthService = class extends core.BaseService {
689
816
  const session = store.token && store.user ? {
690
817
  access_token: store.token,
691
818
  refresh_token: store.refreshToken ?? "",
692
- accessToken: store.token,
693
- refreshToken: store.refreshToken ?? "",
694
819
  token_type: "Bearer",
695
820
  expires_in: 900,
696
821
  user: store.user
@@ -701,11 +826,10 @@ var AuthService = class extends core.BaseService {
701
826
  return result;
702
827
  }
703
828
  _emitEvent(event, user, accessToken, extra) {
829
+ if (event === "SIGNED_OUT") this._signedOutSeq = this._clearSeq;
704
830
  const session = accessToken && user ? {
705
831
  access_token: accessToken,
706
832
  refresh_token: this.client.authStore.refreshToken ?? "",
707
- accessToken,
708
- refreshToken: this.client.authStore.refreshToken ?? "",
709
833
  token_type: "Bearer",
710
834
  expires_in: 900,
711
835
  user
@@ -736,15 +860,15 @@ var AuthService = class extends core.BaseService {
736
860
  if (req.options?.data) payload.data = req.options.data;
737
861
  if (req.options?.emailRedirectTo) payload.email_redirect_to = req.options.emailRedirectTo;
738
862
  const res = await this.request("POST", `${this.prefix}/register`, payload);
863
+ if (res.data && !isMfaChallenge(res.data) && !res.data.access_token) {
864
+ res.data.session = null;
865
+ return res;
866
+ }
739
867
  if (res.data && !isMfaChallenge(res.data)) {
740
868
  const tp = res.data;
741
- tp.accessToken = tp.access_token;
742
- tp.refreshToken = tp.refresh_token;
743
869
  tp.session = {
744
870
  access_token: tp.access_token,
745
871
  refresh_token: tp.refresh_token,
746
- accessToken: tp.access_token,
747
- refreshToken: tp.refresh_token,
748
872
  token_type: tp.token_type,
749
873
  expires_in: tp.expires_in,
750
874
  user: tp.user,
@@ -756,11 +880,6 @@ var AuthService = class extends core.BaseService {
756
880
  }
757
881
  if (res.data && isMfaChallenge(res.data)) {
758
882
  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
883
  this._emitEvent("MFA_CHALLENGE", null, null, {
765
884
  mfaToken: mfa.mfa_token,
766
885
  mfaSetupRequired: mfa.mfa_setup_required
@@ -782,13 +901,9 @@ var AuthService = class extends core.BaseService {
782
901
  const res = await this.request("POST", `${this.prefix}/login`, payload);
783
902
  if (res.data && !isMfaChallenge(res.data)) {
784
903
  const tp = res.data;
785
- tp.accessToken = tp.access_token;
786
- tp.refreshToken = tp.refresh_token;
787
904
  tp.session = {
788
905
  access_token: tp.access_token,
789
906
  refresh_token: tp.refresh_token,
790
- accessToken: tp.access_token,
791
- refreshToken: tp.refresh_token,
792
907
  token_type: tp.token_type,
793
908
  expires_in: tp.expires_in,
794
909
  user: tp.user,
@@ -800,11 +915,6 @@ var AuthService = class extends core.BaseService {
800
915
  }
801
916
  if (res.data && isMfaChallenge(res.data)) {
802
917
  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
918
  this._emitEvent("MFA_CHALLENGE", null, null, {
809
919
  mfaToken: mfa.mfa_token,
810
920
  mfaSetupRequired: mfa.mfa_setup_required
@@ -833,7 +943,7 @@ var AuthService = class extends core.BaseService {
833
943
  error: new core.AuraError(0, {
834
944
  error: {
835
945
  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 })"
946
+ message: "signInWithOAuth: the provider must be a non-empty string, e.g. signInWithOAuth('google', { redirectTo }) or signInWithOAuth({ provider: 'google', redirectTo })"
837
947
  }
838
948
  })
839
949
  };
@@ -847,22 +957,22 @@ var AuthService = class extends core.BaseService {
847
957
  params.set(k, v);
848
958
  continue;
849
959
  }
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(", ")}`;
960
+ 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
961
  return {
852
962
  data: null,
853
963
  error: new core.AuraError(400, {
854
964
  error: {
855
965
  code: "invalid_oauth_query_param",
856
- message: `signInWithOAuth: queryParams.${k} refus\xE9 \u2014 ${raison}.`
966
+ message: `signInWithOAuth: queryParams.${k} rejected \u2014 ${raison}.`
857
967
  }
858
968
  })
859
969
  };
860
970
  }
861
971
  }
862
- if (options?.flowType === "pkce") {
972
+ if ((options?.flowType ?? this._defaultFlowType) === "pkce") {
863
973
  if (typeof window !== "undefined" && !this.client.authStore.persistent) {
864
974
  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.`
975
+ '[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
976
  );
867
977
  }
868
978
  try {
@@ -877,7 +987,7 @@ var AuthService = class extends core.BaseService {
877
987
  error: new core.AuraError(0, {
878
988
  error: {
879
989
  code: "pkce_generation_failed",
880
- message: "PKCE: g\xE9n\xE9ration du code_verifier/challenge impossible (WebCrypto indisponible)"
990
+ message: "PKCE: cannot generate the code_verifier/challenge (WebCrypto unavailable)"
881
991
  }
882
992
  })
883
993
  };
@@ -885,7 +995,7 @@ var AuthService = class extends core.BaseService {
885
995
  } else {
886
996
  if (typeof window !== "undefined" && !this.client.authStore.persistent) {
887
997
  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).'
998
+ '[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
999
  );
890
1000
  }
891
1001
  try {
@@ -894,7 +1004,7 @@ var AuthService = class extends core.BaseService {
894
1004
  }
895
1005
  }
896
1006
  const url = `${this.client.baseUrl}/v1/auth/oauth/${encodeURIComponent(provider)}/start?${params.toString()}`;
897
- if (typeof window !== "undefined") {
1007
+ if (typeof window !== "undefined" && window.location) {
898
1008
  window.location.href = url;
899
1009
  }
900
1010
  return { data: { url }, error: null };
@@ -951,11 +1061,9 @@ var AuthService = class extends core.BaseService {
951
1061
  return res;
952
1062
  }
953
1063
  /**
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)
1064
+ * Session courante lue depuis le store (pas d'appel réseau). Forme unique (AUTH-019) :
1065
+ * `const { data: { session } } = await aura.auth.getSession()`.
1066
+ * Côté serveur, cette session n'est PAS vérifiée : autorisez avec `getUser()` (AUTH-004).
959
1067
  */
960
1068
  getSession() {
961
1069
  const token = this.client.authStore.token;
@@ -963,19 +1071,11 @@ var AuthService = class extends core.BaseService {
963
1071
  const session = token && user ? {
964
1072
  access_token: token,
965
1073
  refresh_token: this.client.authStore.refreshToken ?? "",
966
- accessToken: token,
967
- refreshToken: this.client.authStore.refreshToken ?? "",
968
1074
  token_type: "Bearer",
969
1075
  expires_in: 900,
970
1076
  user
971
1077
  } : null;
972
- return Promise.resolve({
973
- data: { session },
974
- error: null,
975
- accessToken: token,
976
- access_token: token,
977
- user
978
- });
1078
+ return Promise.resolve({ data: { session }, error: null });
979
1079
  }
980
1080
  /**
981
1081
  * 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 +1343,7 @@ var AuthService = class extends core.BaseService {
1243
1343
  error: new core.AuraError(0, {
1244
1344
  error: {
1245
1345
  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)"
1346
+ message: "getClaims: allowExpired cannot be honored on the server validation path (HS256/JWKS fallback)"
1247
1347
  }
1248
1348
  })
1249
1349
  };
@@ -1271,7 +1371,7 @@ var AuthService = class extends core.BaseService {
1271
1371
  error: new core.AuraError(0, {
1272
1372
  error: {
1273
1373
  code: "invalid_response",
1274
- message: "getClaims: la validation serveur du token (getUser) a r\xE9pondu sans contenu \u2014 claims ind\xE9terminables"
1374
+ message: "getClaims: the server token validation (getUser) answered with no content, so the claims cannot be determined"
1275
1375
  }
1276
1376
  })
1277
1377
  };
@@ -1287,6 +1387,17 @@ var AuthService = class extends core.BaseService {
1287
1387
  * Supprime le compte de l'utilisateur connecté après confirmation par son mot de passe, puis vide la session locale.
1288
1388
  */
1289
1389
  async deleteUser(password) {
1390
+ if (!password || typeof password !== "string" || password.trim() === "") {
1391
+ return {
1392
+ data: null,
1393
+ error: new core.AuraError(400, {
1394
+ error: {
1395
+ code: "PASSWORD_REQUIRED",
1396
+ message: "Password is required to delete account"
1397
+ }
1398
+ })
1399
+ };
1400
+ }
1290
1401
  const res = await this.request("DELETE", `${this.prefix}/user`, { password });
1291
1402
  if (!res.error) {
1292
1403
  this.client.authStore.clear();
@@ -1348,9 +1459,107 @@ var AuthService = class extends core.BaseService {
1348
1459
  regenerateRecoveryCodes: (codeOrParams) => {
1349
1460
  const code = typeof codeOrParams === "string" ? codeOrParams : codeOrParams.code;
1350
1461
  return this.regenerateRecoveryCodes(code);
1462
+ },
1463
+ factors: (mfaToken) => this.request("POST", `${this.prefix}/mfa/factors`, { mfa_token: mfaToken }),
1464
+ sms: {
1465
+ setup: (phone) => this.request("POST", `${this.prefix}/mfa/sms/setup`, { phone }),
1466
+ verifySetup: (code) => this.request("POST", `${this.prefix}/mfa/sms/setup/verify`, { code }),
1467
+ send: (mfaToken) => this.request("POST", `${this.prefix}/mfa/sms/send`, {
1468
+ mfa_token: mfaToken
1469
+ }),
1470
+ remove: (password) => this.request("DELETE", `${this.prefix}/mfa/sms`, { password })
1471
+ },
1472
+ webauthn: {
1473
+ register: (params) => this.webauthnRegister(params),
1474
+ verify: (params) => this.webauthnVerify(params),
1475
+ list: () => this.request("GET", `${this.prefix}/mfa/webauthn`),
1476
+ remove: (id, password) => this.request(
1477
+ "DELETE",
1478
+ `${this.prefix}/mfa/webauthn/${encodeURIComponent(id)}`,
1479
+ { password }
1480
+ )
1351
1481
  }
1352
1482
  };
1353
1483
  }
1484
+ webauthnIndisponible() {
1485
+ return {
1486
+ data: null,
1487
+ error: new core.AuraError(0, {
1488
+ error: {
1489
+ code: "webauthn_unavailable",
1490
+ message: "WebAuthn is not available here: navigator.credentials is missing. Use a browser in a secure context (https or localhost), or pass `credentials` explicitly."
1491
+ }
1492
+ })
1493
+ };
1494
+ }
1495
+ /**
1496
+ * Enrôle une passkey WebAuthn pour l'utilisateur connecté (cérémonie complète : options du serveur,
1497
+ * `navigator.credentials.create`, confirmation). Le premier facteur du compte active le MFA et
1498
+ * renvoie les codes de récupération (`recovery_codes`) — à afficher une seule fois.
1499
+ */
1500
+ async webauthnRegister(params) {
1501
+ const conteneur = params?.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
1502
+ if (!conteneur) return this.webauthnIndisponible();
1503
+ const debut = await this.request(
1504
+ "POST",
1505
+ `${this.prefix}/mfa/webauthn/register/start`,
1506
+ { friendly_name: params?.friendlyName }
1507
+ );
1508
+ if (!debut.data) return debut;
1509
+ let cred;
1510
+ try {
1511
+ cred = await conteneur.create(creationOptionsFromJson(debut.data.options));
1512
+ } catch (e) {
1513
+ return this.webauthnAnnule(e);
1514
+ }
1515
+ if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
1516
+ return this.request("POST", `${this.prefix}/mfa/webauthn/register/finish`, {
1517
+ credential: attestationToJson(cred),
1518
+ friendly_name: params?.friendlyName
1519
+ });
1520
+ }
1521
+ /**
1522
+ * Relève un défi MFA (`mfaToken`, reçu d'un `signIn`) avec une passkey : options du serveur,
1523
+ * `navigator.credentials.get`, vérification. Enregistre la session et émet `SIGNED_IN`.
1524
+ */
1525
+ async webauthnVerify(params) {
1526
+ const conteneur = params.credentials ?? (typeof navigator !== "undefined" ? navigator.credentials : void 0);
1527
+ if (!conteneur) return this.webauthnIndisponible();
1528
+ const debut = await this.request(
1529
+ "POST",
1530
+ `${this.prefix}/mfa/webauthn/verify/start`,
1531
+ { mfa_token: params.mfaToken }
1532
+ );
1533
+ if (!debut.data) return debut;
1534
+ let cred;
1535
+ try {
1536
+ cred = await conteneur.get(requestOptionsFromJson(debut.data.options));
1537
+ } catch (e) {
1538
+ return this.webauthnAnnule(e);
1539
+ }
1540
+ if (!cred) return this.webauthnAnnule(new Error("no credential returned"));
1541
+ const res = await this.request("POST", `${this.prefix}/mfa/webauthn/verify/finish`, {
1542
+ mfa_token: params.mfaToken,
1543
+ credential: assertionToJson(cred)
1544
+ });
1545
+ if (res.data) {
1546
+ this.client.authStore.save(res.data.access_token, res.data.refresh_token, res.data.user);
1547
+ this._emitEvent("SIGNED_IN", res.data.user, res.data.access_token);
1548
+ }
1549
+ return res;
1550
+ }
1551
+ webauthnAnnule(cause) {
1552
+ const nom = cause instanceof Error ? cause.name : "Error";
1553
+ return {
1554
+ data: null,
1555
+ error: new core.AuraError(0, {
1556
+ error: {
1557
+ code: nom === "NotAllowedError" ? "webauthn_cancelled" : "webauthn_failed",
1558
+ message: nom === "NotAllowedError" ? "The passkey prompt was cancelled or timed out." : `The passkey ceremony failed: ${cause instanceof Error ? cause.message : String(cause)}`
1559
+ }
1560
+ })
1561
+ };
1562
+ }
1354
1563
  /** MFA : démarre l'enrôlement TOTP et renvoie le `secret` et l'URL `otpauth_url` à afficher en QR code. */
1355
1564
  async mfaEnroll() {
1356
1565
  return this.request("POST", `${this.prefix}/mfa/setup`);
@@ -1520,10 +1729,10 @@ var AuthService = class extends core.BaseService {
1520
1729
  async getSettings() {
1521
1730
  return this.request("GET", `${this.prefix}/settings`);
1522
1731
  }
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
- }
1732
+ // AUTH-023 — `getAnonKey()` retirée : la route `/anon-key` n'est servie qu'aux projets qui
1733
+ // activent l'auth hébergée (garde C1, aucun BaaS n'expose la clé d'un projet à un anonyme), et
1734
+ // un client qui l'appelle possède déjà la clé anon (passée à createClient). La méthode
1735
+ // répondait donc 404 pour presque tous les projets.
1527
1736
  // -- Password recovery (token consumption) ----------------------------------
1528
1737
  /**
1529
1738
  * Consomme le token de récupération de mot de passe et applique le nouveau mot de passe.
@@ -1563,4 +1772,10 @@ var AuthService = class extends core.BaseService {
1563
1772
  exports.AuthAdminService = AuthAdminService;
1564
1773
  exports.AuthService = AuthService;
1565
1774
  exports.SessionManager = SessionManager;
1775
+ exports.assertionToJson = assertionToJson;
1776
+ exports.attestationToJson = attestationToJson;
1777
+ exports.base64urlToBytes = base64urlToBytes;
1778
+ exports.bytesToBase64url = bytesToBase64url;
1779
+ exports.creationOptionsFromJson = creationOptionsFromJson;
1566
1780
  exports.isMfaChallenge = isMfaChallenge;
1781
+ exports.requestOptionsFromJson = requestOptionsFromJson;