@aurabase/core 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -76,6 +76,12 @@ var AuraError = class _AuraError extends Error {
76
76
  const hint = body?.error?.hint;
77
77
  if (typeof hint === "string" && hint) {
78
78
  this.hint = hint;
79
+ } else {
80
+ const serverDetails = JSON.stringify(body ?? "");
81
+ const isJsonCastError = rawMessage.includes("invalid input syntax for type json") || serverDetails.includes("22P02") && serverDetails.includes("json");
82
+ if (isJsonCastError) {
83
+ this.hint = "To filter on a text value inside a JSON column, use the '->>' operator instead of '->' (e.g. .eq('metadata->>theme', 'dark')).";
84
+ }
79
85
  }
80
86
  const rawCode = typeof body?.error?.code === "string" ? body.error.code : void 0;
81
87
  const canonical = rawCode ? normalizeErrorCode(rawCode) : void 0;
@@ -431,14 +437,38 @@ var BaseAuthStore = class {
431
437
  }
432
438
  /**
433
439
  * Populate the store from a raw `Cookie` header string (server-side).
434
- * Looks for the key matching `this.key`.
440
+ * Supports both single cookie `${this.key}=...` and numbered chunk cookies `${this.key}.0=...`, `${this.key}.1=...`
441
+ * automatically reassembled in order (AUTH-010).
435
442
  */
436
443
  loadFromCookie(cookieString) {
444
+ if (!cookieString) return;
445
+ const entries = cookieString.split(";").map((c) => c.trim()).filter(Boolean);
446
+ let encodedData = null;
437
447
  const prefix = `${this.key}=`;
438
- const entry = cookieString.split(";").map((c) => c.trim()).find((c) => c.startsWith(prefix));
439
- if (!entry) return;
448
+ const singleEntry = entries.find((c) => c.startsWith(prefix));
449
+ if (singleEntry) {
450
+ encodedData = singleEntry.slice(prefix.length);
451
+ } else {
452
+ const escapedKey = this.key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
453
+ const chunkRegex = new RegExp(`^${escapedKey}\\.(\\d+)=(.*)$`);
454
+ const chunks = [];
455
+ for (const entry of entries) {
456
+ const match = entry.match(chunkRegex);
457
+ if (match) {
458
+ chunks.push({
459
+ index: parseInt(match[1], 10),
460
+ value: match[2]
461
+ });
462
+ }
463
+ }
464
+ if (chunks.length > 0) {
465
+ chunks.sort((a, b) => a.index - b.index);
466
+ encodedData = chunks.map((c) => c.value).join("");
467
+ }
468
+ }
469
+ if (!encodedData) return;
440
470
  try {
441
- const raw = decodeURIComponent(entry.slice(prefix.length));
471
+ const raw = decodeURIComponent(encodedData);
442
472
  const parsed = JSON.parse(raw);
443
473
  const user = parsed.token ? userFromAccessToken(parsed.token) : null;
444
474
  if (parsed.token && parsed.refreshToken && user) {
@@ -448,23 +478,47 @@ var BaseAuthStore = class {
448
478
  }
449
479
  }
450
480
  /**
451
- * Serialize the session tokens into a `Set-Cookie` value. The `user` object is NOT included: its
452
- * `user_metadata` is already inside the access token.
481
+ * Serialize the session tokens into an array of `Set-Cookie` header strings.
482
+ * If the payload exceeds `maxChunkSize` (default 3500 bytes), it is automatically split into
483
+ * numbered chunks `${key}.0`, `${key}.1`, etc. to avoid browser cookie size limits (AUTH-010).
453
484
  */
454
- exportToCookie(options = {}) {
485
+ exportToCookies(options = {}, maxChunkSize = 3500) {
455
486
  const data = {
456
487
  token: this.token,
457
488
  refreshToken: this.refreshToken
458
489
  };
459
490
  const encoded = encodeURIComponent(JSON.stringify(data));
460
- const parts = [`${this.key}=${encoded}`];
461
- if (options.path) parts.push(`Path=${options.path}`);
462
- if (options.domain) parts.push(`Domain=${options.domain}`);
463
- if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
464
- if (options.secure) parts.push("Secure");
465
- if (options.sameSite) parts.push(`SameSite=${sameSiteAttribute(options.sameSite)}`);
466
- if (options.httpOnly) parts.push("HttpOnly");
467
- return parts.join("; ");
491
+ const buildCookie = (name, value) => {
492
+ const parts = [`${name}=${value}`];
493
+ if (options.path) parts.push(`Path=${options.path}`);
494
+ if (options.domain) parts.push(`Domain=${options.domain}`);
495
+ if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
496
+ if (options.secure) parts.push("Secure");
497
+ if (options.sameSite) parts.push(`SameSite=${sameSiteAttribute(options.sameSite)}`);
498
+ if (options.httpOnly) parts.push("HttpOnly");
499
+ return parts.join("; ");
500
+ };
501
+ const limit = Math.max(1, maxChunkSize);
502
+ if (encoded.length <= limit) {
503
+ return [buildCookie(this.key, encoded)];
504
+ }
505
+ const cookies = [];
506
+ let offset = 0;
507
+ let index = 0;
508
+ while (offset < encoded.length) {
509
+ const chunk = encoded.slice(offset, offset + limit);
510
+ cookies.push(buildCookie(`${this.key}.${index}`, chunk));
511
+ offset += limit;
512
+ index++;
513
+ }
514
+ return cookies;
515
+ }
516
+ /**
517
+ * Serialize the session tokens into a single `Set-Cookie` value.
518
+ * Note: If the token payload may exceed 3.5KB, use `exportToCookies()` instead for automatic chunking (AUTH-010).
519
+ */
520
+ exportToCookie(options = {}) {
521
+ return this.exportToCookies(options, Infinity)[0];
468
522
  }
469
523
  // -- Event emitter ----------------------------------------------------------
470
524
  /**
@@ -509,14 +563,42 @@ function unwrapEnvelope(body) {
509
563
  }
510
564
  return body;
511
565
  }
566
+ function parseJsonBody(raw, parseBigIntAsString) {
567
+ if (parseBigIntAsString) {
568
+ const safe = raw.replace(/([:\[,]\s*)(-?\d{16,})(?=\s*[,}\]])/g, '$1"$2"');
569
+ return JSON.parse(safe);
570
+ }
571
+ return JSON.parse(raw);
572
+ }
512
573
  function parseContentRangeTotal(headerVal) {
513
- if (!headerVal) return void 0;
574
+ return parseContentRangeDetails(headerVal).total;
575
+ }
576
+ function parseContentRangeDetails(headerVal) {
577
+ if (!headerVal) return { total: void 0, hasMore: void 0 };
514
578
  const parts = headerVal.split("/");
515
- if (parts.length === 2 && parts[1] !== "*") {
516
- const n = parseInt(parts[1], 10);
517
- if (!Number.isNaN(n)) return n;
579
+ const totalStr = parts[1];
580
+ let total;
581
+ if (totalStr && totalStr !== "*") {
582
+ const n = parseInt(totalStr, 10);
583
+ if (!Number.isNaN(n)) total = n;
518
584
  }
519
- return void 0;
585
+ let hasMore;
586
+ const rangePart = parts[0];
587
+ if (rangePart) {
588
+ const rangeBounds = rangePart.split("-");
589
+ if (rangeBounds.length === 2) {
590
+ const start = parseInt(rangeBounds[0], 10);
591
+ const end = parseInt(rangeBounds[1], 10);
592
+ if (!Number.isNaN(start) && !Number.isNaN(end)) {
593
+ if (total !== void 0) {
594
+ hasMore = end + 1 < total;
595
+ } else if (end - start + 1 >= 1e3) {
596
+ hasMore = true;
597
+ }
598
+ }
599
+ }
600
+ }
601
+ return { total, hasMore };
520
602
  }
521
603
  async function readTextBounded(res, timeoutMs) {
522
604
  const body = res.body;
@@ -909,7 +991,7 @@ async function fetchWithAuth(url, init, client) {
909
991
  }
910
992
  let body;
911
993
  try {
912
- body = JSON.parse(raw);
994
+ body = parseJsonBody(raw, client.parseBigIntAsString === true);
913
995
  } catch {
914
996
  const ctRaw = res.headers.get("Content-Type") ?? "unknown";
915
997
  const ct = ctRaw.length > 64 ? `${ctRaw.slice(0, 64)}\u2026` : ctRaw;
@@ -925,10 +1007,21 @@ async function fetchWithAuth(url, init, client) {
925
1007
  }
926
1008
  const unwrapped = unwrapEnvelope(body);
927
1009
  const cr = res.headers.get("content-range") ?? res.headers.get("Content-Range");
928
- const totalFromCr = parseContentRangeTotal(cr);
1010
+ const crDetails = parseContentRangeDetails(cr);
1011
+ const totalFromCr = crDetails.total;
1012
+ const hasMore = crDetails.hasMore;
929
1013
  const rawMeta = body !== null && typeof body === "object" && "meta" in body ? body.meta : void 0;
930
- const meta = rawMeta !== void 0 ? rawMeta : totalFromCr !== void 0 ? { total: totalFromCr } : void 0;
1014
+ const meta = rawMeta !== void 0 ? { ...rawMeta, ...hasMore !== void 0 ? { has_more: hasMore } : {} } : totalFromCr !== void 0 || hasMore !== void 0 ? {
1015
+ ...totalFromCr !== void 0 ? { total: totalFromCr } : {},
1016
+ ...hasMore !== void 0 ? { has_more: hasMore } : {}
1017
+ } : void 0;
931
1018
  const count = meta?.total !== void 0 ? meta.total : totalFromCr ?? null;
1019
+ const isDev = Boolean(client.debug) || typeof globalThis !== "undefined" && globalThis.process?.env?.NODE_ENV === "development";
1020
+ if (hasMore && isDev) {
1021
+ console.warn(
1022
+ "[Aurabase] Query result reached pagination limit of 1,000 rows. Use .range() to paginate further results."
1023
+ );
1024
+ }
932
1025
  if (unwrapped === null) {
933
1026
  return { data: null, error: null, meta, count };
934
1027
  }
@@ -1040,48 +1133,6 @@ var LocalAuthStore = class extends BaseAuthStore {
1040
1133
  this._emitStorageBlocked("removeItem");
1041
1134
  }
1042
1135
  }
1043
- // -- Cookie helpers (SSR) ---------------------------------------------------
1044
- /**
1045
- * Populate the store from a raw `Cookie` header string (server-side).
1046
- * Looks for the key matching `this.key`.
1047
- *
1048
- * Le cookie ne porte que `{ token, refreshToken }` (AUTH-036) : l'utilisateur est reconstruit à partir des
1049
- * claims du jeton (non vérifié — `auth.getUser()` le fait vérifier par le serveur).
1050
- */
1051
- loadFromCookie(cookieString) {
1052
- const prefix = `${this.key}=`;
1053
- const entry = cookieString.split(";").map((c) => c.trim()).find((c) => c.startsWith(prefix));
1054
- if (!entry) return;
1055
- try {
1056
- const raw = decodeURIComponent(entry.slice(prefix.length));
1057
- const parsed = JSON.parse(raw);
1058
- const user = parsed.token ? userFromAccessToken(parsed.token) : null;
1059
- if (parsed.token && parsed.refreshToken && user) {
1060
- super.save(parsed.token, parsed.refreshToken, user);
1061
- }
1062
- } catch {
1063
- }
1064
- }
1065
- /**
1066
- * Serialize the session tokens into a `Set-Cookie` value. The `user` object is NOT included: its
1067
- * `user_metadata` is already inside the access token, and sending it twice doubled the cookie
1068
- * (5 KB of metadata gave 4 cookies and 13 139 bytes).
1069
- */
1070
- exportToCookie(options = {}) {
1071
- const data = {
1072
- token: this.token,
1073
- refreshToken: this.refreshToken
1074
- };
1075
- const encoded = encodeURIComponent(JSON.stringify(data));
1076
- const parts = [`${this.key}=${encoded}`];
1077
- if (options.path) parts.push(`Path=${options.path}`);
1078
- if (options.domain) parts.push(`Domain=${options.domain}`);
1079
- if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
1080
- if (options.secure) parts.push("Secure");
1081
- if (options.sameSite) parts.push(`SameSite=${sameSiteAttribute(options.sameSite)}`);
1082
- if (options.httpOnly) parts.push("HttpOnly");
1083
- return parts.join("; ");
1084
- }
1085
1136
  // -- Private ----------------------------------------------------------------
1086
1137
  _getStorage() {
1087
1138
  if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
@@ -1094,7 +1145,9 @@ var LocalAuthStore = class extends BaseAuthStore {
1094
1145
  const env = proc.env;
1095
1146
  const isTest = !!(env?.VITEST || env?.NODE_ENV === "test" || env?.JEST_WORKER_ID);
1096
1147
  const execArgv = proc.execArgv;
1097
- const hasFlag = Array.isArray(execArgv) && execArgv.some((a) => typeof a === "string" && a.startsWith("--localstorage-file"));
1148
+ const hasFlag = Array.isArray(execArgv) && execArgv.some(
1149
+ (a) => typeof a === "string" && a.startsWith("--localstorage-file")
1150
+ );
1098
1151
  if (isTest || hasFlag) {
1099
1152
  try {
1100
1153
  const s = g.localStorage;
package/dist/index.d.cts CHANGED
@@ -102,6 +102,10 @@ interface Meta {
102
102
  * échoue avec un 400 explicite plutôt que d'émettre ce champ.
103
103
  */
104
104
  next_cursor?: string;
105
+ /**
106
+ * DB-005 : Indique si la requête a été tronquée par le serveur (ex: limite 1 000 lignes de PostgREST).
107
+ */
108
+ has_more?: boolean;
105
109
  }
106
110
  /**
107
111
  * Réponse en SUCCÈS porteuse de contenu. `error` y est `null` par construction.
@@ -1572,6 +1576,12 @@ interface AuraClientOptions {
1572
1576
  * Défaut `false`.
1573
1577
  */
1574
1578
  debug?: boolean;
1579
+ /**
1580
+ * DB-006: Preserves 64-bit integer precision by parsing BIGINT values as strings
1581
+ * instead of JavaScript IEEE 754 floats when exceeding Number.MAX_SAFE_INTEGER (9007199254740991).
1582
+ * Default: false (native JSON.parse).
1583
+ */
1584
+ parseBigIntAsString?: boolean;
1575
1585
  }
1576
1586
  interface IdentityInfo {
1577
1587
  id: string;
@@ -1912,12 +1922,19 @@ declare class BaseAuthStore {
1912
1922
  protected _emitStorageBlocked(op: string): void;
1913
1923
  /**
1914
1924
  * Populate the store from a raw `Cookie` header string (server-side).
1915
- * Looks for the key matching `this.key`.
1925
+ * Supports both single cookie `${this.key}=...` and numbered chunk cookies `${this.key}.0=...`, `${this.key}.1=...`
1926
+ * automatically reassembled in order (AUTH-010).
1916
1927
  */
1917
1928
  loadFromCookie(cookieString: string): void;
1918
1929
  /**
1919
- * Serialize the session tokens into a `Set-Cookie` value. The `user` object is NOT included: its
1920
- * `user_metadata` is already inside the access token.
1930
+ * Serialize the session tokens into an array of `Set-Cookie` header strings.
1931
+ * If the payload exceeds `maxChunkSize` (default 3500 bytes), it is automatically split into
1932
+ * numbered chunks `${key}.0`, `${key}.1`, etc. to avoid browser cookie size limits (AUTH-010).
1933
+ */
1934
+ exportToCookies(options?: CookieOptions, maxChunkSize?: number): string[];
1935
+ /**
1936
+ * Serialize the session tokens into a single `Set-Cookie` value.
1937
+ * Note: If the token payload may exceed 3.5KB, use `exportToCookies()` instead for automatic chunking (AUTH-010).
1921
1938
  */
1922
1939
  exportToCookie(options?: CookieOptions): string;
1923
1940
  /**
@@ -1969,6 +1986,8 @@ interface AuraClientLike {
1969
1986
  readonly onEvent?: (event: AuraObservabilityEvent) => void;
1970
1987
  /** Si `true`, chaque `AuraObservabilityEvent` est aussi journalisé via `console.debug`. */
1971
1988
  readonly debug?: boolean;
1989
+ /** DB-006: Preserves 64-bit integers as strings when true. */
1990
+ readonly parseBigIntAsString?: boolean;
1972
1991
  }
1973
1992
 
1974
1993
  /**
@@ -2113,20 +2132,6 @@ declare class LocalAuthStore extends BaseAuthStore {
2113
2132
  setItem(key: string, value: string): void;
2114
2133
  getItem(key: string): string | null;
2115
2134
  removeItem(key: string): void;
2116
- /**
2117
- * Populate the store from a raw `Cookie` header string (server-side).
2118
- * Looks for the key matching `this.key`.
2119
- *
2120
- * Le cookie ne porte que `{ token, refreshToken }` (AUTH-036) : l'utilisateur est reconstruit à partir des
2121
- * claims du jeton (non vérifié — `auth.getUser()` le fait vérifier par le serveur).
2122
- */
2123
- loadFromCookie(cookieString: string): void;
2124
- /**
2125
- * Serialize the session tokens into a `Set-Cookie` value. The `user` object is NOT included: its
2126
- * `user_metadata` is already inside the access token, and sending it twice doubled the cookie
2127
- * (5 KB of metadata gave 4 cookies and 13 139 bytes).
2128
- */
2129
- exportToCookie(options?: CookieOptions): string;
2130
2135
  private _getStorage;
2131
2136
  private _loadFromStorage;
2132
2137
  private _persist;
package/dist/index.d.ts CHANGED
@@ -102,6 +102,10 @@ interface Meta {
102
102
  * échoue avec un 400 explicite plutôt que d'émettre ce champ.
103
103
  */
104
104
  next_cursor?: string;
105
+ /**
106
+ * DB-005 : Indique si la requête a été tronquée par le serveur (ex: limite 1 000 lignes de PostgREST).
107
+ */
108
+ has_more?: boolean;
105
109
  }
106
110
  /**
107
111
  * Réponse en SUCCÈS porteuse de contenu. `error` y est `null` par construction.
@@ -1572,6 +1576,12 @@ interface AuraClientOptions {
1572
1576
  * Défaut `false`.
1573
1577
  */
1574
1578
  debug?: boolean;
1579
+ /**
1580
+ * DB-006: Preserves 64-bit integer precision by parsing BIGINT values as strings
1581
+ * instead of JavaScript IEEE 754 floats when exceeding Number.MAX_SAFE_INTEGER (9007199254740991).
1582
+ * Default: false (native JSON.parse).
1583
+ */
1584
+ parseBigIntAsString?: boolean;
1575
1585
  }
1576
1586
  interface IdentityInfo {
1577
1587
  id: string;
@@ -1912,12 +1922,19 @@ declare class BaseAuthStore {
1912
1922
  protected _emitStorageBlocked(op: string): void;
1913
1923
  /**
1914
1924
  * Populate the store from a raw `Cookie` header string (server-side).
1915
- * Looks for the key matching `this.key`.
1925
+ * Supports both single cookie `${this.key}=...` and numbered chunk cookies `${this.key}.0=...`, `${this.key}.1=...`
1926
+ * automatically reassembled in order (AUTH-010).
1916
1927
  */
1917
1928
  loadFromCookie(cookieString: string): void;
1918
1929
  /**
1919
- * Serialize the session tokens into a `Set-Cookie` value. The `user` object is NOT included: its
1920
- * `user_metadata` is already inside the access token.
1930
+ * Serialize the session tokens into an array of `Set-Cookie` header strings.
1931
+ * If the payload exceeds `maxChunkSize` (default 3500 bytes), it is automatically split into
1932
+ * numbered chunks `${key}.0`, `${key}.1`, etc. to avoid browser cookie size limits (AUTH-010).
1933
+ */
1934
+ exportToCookies(options?: CookieOptions, maxChunkSize?: number): string[];
1935
+ /**
1936
+ * Serialize the session tokens into a single `Set-Cookie` value.
1937
+ * Note: If the token payload may exceed 3.5KB, use `exportToCookies()` instead for automatic chunking (AUTH-010).
1921
1938
  */
1922
1939
  exportToCookie(options?: CookieOptions): string;
1923
1940
  /**
@@ -1969,6 +1986,8 @@ interface AuraClientLike {
1969
1986
  readonly onEvent?: (event: AuraObservabilityEvent) => void;
1970
1987
  /** Si `true`, chaque `AuraObservabilityEvent` est aussi journalisé via `console.debug`. */
1971
1988
  readonly debug?: boolean;
1989
+ /** DB-006: Preserves 64-bit integers as strings when true. */
1990
+ readonly parseBigIntAsString?: boolean;
1972
1991
  }
1973
1992
 
1974
1993
  /**
@@ -2113,20 +2132,6 @@ declare class LocalAuthStore extends BaseAuthStore {
2113
2132
  setItem(key: string, value: string): void;
2114
2133
  getItem(key: string): string | null;
2115
2134
  removeItem(key: string): void;
2116
- /**
2117
- * Populate the store from a raw `Cookie` header string (server-side).
2118
- * Looks for the key matching `this.key`.
2119
- *
2120
- * Le cookie ne porte que `{ token, refreshToken }` (AUTH-036) : l'utilisateur est reconstruit à partir des
2121
- * claims du jeton (non vérifié — `auth.getUser()` le fait vérifier par le serveur).
2122
- */
2123
- loadFromCookie(cookieString: string): void;
2124
- /**
2125
- * Serialize the session tokens into a `Set-Cookie` value. The `user` object is NOT included: its
2126
- * `user_metadata` is already inside the access token, and sending it twice doubled the cookie
2127
- * (5 KB of metadata gave 4 cookies and 13 139 bytes).
2128
- */
2129
- exportToCookie(options?: CookieOptions): string;
2130
2135
  private _getStorage;
2131
2136
  private _loadFromStorage;
2132
2137
  private _persist;
package/dist/index.js CHANGED
@@ -74,6 +74,12 @@ var AuraError = class _AuraError extends Error {
74
74
  const hint = body?.error?.hint;
75
75
  if (typeof hint === "string" && hint) {
76
76
  this.hint = hint;
77
+ } else {
78
+ const serverDetails = JSON.stringify(body ?? "");
79
+ const isJsonCastError = rawMessage.includes("invalid input syntax for type json") || serverDetails.includes("22P02") && serverDetails.includes("json");
80
+ if (isJsonCastError) {
81
+ this.hint = "To filter on a text value inside a JSON column, use the '->>' operator instead of '->' (e.g. .eq('metadata->>theme', 'dark')).";
82
+ }
77
83
  }
78
84
  const rawCode = typeof body?.error?.code === "string" ? body.error.code : void 0;
79
85
  const canonical = rawCode ? normalizeErrorCode(rawCode) : void 0;
@@ -429,14 +435,38 @@ var BaseAuthStore = class {
429
435
  }
430
436
  /**
431
437
  * Populate the store from a raw `Cookie` header string (server-side).
432
- * Looks for the key matching `this.key`.
438
+ * Supports both single cookie `${this.key}=...` and numbered chunk cookies `${this.key}.0=...`, `${this.key}.1=...`
439
+ * automatically reassembled in order (AUTH-010).
433
440
  */
434
441
  loadFromCookie(cookieString) {
442
+ if (!cookieString) return;
443
+ const entries = cookieString.split(";").map((c) => c.trim()).filter(Boolean);
444
+ let encodedData = null;
435
445
  const prefix = `${this.key}=`;
436
- const entry = cookieString.split(";").map((c) => c.trim()).find((c) => c.startsWith(prefix));
437
- if (!entry) return;
446
+ const singleEntry = entries.find((c) => c.startsWith(prefix));
447
+ if (singleEntry) {
448
+ encodedData = singleEntry.slice(prefix.length);
449
+ } else {
450
+ const escapedKey = this.key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
451
+ const chunkRegex = new RegExp(`^${escapedKey}\\.(\\d+)=(.*)$`);
452
+ const chunks = [];
453
+ for (const entry of entries) {
454
+ const match = entry.match(chunkRegex);
455
+ if (match) {
456
+ chunks.push({
457
+ index: parseInt(match[1], 10),
458
+ value: match[2]
459
+ });
460
+ }
461
+ }
462
+ if (chunks.length > 0) {
463
+ chunks.sort((a, b) => a.index - b.index);
464
+ encodedData = chunks.map((c) => c.value).join("");
465
+ }
466
+ }
467
+ if (!encodedData) return;
438
468
  try {
439
- const raw = decodeURIComponent(entry.slice(prefix.length));
469
+ const raw = decodeURIComponent(encodedData);
440
470
  const parsed = JSON.parse(raw);
441
471
  const user = parsed.token ? userFromAccessToken(parsed.token) : null;
442
472
  if (parsed.token && parsed.refreshToken && user) {
@@ -446,23 +476,47 @@ var BaseAuthStore = class {
446
476
  }
447
477
  }
448
478
  /**
449
- * Serialize the session tokens into a `Set-Cookie` value. The `user` object is NOT included: its
450
- * `user_metadata` is already inside the access token.
479
+ * Serialize the session tokens into an array of `Set-Cookie` header strings.
480
+ * If the payload exceeds `maxChunkSize` (default 3500 bytes), it is automatically split into
481
+ * numbered chunks `${key}.0`, `${key}.1`, etc. to avoid browser cookie size limits (AUTH-010).
451
482
  */
452
- exportToCookie(options = {}) {
483
+ exportToCookies(options = {}, maxChunkSize = 3500) {
453
484
  const data = {
454
485
  token: this.token,
455
486
  refreshToken: this.refreshToken
456
487
  };
457
488
  const encoded = encodeURIComponent(JSON.stringify(data));
458
- const parts = [`${this.key}=${encoded}`];
459
- if (options.path) parts.push(`Path=${options.path}`);
460
- if (options.domain) parts.push(`Domain=${options.domain}`);
461
- if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
462
- if (options.secure) parts.push("Secure");
463
- if (options.sameSite) parts.push(`SameSite=${sameSiteAttribute(options.sameSite)}`);
464
- if (options.httpOnly) parts.push("HttpOnly");
465
- return parts.join("; ");
489
+ const buildCookie = (name, value) => {
490
+ const parts = [`${name}=${value}`];
491
+ if (options.path) parts.push(`Path=${options.path}`);
492
+ if (options.domain) parts.push(`Domain=${options.domain}`);
493
+ if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
494
+ if (options.secure) parts.push("Secure");
495
+ if (options.sameSite) parts.push(`SameSite=${sameSiteAttribute(options.sameSite)}`);
496
+ if (options.httpOnly) parts.push("HttpOnly");
497
+ return parts.join("; ");
498
+ };
499
+ const limit = Math.max(1, maxChunkSize);
500
+ if (encoded.length <= limit) {
501
+ return [buildCookie(this.key, encoded)];
502
+ }
503
+ const cookies = [];
504
+ let offset = 0;
505
+ let index = 0;
506
+ while (offset < encoded.length) {
507
+ const chunk = encoded.slice(offset, offset + limit);
508
+ cookies.push(buildCookie(`${this.key}.${index}`, chunk));
509
+ offset += limit;
510
+ index++;
511
+ }
512
+ return cookies;
513
+ }
514
+ /**
515
+ * Serialize the session tokens into a single `Set-Cookie` value.
516
+ * Note: If the token payload may exceed 3.5KB, use `exportToCookies()` instead for automatic chunking (AUTH-010).
517
+ */
518
+ exportToCookie(options = {}) {
519
+ return this.exportToCookies(options, Infinity)[0];
466
520
  }
467
521
  // -- Event emitter ----------------------------------------------------------
468
522
  /**
@@ -507,14 +561,42 @@ function unwrapEnvelope(body) {
507
561
  }
508
562
  return body;
509
563
  }
564
+ function parseJsonBody(raw, parseBigIntAsString) {
565
+ if (parseBigIntAsString) {
566
+ const safe = raw.replace(/([:\[,]\s*)(-?\d{16,})(?=\s*[,}\]])/g, '$1"$2"');
567
+ return JSON.parse(safe);
568
+ }
569
+ return JSON.parse(raw);
570
+ }
510
571
  function parseContentRangeTotal(headerVal) {
511
- if (!headerVal) return void 0;
572
+ return parseContentRangeDetails(headerVal).total;
573
+ }
574
+ function parseContentRangeDetails(headerVal) {
575
+ if (!headerVal) return { total: void 0, hasMore: void 0 };
512
576
  const parts = headerVal.split("/");
513
- if (parts.length === 2 && parts[1] !== "*") {
514
- const n = parseInt(parts[1], 10);
515
- if (!Number.isNaN(n)) return n;
577
+ const totalStr = parts[1];
578
+ let total;
579
+ if (totalStr && totalStr !== "*") {
580
+ const n = parseInt(totalStr, 10);
581
+ if (!Number.isNaN(n)) total = n;
516
582
  }
517
- return void 0;
583
+ let hasMore;
584
+ const rangePart = parts[0];
585
+ if (rangePart) {
586
+ const rangeBounds = rangePart.split("-");
587
+ if (rangeBounds.length === 2) {
588
+ const start = parseInt(rangeBounds[0], 10);
589
+ const end = parseInt(rangeBounds[1], 10);
590
+ if (!Number.isNaN(start) && !Number.isNaN(end)) {
591
+ if (total !== void 0) {
592
+ hasMore = end + 1 < total;
593
+ } else if (end - start + 1 >= 1e3) {
594
+ hasMore = true;
595
+ }
596
+ }
597
+ }
598
+ }
599
+ return { total, hasMore };
518
600
  }
519
601
  async function readTextBounded(res, timeoutMs) {
520
602
  const body = res.body;
@@ -907,7 +989,7 @@ async function fetchWithAuth(url, init, client) {
907
989
  }
908
990
  let body;
909
991
  try {
910
- body = JSON.parse(raw);
992
+ body = parseJsonBody(raw, client.parseBigIntAsString === true);
911
993
  } catch {
912
994
  const ctRaw = res.headers.get("Content-Type") ?? "unknown";
913
995
  const ct = ctRaw.length > 64 ? `${ctRaw.slice(0, 64)}\u2026` : ctRaw;
@@ -923,10 +1005,21 @@ async function fetchWithAuth(url, init, client) {
923
1005
  }
924
1006
  const unwrapped = unwrapEnvelope(body);
925
1007
  const cr = res.headers.get("content-range") ?? res.headers.get("Content-Range");
926
- const totalFromCr = parseContentRangeTotal(cr);
1008
+ const crDetails = parseContentRangeDetails(cr);
1009
+ const totalFromCr = crDetails.total;
1010
+ const hasMore = crDetails.hasMore;
927
1011
  const rawMeta = body !== null && typeof body === "object" && "meta" in body ? body.meta : void 0;
928
- const meta = rawMeta !== void 0 ? rawMeta : totalFromCr !== void 0 ? { total: totalFromCr } : void 0;
1012
+ const meta = rawMeta !== void 0 ? { ...rawMeta, ...hasMore !== void 0 ? { has_more: hasMore } : {} } : totalFromCr !== void 0 || hasMore !== void 0 ? {
1013
+ ...totalFromCr !== void 0 ? { total: totalFromCr } : {},
1014
+ ...hasMore !== void 0 ? { has_more: hasMore } : {}
1015
+ } : void 0;
929
1016
  const count = meta?.total !== void 0 ? meta.total : totalFromCr ?? null;
1017
+ const isDev = Boolean(client.debug) || typeof globalThis !== "undefined" && globalThis.process?.env?.NODE_ENV === "development";
1018
+ if (hasMore && isDev) {
1019
+ console.warn(
1020
+ "[Aurabase] Query result reached pagination limit of 1,000 rows. Use .range() to paginate further results."
1021
+ );
1022
+ }
930
1023
  if (unwrapped === null) {
931
1024
  return { data: null, error: null, meta, count };
932
1025
  }
@@ -1038,48 +1131,6 @@ var LocalAuthStore = class extends BaseAuthStore {
1038
1131
  this._emitStorageBlocked("removeItem");
1039
1132
  }
1040
1133
  }
1041
- // -- Cookie helpers (SSR) ---------------------------------------------------
1042
- /**
1043
- * Populate the store from a raw `Cookie` header string (server-side).
1044
- * Looks for the key matching `this.key`.
1045
- *
1046
- * Le cookie ne porte que `{ token, refreshToken }` (AUTH-036) : l'utilisateur est reconstruit à partir des
1047
- * claims du jeton (non vérifié — `auth.getUser()` le fait vérifier par le serveur).
1048
- */
1049
- loadFromCookie(cookieString) {
1050
- const prefix = `${this.key}=`;
1051
- const entry = cookieString.split(";").map((c) => c.trim()).find((c) => c.startsWith(prefix));
1052
- if (!entry) return;
1053
- try {
1054
- const raw = decodeURIComponent(entry.slice(prefix.length));
1055
- const parsed = JSON.parse(raw);
1056
- const user = parsed.token ? userFromAccessToken(parsed.token) : null;
1057
- if (parsed.token && parsed.refreshToken && user) {
1058
- super.save(parsed.token, parsed.refreshToken, user);
1059
- }
1060
- } catch {
1061
- }
1062
- }
1063
- /**
1064
- * Serialize the session tokens into a `Set-Cookie` value. The `user` object is NOT included: its
1065
- * `user_metadata` is already inside the access token, and sending it twice doubled the cookie
1066
- * (5 KB of metadata gave 4 cookies and 13 139 bytes).
1067
- */
1068
- exportToCookie(options = {}) {
1069
- const data = {
1070
- token: this.token,
1071
- refreshToken: this.refreshToken
1072
- };
1073
- const encoded = encodeURIComponent(JSON.stringify(data));
1074
- const parts = [`${this.key}=${encoded}`];
1075
- if (options.path) parts.push(`Path=${options.path}`);
1076
- if (options.domain) parts.push(`Domain=${options.domain}`);
1077
- if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
1078
- if (options.secure) parts.push("Secure");
1079
- if (options.sameSite) parts.push(`SameSite=${sameSiteAttribute(options.sameSite)}`);
1080
- if (options.httpOnly) parts.push("HttpOnly");
1081
- return parts.join("; ");
1082
- }
1083
1134
  // -- Private ----------------------------------------------------------------
1084
1135
  _getStorage() {
1085
1136
  if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
@@ -1092,7 +1143,9 @@ var LocalAuthStore = class extends BaseAuthStore {
1092
1143
  const env = proc.env;
1093
1144
  const isTest = !!(env?.VITEST || env?.NODE_ENV === "test" || env?.JEST_WORKER_ID);
1094
1145
  const execArgv = proc.execArgv;
1095
- const hasFlag = Array.isArray(execArgv) && execArgv.some((a) => typeof a === "string" && a.startsWith("--localstorage-file"));
1146
+ const hasFlag = Array.isArray(execArgv) && execArgv.some(
1147
+ (a) => typeof a === "string" && a.startsWith("--localstorage-file")
1148
+ );
1096
1149
  if (isTest || hasFlag) {
1097
1150
  try {
1098
1151
  const s = g.localStorage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurabase/core",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Cœur du SDK Aurabase : fetch résilient, erreurs, stores auth, types de réponse (infrastructure interne)",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",