@ragable/sdk 0.5.1 → 0.6.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/dist/index.mjs CHANGED
@@ -10,14 +10,49 @@ function bindFetch(custom) {
10
10
  };
11
11
  }
12
12
  var DEFAULT_RAGABLE_API_BASE = "https://ragable-341305259977.asia-southeast1.run.app/api";
13
- var RagableError = class extends Error {
13
+ var RagableSdkError = class extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = this.constructor.name;
17
+ }
18
+ };
19
+ var RagableError = class extends RagableSdkError {
14
20
  constructor(message, status, body) {
15
21
  super(message);
22
+ __publicField(this, "__type", "RagableError");
16
23
  __publicField(this, "status");
17
24
  __publicField(this, "body");
18
- this.name = "RagableError";
25
+ __publicField(this, "code");
26
+ __publicField(this, "details");
19
27
  this.status = status;
20
28
  this.body = body;
29
+ if (body && typeof body === "object") {
30
+ const b = body;
31
+ this.code = typeof b.code === "string" ? b.code : void 0;
32
+ this.details = typeof b.details === "string" ? b.details : void 0;
33
+ }
34
+ }
35
+ };
36
+ var RagableNetworkError = class extends RagableSdkError {
37
+ constructor(message, cause) {
38
+ super(message);
39
+ __publicField(this, "__type", "RagableNetworkError");
40
+ __publicField(this, "cause");
41
+ this.cause = cause;
42
+ }
43
+ };
44
+ var RagableAbortError = class extends RagableSdkError {
45
+ constructor(message = "Request aborted") {
46
+ super(message);
47
+ __publicField(this, "__type", "RagableAbortError");
48
+ }
49
+ };
50
+ var RagableTimeoutError = class extends RagableSdkError {
51
+ constructor(timeoutMs) {
52
+ super(`Request timed out after ${timeoutMs}ms`);
53
+ __publicField(this, "__type", "RagableTimeoutError");
54
+ __publicField(this, "timeoutMs");
55
+ this.timeoutMs = timeoutMs;
21
56
  }
22
57
  };
23
58
  function extractErrorMessage(payload, fallback) {
@@ -345,42 +380,191 @@ var AgentsClient = class {
345
380
  }
346
381
  };
347
382
 
348
- // src/browser-postgrest.ts
349
- function assertIdent(name, ctx) {
350
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
351
- throw new RagableError(
352
- `Invalid ${ctx} identifier "${name}" (use letters, numbers, underscore)`,
353
- 400,
354
- null
355
- );
383
+ // src/transport.ts
384
+ var DEFAULT_RETRY = {
385
+ maxRetries: 3,
386
+ baseDelayMs: 200,
387
+ maxDelayMs: 5e3,
388
+ retryOn: [408, 425, 429, 502, 503, 504],
389
+ respectRetryAfter: true
390
+ };
391
+ var DEFAULT_TIMEOUT_MS = 3e4;
392
+ function jitteredDelay(base, attempt, max) {
393
+ const exp = Math.min(base * 2 ** attempt, max);
394
+ return Math.round(exp * (0.5 + Math.random() * 0.5));
395
+ }
396
+ function parseRetryAfter(header) {
397
+ if (!header) return null;
398
+ const seconds = Number(header);
399
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
400
+ const date = Date.parse(header);
401
+ if (Number.isFinite(date)) return Math.max(0, date - Date.now());
402
+ return null;
403
+ }
404
+ var _uuidCounter = 0;
405
+ function generateIdempotencyKey() {
406
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
407
+ return crypto.randomUUID();
356
408
  }
409
+ _uuidCounter++;
410
+ return `idk-${Date.now()}-${_uuidCounter}-${Math.random().toString(36).slice(2, 10)}`;
357
411
  }
