@aurabase/core 0.4.0 → 0.5.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 +10 -5
- package/dist/index.cjs +308 -103
- package/dist/index.d.cts +611 -72
- package/dist/index.d.ts +611 -72
- package/dist/index.js +306 -104
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ class WidgetsService extends BaseService {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
private get prefix(): string {
|
|
116
|
-
return
|
|
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(
|
|
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(
|
|
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`, `
|
|
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;
|
|
@@ -235,6 +300,56 @@ function unwrapEnvelope(body) {
|
|
|
235
300
|
}
|
|
236
301
|
return body;
|
|
237
302
|
}
|
|
303
|
+
async function readTextBounded(res, timeoutMs) {
|
|
304
|
+
const body = res.body;
|
|
305
|
+
if (!body || typeof body.getReader !== "function" || !(timeoutMs > 0)) {
|
|
306
|
+
try {
|
|
307
|
+
return { ok: true, text: await res.text() };
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
timedOut: false,
|
|
312
|
+
message: err instanceof Error ? err.message : "read failed"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
let timer;
|
|
317
|
+
const TIMED_OUT = /* @__PURE__ */ Symbol("body-timeout");
|
|
318
|
+
try {
|
|
319
|
+
const reader = body.getReader();
|
|
320
|
+
const decoder = new TextDecoder();
|
|
321
|
+
let out = "";
|
|
322
|
+
const deadline = new Promise((resolve) => {
|
|
323
|
+
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
324
|
+
timer.unref?.();
|
|
325
|
+
});
|
|
326
|
+
for (; ; ) {
|
|
327
|
+
const step = await Promise.race([reader.read(), deadline]);
|
|
328
|
+
if (step === TIMED_OUT) {
|
|
329
|
+
await reader.cancel().catch(() => {
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
timedOut: true,
|
|
334
|
+
message: `Response body not fully received within ${timeoutMs}ms`
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (step.done) {
|
|
338
|
+
out += decoder.decode();
|
|
339
|
+
return { ok: true, text: out };
|
|
340
|
+
}
|
|
341
|
+
if (step.value) out += decoder.decode(step.value, { stream: true });
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return {
|
|
345
|
+
ok: false,
|
|
346
|
+
timedOut: false,
|
|
347
|
+
message: err instanceof Error ? err.message : "read failed"
|
|
348
|
+
};
|
|
349
|
+
} finally {
|
|
350
|
+
if (timer) clearTimeout(timer);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
238
353
|
var inflightRefreshes = /* @__PURE__ */ new WeakMap();
|
|
239
354
|
async function doRefresh(client) {
|
|
240
355
|
const store = client.authStore;
|
|
@@ -242,32 +357,68 @@ async function doRefresh(client) {
|
|
|
242
357
|
if (!rt) {
|
|
243
358
|
return {
|
|
244
359
|
data: null,
|
|
245
|
-
error: new AuraError(0, {
|
|
360
|
+
error: new AuraError(0, {
|
|
361
|
+
error: { code: "no_session", message: "No refresh token available" }
|
|
362
|
+
})
|
|
246
363
|
};
|
|
247
364
|
}
|
|
248
365
|
const epoch = store.removalEpoch;
|
|
249
366
|
const headers = { "Content-Type": "application/json" };
|
|
250
367
|
if (client.apiKey) headers["apikey"] = client.apiKey;
|
|
368
|
+
const timeoutMs = client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
369
|
+
let timedOut = false;
|
|
370
|
+
let refreshTimer;
|
|
371
|
+
let refreshSignal;
|
|
372
|
+
if (timeoutMs > 0 && typeof AbortController !== "undefined") {
|
|
373
|
+
const ctrl = new AbortController();
|
|
374
|
+
refreshTimer = setTimeout(() => {
|
|
375
|
+
timedOut = true;
|
|
376
|
+
ctrl.abort();
|
|
377
|
+
}, timeoutMs);
|
|
378
|
+
refreshTimer.unref?.();
|
|
379
|
+
refreshSignal = ctrl.signal;
|
|
380
|
+
}
|
|
381
|
+
const clearRefreshTimer = () => {
|
|
382
|
+
if (refreshTimer) {
|
|
383
|
+
clearTimeout(refreshTimer);
|
|
384
|
+
refreshTimer = void 0;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
251
387
|
let res;
|
|
252
388
|
try {
|
|
253
|
-
res = await client.fetchFn(`${client.baseUrl}/v1/auth
|
|
389
|
+
res = await client.fetchFn(`${client.baseUrl}/v1/auth/refresh`, {
|
|
254
390
|
method: "POST",
|
|
255
391
|
headers,
|
|
256
|
-
body: serializeBody({ refresh_token: rt })
|
|
392
|
+
body: serializeBody({ refresh_token: rt }),
|
|
393
|
+
signal: refreshSignal
|
|
257
394
|
});
|
|
258
395
|
} catch (err) {
|
|
396
|
+
clearRefreshTimer();
|
|
397
|
+
if (timedOut) {
|
|
398
|
+
emitAuraEvent(client.onEvent, client.debug, {
|
|
399
|
+
kind: "timeout",
|
|
400
|
+
url: `${client.baseUrl}/v1/auth/refresh`
|
|
401
|
+
});
|
|
402
|
+
}
|
|
259
403
|
return {
|
|
260
404
|
data: null,
|
|
261
405
|
error: new AuraError(0, {
|
|
262
|
-
error: { code: "
|
|
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
|
+
}
|
|
263
410
|
})
|
|
264
411
|
};
|
|
265
412
|
}
|
|
413
|
+
clearRefreshTimer();
|
|
266
414
|
if (!res.ok) {
|
|
267
415
|
let body2 = null;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
416
|
+
const errRead = await readTextBounded(res, timeoutMs);
|
|
417
|
+
if (errRead.ok && errRead.text.trim() !== "") {
|
|
418
|
+
try {
|
|
419
|
+
body2 = JSON.parse(errRead.text);
|
|
420
|
+
} catch {
|
|
421
|
+
}
|
|
271
422
|
}
|
|
272
423
|
const code = typeof body2?.error?.code === "string" ? body2.error.code : null;
|
|
273
424
|
const definitivelyRejected = res.status === 403 || res.status === 401 && code === "REFRESH_TOKEN_REJECTED";
|
|
@@ -279,12 +430,29 @@ async function doRefresh(client) {
|
|
|
279
430
|
return { data: null, error: new AuraError(res.status, body2) };
|
|
280
431
|
}
|
|
281
432
|
let body;
|
|
433
|
+
const okRead = await readTextBounded(res, timeoutMs);
|
|
434
|
+
if (!okRead.ok) {
|
|
435
|
+
if (okRead.timedOut) {
|
|
436
|
+
emitAuraEvent(client.onEvent, client.debug, {
|
|
437
|
+
kind: "timeout",
|
|
438
|
+
url: `${client.baseUrl}/v1/auth/refresh`
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
data: null,
|
|
443
|
+
error: new AuraError(0, {
|
|
444
|
+
error: okRead.timedOut ? { code: "timeout", message: `Refresh body not received within ${timeoutMs}ms` } : { code: "invalid_response", message: `Invalid refresh response (${okRead.message})` }
|
|
445
|
+
})
|
|
446
|
+
};
|
|
447
|
+
}
|
|
282
448
|
try {
|
|
283
|
-
body =
|
|
449
|
+
body = JSON.parse(okRead.text);
|
|
284
450
|
} catch {
|
|
285
451
|
return {
|
|
286
452
|
data: null,
|
|
287
|
-
error: new AuraError(0, {
|
|
453
|
+
error: new AuraError(0, {
|
|
454
|
+
error: { code: "invalid_response", message: "Invalid refresh response (non-JSON body)" }
|
|
455
|
+
})
|
|
288
456
|
};
|
|
289
457
|
}
|
|
290
458
|
const data = unwrapEnvelope(body);
|
|
@@ -296,7 +464,9 @@ async function doRefresh(client) {
|
|
|
296
464
|
});
|
|
297
465
|
return {
|
|
298
466
|
data: null,
|
|
299
|
-
error: new AuraError(409, {
|
|
467
|
+
error: new AuraError(409, {
|
|
468
|
+
error: { code: "refresh_discarded", message: "Session changed during refresh" }
|
|
469
|
+
})
|
|
300
470
|
};
|
|
301
471
|
}
|
|
302
472
|
store.save(data.access_token, data.refresh_token, data.user);
|
|
@@ -305,7 +475,9 @@ async function doRefresh(client) {
|
|
|
305
475
|
}
|
|
306
476
|
return {
|
|
307
477
|
data: null,
|
|
308
|
-
error: new AuraError(0, {
|
|
478
|
+
error: new AuraError(0, {
|
|
479
|
+
error: { code: "invalid_response", message: "Invalid refresh response" }
|
|
480
|
+
})
|
|
309
481
|
};
|
|
310
482
|
}
|
|
311
483
|
function withRefreshLock(client, fn) {
|
|
@@ -316,7 +488,7 @@ function withRefreshLock(client, fn) {
|
|
|
316
488
|
return fn();
|
|
317
489
|
}
|
|
318
490
|
const request = locks.request;
|
|
319
|
-
const lockName = `aura-refresh-${client.
|
|
491
|
+
const lockName = `aura-refresh-${clientScope(client.baseUrl)}`;
|
|
320
492
|
const timeoutMs = (client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS) || DEFAULT_REQUEST_TIMEOUT_MS;
|
|
321
493
|
let timer;
|
|
322
494
|
const options = {};
|
|
@@ -373,6 +545,7 @@ async function fetchWithAuth(url, init, client) {
|
|
|
373
545
|
const isFormData = typeof FormData !== "undefined" && init.body instanceof FormData;
|
|
374
546
|
const timeoutMs = client.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
375
547
|
const applyTimeout = timeoutMs > 0 && init.signal === void 0 && !isFormData;
|
|
548
|
+
const bodyTimeoutMs = init.signal === void 0 ? timeoutMs : 0;
|
|
376
549
|
let attempt = 0;
|
|
377
550
|
for (; ; ) {
|
|
378
551
|
const headers = { ...init.headers };
|
|
@@ -431,7 +604,10 @@ async function fetchWithAuth(url, init, client) {
|
|
|
431
604
|
return {
|
|
432
605
|
data: null,
|
|
433
606
|
error: new AuraError(0, {
|
|
434
|
-
error: timedOut ? { code: "timeout", message: `Request timed out after ${timeoutMs}ms` } : {
|
|
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
|
+
}
|
|
435
611
|
})
|
|
436
612
|
};
|
|
437
613
|
}
|
|
@@ -444,7 +620,12 @@ async function fetchWithAuth(url, init, client) {
|
|
|
444
620
|
await res.body?.cancel().catch(() => {
|
|
445
621
|
});
|
|
446
622
|
const nextAttempt = attempt + 1;
|
|
447
|
-
emitAuraEvent(client.onEvent, client.debug, {
|
|
623
|
+
emitAuraEvent(client.onEvent, client.debug, {
|
|
624
|
+
kind: "retry",
|
|
625
|
+
attempt: nextAttempt,
|
|
626
|
+
status: res.status,
|
|
627
|
+
url
|
|
628
|
+
});
|
|
448
629
|
await abortableSleep(delay, init.signal);
|
|
449
630
|
attempt = nextAttempt;
|
|
450
631
|
continue;
|
|
@@ -462,26 +643,42 @@ async function fetchWithAuth(url, init, client) {
|
|
|
462
643
|
}
|
|
463
644
|
if (!res.ok) {
|
|
464
645
|
let body2 = null;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
646
|
+
const errRead = await readTextBounded(res, bodyTimeoutMs);
|
|
647
|
+
if (errRead.ok && errRead.text.trim() !== "") {
|
|
648
|
+
try {
|
|
649
|
+
body2 = JSON.parse(errRead.text);
|
|
650
|
+
} catch {
|
|
651
|
+
}
|
|
652
|
+
} else if (!errRead.ok && errRead.timedOut) {
|
|
653
|
+
emitAuraEvent(client.onEvent, client.debug, { kind: "timeout", url });
|
|
468
654
|
}
|
|
469
655
|
return { data: null, error: new AuraError(res.status, body2) };
|
|
470
656
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
657
|
+
const read = await readTextBounded(res, bodyTimeoutMs);
|
|
658
|
+
if (!read.ok) {
|
|
659
|
+
if (read.timedOut) {
|
|
660
|
+
emitAuraEvent(client.onEvent, client.debug, { kind: "timeout", url });
|
|
661
|
+
return {
|
|
662
|
+
data: null,
|
|
663
|
+
error: new AuraError(0, {
|
|
664
|
+
error: {
|
|
665
|
+
code: "timeout",
|
|
666
|
+
message: `Response body not fully received within ${bodyTimeoutMs}ms (HTTP ${res.status})`
|
|
667
|
+
}
|
|
668
|
+
})
|
|
669
|
+
};
|
|
670
|
+
}
|
|
475
671
|
return {
|
|
476
672
|
data: null,
|
|
477
673
|
error: new AuraError(0, {
|
|
478
674
|
error: {
|
|
479
675
|
code: "invalid_response",
|
|
480
|
-
message: `Unreadable response body (HTTP ${res.status}): ${
|
|
676
|
+
message: `Unreadable response body (HTTP ${res.status}): ${read.message}`
|
|
481
677
|
}
|
|
482
678
|
})
|
|
483
679
|
};
|
|
484
680
|
}
|
|
681
|
+
const raw = read.text;
|
|
485
682
|
if (raw.trim() === "") {
|
|
486
683
|
return { data: null, error: null };
|
|
487
684
|
}
|
|
@@ -534,12 +731,40 @@ var BaseService = class {
|
|
|
534
731
|
}
|
|
535
732
|
};
|
|
536
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
|
+
|
|
537
758
|
// src/stores/LocalAuthStore.ts
|
|
538
759
|
var LocalAuthStore = class extends BaseAuthStore {
|
|
539
760
|
/** Persiste en localStorage → survit au redirect pleine page (cf. PKCE). */
|
|
540
761
|
persistent = true;
|
|
541
762
|
key;
|
|
542
|
-
|
|
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) {
|
|
543
768
|
super();
|
|
544
769
|
this.key = key;
|
|
545
770
|
this._loadFromStorage();
|
|
@@ -556,11 +781,12 @@ var LocalAuthStore = class extends BaseAuthStore {
|
|
|
556
781
|
}
|
|
557
782
|
/**
|
|
558
783
|
* Supprime de localStorage tous les artefacts d'auth éphémères sur disque :
|
|
559
|
-
* - `code_verifier` PKCE (`aura-<
|
|
560
|
-
* - marqueur d'initiation OAuth implicit (`aura-<
|
|
784
|
+
* - `code_verifier` PKCE (`aura-<portée>-pkce-code-verifier`),
|
|
785
|
+
* - marqueur d'initiation OAuth implicit (`aura-<portée>-oauth-implicit-initiated`,
|
|
561
786
|
* SEC-F3). Un `signOut`/`clear()` ne doit laisser aucun résidu réutilisable (parité
|
|
562
787
|
* `@supabase/ssr` : ces artefacts sont éphémères). On itère les clés préfixées pour
|
|
563
|
-
* couvrir un store partagé entre plusieurs projets, sans dépendre
|
|
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).
|
|
564
790
|
*/
|
|
565
791
|
_removePkceVerifiers() {
|
|
566
792
|
if (typeof localStorage === "undefined") return;
|
|
@@ -681,38 +907,17 @@ var LocalAuthStore = class extends BaseAuthStore {
|
|
|
681
907
|
}
|
|
682
908
|
};
|
|
683
909
|
|
|
684
|
-
|
|
685
|
-
function parseCookieHeader(header) {
|
|
686
|
-
const result = {};
|
|
687
|
-
if (!header) return result;
|
|
688
|
-
for (const pair of header.split(";")) {
|
|
689
|
-
const idx = pair.indexOf("=");
|
|
690
|
-
if (idx === -1) continue;
|
|
691
|
-
const key = pair.slice(0, idx).trim();
|
|
692
|
-
const value = pair.slice(idx + 1).trim();
|
|
693
|
-
if (key) result[key] = decodeURIComponent(value);
|
|
694
|
-
}
|
|
695
|
-
return result;
|
|
696
|
-
}
|
|
697
|
-
function serializeCookieHeader(name, value, options = {}) {
|
|
698
|
-
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
699
|
-
if (options.path) parts.push(`Path=${options.path}`);
|
|
700
|
-
if (options.domain) parts.push(`Domain=${options.domain}`);
|
|
701
|
-
if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
|
|
702
|
-
if (options.secure) parts.push("Secure");
|
|
703
|
-
if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
|
|
704
|
-
if (options.httpOnly) parts.push("HttpOnly");
|
|
705
|
-
return parts.join("; ");
|
|
706
|
-
}
|
|
707
|
-
|
|
910
|
+
exports.AUTH_SETTINGS_WRITABLE_FIELDS = AUTH_SETTINGS_WRITABLE_FIELDS;
|
|
708
911
|
exports.AuraError = AuraError;
|
|
709
912
|
exports.BaseAuthStore = BaseAuthStore;
|
|
710
913
|
exports.BaseService = BaseService;
|
|
711
914
|
exports.DEFAULT_MAX_RETRIES = DEFAULT_MAX_RETRIES;
|
|
915
|
+
exports.DEFAULT_REQUEST_TIMEOUT_MS = DEFAULT_REQUEST_TIMEOUT_MS;
|
|
712
916
|
exports.LocalAuthStore = LocalAuthStore;
|
|
713
917
|
exports.RETRYABLE_METHODS = RETRYABLE_METHODS;
|
|
714
918
|
exports.RETRYABLE_STATUS = RETRYABLE_STATUS;
|
|
715
919
|
exports.abortableSleep = abortableSleep;
|
|
920
|
+
exports.clientScope = clientScope;
|
|
716
921
|
exports.computeBackoff = computeBackoff;
|
|
717
922
|
exports.emitAuraEvent = emitAuraEvent;
|
|
718
923
|
exports.fetchWithAuth = fetchWithAuth;
|