@aurabase/core 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 CHANGED
@@ -113,7 +113,7 @@ class WidgetsService extends BaseService {
113
113
  }
114
114
 
115
115
  private get prefix(): string {
116
- return `/v1/widgets/${this.client.projectId}`
116
+ return '/v1/widgets'
117
117
  }
118
118
 
119
119
  async list(): Promise<AuraResponse<Widget[]>> {
@@ -132,16 +132,21 @@ class WidgetsService extends BaseService {
132
132
 
133
133
  `BaseAuthStore` est en mémoire (event-emitter `onChange`, stockage clé/valeur générique pour le PKCE `code_verifier`). `LocalAuthStore` l'étend avec persistance `localStorage` et des helpers cookie pour le rendu serveur.
134
134
 
135
+ Son argument `key` est **obligatoire** et doit être SCOPÉ par projet : deux clients pointant sur deux projets dans la même page partagent un seul `localStorage` (celui de la page), et une clé constante les ferait s'écraser. Le nom canonique est `aura-${clientScope(baseUrl)}-auth` — celui que posent déjà `AuraClient` et `createServerClient`.
136
+
135
137
  ```ts
136
- import { LocalAuthStore } from '@aurabase/core'
138
+ import { LocalAuthStore, clientScope } from '@aurabase/core'
139
+
140
+ const PROJET = 'https://mon-projet.aurabase.cloud'
141
+ const CLE = `aura-${clientScope(PROJET)}-auth`
137
142
 
138
143
  function hydrateFromCookieHeader(cookieHeader: string) {
139
- const store = new LocalAuthStore('aurabase_auth')
144
+ const store = new LocalAuthStore(CLE)
140
145
  store.loadFromCookie(cookieHeader) // hydrate depuis l'en-tête Cookie de la requête entrante
141
146
  return store
142
147
  }
143
148
 
144
- const store = hydrateFromCookieHeader('aurabase_auth=%7B%22token%22%3Anull%7D')
149
+ const store = hydrateFromCookieHeader(`${CLE}=%7B%22token%22%3Anull%7D`)
145
150
 
146
151
  // Après une mutation de session, sérialiser l'état courant en Set-Cookie.
147
152
  const setCookieValue = store.exportToCookie({ path: '/', secure: true, sameSite: 'Lax', httpOnly: true })
@@ -163,7 +168,7 @@ Cette couche (`fetchWithAuth`, `refreshOnce`) est exportée mais reste un détai
163
168
 
164
169
  - Package d'infrastructure : pas d'API « produit ». Pour construire une application, utilisez le méta-package [`aurabase`](../aurabase), qui assemble tous les services derrière un seul client.
165
170
  - `BaseAuthStore` est en mémoire — l'état est perdu au reload ; `LocalAuthStore` nécessite un `localStorage` disponible (sinon retombe silencieusement sur le comportement en mémoire, sauf via les helpers cookie explicites).
166
- - `AuraClientLike` est un contrat minimal (`baseUrl`, `projectId`, `apiKey`, `fetchFn`, `authStore`, `auth.refreshSession`, `retryConfig?`, `requestTimeoutMs?`) : un client maison doit l'implémenter intégralement pour rester compatible avec `BaseService`.
171
+ - `AuraClientLike` est un contrat minimal (`baseUrl`, `apiKey`, `fetchFn`, `authStore`, `auth.refreshSession`, `retryConfig?`, `requestTimeoutMs?`) : un client maison doit l'implémenter intégralement pour rester compatible avec `BaseService`.
167
172
  - La couche fetch bas niveau (`fetchWithAuth`/`refreshOnce`) est un détail d'implémentation interne, pas une API publique stabilisée.
168
173
 
169
174
  ## Documentation
package/dist/index.cjs CHANGED
@@ -33,6 +33,70 @@ function isAuraError(error) {
33
33
  return error instanceof AuraError || typeof error === "object" && error !== null && error.__isAuraError === true;
34
34
  }
35
35
 
36
+ // src/retry.ts
37
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 502, 503, 504]);
38
+ var RETRYABLE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
39
+ var DEFAULT_MAX_RETRIES = 3;
40
+ var BASE_MS = 500;
41
+ var CAP_MS = 1e4;
42
+ function isRetryableStatus(status) {
43
+ return RETRYABLE_STATUS.has(status);
44
+ }
45
+ function isRetryableMethod(method) {
46
+ return RETRYABLE_METHODS.has(method.toUpperCase());
47
+ }
48
+ function computeBackoff(attempt, rand = Math.random) {
49
+ const bound = Math.min(BASE_MS * 2 ** attempt, CAP_MS);
50
+ return Math.floor(rand() * bound);
51
+ }
52
+ function parseRetryAfter(header, now = Date.now()) {
53
+ if (header === null) return null;
54
+ const trimmed = header.trim();
55
+ if (/^\d+$/.test(trimmed)) return parseInt(trimmed, 10) * 1e3;
56
+ const dateMs = Date.parse(trimmed);
57
+ if (Number.isNaN(dateMs)) return null;
58
+ return Math.max(0, dateMs - now);
59
+ }
60
+ function abortableSleep(ms, signal) {
61
+ return new Promise((resolve) => {
62
+ if (signal?.aborted) {
63
+ resolve();
64
+ return;
65
+ }
66
+ const onAbort = () => {
67
+ clearTimeout(timer);
68
+ resolve();
69
+ };
70
+ const timer = setTimeout(() => {
71
+ signal?.removeEventListener("abort", onAbort);
72
+ resolve();
73
+ }, ms);
74
+ signal?.addEventListener("abort", onAbort, { once: true });
75
+ });
76
+ }
77
+ function isAbortError(err) {
78
+ return typeof err === "object" && err !== null && err.name === "AbortError";
79
+ }
80
+
81
+ // src/scope.ts
82
+ function clientScope(baseUrl) {
83
+ let host;
84
+ try {
85
+ host = new URL(baseUrl).host;
86
+ } catch {
87
+ host = baseUrl;
88
+ }
89
+ return host.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "default";
90
+ }
91
+
92
+ // src/serialize.ts
93
+ function serializeBody(body) {
94
+ return JSON.stringify(
95
+ body,
96
+ (_key, value) => typeof value === "bigint" ? value.toString() : value
97
+ );
98
+ }
99
+
36
100
  // src/types.ts
37
101
  function redactUrl(u) {
38
102
  const cut = u.search(/[?#]/);
@@ -52,6 +116,60 @@ function emitAuraEvent(onEvent, debug, event) {
52
116
  } catch {
53
117
  }
54
118
  }
119
+ var AUTH_SETTINGS_WRITABLE_FIELDS = [
120
+ "allow_signup",
121
+ "email_confirmation_required",
122
+ "password_min_length",
123
+ "mfa_enabled",
124
+ "allow_anonymous_users",
125
+ "jwt_expiry_seconds",
126
+ "refresh_token_expiry_days",
127
+ "site_url",
128
+ "oauth_providers",
129
+ "new_device_alerts_enabled",
130
+ "webhook_url",
131
+ "webhook_secret",
132
+ "smtp_host",
133
+ "smtp_port",
134
+ "smtp_username",
135
+ "smtp_password",
136
+ "smtp_from_email",
137
+ "smtp_from_name",
138
+ "smtp_clear",
139
+ "oauth_credentials",
140
+ "oauth_credentials_clear",
141
+ "captcha_enabled",
142
+ "captcha_provider",
143
+ "captcha_site_key",
144
+ "captcha_secret_key",
145
+ "password_check_hibp",
146
+ "sms_provider",
147
+ "sms_api_key",
148
+ "sms_api_secret",
149
+ "sms_from_number",
150
+ "sms_clear",
151
+ "redirect_urls",
152
+ "session_inactivity_timeout_seconds",
153
+ "hook_send_sms_url",
154
+ "hook_send_email_url",
155
+ "hook_mfa_verification_url",
156
+ "hook_password_verification_url",
157
+ "auto_link_by_email",
158
+ "rate_limit_login_per_hour",
159
+ "rate_limit_emails_per_hour",
160
+ "rate_limit_signup_per_hour",
161
+ "revoke_on_new_device",
162
+ "enforce_ip_binding",
163
+ "allowed_email_domains",
164
+ "blocked_email_domains",
165
+ "password_require_uppercase",
166
+ "password_require_number",
167
+ "password_require_special",
168
+ "hook_before_signup_url",
169
+ "hook_after_signup_url",
170
+ "hook_custom_access_token_url",
171
+ "hook_secret"
172
+ ];
55
173
 
56
174
  // src/stores/BaseAuthStore.ts
57
175
  function decodeJwtPayload(token) {
@@ -172,59 +290,6 @@ var BaseAuthStore = class {
172
290
  }
173
291
  };
174
292
 
175
- // src/retry.ts
176
- var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 502, 503, 504]);
177
- var RETRYABLE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
178
- var DEFAULT_MAX_RETRIES = 3;
179
- var BASE_MS = 500;
180
- var CAP_MS = 1e4;
181
- function isRetryableStatus(status) {
182
- return RETRYABLE_STATUS.has(status);
183
- }
184
- function isRetryableMethod(method) {
185
- return RETRYABLE_METHODS.has(method.toUpperCase());
186
- }
187
- function computeBackoff(attempt, rand = Math.random) {
188
- const bound = Math.min(BASE_MS * 2 ** attempt, CAP_MS);
189
- return Math.floor(rand() * bound);
190
- }
191
- function parseRetryAfter(header, now = Date.now()) {
192
- if (header === null) return null;
193
- const trimmed = header.trim();
194
- if (/^\d+$/.test(trimmed)) return parseInt(trimmed, 10) * 1e3;
195
- const dateMs = Date.parse(trimmed);
196
- if (Number.isNaN(dateMs)) return null;
197
- return Math.max(0, dateMs - now);
198
- }
199
- function abortableSleep(ms, signal) {
200
- return new Promise((resolve) => {
201
- if (signal?.aborted) {
202
- resolve();
203
- return;
204
- }
205
- const onAbort = () => {
206
- clearTimeout(timer);
207
- resolve();
208
- };
209
- const timer = setTimeout(() => {
210
- signal?.removeEventListener("abort", onAbort);
211
- resolve();
212
- }, ms);
213
- signal?.addEventListener("abort", onAbort, { once: true });
214
- });
215
- }
216
- function isAbortError(err) {
217
- return typeof err === "object" && err !== null && err.name === "AbortError";
218
- }
219
-
220
- // src/serialize.ts
221
- function serializeBody(body) {
222
- return JSON.stringify(
223
- body,
224
- (_key, value) => typeof value === "bigint" ? value.toString() : value
225
- );
226
- }
227
-
228
293
  // src/fetch.ts
229
294
  var REFRESH_MARGIN_SEC = 30;
230
295
  var RETRY_AFTER_CAP_MS = 6e4;
@@ -292,7 +357,9 @@ async function doRefresh(client) {
292
357
  if (!rt) {
293
358
  return {
294
359
  data: null,
295
- error: new AuraError(0, { error: { code: "no_session", message: "No refresh token available" } })
360
+ error: new AuraError(0, {
361
+ error: { code: "no_session", message: "No refresh token available" }
362
+ })
296
363
  };
297
364
  }
298
365
  const epoch = store.removalEpoch;
@@ -319,7 +386,7 @@ async function doRefresh(client) {
319
386
  };
320
387
  let res;
321
388
  try {
322
- res = await client.fetchFn(`${client.baseUrl}/v1/auth/${client.projectId}/refresh`, {
389
+ res = await client.fetchFn(`${client.baseUrl}/v1/auth/refresh`, {
323
390
  method: "POST",
324
391
  headers,
325
392
  body: serializeBody({ refresh_token: rt }),
@@ -330,13 +397,16 @@ async function doRefresh(client) {
330
397
  if (timedOut) {
331
398
  emitAuraEvent(client.onEvent, client.debug, {
332
399
  kind: "timeout",
333
- url: `${client.baseUrl}/v1/auth/${client.projectId}/refresh`
400
+ url: `${client.baseUrl}/v1/auth/refresh`
334
401
  });
335
402
  }
336
403
  return {
337
404
  data: null,
338
405
  error: new AuraError(0, {
339
- error: timedOut ? { code: "timeout", message: `Refresh timed out after ${timeoutMs}ms` } : { code: "network_error", message: err instanceof Error ? err.message : "Network request failed" }
406
+ error: timedOut ? { code: "timeout", message: `Refresh timed out after ${timeoutMs}ms` } : {
407
+ code: "network_error",
408
+ message: err instanceof Error ? err.message : "Network request failed"
409
+ }
340
410
  })
341
411
  };
342
412
  }
@@ -365,7 +435,7 @@ async function doRefresh(client) {
365
435
  if (okRead.timedOut) {
366
436
  emitAuraEvent(client.onEvent, client.debug, {
367
437
  kind: "timeout",
368
- url: `${client.baseUrl}/v1/auth/${client.projectId}/refresh`
438
+ url: `${client.baseUrl}/v1/auth/refresh`
369
439
  });
370
440
  }
371
441
  return {
@@ -380,7 +450,9 @@ async function doRefresh(client) {
380
450
  } catch {
381
451
  return {
382
452
  data: null,
383
- error: new AuraError(0, { error: { code: "invalid_response", message: "Invalid refresh response (non-JSON body)" } })
453
+ error: new AuraError(0, {
454
+ error: { code: "invalid_response", message: "Invalid refresh response (non-JSON body)" }
455
+ })
384
456
  };
385
457
  }
386
458
  const data = unwrapEnvelope(body);
@@ -392,7 +464,9 @@ async function doRefresh(client) {
392
464
  });
393
465
  return {
394
466
  data: null,
395
- error: new AuraError(409, { error: { code: "refresh_discarded", message: "Session changed during refresh" } })
467
+ error: new AuraError(409, {
468
+ error: { code: "refresh_discarded", message: "Session changed during refresh" }
469
+ })
396
470
  };
397
471
  }
398
472
  store.save(data.access_token, data.refresh_token, data.user);
@@ -401,7 +475,9 @@ async function doRefresh(client) {
401
475
  }
402
476
  return {
403
477
  data: null,
404
- error: new AuraError(0, { error: { code: "invalid_response", message: "Invalid refresh response" } })
478
+ error: new AuraError(0, {
479
+ error: { code: "invalid_response", message: "Invalid refresh response" }
480
+ })
405
481
  };
406
482
  }
407
483
  function withRefreshLock(client, fn) {
@@ -412,7 +488,7 @@ function withRefreshLock(client, fn) {
412
488
  return fn();
413
489
  }
414
490
  const request = locks.request;
415
- const lockName = `aura-refresh-${client.projectId}`;
491
+ const lockName = `aura-refresh-${clientScope(client.baseUrl)}`;
416
492
  const timeoutMs = (client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS) || DEFAULT_REQUEST_TIMEOUT_MS;
417
493
  let timer;
418
494
  const options = {};
@@ -528,7 +604,10 @@ async function fetchWithAuth(url, init, client) {
528
604
  return {
529
605
  data: null,
530
606
  error: new AuraError(0, {
531
- error: timedOut ? { code: "timeout", message: `Request timed out after ${timeoutMs}ms` } : { code: "network_error", message: err instanceof Error ? err.message : "Network request failed" }
607
+ error: timedOut ? { code: "timeout", message: `Request timed out after ${timeoutMs}ms` } : {
608
+ code: "network_error",
609
+ message: err instanceof Error ? err.message : "Network request failed"
610
+ }
532
611
  })
533
612
  };
534
613
  }
@@ -541,7 +620,12 @@ async function fetchWithAuth(url, init, client) {
541
620
  await res.body?.cancel().catch(() => {
542
621
  });
543
622
  const nextAttempt = attempt + 1;
544
- emitAuraEvent(client.onEvent, client.debug, { kind: "retry", attempt: nextAttempt, status: res.status, url });
623
+ emitAuraEvent(client.onEvent, client.debug, {
624
+ kind: "retry",
625
+ attempt: nextAttempt,
626
+ status: res.status,
627
+ url
628
+ });
545
629
  await abortableSleep(delay, init.signal);
546
630
  attempt = nextAttempt;
547
631
  continue;
@@ -647,12 +731,40 @@ var BaseService = class {
647
731
  }
648
732
  };
649
733
 
734
+ // src/cookie-utils.ts
735
+ function parseCookieHeader(header) {
736
+ const result = {};
737
+ if (!header) return result;
738
+ for (const pair of header.split(";")) {
739
+ const idx = pair.indexOf("=");
740
+ if (idx === -1) continue;
741
+ const key = pair.slice(0, idx).trim();
742
+ const value = pair.slice(idx + 1).trim();
743
+ if (key) result[key] = decodeURIComponent(value);
744
+ }
745
+ return result;
746
+ }
747
+ function serializeCookieHeader(name, value, options = {}) {
748
+ const parts = [`${name}=${encodeURIComponent(value)}`];
749
+ if (options.path) parts.push(`Path=${options.path}`);
750
+ if (options.domain) parts.push(`Domain=${options.domain}`);
751
+ if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
752
+ if (options.secure) parts.push("Secure");
753
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
754
+ if (options.httpOnly) parts.push("HttpOnly");
755
+ return parts.join("; ");
756
+ }
757
+
650
758
  // src/stores/LocalAuthStore.ts
651
759
  var LocalAuthStore = class extends BaseAuthStore {
652
760
  /** Persiste en localStorage → survit au redirect pleine page (cf. PKCE). */
653
761
  persistent = true;
654
762
  key;
655
- constructor(key = "aurabase_auth") {
763
+ /**
764
+ * @param key Nom de l'entrée `localStorage` (et du cookie SSR). Scopez-le par projet :
765
+ * `aura-${clientScope(baseUrl)}-auth`.
766
+ */
767
+ constructor(key) {
656
768
  super();
657
769
  this.key = key;
658
770
  this._loadFromStorage();
@@ -669,11 +781,12 @@ var LocalAuthStore = class extends BaseAuthStore {
669
781
  }
670
782
  /**
671
783
  * Supprime de localStorage tous les artefacts d'auth éphémères sur disque :
672
- * - `code_verifier` PKCE (`aura-<projectId>-pkce-code-verifier`),
673
- * - marqueur d'initiation OAuth implicit (`aura-<projectId>-oauth-implicit-initiated`,
784
+ * - `code_verifier` PKCE (`aura-<portée>-pkce-code-verifier`),
785
+ * - marqueur d'initiation OAuth implicit (`aura-<portée>-oauth-implicit-initiated`,
674
786
  * SEC-F3). Un `signOut`/`clear()` ne doit laisser aucun résidu réutilisable (parité
675
787
  * `@supabase/ssr` : ces artefacts sont éphémères). On itère les clés préfixées pour
676
- * couvrir un store partagé entre plusieurs projets, sans dépendre du `projectId` courant.
788
+ * couvrir un store partagé entre plusieurs projets, sans dépendre de la portée courante
789
+ * (`clientScope`, dérivée de l'URL du projet).
677
790
  */
678
791
  _removePkceVerifiers() {
679
792
  if (typeof localStorage === "undefined") return;
@@ -794,30 +907,7 @@ var LocalAuthStore = class extends BaseAuthStore {
794
907
  }
795
908
  };
796
909
 
797
- // src/cookie-utils.ts
798
- function parseCookieHeader(header) {
799
- const result = {};
800
- if (!header) return result;
801
- for (const pair of header.split(";")) {
802
- const idx = pair.indexOf("=");
803
- if (idx === -1) continue;
804
- const key = pair.slice(0, idx).trim();
805
- const value = pair.slice(idx + 1).trim();
806
- if (key) result[key] = decodeURIComponent(value);
807
- }
808
- return result;
809
- }
810
- function serializeCookieHeader(name, value, options = {}) {
811
- const parts = [`${name}=${encodeURIComponent(value)}`];
812
- if (options.path) parts.push(`Path=${options.path}`);
813
- if (options.domain) parts.push(`Domain=${options.domain}`);
814
- if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
815
- if (options.secure) parts.push("Secure");
816
- if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
817
- if (options.httpOnly) parts.push("HttpOnly");
818
- return parts.join("; ");
819
- }
820
-
910
+ exports.AUTH_SETTINGS_WRITABLE_FIELDS = AUTH_SETTINGS_WRITABLE_FIELDS;
821
911
  exports.AuraError = AuraError;
822
912
  exports.BaseAuthStore = BaseAuthStore;
823
913
  exports.BaseService = BaseService;
@@ -827,6 +917,7 @@ exports.LocalAuthStore = LocalAuthStore;
827
917
  exports.RETRYABLE_METHODS = RETRYABLE_METHODS;
828
918
  exports.RETRYABLE_STATUS = RETRYABLE_STATUS;
829
919
  exports.abortableSleep = abortableSleep;
920
+ exports.clientScope = clientScope;
830
921
  exports.computeBackoff = computeBackoff;
831
922
  exports.emitAuraEvent = emitAuraEvent;
832
923
  exports.fetchWithAuth = fetchWithAuth;