@aurabase/auth 0.4.1 → 0.5.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 +89 -7
- package/dist/index.cjs +347 -202
- package/dist/index.d.cts +160 -88
- package/dist/index.d.ts +160 -88
- package/dist/index.js +348 -203
- package/package.json +16 -14
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/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
37
|
-
this.
|
|
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
|
-
|
|
44
|
-
this.
|
|
28
|
+
async updateUser(userId, data) {
|
|
29
|
+
return this.request("PUT", `${this.prefix}/users/${userId}`, data);
|
|
45
30
|
}
|
|
46
|
-
|
|
47
|
-
this.
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
685
|
-
this.
|
|
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, {
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
1075
|
-
this.
|
|
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(
|
|
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 };
|