358
- function quoteIdent(name) {
359
- assertIdent(name, "column");
360
- return `"${name}"`;
412
+ function requestCacheKey(req) {
413
+ return `${req.method}:${req.url}`;
361
414
  }
362
- function parseConflictColumns(onConflict) {
363
- const parts = onConflict.split(",").map((s) => s.trim()).filter(Boolean);
364
- if (parts.length === 0) {
365
- throw new RagableError(
366
- "upsert requires onConflict with at least one column (e.g. 'id' or 'org_id,user_id')",
367
- 400,
368
- null
415
+ var Transport = class {
416
+ constructor(options = {}) {
417
+ __publicField(this, "fetchImpl");
418
+ __publicField(this, "defaultHeaders");
419
+ __publicField(this, "defaultRetry");
420
+ __publicField(this, "defaultTimeoutMs");
421
+ __publicField(this, "onRequest");
422
+ __publicField(this, "onResponse");
423
+ __publicField(this, "onRetry");
424
+ __publicField(this, "inflightGets", /* @__PURE__ */ new Map());
425
+ __publicField(this, "_refreshHandler", null);
426
+ this.fetchImpl = bindFetch(options.fetch);
427
+ this.defaultHeaders = options.headers;
428
+ this.defaultRetry = { ...DEFAULT_RETRY, ...options.retry };
429
+ this.defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
430
+ this.onRequest = options.onRequest;
431
+ this.onResponse = options.onResponse;
432
+ this.onRetry = options.onRetry;
433
+ }
434
+ setRefreshHandler(handler) {
435
+ this._refreshHandler = handler;
436
+ }
437
+ async execute(req) {
438
+ if (req.method === "GET") {
439
+ const key = requestCacheKey(req);
440
+ const existing = this.inflightGets.get(key);
441
+ if (existing) return existing;
442
+ const promise = this._executeWithRetry(req).finally(() => {
443
+ this.inflightGets.delete(key);
444
+ });
445
+ this.inflightGets.set(key, promise);
446
+ return promise;
447
+ }
448
+ return this._executeWithRetry(req);
449
+ }
450
+ async _executeWithRetry(req) {
451
+ const retryOpts = {
452
+ ...this.defaultRetry,
453
+ ...req.retry
454
+ };
455
+ const timeoutMs = req.timeoutMs ?? this.defaultTimeoutMs;
456
+ const headers = new Headers(this.defaultHeaders);
457
+ req.headers.forEach((v, k) => headers.set(k, v));
458
+ if (req.idempotencyKey) {
459
+ headers.set("Idempotency-Key", req.idempotencyKey);
460
+ }
461
+ const finalReq = { ...req, headers };
462
+ this.onRequest?.(finalReq);
463
+ let lastError;
464
+ const maxAttempts = 1 + retryOpts.maxRetries;
465
+ let did401Refresh = false;
466
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
467
+ try {
468
+ const response = await this._singleFetch(finalReq, timeoutMs);
469
+ if (response.status === 401 && this._refreshHandler && !did401Refresh) {
470
+ did401Refresh = true;
471
+ const newToken = await this._refreshHandler();
472
+ if (newToken) {
473
+ finalReq.headers.set("Authorization", `Bearer ${newToken}`);
474
+ attempt--;
475
+ continue;
476
+ }
477
+ }
478
+ if (!response.ok && retryOpts.retryOn.includes(response.status) && attempt < maxAttempts - 1) {
479
+ let delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
480
+ if (retryOpts.respectRetryAfter) {
481
+ const ra = parseRetryAfter(response.headers.get("retry-after"));
482
+ if (ra !== null) delayMs = Math.min(ra, retryOpts.maxDelayMs);
483
+ }
484
+ this.onRetry?.(finalReq, attempt + 1, delayMs, `HTTP ${response.status}`);
485
+ await sleep(delayMs);
486
+ continue;
487
+ }
488
+ return response;
489
+ } catch (e) {
490
+ if (e instanceof RagableAbortError || e instanceof RagableTimeoutError) {
491
+ throw e;
492
+ }
493
+ lastError = e;
494
+ if (attempt < maxAttempts - 1) {
495
+ const delayMs = jitteredDelay(retryOpts.baseDelayMs, attempt, retryOpts.maxDelayMs);
496
+ this.onRetry?.(finalReq, attempt + 1, delayMs, e.message);
497
+ await sleep(delayMs);
498
+ continue;
499
+ }
500
+ }
501
+ }
502
+ throw lastError instanceof RagableNetworkError ? lastError : new RagableNetworkError(
503
+ lastError?.message ?? "Network request failed",
504
+ lastError
369
505
  );
370
506
  }
371
- for (const p of parts) assertIdent(p, "onConflict");
372
- return parts;
373
- }
374
- var OP_SQL = {
375
- eq: "=",
376
- neq: "<>",
377
- gt: ">",
378
- gte: ">=",
379
- lt: "<",
380
- lte: "<=",
381
- like: "LIKE",
382
- ilike: "ILIKE"
507
+ async _singleFetch(req, timeoutMs) {
508
+ const controller = new AbortController();
509
+ const signals = [controller.signal];
510
+ if (req.signal) signals.push(req.signal);
511
+ const combinedSignal = signals.length === 1 ? controller.signal : AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
512
+ if (req.signal?.aborted) {
513
+ throw new RagableAbortError();
514
+ }
515
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
516
+ const externalAbortHandler = req.signal ? () => controller.abort() : null;
517
+ if (externalAbortHandler && req.signal) {
518
+ req.signal.addEventListener("abort", externalAbortHandler, { once: true });
519
+ }
520
+ const start = Date.now();
521
+ try {
522
+ const response = await this.fetchImpl(req.url, {
523
+ method: req.method,
524
+ headers: req.headers,
525
+ body: req.body,
526
+ signal: combinedSignal
527
+ });
528
+ this.onResponse?.(req, response, Date.now() - start);
529
+ return response;
530
+ } catch (e) {
531
+ if (e.name === "AbortError") {
532
+ if (req.signal?.aborted) throw new RagableAbortError();
533
+ throw new RagableTimeoutError(timeoutMs);
534
+ }
535
+ throw new RagableNetworkError(e.message, e);
536
+ } finally {
537
+ clearTimeout(timer);
538
+ if (externalAbortHandler && req.signal) {
539
+ req.signal.removeEventListener("abort", externalAbortHandler);
540
+ }
541
+ }
542
+ }
383
543
  };
544
+ function sleep(ms) {
545
+ return new Promise((resolve) => setTimeout(resolve, ms));
546
+ }
547
+ async function parseTransportResponse(response) {
548
+ if (response.status === 204) return null;
549
+ const text = await response.text();
550
+ if (!text) return null;
551
+ let payload;
552
+ try {
553
+ payload = JSON.parse(text);
554
+ } catch {
555
+ if (!response.ok) {
556
+ throw new RagableError(text.slice(0, 200), response.status, null);
557
+ }
558
+ return text;
559
+ }
560
+ if (!response.ok) {
561
+ const message = extractErrorMessage(payload, response.statusText);
562
+ throw new RagableError(message, response.status, payload);
563
+ }
564
+ return payload;
565
+ }
566
+
567
+ // src/browser-postgrest.ts
384
568
  async function asPostgrestResponse(fn) {
385
569
  try {
386
570
  const data = await fn();
@@ -390,28 +574,75 @@ async function asPostgrestResponse(fn) {
390
574
  return { data: null, error: err };
391
575
  }
392
576
  }
393
- function buildWhere(filters, params) {
394
- if (filters.length === 0) return { clause: "", nextIdx: 1 };
395
- const parts = [];
396
- let i = params.length;
397
- for (const f of filters) {
398
- assertIdent(f.column, "filter");
399
- i += 1;
400
- parts.push(`${quoteIdent(f.column)} ${OP_SQL[f.op]} $${i}`);
401
- params.push(f.value);
402
- }
403
- return { clause: ` WHERE ${parts.join(" AND ")}`, nextIdx: i };
577
+ function encodeFilterValue(op, value) {
578
+ if (op === "is") return `is.${value}`;
579
+ if (op === "in") {
580
+ const vals = value;
581
+ return `in.(${vals.map(String).join(",")})`;
582
+ }
583
+ return `${op}.${value}`;
584
+ }
585
+ async function parsePostgRESTResponse(response) {
586
+ if (response.status === 204) return null;
587
+ const text = await response.text();
588
+ if (!text) return null;
589
+ let payload;
590
+ try {
591
+ payload = JSON.parse(text);
592
+ } catch {
593
+ throw new RagableError(
594
+ `PostgREST response parse error: ${text.slice(0, 200)}`,
595
+ response.status,
596
+ null
597
+ );
598
+ }
599
+ if (!response.ok) {
600
+ const msg = typeof payload === "object" && payload !== null ? payload.message ?? payload.error ?? response.statusText : response.statusText;
601
+ const code = typeof payload === "object" && payload !== null ? payload.code : void 0;
602
+ throw new RagableError(String(msg), response.status, { code });
603
+ }
604
+ return payload;
605
+ }
606
+ function addFilterMethods(builder) {
607
+ const b = builder;
608
+ for (const op of ["eq", "neq", "gt", "gte", "lt", "lte", "like", "ilike"]) {
609
+ b[op] = function(column, value) {
610
+ b.filters.push({ op, column, value });
611
+ return b;
612
+ };
613
+ }
614
+ b.is = function(column, value) {
615
+ b.filters.push({ op: "is", column, value });
616
+ return b;
617
+ };
618
+ b.in = function(column, values) {
619
+ b.filters.push({ op: "in", column, value: values });
620
+ return b;
621
+ };
622
+ b.match = function(query) {
623
+ for (const [col, val] of Object.entries(query)) {
624
+ if (val === null) {
625
+ b.filters.push({ op: "is", column: col, value: null });
626
+ } else {
627
+ b.filters.push({ op: "eq", column: col, value: val });
628
+ }
629
+ }
630
+ return b;
631
+ };
632
+ return b;
404
633
  }
405
634
  var PostgrestSelectBuilder = class {
406
- constructor(run, databaseInstanceId, table, columns) {
407
- this.run = run;
635
+ constructor(pgFetch, databaseInstanceId, table, columns) {
636
+ this.pgFetch = pgFetch;
408
637
  this.databaseInstanceId = databaseInstanceId;
409
638
  this.table = table;
410
639
  this.columns = columns;
411
640
  __publicField(this, "filters", []);
412
641
  __publicField(this, "_limit");
642
+ __publicField(this, "_offset");
413
643
  __publicField(this, "_order");
414
- assertIdent(table, "table");
644
+ __publicField(this, "_signal");
645
+ addFilterMethods(this);
415
646
  }
416
647
  eq(column, value) {
417
648
  this.filters.push({ op: "eq", column, value });
@@ -445,140 +676,173 @@ var PostgrestSelectBuilder = class {
445
676
  this.filters.push({ op: "ilike", column, value });
446
677
  return this;
447
678
  }
679
+ is(column, value) {
680
+ this.filters.push({ op: "is", column, value });
681
+ return this;
682
+ }
683
+ in(column, values) {
684
+ this.filters.push({ op: "in", column, value: values });
685
+ return this;
686
+ }
687
+ match(query) {
688
+ for (const [col, val] of Object.entries(query)) {
689
+ if (val === null) {
690
+ this.filters.push({ op: "is", column: col, value: null });
691
+ } else {
692
+ this.filters.push({ op: "eq", column: col, value: val });
693
+ }
694
+ }
695
+ return this;
696
+ }
448
697
  limit(n) {
449
698
  this._limit = n;
450
699
  return this;
451
700
  }
701
+ offset(n) {
702
+ this._offset = n;
703
+ return this;
704
+ }
705
+ range(from, to) {
706
+ this._offset = from;
707
+ this._limit = to - from + 1;
708
+ return this;
709
+ }
452
710
  order(column, options) {
453
- this._order = { column, ascending: options?.ascending !== false };
711
+ this._order = {
712
+ column,
713
+ ascending: options?.ascending !== false,
714
+ nullsFirst: options?.nullsFirst
715
+ };
716
+ return this;
717
+ }
718
+ abortSignal(signal) {
719
+ this._signal = signal;
454
720
  return this;
455
721
  }
456
- buildSelectCore(params, includeUserLimit) {
457
- const tbl = quoteIdent(this.table);
458
- const { clause } = buildWhere(this.filters, params);
459
- let sql = `SELECT ${this.columns} FROM ${tbl}${clause}`;
722
+ buildSearchParams() {
723
+ const sp = new URLSearchParams();
724
+ if (this.columns && this.columns !== "*") {
725
+ sp.set("select", this.columns);
726
+ }
727
+ for (const f of this.filters) {
728
+ sp.append(f.column, encodeFilterValue(f.op, f.value));
729
+ }
460
730
  if (this._order) {
461
- sql += ` ORDER BY ${quoteIdent(this._order.column)} ${this._order.ascending ? "ASC" : "DESC"}`;
731
+ let orderStr = `${this._order.column}.${this._order.ascending ? "asc" : "desc"}`;
732
+ if (this._order.nullsFirst === true) orderStr += ".nullsfirst";
733
+ else if (this._order.nullsFirst === false) orderStr += ".nullslast";
734
+ sp.set("order", orderStr);
735
+ }
736
+ if (this._limit != null) {
737
+ sp.set("limit", String(Math.max(0, Math.floor(this._limit))));
462
738
  }
463
- if (includeUserLimit && this._limit != null) {
464
- sql += ` LIMIT ${Math.max(0, Math.floor(this._limit))}`;
739
+ if (this._offset != null && this._offset > 0) {
740
+ sp.set("offset", String(Math.max(0, Math.floor(this._offset))));
465
741
  }
466
- return sql;
742
+ return sp;
467
743
  }
468
744
  then(onfulfilled, onrejected) {
469
745
  return this.executeMany().then(onfulfilled, onrejected);
470
746
  }
471
747
  async executeMany() {
472
748
  return asPostgrestResponse(async () => {
473
- const params = [];
474
- const sql = this.buildSelectCore(params, true);
475
- const res = await this.run({
749
+ const response = await this.pgFetch({
750
+ method: "GET",
751
+ table: this.table,
752
+ searchParams: this.buildSearchParams(),
476
753
  databaseInstanceId: this.databaseInstanceId,
477
- sql,
478
- params,
479
- readOnly: true
754
+ signal: this._signal
480
755
  });
481
- return res.rows;
756
+ return parsePostgRESTResponse(response);
482
757
  });
483
758
  }
484
759
  async single() {
485
760
  return asPostgrestResponse(async () => {
486
- const params = [];
487
- const base = this.buildSelectCore(params, false);
488
- const sql = `${base} LIMIT 2`;
489
- const res = await this.run({
761
+ const sp = this.buildSearchParams();
762
+ const response = await this.pgFetch({
763
+ method: "GET",
764
+ table: this.table,
765
+ searchParams: sp,
766
+ headers: { Accept: "application/vnd.pgrst.object+json" },
490
767
  databaseInstanceId: this.databaseInstanceId,
491
- sql,
492
- params,
493
- readOnly: true
768
+ signal: this._signal
494
769
  });
495
- if (res.rows.length === 0 || res.rows.length > 1) {
496
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
497
- code: "PGRST116"
498
- });
499
- }
500
- return res.rows[0];
770
+ return parsePostgRESTResponse(response);
501
771
  });
502
772
  }
503
773
  async maybeSingle() {
504
774
  return asPostgrestResponse(async () => {
505
- const params = [];
506
- const base = this.buildSelectCore(params, false);
507
- const sql = `${base} LIMIT 2`;
508
- const res = await this.run({
775
+ const response = await this.pgFetch({
776
+ method: "GET",
777
+ table: this.table,
778
+ searchParams: this.buildSearchParams(),
509
779
  databaseInstanceId: this.databaseInstanceId,
510
- sql,
511
- params,
512
- readOnly: true
780
+ signal: this._signal
513
781
  });
514
- if (res.rows.length > 1) {
515
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
516
- code: "PGRST116"
517
- });
782
+ const rows = await parsePostgRESTResponse(response);
783
+ if (rows.length > 1) {
784
+ throw new RagableError(
785
+ "JSON object requested, multiple (or no) rows returned",
786
+ 406,
787
+ { code: "PGRST116" }
788
+ );
518
789
  }
519
- return res.rows[0] ?? null;
790
+ return rows[0] ?? null;
520
791
  });
521
792
  }
522
793
  };
523
794
  var PostgrestInsertRootBuilder = class {
524
- constructor(run, databaseInstanceId, table, rows) {
525
- this.run = run;
795
+ constructor(pgFetch, databaseInstanceId, table, rows) {
796
+ this.pgFetch = pgFetch;
526
797
  this.databaseInstanceId = databaseInstanceId;
527
798
  this.table = table;
528
799
  this.rows = rows;
529
- assertIdent(table, "table");
800
+ __publicField(this, "_signal");
530
801
  }
531
- /**
532
- * Return inserted rows (Supabase: chain `.select()` to get data).
533
- * @see https://supabase.com/docs/reference/javascript/insert
534
- */
535
802
  select(columns = "*") {
536
803
  return new PostgrestInsertReturningBuilder(
537
- this.run,
804
+ this.pgFetch,
538
805
  this.databaseInstanceId,
539
806
  this.table,
540
807
  this.rows,
541
- columns
808
+ columns,
809
+ this._signal
542
810
  );
543
811
  }
812
+ abortSignal(signal) {
813
+ this._signal = signal;
814
+ return this;
815
+ }
544
816
  then(onfulfilled, onrejected) {
545
817
  return this.executeNoReturn().then(onfulfilled, onrejected);
546
818
  }
547
819
  async executeNoReturn() {
548
820
  return asPostgrestResponse(async () => {
549
821
  if (this.rows.length === 0) return null;
550
- const keys = Object.keys(this.rows[0]);
551
- for (const k of keys) assertIdent(k, "column");
552
- const tbl = quoteIdent(this.table);
553
- const params = [];
554
- const valueGroups = [];
555
- for (const row of this.rows) {
556
- const placeholders = [];
557
- for (const k of keys) {
558
- params.push(row[k]);
559
- placeholders.push(`$${params.length}`);
560
- }
561
- valueGroups.push(`(${placeholders.join(", ")})`);
562
- }
563
- const cols = keys.map(quoteIdent).join(", ");
564
- const sql = `INSERT INTO ${tbl} (${cols}) VALUES ${valueGroups.join(", ")}`;
565
- await this.run({
822
+ const body = this.rows.length === 1 ? this.rows[0] : this.rows;
823
+ const response = await this.pgFetch({
824
+ method: "POST",
825
+ table: this.table,
826
+ searchParams: new URLSearchParams(),
827
+ body,
828
+ headers: { Prefer: "return=minimal" },
566
829
  databaseInstanceId: this.databaseInstanceId,
567
- sql,
568
- params,
569
- readOnly: false
830
+ signal: this._signal,
831
+ idempotencyKey: generateIdempotencyKey()
570
832
  });
833
+ await parsePostgRESTResponse(response);
571
834
  return null;
572
835
  });
573
836
  }
574
837
  };
575
838
  var PostgrestInsertReturningBuilder = class {
576
- constructor(run, databaseInstanceId, table, rows, returning) {
577
- this.run = run;
839
+ constructor(pgFetch, databaseInstanceId, table, rows, returning, _signal) {
840
+ this.pgFetch = pgFetch;
578
841
  this.databaseInstanceId = databaseInstanceId;
579
842
  this.table = table;
580
843
  this.rows = rows;
581
844
  this.returning = returning;
845
+ this._signal = _signal;
582
846
  }
583
847
  then(onfulfilled, onrejected) {
584
848
  return this.executeMany().then(onfulfilled, onrejected);
@@ -586,28 +850,22 @@ var PostgrestInsertReturningBuilder = class {
586
850
  async executeMany() {
587
851
  return asPostgrestResponse(async () => {
588
852
  if (this.rows.length === 0) return [];
589
- const keys = Object.keys(this.rows[0]);
590
- for (const k of keys) assertIdent(k, "column");
591
- const tbl = quoteIdent(this.table);
592
- const params = [];
593
- const valueGroups = [];
594
- for (const row of this.rows) {
595
- const placeholders = [];
596
- for (const k of keys) {
597
- params.push(row[k]);
598
- placeholders.push(`$${params.length}`);
599
- }
600
- valueGroups.push(`(${placeholders.join(", ")})`);
853
+ const body = this.rows.length === 1 ? this.rows[0] : this.rows;
854
+ const sp = new URLSearchParams();
855
+ if (this.returning && this.returning !== "*") {
856
+ sp.set("select", this.returning);
601
857
  }
602
- const cols = keys.map(quoteIdent).join(", ");
603
- const sql = `INSERT INTO ${tbl} (${cols}) VALUES ${valueGroups.join(", ")} RETURNING ${this.returning}`;
604
- const res = await this.run({
858
+ const response = await this.pgFetch({
859
+ method: "POST",
860
+ table: this.table,
861
+ searchParams: sp,
862
+ body,
863
+ headers: { Prefer: "return=representation" },
605
864
  databaseInstanceId: this.databaseInstanceId,
606
- sql,
607
- params,
608
- readOnly: false
865
+ signal: this._signal,
866
+ idempotencyKey: generateIdempotencyKey()
609
867
  });
610
- return res.rows;
868
+ return parsePostgRESTResponse(response);
611
869
  });
612
870
  }
613
871
  async single() {
@@ -616,9 +874,11 @@ var PostgrestInsertReturningBuilder = class {
616
874
  if (error) throw error;
617
875
  const rows = data ?? [];
618
876
  if (rows.length === 0 || rows.length > 1) {
619
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
620
- code: "PGRST116"
621
- });
877
+ throw new RagableError(
878
+ "JSON object requested, multiple (or no) rows returned",
879
+ 406,
880
+ { code: "PGRST116" }
881
+ );
622
882
  }
623
883
  return rows[0];
624
884
  });
@@ -629,22 +889,24 @@ var PostgrestInsertReturningBuilder = class {
629
889
  if (error) throw error;
630
890
  const rows = data ?? [];
631
891
  if (rows.length > 1) {
632
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
633
- code: "PGRST116"
634
- });
892
+ throw new RagableError(
893
+ "JSON object requested, multiple (or no) rows returned",
894
+ 406,
895
+ { code: "PGRST116" }
896
+ );
635
897
  }
636
898
  return rows[0] ?? null;
637
899
  });
638
900
  }
639
901
  };
640
902
  var PostgrestUpdateRootBuilder = class {
641
- constructor(run, databaseInstanceId, table, patch) {
642
- this.run = run;
903
+ constructor(pgFetch, databaseInstanceId, table, patch) {
904
+ this.pgFetch = pgFetch;
643
905
  this.databaseInstanceId = databaseInstanceId;
644
906
  this.table = table;
645
907
  this.patch = patch;
646
908
  __publicField(this, "filters", []);
647
- assertIdent(table, "table");
909
+ __publicField(this, "_signal");
648
910
  }
649
911
  eq(column, value) {
650
912
  this.filters.push({ op: "eq", column, value });
@@ -678,64 +940,79 @@ var PostgrestUpdateRootBuilder = class {
678
940
  this.filters.push({ op: "ilike", column, value });
679
941
  return this;
680
942
  }
681
- /**
682
- * Return updated rows (Supabase: `.update().eq().select()`).
683
- * @see https://supabase.com/docs/reference/javascript/update
684
- */
943
+ is(column, value) {
944
+ this.filters.push({ op: "is", column, value });
945
+ return this;
946
+ }
947
+ in(column, values) {
948
+ this.filters.push({ op: "in", column, value: values });
949
+ return this;
950
+ }
951
+ match(query) {
952
+ for (const [col, val] of Object.entries(query)) {
953
+ if (val === null) {
954
+ this.filters.push({ op: "is", column: col, value: null });
955
+ } else {
956
+ this.filters.push({ op: "eq", column: col, value: val });
957
+ }
958
+ }
959
+ return this;
960
+ }
685
961
  select(columns = "*") {
686
962
  return new PostgrestUpdateReturningBuilder(
687
- this.run,
963
+ this.pgFetch,
688
964
  this.databaseInstanceId,
689
965
  this.table,
690
966
  this.patch,
691
967
  this.filters,
692
- columns
968
+ columns,
969
+ this._signal
693
970
  );
694
971
  }
972
+ abortSignal(signal) {
973
+ this._signal = signal;
974
+ return this;
975
+ }
695
976
  then(onfulfilled, onrejected) {
696
977
  return this.executeNoReturn().then(onfulfilled, onrejected);
697
978
  }
979
+ buildSearchParams() {
980
+ const sp = new URLSearchParams();
981
+ for (const f of this.filters) {
982
+ sp.append(f.column, encodeFilterValue(f.op, f.value));
983
+ }
984
+ return sp;
985
+ }
698
986
  async executeNoReturn() {
699
987
  return asPostgrestResponse(async () => {
700
988
  const keys = Object.keys(this.patch);
701
989
  if (keys.length === 0) {
702
990
  throw new RagableError("Empty update payload", 400, null);
703
991
  }
704
- for (const k of keys) assertIdent(k, "column");
705
- if (this.filters.length === 0) {
706
- throw new RagableError(
707
- "UPDATE requires a filter (e.g. .eq('id', value)) \u2014 refusing unscoped update",
708
- 400,
709
- null
710
- );
711
- }
712
- const params = [];
713
- const sets = [];
714
- for (const k of keys) {
715
- params.push(this.patch[k]);
716
- sets.push(`${quoteIdent(k)} = $${params.length}`);
717
- }
718
- const { clause } = buildWhere(this.filters, params);
719
- const tbl = quoteIdent(this.table);
720
- const sql = `UPDATE ${tbl} SET ${sets.join(", ")}${clause}`;
721
- await this.run({
992
+ const response = await this.pgFetch({
993
+ method: "PATCH",
994
+ table: this.table,
995
+ searchParams: this.buildSearchParams(),
996
+ body: this.patch,
997
+ headers: { Prefer: "return=minimal" },
722
998
  databaseInstanceId: this.databaseInstanceId,
723
- sql,
724
- params,
725
- readOnly: false
999
+ signal: this._signal,
1000
+ idempotencyKey: generateIdempotencyKey()
726
1001
  });
1002
+ await parsePostgRESTResponse(response);
727
1003
  return null;
728
1004
  });
729
1005
  }
730
1006
  };
731
1007
  var PostgrestUpdateReturningBuilder = class {
732
- constructor(run, databaseInstanceId, table, patch, filters, returning) {
733
- this.run = run;
1008
+ constructor(pgFetch, databaseInstanceId, table, patch, filters, returning, _signal) {
1009
+ this.pgFetch = pgFetch;
734
1010
  this.databaseInstanceId = databaseInstanceId;
735
1011
  this.table = table;
736
1012
  this.patch = patch;
737
1013
  this.filters = filters;
738
1014
  this.returning = returning;
1015
+ this._signal = _signal;
739
1016
  }
740
1017
  then(onfulfilled, onrejected) {
741
1018
  return this.executeMany().then(onfulfilled, onrejected);
@@ -746,30 +1023,24 @@ var PostgrestUpdateReturningBuilder = class {
746
1023
  if (keys.length === 0) {
747
1024
  throw new RagableError("Empty update payload", 400, null);
748
1025
  }
749
- for (const k of keys) assertIdent(k, "column");
750
- if (this.filters.length === 0) {
751
- throw new RagableError(
752
- "UPDATE requires a filter (e.g. .eq('id', value)) \u2014 refusing unscoped update",
753
- 400,
754
- null
755
- );
1026
+ const sp = new URLSearchParams();
1027
+ for (const f of this.filters) {
1028
+ sp.append(f.column, encodeFilterValue(f.op, f.value));
756
1029
  }
757
- const params = [];
758
- const sets = [];
759
- for (const k of keys) {
760
- params.push(this.patch[k]);
761
- sets.push(`${quoteIdent(k)} = $${params.length}`);
1030
+ if (this.returning && this.returning !== "*") {
1031
+ sp.set("select", this.returning);
762
1032
  }
763
- const { clause } = buildWhere(this.filters, params);
764
- const tbl = quoteIdent(this.table);
765
- const sql = `UPDATE ${tbl} SET ${sets.join(", ")}${clause} RETURNING ${this.returning}`;
766
- const res = await this.run({
1033
+ const response = await this.pgFetch({
1034
+ method: "PATCH",
1035
+ table: this.table,
1036
+ searchParams: sp,
1037
+ body: this.patch,
1038
+ headers: { Prefer: "return=representation" },
767
1039
  databaseInstanceId: this.databaseInstanceId,
768
- sql,
769
- params,
770
- readOnly: false
1040
+ signal: this._signal,
1041
+ idempotencyKey: generateIdempotencyKey()
771
1042
  });
772
- return res.rows;
1043
+ return parsePostgRESTResponse(response);
773
1044
  });
774
1045
  }
775
1046
  async single() {
@@ -778,9 +1049,11 @@ var PostgrestUpdateReturningBuilder = class {
778
1049
  if (error) throw error;
779
1050
  const rows = data ?? [];
780
1051
  if (rows.length === 0 || rows.length > 1) {
781
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
782
- code: "PGRST116"
783
- });
1052
+ throw new RagableError(
1053
+ "JSON object requested, multiple (or no) rows returned",
1054
+ 406,
1055
+ { code: "PGRST116" }
1056
+ );
784
1057
  }
785
1058
  return rows[0];
786
1059
  });
@@ -791,21 +1064,23 @@ var PostgrestUpdateReturningBuilder = class {
791
1064
  if (error) throw error;
792
1065
  const rows = data ?? [];
793
1066
  if (rows.length > 1) {
794
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
795
- code: "PGRST116"
796
- });
1067
+ throw new RagableError(
1068
+ "JSON object requested, multiple (or no) rows returned",
1069
+ 406,
1070
+ { code: "PGRST116" }
1071
+ );
797
1072
  }
798
1073
  return rows[0] ?? null;
799
1074
  });
800
1075
  }
801
1076
  };
802
1077
  var PostgrestDeleteRootBuilder = class {
803
- constructor(run, databaseInstanceId, table) {
804
- this.run = run;
1078
+ constructor(pgFetch, databaseInstanceId, table) {
1079
+ this.pgFetch = pgFetch;
805
1080
  this.databaseInstanceId = databaseInstanceId;
806
1081
  this.table = table;
807
1082
  __publicField(this, "filters", []);
808
- assertIdent(table, "table");
1083
+ __publicField(this, "_signal");
809
1084
  }
810
1085
  eq(column, value) {
811
1086
  this.filters.push({ op: "eq", column, value });
@@ -839,76 +1114,92 @@ var PostgrestDeleteRootBuilder = class {
839
1114
  this.filters.push({ op: "ilike", column, value });
840
1115
  return this;
841
1116
  }
842
- /**
843
- * Return deleted rows (Supabase: `.delete().eq().select()`).
844
- * @see https://supabase.com/docs/reference/javascript/delete
845
- */
1117
+ is(column, value) {
1118
+ this.filters.push({ op: "is", column, value });
1119
+ return this;
1120
+ }
1121
+ in(column, values) {
1122
+ this.filters.push({ op: "in", column, value: values });
1123
+ return this;
1124
+ }
1125
+ match(query) {
1126
+ for (const [col, val] of Object.entries(query)) {
1127
+ if (val === null) {
1128
+ this.filters.push({ op: "is", column: col, value: null });
1129
+ } else {
1130
+ this.filters.push({ op: "eq", column: col, value: val });
1131
+ }
1132
+ }
1133
+ return this;
1134
+ }
846
1135
  select(columns = "*") {
847
1136
  return new PostgrestDeleteReturningBuilder(
848
- this.run,
1137
+ this.pgFetch,
849
1138
  this.databaseInstanceId,
850
1139
  this.table,
851
1140
  this.filters,
852
- columns
1141
+ columns,
1142
+ this._signal
853
1143
  );
854
1144
  }
1145
+ abortSignal(signal) {
1146
+ this._signal = signal;
1147
+ return this;
1148
+ }
855
1149
  then(onfulfilled, onrejected) {
856
1150
  return this.executeNoReturn().then(onfulfilled, onrejected);
857
1151
  }
858
1152
  async executeNoReturn() {
859
1153
  return asPostgrestResponse(async () => {
860
- if (this.filters.length === 0) {
861
- throw new RagableError(
862
- "DELETE requires a filter (e.g. .eq('id', value)) \u2014 refusing unscoped delete",
863
- 400,
864
- null
865
- );
1154
+ const sp = new URLSearchParams();
1155
+ for (const f of this.filters) {
1156
+ sp.append(f.column, encodeFilterValue(f.op, f.value));
866
1157
  }
867
- const params = [];
868
- const { clause } = buildWhere(this.filters, params);
869
- const tbl = quoteIdent(this.table);
870
- const sql = `DELETE FROM ${tbl}${clause}`;
871
- await this.run({
1158
+ const response = await this.pgFetch({
1159
+ method: "DELETE",
1160
+ table: this.table,
1161
+ searchParams: sp,
1162
+ headers: { Prefer: "return=minimal" },
872
1163
  databaseInstanceId: this.databaseInstanceId,
873
- sql,
874
- params,
875
- readOnly: false
1164
+ signal: this._signal,
1165
+ idempotencyKey: generateIdempotencyKey()
876
1166
  });
1167
+ await parsePostgRESTResponse(response);
877
1168
  return null;
878
1169
  });
879
1170
  }
880
1171
  };
881
1172
  var PostgrestDeleteReturningBuilder = class {
882
- constructor(run, databaseInstanceId, table, filters, returning) {
883
- this.run = run;
1173
+ constructor(pgFetch, databaseInstanceId, table, filters, returning, _signal) {
1174
+ this.pgFetch = pgFetch;
884
1175
  this.databaseInstanceId = databaseInstanceId;
885
1176
  this.table = table;
886
1177
  this.filters = filters;
887
1178
  this.returning = returning;
1179
+ this._signal = _signal;
888
1180
  }
889
1181
  then(onfulfilled, onrejected) {
890
1182
  return this.executeMany().then(onfulfilled, onrejected);
891
1183
  }
892
1184
  async executeMany() {
893
1185
  return asPostgrestResponse(async () => {
894
- if (this.filters.length === 0) {
895
- throw new RagableError(
896
- "DELETE requires a filter (e.g. .eq('id', value)) \u2014 refusing unscoped delete",
897
- 400,
898
- null
899
- );
1186
+ const sp = new URLSearchParams();
1187
+ for (const f of this.filters) {
1188
+ sp.append(f.column, encodeFilterValue(f.op, f.value));
1189
+ }
1190
+ if (this.returning && this.returning !== "*") {
1191
+ sp.set("select", this.returning);
900
1192
  }
901
- const params = [];
902
- const { clause } = buildWhere(this.filters, params);
903
- const tbl = quoteIdent(this.table);
904
- const sql = `DELETE FROM ${tbl}${clause} RETURNING ${this.returning}`;
905
- const res = await this.run({
1193
+ const response = await this.pgFetch({
1194
+ method: "DELETE",
1195
+ table: this.table,
1196
+ searchParams: sp,
1197
+ headers: { Prefer: "return=representation" },
906
1198
  databaseInstanceId: this.databaseInstanceId,
907
- sql,
908
- params,
909
- readOnly: false
1199
+ signal: this._signal,
1200
+ idempotencyKey: generateIdempotencyKey()
910
1201
  });
911
- return res.rows;
1202
+ return parsePostgRESTResponse(response);
912
1203
  });
913
1204
  }
914
1205
  async single() {
@@ -917,9 +1208,11 @@ var PostgrestDeleteReturningBuilder = class {
917
1208
  if (error) throw error;
918
1209
  const rows = data ?? [];
919
1210
  if (rows.length === 0 || rows.length > 1) {
920
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
921
- code: "PGRST116"
922
- });
1211
+ throw new RagableError(
1212
+ "JSON object requested, multiple (or no) rows returned",
1213
+ 406,
1214
+ { code: "PGRST116" }
1215
+ );
923
1216
  }
924
1217
  return rows[0];
925
1218
  });
@@ -930,84 +1223,66 @@ var PostgrestDeleteReturningBuilder = class {
930
1223
  if (error) throw error;
931
1224
  const rows = data ?? [];
932
1225
  if (rows.length > 1) {
933
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
934
- code: "PGRST116"
935
- });
1226
+ throw new RagableError(
1227
+ "JSON object requested, multiple (or no) rows returned",
1228
+ 406,
1229
+ { code: "PGRST116" }
1230
+ );
936
1231
  }
937
1232
  return rows[0] ?? null;
938
1233
  });
939
1234
  }
940
1235
  };
941
1236
  var PostgrestUpsertRootBuilder = class {
942
- constructor(run, databaseInstanceId, table, rows, onConflict, ignoreDuplicates) {
943
- this.run = run;
1237
+ constructor(pgFetch, databaseInstanceId, table, rows, onConflict, ignoreDuplicates) {
1238
+ this.pgFetch = pgFetch;
944
1239
  this.databaseInstanceId = databaseInstanceId;
945
1240
  this.table = table;
946
1241
  this.rows = rows;
1242
+ this.onConflict = onConflict;
947
1243
  this.ignoreDuplicates = ignoreDuplicates;
948
- __publicField(this, "conflictCols");
949
- assertIdent(table, "table");
950
- this.conflictCols = parseConflictColumns(onConflict);
1244
+ __publicField(this, "_signal");
951
1245
  }
952
- /**
953
- * Return upserted rows (Supabase: `.upsert().select()`).
954
- * @see https://supabase.com/docs/reference/javascript/upsert
955
- */
956
1246
  select(columns = "*") {
957
1247
  return new PostgrestUpsertReturningBuilder(this, columns);
958
1248
  }
1249
+ abortSignal(signal) {
1250
+ this._signal = signal;
1251
+ return this;
1252
+ }
959
1253
  then(onfulfilled, onrejected) {
960
1254
  return this.executeNoReturn().then(onfulfilled, onrejected);
961
1255
  }
962
1256
  async executeNoReturn() {
963
1257
  return asPostgrestResponse(async () => {
964
- await this.runUpsert(null);
1258
+ await this.runUpsert("return=minimal", null);
965
1259
  return null;
966
1260
  });
967
1261
  }
968
- async runUpsert(returning) {
969
- if (this.rows.length === 0) {
970
- return { command: "INSERT", rowCount: 0, truncated: false, rows: [] };
971
- }
972
- const keys = Object.keys(this.rows[0]);
973
- for (const k of keys) assertIdent(k, "column");
974
- const tbl = quoteIdent(this.table);
975
- const conflictQuoted = this.conflictCols.map(quoteIdent).join(", ");
976
- const params = [];
977
- const valueGroups = [];
978
- for (const row of this.rows) {
979
- const placeholders = [];
980
- for (const k of keys) {
981
- params.push(row[k]);
982
- placeholders.push(`$${params.length}`);
983
- }
984
- valueGroups.push(`(${placeholders.join(", ")})`);
985
- }
986
- const cols = keys.map(quoteIdent).join(", ");
987
- let sql = `INSERT INTO ${tbl} (${cols}) VALUES ${valueGroups.join(", ")} ON CONFLICT (${conflictQuoted})`;
988
- if (this.ignoreDuplicates) {
989
- sql += " DO NOTHING";
990
- } else {
991
- const setParts = keys.filter((k) => !this.conflictCols.includes(k)).map((k) => `${quoteIdent(k)} = EXCLUDED.${quoteIdent(k)}`);
992
- if (setParts.length === 0) {
993
- sql += " DO NOTHING";
994
- } else {
995
- sql += ` DO UPDATE SET ${setParts.join(", ")}`;
996
- }
997
- }
998
- if (returning) {
999
- sql += ` RETURNING ${returning}`;
1262
+ async runUpsert(prefer, selectCols) {
1263
+ if (this.rows.length === 0) return [];
1264
+ const body = this.rows.length === 1 ? this.rows[0] : this.rows;
1265
+ const sp = new URLSearchParams();
1266
+ sp.set("on_conflict", this.onConflict);
1267
+ if (selectCols && selectCols !== "*") {
1268
+ sp.set("select", selectCols);
1000
1269
  }
1001
- return this.run({
1270
+ const resolution = this.ignoreDuplicates ? "resolution=ignore-duplicates" : "resolution=merge-duplicates";
1271
+ const response = await this.pgFetch({
1272
+ method: "POST",
1273
+ table: this.table,
1274
+ searchParams: sp,
1275
+ body,
1276
+ headers: { Prefer: `${prefer},${resolution}` },
1002
1277
  databaseInstanceId: this.databaseInstanceId,
1003
- sql,
1004
- params,
1005
- readOnly: false
1278
+ signal: this._signal,
1279
+ idempotencyKey: generateIdempotencyKey()
1006
1280
  });
1007
- }
1008
- /** Used by returning builder */
1009
- async runWithReturning(returning) {
1010
- return this.runUpsert(returning);
1281
+ if (prefer.includes("return=minimal")) {
1282
+ await parsePostgRESTResponse(response);
1283
+ return [];
1284
+ }
1285
+ return parsePostgRESTResponse(response);
1011
1286
  }
1012
1287
  };
1013
1288
  var PostgrestUpsertReturningBuilder = class {
@@ -1020,8 +1295,7 @@ var PostgrestUpsertReturningBuilder = class {
1020
1295
  }
1021
1296
  async executeMany() {
1022
1297
  return asPostgrestResponse(async () => {
1023
- const res = await this.root.runWithReturning(this.returning);
1024
- return res.rows;
1298
+ return this.root.runUpsert("return=representation", this.returning);
1025
1299
  });
1026
1300
  }
1027
1301
  async single() {
@@ -1030,9 +1304,11 @@ var PostgrestUpsertReturningBuilder = class {
1030
1304
  if (error) throw error;
1031
1305
  const rows = data ?? [];
1032
1306
  if (rows.length === 0 || rows.length > 1) {
1033
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
1034
- code: "PGRST116"
1035
- });
1307
+ throw new RagableError(
1308
+ "JSON object requested, multiple (or no) rows returned",
1309
+ 406,
1310
+ { code: "PGRST116" }
1311
+ );
1036
1312
  }
1037
1313
  return rows[0];
1038
1314
  });
@@ -1043,23 +1319,25 @@ var PostgrestUpsertReturningBuilder = class {
1043
1319
  if (error) throw error;
1044
1320
  const rows = data ?? [];
1045
1321
  if (rows.length > 1) {
1046
- throw new RagableError("JSON object requested, multiple (or no) rows returned", 406, {
1047
- code: "PGRST116"
1048
- });
1322
+ throw new RagableError(
1323
+ "JSON object requested, multiple (or no) rows returned",
1324
+ 406,
1325
+ { code: "PGRST116" }
1326
+ );
1049
1327
  }
1050
1328
  return rows[0] ?? null;
1051
1329
  });
1052
1330
  }
1053
1331
  };
1054
1332
  var PostgrestTableApi = class {
1055
- constructor(run, databaseInstanceId, table) {
1056
- this.run = run;
1333
+ constructor(pgFetch, databaseInstanceId, table) {
1334
+ this.pgFetch = pgFetch;
1057
1335
  this.databaseInstanceId = databaseInstanceId;
1058
1336
  this.table = table;
1059
1337
  }
1060
1338
  select(columns = "*") {
1061
1339
  return new PostgrestSelectBuilder(
1062
- this.run,
1340
+ this.pgFetch,
1063
1341
  this.databaseInstanceId,
1064
1342
  this.table,
1065
1343
  columns
@@ -1068,7 +1346,7 @@ var PostgrestTableApi = class {
1068
1346
  insert(values) {
1069
1347
  const rows = Array.isArray(values) ? values : [values];
1070
1348
  return new PostgrestInsertRootBuilder(
1071
- this.run,
1349
+ this.pgFetch,
1072
1350
  this.databaseInstanceId,
1073
1351
  this.table,
1074
1352
  rows
@@ -1076,7 +1354,7 @@ var PostgrestTableApi = class {
1076
1354
  }
1077
1355
  update(patch) {
1078
1356
  return new PostgrestUpdateRootBuilder(
1079
- this.run,
1357
+ this.pgFetch,
1080
1358
  this.databaseInstanceId,
1081
1359
  this.table,
1082
1360
  patch
@@ -1084,19 +1362,15 @@ var PostgrestTableApi = class {
1084
1362
  }
1085
1363
  delete() {
1086
1364
  return new PostgrestDeleteRootBuilder(
1087
- this.run,
1365
+ this.pgFetch,
1088
1366
  this.databaseInstanceId,
1089
1367
  this.table
1090
1368
  );
1091
1369
  }
1092
- /**
1093
- * `INSERT ... ON CONFLICT ... DO UPDATE` (or `DO NOTHING` with `ignoreDuplicates`).
1094
- * @see https://supabase.com/docs/reference/javascript/upsert
1095
- */
1096
1370
  upsert(values, options) {
1097
1371
  const rows = Array.isArray(values) ? values : [values];
1098
1372
  return new PostgrestUpsertRootBuilder(
1099
- this.run,
1373
+ this.pgFetch,
1100
1374
  this.databaseInstanceId,
1101
1375
  this.table,
1102
1376
  rows,
@@ -1106,57 +1380,139 @@ var PostgrestTableApi = class {
1106
1380
  }
1107
1381
  };
1108
1382
 
1109
- // src/browser.ts
1110
- function normalizeBrowserApiBase(baseUrl) {
1111
- const trimmed = (baseUrl ?? DEFAULT_RAGABLE_API_BASE).trim().replace(/\/+$/, "");
1112
- return trimmed.endsWith("/api") ? trimmed : `${trimmed}/api`;
1113
- }
1114
- function requireAuthGroupId(options) {
1115
- const id = options.authGroupId?.trim();
1116
- if (!id) {
1117
- throw new Error(
1118
- "authGroupId is required for auth and database methods on the browser client"
1119
- );
1383
+ // src/auth-storage.ts
1384
+ var LocalStorageAdapter = class {
1385
+ getItem(key) {
1386
+ try {
1387
+ return globalThis.localStorage.getItem(key);
1388
+ } catch {
1389
+ return null;
1390
+ }
1120
1391
  }
1121
- return id;
1122
- }
1123
- async function requireAccessToken(options) {
1124
- const getter = options.getAccessToken;
1125
- if (!getter) {
1126
- throw new Error(
1127
- "getAccessToken is required for this call (return the end-user access JWT)"
1128
- );
1392
+ setItem(key, value) {
1393
+ try {
1394
+ globalThis.localStorage.setItem(key, value);
1395
+ } catch {
1396
+ }
1129
1397
  }
1130
- const token = await getter();
1131
- if (!token?.trim()) {
1132
- throw new Error("getAccessToken returned no token");
1398
+ removeItem(key) {
1399
+ try {
1400
+ globalThis.localStorage.removeItem(key);
1401
+ } catch {
1402
+ }
1133
1403
  }
1134
- return token.trim();
1135
- }
1136
- async function resolveDatabaseAuthBearer(options) {
1137
- const mode = options.dataAuth ?? "user";
1138
- if (mode === "user") {
1139
- return requireAccessToken(options);
1404
+ };
1405
+ var SessionStorageAdapter = class {
1406
+ getItem(key) {
1407
+ try {
1408
+ return globalThis.sessionStorage.getItem(key);
1409
+ } catch {
1410
+ return null;
1411
+ }
1140
1412
  }
1141
- const fromGetter = options.getDataStaticKey ? await options.getDataStaticKey() : null;
1142
- const key = (fromGetter?.trim() || options.dataStaticKey?.trim()) ?? "";
1143
- if (!key) {
1144
- throw new Error(
1145
- mode === "publicAnon" ? "dataAuth publicAnon requires getDataStaticKey or dataStaticKey (rotate key in dashboard)" : "dataAuth admin requires getDataStaticKey or dataStaticKey (server-side only; rotate in dashboard)"
1146
- );
1413
+ setItem(key, value) {
1414
+ try {
1415
+ globalThis.sessionStorage.setItem(key, value);
1416
+ } catch {
1417
+ }
1147
1418
  }
1148
- return key;
1149
- }
1150
- async function parseJsonOrThrow(response) {
1151
- const payload = await parseMaybeJsonBody(response);
1152
- if (!response.ok) {
1153
- const message = extractErrorMessage(payload, response.statusText);
1154
- throw new RagableError(message, response.status, payload);
1419
+ removeItem(key) {
1420
+ try {
1421
+ globalThis.sessionStorage.removeItem(key);
1422
+ } catch {
1423
+ }
1155
1424
  }
1156
- return payload;
1425
+ };
1426
+ var MemoryStorageAdapter = class {
1427
+ constructor() {
1428
+ __publicField(this, "store", /* @__PURE__ */ new Map());
1429
+ }
1430
+ getItem(key) {
1431
+ return this.store.get(key) ?? null;
1432
+ }
1433
+ setItem(key, value) {
1434
+ this.store.set(key, value);
1435
+ }
1436
+ removeItem(key) {
1437
+ this.store.delete(key);
1438
+ }
1439
+ };
1440
+ var CookieStorageAdapter = class {
1441
+ constructor(maxAge = 30 * 24 * 60 * 60, path = "/", sameSite = "Lax") {
1442
+ this.maxAge = maxAge;
1443
+ this.path = path;
1444
+ this.sameSite = sameSite;
1445
+ }
1446
+ getItem(key) {
1447
+ if (typeof document === "undefined") return null;
1448
+ const match = document.cookie.split("; ").find((c) => c.startsWith(`${encodeURIComponent(key)}=`));
1449
+ if (!match) return null;
1450
+ return decodeURIComponent(match.split("=").slice(1).join("="));
1451
+ }
1452
+ setItem(key, value) {
1453
+ if (typeof document === "undefined") return;
1454
+ const parts = [
1455
+ `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
1456
+ `path=${this.path}`,
1457
+ `max-age=${this.maxAge}`,
1458
+ `SameSite=${this.sameSite}`
1459
+ ];
1460
+ if (this.sameSite === "None") parts.push("Secure");
1461
+ document.cookie = parts.join("; ");
1462
+ }
1463
+ removeItem(key) {
1464
+ if (typeof document === "undefined") return;
1465
+ document.cookie = `${encodeURIComponent(key)}=; path=${this.path}; max-age=0`;
1466
+ }
1467
+ };
1468
+ function detectStorage() {
1469
+ if (typeof globalThis !== "undefined" && typeof globalThis.localStorage !== "undefined") {
1470
+ try {
1471
+ const testKey = "__ragable_test__";
1472
+ globalThis.localStorage.setItem(testKey, "1");
1473
+ globalThis.localStorage.removeItem(testKey);
1474
+ return new LocalStorageAdapter();
1475
+ } catch {
1476
+ }
1477
+ }
1478
+ return new MemoryStorageAdapter();
1157
1479
  }
1158
- function parseExpiresInSeconds(expiresIn) {
1159
- const s = expiresIn.trim().toLowerCase();
1480
+ var AuthBroadcastChannel = class {
1481
+ constructor(channelName) {
1482
+ __publicField(this, "channel", null);
1483
+ if (typeof BroadcastChannel !== "undefined") {
1484
+ try {
1485
+ this.channel = new BroadcastChannel(channelName);
1486
+ } catch {
1487
+ }
1488
+ }
1489
+ }
1490
+ onMessage(cb) {
1491
+ if (this.channel) {
1492
+ this.channel.onmessage = (e) => {
1493
+ const data = e.data;
1494
+ if (data && typeof data.type === "string") {
1495
+ cb(data);
1496
+ }
1497
+ };
1498
+ }
1499
+ }
1500
+ postSessionUpdated(serialized) {
1501
+ this.channel?.postMessage({ type: "SESSION_UPDATED", payload: serialized });
1502
+ }
1503
+ postSessionRemoved() {
1504
+ this.channel?.postMessage({ type: "SESSION_REMOVED" });
1505
+ }
1506
+ close() {
1507
+ this.channel?.close();
1508
+ this.channel = null;
1509
+ }
1510
+ };
1511
+
1512
+ // src/auth.ts
1513
+ function parseExpiresInSeconds(raw) {
1514
+ if (typeof raw === "number") return raw;
1515
+ const s = raw.trim().toLowerCase();
1160
1516
  const m = /^(\d+)([smhd])?$/.exec(s);
1161
1517
  if (m) {
1162
1518
  const n = Number(m[1]);
@@ -1167,147 +1523,541 @@ function parseExpiresInSeconds(expiresIn) {
1167
1523
  const asNum = Number(s);
1168
1524
  return Number.isFinite(asNum) ? asNum : 0;
1169
1525
  }
1170
- function toSupabaseSession(s) {
1171
- return {
1172
- access_token: s.accessToken,
1173
- refresh_token: s.refreshToken,
1174
- expires_in: parseExpiresInSeconds(s.expiresIn),
1175
- token_type: "bearer",
1176
- user: s.user
1177
- };
1178
- }
1179
- var RagableBrowserAuthClient = class {
1180
- constructor(options) {
1181
- this.options = options;
1182
- __publicField(this, "fetchImpl");
1183
- this.fetchImpl = bindFetch(options.fetch);
1526
+ async function parseJsonOrThrow(response) {
1527
+ const text = await response.text();
1528
+ let payload;
1529
+ try {
1530
+ payload = text ? JSON.parse(text) : null;
1531
+ } catch {
1532
+ throw new RagableError(`Response parse error: ${text.slice(0, 200)}`, response.status, null);
1184
1533
  }
1185
- toUrl(path) {
1186
- return `${normalizeBrowserApiBase(this.options.baseUrl)}${path.startsWith("/") ? path : `/${path}`}`;
1534
+ if (!response.ok) {
1535
+ const message = extractErrorMessage(payload, response.statusText);
1536
+ throw new RagableError(message, response.status, payload);
1187
1537
  }
1188
- baseHeaders(json) {
1189
- const h = new Headers(this.options.headers);
1190
- if (json) {
1191
- h.set("Content-Type", "application/json");
1538
+ return payload;
1539
+ }
1540
+ var _subCounter = 0;
1541
+ var RagableAuth = class {
1542
+ constructor(config) {
1543
+ __publicField(this, "fetchImpl");
1544
+ __publicField(this, "baseUrl");
1545
+ __publicField(this, "authGroupId");
1546
+ __publicField(this, "defaultHeaders");
1547
+ __publicField(this, "persistSession");
1548
+ __publicField(this, "autoRefreshToken");
1549
+ __publicField(this, "storage");
1550
+ __publicField(this, "storageKey");
1551
+ __publicField(this, "refreshSkewSeconds");
1552
+ __publicField(this, "debug");
1553
+ __publicField(this, "currentSession", null);
1554
+ __publicField(this, "refreshTimer", null);
1555
+ __publicField(this, "refreshPromise", null);
1556
+ __publicField(this, "listeners", /* @__PURE__ */ new Map());
1557
+ __publicField(this, "broadcast", null);
1558
+ __publicField(this, "visibilityHandler", null);
1559
+ __publicField(this, "initialized", false);
1560
+ this.baseUrl = config.baseUrl;
1561
+ this.authGroupId = config.authGroupId;
1562
+ this.fetchImpl = bindFetch(config.fetch);
1563
+ this.defaultHeaders = config.headers;
1564
+ const auth = config.auth ?? {};
1565
+ this.persistSession = auth.persistSession !== false;
1566
+ this.autoRefreshToken = auth.autoRefreshToken !== false;
1567
+ this.storage = auth.storage ?? detectStorage();
1568
+ this.storageKey = auth.storageKey ?? `ragable.session.${this.authGroupId}`;
1569
+ this.refreshSkewSeconds = auth.refreshSkewSeconds ?? 60;
1570
+ this.debug = auth.debug ?? false;
1571
+ this.broadcast = new AuthBroadcastChannel(`ragable-auth-${this.authGroupId}`);
1572
+ this.broadcast.onMessage((msg) => {
1573
+ if (msg.type === "SESSION_UPDATED") {
1574
+ try {
1575
+ const session = JSON.parse(msg.payload);
1576
+ this.currentSession = session;
1577
+ this.scheduleRefresh(session);
1578
+ this.emit("TOKEN_REFRESHED", session);
1579
+ } catch {
1580
+ }
1581
+ } else if (msg.type === "SESSION_REMOVED") {
1582
+ this.currentSession = null;
1583
+ this.clearRefreshTimer();
1584
+ this.emit("SIGNED_OUT", null);
1585
+ }
1586
+ });
1587
+ this.setupVisibilityListener();
1588
+ }
1589
+ log(...args) {
1590
+ if (this.debug) console.debug("[RagableAuth]", ...args);
1591
+ }
1592
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
1593
+ async initialize() {
1594
+ if (this.initialized) return this.currentSession;
1595
+ this.initialized = true;
1596
+ if (this.persistSession) {
1597
+ try {
1598
+ const raw = await this.storage.getItem(this.storageKey);
1599
+ if (raw) {
1600
+ const session = JSON.parse(raw);
1601
+ if (session.expires_at && session.expires_at > nowSeconds()) {
1602
+ this.currentSession = session;
1603
+ this.scheduleRefresh(session);
1604
+ this.log("Restored session from storage");
1605
+ } else if (session.refresh_token) {
1606
+ this.log("Stored session expired, attempting refresh");
1607
+ const refreshed = await this._doRefresh(session.refresh_token);
1608
+ if (refreshed) {
1609
+ this.currentSession = refreshed;
1610
+ }
1611
+ }
1612
+ }
1613
+ } catch (e) {
1614
+ this.log("Failed to restore session", e);
1615
+ }
1192
1616
  }
1193
- return h;
1617
+ this.emit("INITIAL_SESSION", this.currentSession);
1618
+ return this.currentSession;
1194
1619
  }
1195
- authPrefix() {
1196
- const gid = requireAuthGroupId(this.options);
1197
- return `/auth-groups/${gid}/auth`;
1198
- }
1199
- /** Supabase: `signUp` → `{ data: { user, session }, error }` */
1620
+ // ── Auth methods ───────────────────────────────────────────────────────────
1200
1621
  async signUp(credentials) {
1201
1622
  return asPostgrestResponse(async () => {
1202
1623
  const name = typeof credentials.options?.data?.name === "string" ? credentials.options.data.name : void 0;
1203
- const session = await this.register({
1624
+ const raw = await this.fetchAuth("/register", "POST", {
1204
1625
  email: credentials.email,
1205
1626
  password: credentials.password,
1206
- name
1627
+ ...name !== void 0 ? { name } : {}
1207
1628
  });
1208
- return { user: session.user, session: toSupabaseSession(session) };
1629
+ const session = this.rawToSession(raw);
1630
+ await this.setSessionInternal(session, "SIGNED_IN");
1631
+ return { user: session.user, session };
1209
1632
  });
1210
1633
  }
1211
- /** Supabase: `signInWithPassword` */
1212
1634
  async signInWithPassword(credentials) {
1213
1635
  return asPostgrestResponse(async () => {
1214
- const session = await this.login(credentials);
1215
- return { user: session.user, session: toSupabaseSession(session) };
1636
+ const raw = await this.fetchAuth("/login", "POST", {
1637
+ email: credentials.email,
1638
+ password: credentials.password
1639
+ });
1640
+ const session = this.rawToSession(raw);
1641
+ await this.setSessionInternal(session, "SIGNED_IN");
1642
+ return { user: session.user, session };
1216
1643
  });
1217
1644
  }
1218
- /** Supabase: `refreshSession` */
1645
+ async signOut(_options) {
1646
+ this.currentSession = null;
1647
+ this.clearRefreshTimer();
1648
+ if (this.persistSession) {
1649
+ await this.storage.removeItem(this.storageKey);
1650
+ }
1651
+ this.broadcast?.postSessionRemoved();
1652
+ this.emit("SIGNED_OUT", null);
1653
+ return { error: null };
1654
+ }
1219
1655
  async refreshSession(refreshToken) {
1220
1656
  return asPostgrestResponse(async () => {
1221
- const tokens = await this.refresh({ refreshToken });
1222
- const me = await this.getUserFromToken(tokens.accessToken);
1657
+ const token = refreshToken ?? this.currentSession?.refresh_token;
1658
+ if (!token) throw new RagableError("No refresh token available", 401, null);
1659
+ const session = await this.singleFlightRefresh(token);
1660
+ if (!session) throw new RagableError("Refresh failed", 401, null);
1661
+ return { session, user: session.user };
1662
+ });
1663
+ }
1664
+ async getSession() {
1665
+ if (!this.initialized) await this.initialize();
1666
+ return { data: { session: this.currentSession }, error: null };
1667
+ }
1668
+ async getUser() {
1669
+ return asPostgrestResponse(async () => {
1670
+ const token = this.currentSession?.access_token;
1671
+ if (!token) throw new RagableError("Not authenticated", 401, null);
1672
+ return this.fetchAuthWithBearer("/me", "GET", token);
1673
+ });
1674
+ }
1675
+ async setSession(tokens) {
1676
+ return asPostgrestResponse(async () => {
1677
+ const me = await this.fetchAuthWithBearer("/me", "GET", tokens.access_token);
1678
+ const decoded = decodeJwtExpiry(tokens.access_token);
1679
+ const expiresIn = decoded ? decoded - nowSeconds() : 3600;
1223
1680
  const session = {
1224
- access_token: tokens.accessToken,
1225
- refresh_token: tokens.refreshToken,
1226
- expires_in: parseExpiresInSeconds(tokens.expiresIn),
1681
+ access_token: tokens.access_token,
1682
+ refresh_token: tokens.refresh_token,
1683
+ expires_in: expiresIn,
1684
+ expires_at: nowSeconds() + expiresIn,
1227
1685
  token_type: "bearer",
1228
1686
  user: me.user
1229
1687
  };
1688
+ await this.setSessionInternal(session, "SIGNED_IN");
1230
1689
  return { session, user: me.user };
1231
1690
  });
1232
1691
  }
1233
- /** Supabase: `getUser()` — needs `getAccessToken` on the client */
1234
- async getUser() {
1235
- return asPostgrestResponse(() => this.getMe());
1236
- }
1237
- /** Supabase: `updateUser` */
1238
1692
  async updateUser(attributes) {
1239
- return asPostgrestResponse(
1240
- () => this.updateMe({
1241
- password: attributes.password,
1242
- name: attributes.data?.name
1243
- })
1244
- );
1693
+ return asPostgrestResponse(async () => {
1694
+ const token = this.currentSession?.access_token;
1695
+ if (!token) throw new RagableError("Not authenticated", 401, null);
1696
+ const result = await this.fetchAuthWithBearer("/me", "PATCH", token, {
1697
+ ...attributes.password !== void 0 ? { password: attributes.password } : {},
1698
+ ...attributes.data?.name !== void 0 ? { name: attributes.data.name } : {}
1699
+ });
1700
+ if (this.currentSession) {
1701
+ this.currentSession = { ...this.currentSession, user: result.user };
1702
+ await this.persistCurrentSession();
1703
+ }
1704
+ this.emit("USER_UPDATED", this.currentSession);
1705
+ return result;
1706
+ });
1245
1707
  }
1246
- /**
1247
- * Supabase/Firebase: no server call — clear tokens in your app.
1248
- * Returns `{ error: null }` for API compatibility.
1249
- */
1250
- async signOut(_options) {
1251
- return { error: null };
1708
+ // ── Event subscription ─────────────────────────────────────────────────────
1709
+ onAuthStateChange(callback) {
1710
+ _subCounter++;
1711
+ const id = `sub-${_subCounter}`;
1712
+ this.listeners.set(id, callback);
1713
+ const unsubscribe = () => {
1714
+ this.listeners.delete(id);
1715
+ };
1716
+ return { data: { subscription: { id, unsubscribe } } };
1252
1717
  }
1253
- async getUserFromToken(accessToken) {
1254
- const headers = this.baseHeaders(false);
1255
- headers.set("Authorization", `Bearer ${accessToken}`);
1256
- const response = await this.fetchImpl(`${this.toUrl(this.authPrefix())}/me`, {
1257
- method: "GET",
1258
- headers
1259
- });
1260
- return parseJsonOrThrow(response);
1718
+ // ── Accessors ──────────────────────────────────────────────────────────────
1719
+ getAccessToken() {
1720
+ return this.currentSession?.access_token ?? null;
1261
1721
  }
1722
+ getCurrentSession() {
1723
+ return this.currentSession;
1724
+ }
1725
+ // ── Back-compat: raw Ragable auth methods ──────────────────────────────────
1262
1726
  async register(body) {
1263
- const response = await this.fetchImpl(`${this.toUrl(this.authPrefix())}/register`, {
1264
- method: "POST",
1265
- headers: this.baseHeaders(true),
1266
- body: JSON.stringify(body)
1267
- });
1268
- return parseJsonOrThrow(response);
1727
+ const raw = await this.fetchAuth("/register", "POST", body);
1728
+ const session = this.rawToSession(raw);
1729
+ await this.setSessionInternal(session, "SIGNED_IN");
1730
+ return raw;
1269
1731
  }
1270
1732
  async login(body) {
1271
- const response = await this.fetchImpl(`${this.toUrl(this.authPrefix())}/login`, {
1272
- method: "POST",
1273
- headers: this.baseHeaders(true),
1274
- body: JSON.stringify(body)
1275
- });
1276
- return parseJsonOrThrow(response);
1733
+ const raw = await this.fetchAuth("/login", "POST", body);
1734
+ const session = this.rawToSession(raw);
1735
+ await this.setSessionInternal(session, "SIGNED_IN");
1736
+ return raw;
1277
1737
  }
1278
1738
  async refresh(body) {
1279
- const response = await this.fetchImpl(`${this.toUrl(this.authPrefix())}/refresh`, {
1280
- method: "POST",
1281
- headers: this.baseHeaders(true),
1282
- body: JSON.stringify(body)
1283
- });
1284
- return parseJsonOrThrow(response);
1739
+ return this.fetchAuth("/refresh", "POST", body);
1285
1740
  }
1286
1741
  async getMe() {
1287
- const token = await requireAccessToken(this.options);
1288
- const headers = this.baseHeaders(false);
1289
- headers.set("Authorization", `Bearer ${token}`);
1290
- const response = await this.fetchImpl(`${this.toUrl(this.authPrefix())}/me`, {
1291
- method: "GET",
1292
- headers
1742
+ const token = this.currentSession?.access_token;
1743
+ if (!token) throw new RagableError("Not authenticated", 401, null);
1744
+ return this.fetchAuthWithBearer("/me", "GET", token);
1745
+ }
1746
+ async updateMe(body) {
1747
+ const token = this.currentSession?.access_token;
1748
+ if (!token) throw new RagableError("Not authenticated", 401, null);
1749
+ const result = await this.fetchAuthWithBearer("/me", "PATCH", token, body);
1750
+ if (this.currentSession) {
1751
+ this.currentSession = { ...this.currentSession, user: result.user };
1752
+ await this.persistCurrentSession();
1753
+ }
1754
+ this.emit("USER_UPDATED", this.currentSession);
1755
+ return result;
1756
+ }
1757
+ // ── Cleanup ────────────────────────────────────────────────────────────────
1758
+ destroy() {
1759
+ this.clearRefreshTimer();
1760
+ this.broadcast?.close();
1761
+ this.listeners.clear();
1762
+ if (this.visibilityHandler && typeof document !== "undefined") {
1763
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
1764
+ }
1765
+ }
1766
+ // ─── Internal ──────────────────────────────────────────────────────────────
1767
+ authPrefix() {
1768
+ return `/auth-groups/${this.authGroupId}/auth`;
1769
+ }
1770
+ toUrl(path) {
1771
+ return `${this.baseUrl}${this.authPrefix()}${path}`;
1772
+ }
1773
+ baseHeaders(json) {
1774
+ const h = new Headers(this.defaultHeaders);
1775
+ if (json) h.set("Content-Type", "application/json");
1776
+ return h;
1777
+ }
1778
+ async fetchAuth(path, method, body) {
1779
+ const headers = this.baseHeaders(body !== void 0);
1780
+ const response = await this.fetchImpl(this.toUrl(path), {
1781
+ method,
1782
+ headers,
1783
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1293
1784
  });
1294
1785
  return parseJsonOrThrow(response);
1295
1786
  }
1296
- async updateMe(body) {
1297
- const token = await requireAccessToken(this.options);
1298
- const headers = this.baseHeaders(true);
1787
+ async fetchAuthWithBearer(path, method, token, body) {
1788
+ const headers = this.baseHeaders(body !== void 0);
1299
1789
  headers.set("Authorization", `Bearer ${token}`);
1300
- const response = await this.fetchImpl(`${this.toUrl(this.authPrefix())}/me`, {
1301
- method: "PATCH",
1790
+ const response = await this.fetchImpl(this.toUrl(path), {
1791
+ method,
1302
1792
  headers,
1303
- body: JSON.stringify(body)
1793
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1304
1794
  });
1305
1795
  return parseJsonOrThrow(response);
1306
1796
  }
1797
+ rawToSession(raw) {
1798
+ const expiresIn = parseExpiresInSeconds(raw.expiresIn);
1799
+ return {
1800
+ access_token: raw.accessToken,
1801
+ refresh_token: raw.refreshToken,
1802
+ expires_in: expiresIn,
1803
+ expires_at: nowSeconds() + expiresIn,
1804
+ token_type: "bearer",
1805
+ user: raw.user
1806
+ };
1807
+ }
1808
+ async setSessionInternal(session, event) {
1809
+ this.currentSession = session;
1810
+ await this.persistCurrentSession();
1811
+ this.broadcast?.postSessionUpdated(JSON.stringify(session));
1812
+ this.scheduleRefresh(session);
1813
+ this.emit(event, session);
1814
+ }
1815
+ async persistCurrentSession() {
1816
+ if (!this.persistSession || !this.currentSession) return;
1817
+ try {
1818
+ await this.storage.setItem(this.storageKey, JSON.stringify(this.currentSession));
1819
+ } catch (e) {
1820
+ this.log("Failed to persist session", e);
1821
+ }
1822
+ }
1823
+ emit(event, session) {
1824
+ this.log(event, session?.user);
1825
+ for (const cb of this.listeners.values()) {
1826
+ try {
1827
+ cb(event, session);
1828
+ } catch (e) {
1829
+ this.log("Listener threw", e);
1830
+ }
1831
+ }
1832
+ }
1833
+ // ─── Refresh scheduling ────────────────────────────────────────────────────
1834
+ scheduleRefresh(session) {
1835
+ this.clearRefreshTimer();
1836
+ if (!this.autoRefreshToken) return;
1837
+ const secondsUntilExpiry = session.expires_at - nowSeconds();
1838
+ const refreshIn = Math.max(0, secondsUntilExpiry - this.refreshSkewSeconds);
1839
+ this.log(`Scheduling refresh in ${refreshIn}s`);
1840
+ this.refreshTimer = setTimeout(() => {
1841
+ this.singleFlightRefresh(session.refresh_token).catch((e) => {
1842
+ this.log("Scheduled refresh failed", e);
1843
+ });
1844
+ }, refreshIn * 1e3);
1845
+ }
1846
+ clearRefreshTimer() {
1847
+ if (this.refreshTimer !== null) {
1848
+ clearTimeout(this.refreshTimer);
1849
+ this.refreshTimer = null;
1850
+ }
1851
+ }
1852
+ async singleFlightRefresh(refreshToken) {
1853
+ if (this.refreshPromise) return this.refreshPromise;
1854
+ this.refreshPromise = this._doRefresh(refreshToken).finally(() => {
1855
+ this.refreshPromise = null;
1856
+ });
1857
+ return this.refreshPromise;
1858
+ }
1859
+ async _doRefresh(refreshToken) {
1860
+ try {
1861
+ const raw = await this.fetchAuth("/refresh", "POST", { refreshToken });
1862
+ const me = await this.fetchAuthWithBearer("/me", "GET", raw.accessToken);
1863
+ const expiresIn = parseExpiresInSeconds(raw.expiresIn);
1864
+ const session = {
1865
+ access_token: raw.accessToken,
1866
+ refresh_token: raw.refreshToken,
1867
+ expires_in: expiresIn,
1868
+ expires_at: nowSeconds() + expiresIn,
1869
+ token_type: "bearer",
1870
+ user: me.user
1871
+ };
1872
+ await this.setSessionInternal(session, "TOKEN_REFRESHED");
1873
+ return session;
1874
+ } catch (e) {
1875
+ this.log("Refresh failed", e);
1876
+ return null;
1877
+ }
1878
+ }
1879
+ // ─── Visibility listener ───────────────────────────────────────────────────
1880
+ setupVisibilityListener() {
1881
+ if (typeof document === "undefined") return;
1882
+ this.visibilityHandler = () => {
1883
+ if (document.visibilityState === "visible" && this.currentSession) {
1884
+ const secondsUntilExpiry = this.currentSession.expires_at - nowSeconds();
1885
+ if (secondsUntilExpiry <= this.refreshSkewSeconds) {
1886
+ this.singleFlightRefresh(this.currentSession.refresh_token).catch(() => {
1887
+ });
1888
+ } else {
1889
+ this.scheduleRefresh(this.currentSession);
1890
+ }
1891
+ } else {
1892
+ this.clearRefreshTimer();
1893
+ }
1894
+ };
1895
+ document.addEventListener("visibilitychange", this.visibilityHandler);
1896
+ }
1897
+ };
1898
+ function nowSeconds() {
1899
+ return Math.floor(Date.now() / 1e3);
1900
+ }
1901
+ function decodeJwtExpiry(jwt) {
1902
+ try {
1903
+ const parts = jwt.split(".");
1904
+ if (parts.length !== 3) return null;
1905
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
1906
+ return typeof payload.exp === "number" ? payload.exp : null;
1907
+ } catch {
1908
+ return null;
1909
+ }
1910
+ }
1911
+
1912
+ // src/browser.ts
1913
+ function normalizeBrowserApiBase(baseUrl) {
1914
+ const trimmed = (baseUrl ?? DEFAULT_RAGABLE_API_BASE).trim().replace(/\/+$/, "");
1915
+ return trimmed.endsWith("/api") ? trimmed : `${trimmed}/api`;
1916
+ }
1917
+ function requireAuthGroupId(options) {
1918
+ const id = options.authGroupId?.trim();
1919
+ if (!id) {
1920
+ throw new Error(
1921
+ "authGroupId is required for auth and database methods on the browser client"
1922
+ );
1923
+ }
1924
+ return id;
1925
+ }
1926
+ async function requireAccessToken(options, ragableAuth) {
1927
+ if (ragableAuth) {
1928
+ const token = ragableAuth.getAccessToken();
1929
+ if (token) return token;
1930
+ }
1931
+ const getter = options.getAccessToken;
1932
+ if (getter) {
1933
+ const token = await getter();
1934
+ if (token?.trim()) return token.trim();
1935
+ }
1936
+ throw new Error(
1937
+ "No access token available (sign in or provide getAccessToken)"
1938
+ );
1939
+ }
1940
+ async function resolveDatabaseAuthBearer(options, ragableAuth) {
1941
+ const mode = options.dataAuth ?? "user";
1942
+ if (mode === "user") {
1943
+ return requireAccessToken(options, ragableAuth);
1944
+ }
1945
+ const fromGetter = options.getDataStaticKey ? await options.getDataStaticKey() : null;
1946
+ const key = (fromGetter?.trim() || options.dataStaticKey?.trim()) ?? "";
1947
+ if (!key) {
1948
+ throw new Error(
1949
+ mode === "publicAnon" ? "dataAuth publicAnon requires getDataStaticKey or dataStaticKey" : "dataAuth admin requires getDataStaticKey or dataStaticKey"
1950
+ );
1951
+ }
1952
+ return key;
1953
+ }
1954
+ async function parseJsonOrThrow2(response) {
1955
+ const payload = await parseMaybeJsonBody(response);
1956
+ if (!response.ok) {
1957
+ const message = extractErrorMessage(payload, response.statusText);
1958
+ throw new RagableError(message, response.status, payload);
1959
+ }
1960
+ return payload;
1961
+ }
1962
+ var RagableBrowserAuthClient = class {
1963
+ constructor(_options, ragableAuth = null) {
1964
+ this.ragableAuth = ragableAuth;
1965
+ }
1966
+ get auth() {
1967
+ if (!this.ragableAuth) {
1968
+ throw new Error("Auth not initialized \u2014 provide authGroupId to enable auth");
1969
+ }
1970
+ return this.ragableAuth;
1971
+ }
1972
+ async signUp(credentials) {
1973
+ const result = await this.auth.signUp(credentials);
1974
+ if (result.error) return { data: null, error: result.error };
1975
+ const session = result.data.session;
1976
+ return {
1977
+ data: {
1978
+ user: session.user,
1979
+ session: {
1980
+ access_token: session.access_token,
1981
+ refresh_token: session.refresh_token,
1982
+ expires_in: session.expires_in,
1983
+ token_type: "bearer",
1984
+ user: session.user
1985
+ }
1986
+ },
1987
+ error: null
1988
+ };
1989
+ }
1990
+ async signInWithPassword(credentials) {
1991
+ const result = await this.auth.signInWithPassword(credentials);
1992
+ if (result.error) return { data: null, error: result.error };
1993
+ const session = result.data.session;
1994
+ return {
1995
+ data: {
1996
+ user: session.user,
1997
+ session: {
1998
+ access_token: session.access_token,
1999
+ refresh_token: session.refresh_token,
2000
+ expires_in: session.expires_in,
2001
+ token_type: "bearer",
2002
+ user: session.user
2003
+ }
2004
+ },
2005
+ error: null
2006
+ };
2007
+ }
2008
+ async refreshSession(refreshToken) {
2009
+ const result = await this.auth.refreshSession(refreshToken);
2010
+ if (result.error) return { data: null, error: result.error };
2011
+ const session = result.data.session;
2012
+ return {
2013
+ data: {
2014
+ user: session.user,
2015
+ session: {
2016
+ access_token: session.access_token,
2017
+ refresh_token: session.refresh_token,
2018
+ expires_in: session.expires_in,
2019
+ token_type: "bearer",
2020
+ user: session.user
2021
+ }
2022
+ },
2023
+ error: null
2024
+ };
2025
+ }
2026
+ async getUser() {
2027
+ return this.auth.getUser();
2028
+ }
2029
+ async updateUser(attributes) {
2030
+ return this.auth.updateUser(attributes);
2031
+ }
2032
+ async signOut(_options) {
2033
+ return this.auth.signOut(_options);
2034
+ }
2035
+ async register(body) {
2036
+ return this.auth.register(body);
2037
+ }
2038
+ async login(body) {
2039
+ return this.auth.login(body);
2040
+ }
2041
+ async refresh(body) {
2042
+ return this.auth.refresh(body);
2043
+ }
2044
+ async getMe() {
2045
+ return this.auth.getMe();
2046
+ }
2047
+ async updateMe(body) {
2048
+ return this.auth.updateMe(body);
2049
+ }
2050
+ onAuthStateChange(callback) {
2051
+ return this.auth.onAuthStateChange(callback);
2052
+ }
2053
+ getSession() {
2054
+ return this.auth.getSession();
2055
+ }
1307
2056
  };
1308
2057
  var RagableBrowserDatabaseClient = class {
1309
- constructor(options) {
2058
+ constructor(options, ragableAuth = null) {
1310
2059
  this.options = options;
2060
+ this.ragableAuth = ragableAuth;
1311
2061
  __publicField(this, "fetchImpl");
1312
2062
  this.fetchImpl = bindFetch(options.fetch);
1313
2063
  }
@@ -1316,7 +2066,7 @@ var RagableBrowserDatabaseClient = class {
1316
2066
  }
1317
2067
  async query(params) {
1318
2068
  const gid = requireAuthGroupId(this.options);
1319
- const token = await resolveDatabaseAuthBearer(this.options);
2069
+ const token = await resolveDatabaseAuthBearer(this.options, this.ragableAuth);
1320
2070
  const databaseInstanceId = params.databaseInstanceId?.trim() || this.options.databaseInstanceId?.trim();
1321
2071
  if (!databaseInstanceId) {
1322
2072
  throw new Error(
@@ -1342,7 +2092,7 @@ var RagableBrowserDatabaseClient = class {
1342
2092
  })
1343
2093
  }
1344
2094
  );
1345
- return parseJsonOrThrow(response);
2095
+ return parseJsonOrThrow2(response);
1346
2096
  }
1347
2097
  baseHeaders() {
1348
2098
  return new Headers(this.options.headers);
@@ -1357,9 +2107,6 @@ var RagableBrowserAgentsClient = class {
1357
2107
  toUrl(path) {
1358
2108
  return `${normalizeBrowserApiBase(this.options.baseUrl)}${path.startsWith("/") ? path : `/${path}`}`;
1359
2109
  }
1360
- /**
1361
- * Stream agent execution as SSE (`POST /public/organizations/:orgId/agents/:agentId/chat/stream`).
1362
- */
1363
2110
  async *chatStream(agentId, params) {
1364
2111
  const orgId = this.options.organizationId;
1365
2112
  const body = {
@@ -1395,16 +2142,40 @@ var RagableBrowser = class {
1395
2142
  __publicField(this, "agents");
1396
2143
  __publicField(this, "auth");
1397
2144
  __publicField(this, "database");
2145
+ __publicField(this, "transport");
1398
2146
  __publicField(this, "options");
2147
+ __publicField(this, "_ragableAuth");
1399
2148
  this.options = options;
2149
+ this.transport = new Transport({
2150
+ fetch: options.fetch,
2151
+ headers: options.headers,
2152
+ ...options.transport
2153
+ });
2154
+ if (options.authGroupId) {
2155
+ this._ragableAuth = new RagableAuth({
2156
+ baseUrl: normalizeBrowserApiBase(options.baseUrl),
2157
+ authGroupId: options.authGroupId,
2158
+ fetch: options.fetch,
2159
+ headers: options.headers,
2160
+ auth: options.auth
2161
+ });
2162
+ this.transport.setRefreshHandler(async () => {
2163
+ const session = await this._ragableAuth.singleFlightRefresh(
2164
+ this._ragableAuth.getCurrentSession()?.refresh_token ?? ""
2165
+ );
2166
+ return session?.access_token ?? null;
2167
+ });
2168
+ if (!options.getAccessToken) {
2169
+ this._ragableAuth.initialize().catch(() => {
2170
+ });
2171
+ }
2172
+ } else {
2173
+ this._ragableAuth = null;
2174
+ }
1400
2175
  this.agents = new RagableBrowserAgentsClient(options);
1401
- this.auth = new RagableBrowserAuthClient(options);
1402
- this.database = new RagableBrowserDatabaseClient(options);
2176
+ this.auth = new RagableBrowserAuthClient(options, this._ragableAuth);
2177
+ this.database = new RagableBrowserDatabaseClient(options, this._ragableAuth);
1403
2178
  }
1404
- /**
1405
- * Supabase-style table API: `.from('items').select().eq('id', 1).single()`.
1406
- * Pass `databaseInstanceId` here or set `databaseInstanceId` on the client options.
1407
- */
1408
2179
  from(table, databaseInstanceId) {
1409
2180
  const id = databaseInstanceId?.trim() || this.options.databaseInstanceId?.trim();
1410
2181
  if (!id) {
@@ -1412,8 +2183,40 @@ var RagableBrowser = class {
1412
2183
  "RagableBrowser.from() requires databaseInstanceId in client options or as the second argument"
1413
2184
  );
1414
2185
  }
1415
- const run = (p) => this.database.query(p);
1416
- return new PostgrestTableApi(run, id, table);
2186
+ const gid = requireAuthGroupId(this.options);
2187
+ const ragableAuth = this._ragableAuth;
2188
+ const opts = this.options;
2189
+ const pgFetch = async (params) => {
2190
+ const token = await resolveDatabaseAuthBearer(opts, ragableAuth);
2191
+ const baseUrl = normalizeBrowserApiBase(opts.baseUrl);
2192
+ const qs = params.searchParams.toString();
2193
+ const url = `${baseUrl}/auth-groups/${gid}/data/rest/${params.table}${qs ? `?${qs}` : ""}`;
2194
+ const headers = new Headers(opts.headers);
2195
+ headers.set("Authorization", `Bearer ${token}`);
2196
+ headers.set("X-Database-Instance-Id", params.databaseInstanceId);
2197
+ if (params.body !== void 0) {
2198
+ headers.set("Content-Type", "application/json");
2199
+ }
2200
+ if (params.headers) {
2201
+ for (const [k, v] of Object.entries(params.headers)) {
2202
+ headers.set(k, v);
2203
+ }
2204
+ }
2205
+ if (params.idempotencyKey) {
2206
+ headers.set("Idempotency-Key", params.idempotencyKey);
2207
+ }
2208
+ const fetchImpl = bindFetch(opts.fetch);
2209
+ return fetchImpl(url, {
2210
+ method: params.method,
2211
+ headers,
2212
+ body: params.body !== void 0 ? JSON.stringify(params.body) : void 0,
2213
+ signal: params.signal
2214
+ });
2215
+ };
2216
+ return new PostgrestTableApi(pgFetch, id, table);
2217
+ }
2218
+ destroy() {
2219
+ this._ragableAuth?.destroy();
1417
2220
  }
1418
2221
  };
1419
2222
  function createBrowserClient(options) {
@@ -1511,7 +2314,11 @@ function createRagableServerClient(options) {
1511
2314
  }
1512
2315
  export {
1513
2316
  AgentsClient,
2317
+ AuthBroadcastChannel,
2318
+ CookieStorageAdapter,
1514
2319
  DEFAULT_RAGABLE_API_BASE,
2320
+ LocalStorageAdapter,
2321
+ MemoryStorageAdapter,
1515
2322
  PostgrestDeleteReturningBuilder,
1516
2323
  PostgrestDeleteRootBuilder,
1517
2324
  PostgrestInsertReturningBuilder,
@@ -1523,13 +2330,20 @@ export {
1523
2330
  PostgrestUpsertReturningBuilder,
1524
2331
  PostgrestUpsertRootBuilder,
1525
2332
  Ragable,
2333
+ RagableAbortError,
2334
+ RagableAuth,
1526
2335
  RagableBrowser,
1527
2336
  RagableBrowserAgentsClient,
1528
2337
  RagableBrowserAuthClient,
1529
2338
  RagableBrowserDatabaseClient,
1530
2339
  RagableError,
2340
+ RagableNetworkError,
1531
2341
  RagableRequestClient,
2342
+ RagableSdkError,
2343
+ RagableTimeoutError,
2344
+ SessionStorageAdapter,
1532
2345
  ShiftClient,
2346
+ Transport,
1533
2347
  asPostgrestResponse,
1534
2348
  bindFetch,
1535
2349
  createBrowserClient,
@@ -1537,10 +2351,13 @@ export {
1537
2351
  createRagPipeline,
1538
2352
  createRagableBrowserClient,
1539
2353
  createRagableServerClient,
2354
+ detectStorage,
1540
2355
  extractErrorMessage,
1541
2356
  formatRetrievalContext,
2357
+ generateIdempotencyKey,
1542
2358
  normalizeBrowserApiBase,
1543
2359
  parseSseDataLine,
2360
+ parseTransportResponse,
1544
2361
  readSseStream
1545
2362
  };
1546
2363
  //# sourceMappingURL=index.mjs.map