@ragable/sdk 0.5.1 → 0.6.1

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