@persql/sdk 0.1.0 → 1.1.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.cjs CHANGED
@@ -22,18 +22,40 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ApprovalRequiredError: () => ApprovalRequiredError,
24
24
  PerSQL: () => PerSQL,
25
+ PerSQLApprovalRules: () => PerSQLApprovalRules,
25
26
  PerSQLApprovals: () => PerSQLApprovals,
26
- PerSQLBlob: () => PerSQLBlob,
27
27
  PerSQLBranches: () => PerSQLBranches,
28
28
  PerSQLDatabase: () => PerSQLDatabase,
29
29
  PerSQLError: () => PerSQLError,
30
30
  PerSQLProposals: () => PerSQLProposals,
31
- PerSQLVectors: () => PerSQLVectors,
32
- RateLimitError: () => RateLimitError
31
+ RateLimitError: () => RateLimitError,
32
+ SupportClient: () => SupportClient
33
33
  });
34
34
  module.exports = __toCommonJS(index_exports);
35
35
 
36
36
  // src/local.ts
37
+ function checkLocalExpect(e, r) {
38
+ const rows = r.rows.length;
39
+ if (e.rows != null && rows !== e.rows) {
40
+ return `expected rows=${e.rows}, got ${rows}`;
41
+ }
42
+ if (e.rowsAtLeast != null && rows < e.rowsAtLeast) {
43
+ return `expected rows>=${e.rowsAtLeast}, got ${rows}`;
44
+ }
45
+ if (e.rowsAtMost != null && rows > e.rowsAtMost) {
46
+ return `expected rows<=${e.rowsAtMost}, got ${rows}`;
47
+ }
48
+ if (e.rowsWritten != null && r.rowsWritten !== e.rowsWritten) {
49
+ return `expected rowsWritten=${e.rowsWritten}, got ${r.rowsWritten}`;
50
+ }
51
+ if (e.rowsWrittenAtLeast != null && r.rowsWritten < e.rowsWrittenAtLeast) {
52
+ return `expected rowsWritten>=${e.rowsWrittenAtLeast}, got ${r.rowsWritten}`;
53
+ }
54
+ if (e.rowsWrittenAtMost != null && r.rowsWritten > e.rowsWrittenAtMost) {
55
+ return `expected rowsWritten<=${e.rowsWrittenAtMost}, got ${r.rowsWritten}`;
56
+ }
57
+ return null;
58
+ }
37
59
  var PROPOSAL_TTL_DEFAULT_SEC = 600;
38
60
  var PROPOSAL_TTL_MAX_SEC = 3600;
39
61
  function newLocalProposalId() {
@@ -72,7 +94,16 @@ var LocalDriver = class {
72
94
  }
73
95
  async batch(statements, transaction) {
74
96
  const db = await this.open();
75
- const exec = () => statements.map((s) => runOne(db, s.sql, s.params ?? []));
97
+ const exec = () => statements.map((s, i) => {
98
+ const r = runOne(db, s.sql, s.params ?? []);
99
+ if (s.expect) {
100
+ const violation = checkLocalExpect(s.expect, r);
101
+ if (violation) {
102
+ throw new Error(`Assertion failed on statement ${i}: ${violation}`);
103
+ }
104
+ }
105
+ return r;
106
+ });
76
107
  if (transaction) return db.transaction(exec)();
77
108
  return exec();
78
109
  }
@@ -234,6 +265,19 @@ var PerSQL = class _PerSQL {
234
265
  close() {
235
266
  this.local?.close();
236
267
  }
268
+ /**
269
+ * Resolve the bearer token's namespace + live usage. Use this once
270
+ * per agent run so you know the slug to put in URLs and how much
271
+ * monthly budget / prepaid balance remains before a 402.
272
+ *
273
+ * Throws in local mode — no server, nothing to ask.
274
+ */
275
+ async me() {
276
+ if (this.local) {
277
+ throw new Error("PerSQL: me() is not available in local mode.");
278
+ }
279
+ return this.request("GET", "/v1/me");
280
+ }
237
281
  /**
238
282
  * Redeem a handoff token (`phand_…`) for a regular PerSQL client
239
283
  * scoped to the handed-off (database, branch, role). Single use:
@@ -307,40 +351,74 @@ var PerSQL = class _PerSQL {
307
351
  }
308
352
  return envelope.data;
309
353
  }
310
- /** @internal — raw upload (e.g. blob PUT) returning the ApiResponse data. */
311
- async requestRaw(method, path, body, contentType) {
312
- const res = await this._fetch(`${this.baseURL}${path}`, {
313
- method,
314
- headers: {
315
- Authorization: `Bearer ${this.token}`,
316
- "Content-Type": contentType ?? "application/octet-stream"
317
- },
318
- body,
319
- // @ts-expect-error — duplex is required by Node fetch for streaming bodies
320
- duplex: "half"
321
- });
322
- if (res.status === 429) {
323
- const retryAfter = Number(res.headers.get("retry-after") ?? "60");
324
- throw new RateLimitError(retryAfter);
325
- }
326
- let envelope = null;
327
- try {
328
- envelope = await res.json();
329
- } catch {
330
- }
331
- if (!res.ok || !envelope?.success) {
332
- const message = envelope?.error ?? `Request failed (${res.status})`;
333
- throw new PerSQLError(res.status, message, envelope?.errorDetail);
334
- }
335
- return envelope.data;
336
- }
337
- /** @internal — raw fetch returning the underlying Response (used by blob GET). */
354
+ /** @internal — raw fetch returning the underlying Response. */
338
355
  fetchRaw(method, path) {
339
356
  return this._fetch(`${this.baseURL}${path}`, {
340
357
  method,
341
358
  headers: { Authorization: `Bearer ${this.token}` }
342
359
  });
343
360
  }
361
+ /**
362
+ * File a support ticket directly from your SDK code. Tickets land in
363
+ * the customer console at `/support`; staff reply from the admin
364
+ * app. Useful for agents and background jobs that hit a PerSQL
365
+ * error and want a human to see it.
366
+ *
367
+ * ```ts
368
+ * try {
369
+ * await db.query("UPDATE ...");
370
+ * } catch (err) {
371
+ * await persql.support.createTicket({
372
+ * subject: "Branch merge keeps 500ing",
373
+ * body: "Repro: ...",
374
+ * error: err,
375
+ * });
376
+ * }
377
+ * ```
378
+ */
379
+ get support() {
380
+ return new SupportClient(this);
381
+ }
382
+ };
383
+ function errorContextFor(err) {
384
+ if (err instanceof PerSQLError) {
385
+ return {
386
+ errorName: err.name,
387
+ errorMessage: err.message,
388
+ status: err.status,
389
+ detail: err.detail ?? null
390
+ };
391
+ }
392
+ if (err instanceof Error) {
393
+ return { errorName: err.name, errorMessage: err.message };
394
+ }
395
+ if (err !== void 0 && err !== null) {
396
+ return { errorMessage: String(err) };
397
+ }
398
+ return {};
399
+ }
400
+ var SupportClient = class {
401
+ constructor(client) {
402
+ this.client = client;
403
+ }
404
+ async createTicket(opts) {
405
+ if (this.client.local) {
406
+ throw new Error("support.createTicket is not supported in local mode");
407
+ }
408
+ const errorContext = {
409
+ ...errorContextFor(opts.error),
410
+ ...opts.errorContext ?? {}
411
+ };
412
+ return this.client.request(
413
+ "POST",
414
+ "/v1/support/tickets",
415
+ {
416
+ subject: opts.subject,
417
+ body: opts.body,
418
+ errorContext: Object.keys(errorContext).length > 0 ? errorContext : void 0
419
+ }
420
+ );
421
+ }
344
422
  };
345
423
  var PerSQLDatabase = class {
346
424
  constructor(client, namespace, slug) {
@@ -394,6 +472,25 @@ var PerSQLDatabase = class {
394
472
  transaction(statements, options = {}) {
395
473
  return this.batch(statements, { ...options, transaction: true });
396
474
  }
475
+ /**
476
+ * Mint a short-lived session JWT for an end user. The server (which
477
+ * holds the bearer token) calls this and hands the resulting token
478
+ * to an untrusted client (browser, mobile app, agent run). The
479
+ * client uses the JWT to call this database's published `/p/*`
480
+ * endpoints on behalf of `userId`. TTL defaults to 1 hour; max 24h.
481
+ */
482
+ async createSession(opts) {
483
+ if (this.client.local) {
484
+ throw new Error("createSession is not supported in local mode");
485
+ }
486
+ return this.client.request("POST", "/v1/sessions", {
487
+ namespaceSlug: this.namespace,
488
+ databaseSlug: this.slug,
489
+ userId: opts.userId,
490
+ email: opts.email,
491
+ expiresIn: opts.expiresIn
492
+ });
493
+ }
397
494
  /**
398
495
  * Engine telemetry — recent /v1/query and /v1/batch calls issued
399
496
  * against this database. Backed by the in-DO `_persql_meta_query_log`
@@ -515,6 +612,61 @@ var PerSQLDatabase = class {
515
612
  if (this.client.local) return this.client.local.explain(sql, params);
516
613
  return this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/explain`, { sql, params });
517
614
  }
615
+ /**
616
+ * Parse + bind-check a SQL statement without executing it. Faster
617
+ * and cheaper than running the query just to catch unknown columns
618
+ * or wrong param counts.
619
+ *
620
+ * Returns `{ ok: true }` on success or `{ ok: false, error, errorDetail }`
621
+ * with the same `errorDetail` envelope as `PerSQLError.detail`.
622
+ */
623
+ async validate(sql, params = []) {
624
+ if (this.client.local) {
625
+ throw new Error("PerSQL: validate is not available in local mode.");
626
+ }
627
+ const data = await this.client.request(
628
+ "POST",
629
+ `/v1/db/${this.namespace}/${this.slug}/validate`,
630
+ { sql, params }
631
+ );
632
+ return data;
633
+ }
634
+ /**
635
+ * Compact text rendering of the database's schema — one line per
636
+ * table with columns, types, PKs, and FK arrows inlined. Designed
637
+ * to stuff into an LLM prompt with minimum token overhead. See
638
+ * `db.describe()` for the structured form.
639
+ */
640
+ async describeCompact() {
641
+ if (this.client.local) {
642
+ throw new Error("PerSQL: describeCompact is not available in local mode.");
643
+ }
644
+ const data = await this.client.request(
645
+ "GET",
646
+ `/v1/db/${this.namespace}/${this.slug}/describe?format=compact`
647
+ );
648
+ return data.text;
649
+ }
650
+ /**
651
+ * First N rows of a table plus per-column stats (null count,
652
+ * distinct count, min, max). One call to size up an unfamiliar
653
+ * table — replaces a hand-rolled `SELECT *`, `COUNT(*)`, and
654
+ * `PRAGMA table_info` sequence.
655
+ *
656
+ * `n` defaults to 10 and is capped at 100.
657
+ */
658
+ async sampleTable(table, opts = {}) {
659
+ if (this.client.local) {
660
+ throw new Error("PerSQL: sampleTable is not available in local mode.");
661
+ }
662
+ const qs = opts.n ? `?n=${Math.max(1, Math.min(100, opts.n | 0))}` : "";
663
+ return this.client.request(
664
+ "GET",
665
+ `/v1/db/${this.namespace}/${this.slug}/tables/${encodeURIComponent(
666
+ table
667
+ )}/sample${qs}`
668
+ );
669
+ }
518
670
  /**
519
671
  * Introspect the database schema. Returns one entry per user table
520
672
  * with column definitions, suitable for codegen tools (the
@@ -538,20 +690,6 @@ var PerSQLDatabase = class {
538
690
  }
539
691
  return out;
540
692
  }
541
- /**
542
- * Per-database semantic search via Cloudflare Vectorize. Use
543
- * `vectors.upsert` to embed and store rows; `vectors.query` to
544
- * retrieve the most similar by free text. Embeddings are computed
545
- * server-side using bge-base-en-v1.5 (768 dim, cosine distance).
546
- */
547
- get vectors() {
548
- if (this.client.local) {
549
- throw new Error(
550
- "PerSQL: vectors require Cloudflare Vectorize and Workers AI \u2014 not available in local mode. Use a server-mode token (psql_live_/psql_test_) to call vectors.upsert/query/delete."
551
- );
552
- }
553
- return new PerSQLVectors(this.client, this.namespace, this.slug);
554
- }
555
693
  /**
556
694
  * Manage preview/PR-style branches of this database. Each branch is
557
695
  * its own DO with its own SQLite file; create-or-reset by ref is
@@ -580,6 +718,19 @@ var PerSQLDatabase = class {
580
718
  }
581
719
  return new PerSQLApprovals(this.client, this.namespace, this.slug);
582
720
  }
721
+ /**
722
+ * Manage the require_approval / deny rules for this database. Reads
723
+ * are open to any bearer with read access; create + delete require
724
+ * an admin-role bearer token.
725
+ */
726
+ get approvalRules() {
727
+ if (this.client.local) {
728
+ throw new Error(
729
+ "PerSQL: approval rules live on the server; local mode does not enforce them."
730
+ );
731
+ }
732
+ return new PerSQLApprovalRules(this.client, this.namespace, this.slug);
733
+ }
583
734
  /**
584
735
  * Pre-flight a write before running it. `propose()` validates the
585
736
  * SQL via EXPLAIN, estimates affected rows, and returns a single-use
@@ -591,20 +742,6 @@ var PerSQLDatabase = class {
591
742
  get proposals() {
592
743
  return new PerSQLProposals(this.client, this.namespace, this.slug);
593
744
  }
594
- /**
595
- * Per-database BLOB storage backed by R2. Use this for anything
596
- * larger than a SQLite cell (images, PDFs, model weights). Each
597
- * database has its own private namespace; keys may be hierarchical
598
- * (`avatars/2025/foo.jpg`) but never start with `/`.
599
- */
600
- get blob() {
601
- if (this.client.local) {
602
- throw new Error(
603
- "PerSQL: blob storage is backed by R2 \u2014 not available in local mode. Use a server-mode token, or store blobs in your test fixtures directly."
604
- );
605
- }
606
- return new PerSQLBlob(this.client, this.namespace, this.slug);
607
- }
608
745
  /**
609
746
  * Subscribe to row-changes via WebSocket — the SQL equivalent of
610
747
  * Postgres `LISTEN`. The callback fires once per write that
@@ -665,6 +802,55 @@ var PerSQLDatabase = class {
665
802
  }
666
803
  };
667
804
  }
805
+ /**
806
+ * Long-poll for row-changes on this database. Returns immediately
807
+ * if changes newer than `since` are already buffered; otherwise
808
+ * blocks server-side for up to `waitMs` (default 25s, max 25s).
809
+ *
810
+ * Most callers want `changes()` instead — an async iterator that
811
+ * loops `waitForChanges` forever, surfacing each batch as it
812
+ * arrives.
813
+ */
814
+ async waitForChanges(opts = {}) {
815
+ if (this.client.local) {
816
+ throw new Error(
817
+ "PerSQL: waitForChanges requires the DO change ring \u2014 not available in local mode."
818
+ );
819
+ }
820
+ const qs = new URLSearchParams();
821
+ if (typeof opts.since === "number") qs.set("since", String(opts.since));
822
+ if (typeof opts.waitMs === "number") qs.set("waitMs", String(opts.waitMs));
823
+ if (opts.tables && opts.tables.length > 0) qs.set("tables", opts.tables.join(","));
824
+ const q = qs.toString();
825
+ return this.client.request(
826
+ "GET",
827
+ `/v1/db/${this.namespace}/${this.slug}/changes${q ? `?${q}` : ""}`
828
+ );
829
+ }
830
+ /**
831
+ * Async-iterator change feed. Loops `waitForChanges` forever and
832
+ * yields one batch per server response. Pass an `AbortSignal` to
833
+ * stop the loop cleanly.
834
+ *
835
+ * ```ts
836
+ * const ctl = new AbortController();
837
+ * for await (const batch of db.changes({ signal: ctl.signal })) {
838
+ * for (const c of batch.changes) console.log(c.kind, c.table);
839
+ * }
840
+ * ```
841
+ */
842
+ async *changes(opts = {}) {
843
+ let cursor = opts.since ?? 0;
844
+ while (!opts.signal?.aborted) {
845
+ const batch = await this.waitForChanges({
846
+ since: cursor,
847
+ waitMs: opts.waitMs,
848
+ tables: opts.tables
849
+ });
850
+ cursor = batch.cursor;
851
+ yield batch;
852
+ }
853
+ }
668
854
  /**
669
855
  * Returns the callback shape `drizzle-orm/sqlite-proxy` expects.
670
856
  * Pair with `drizzle()` from that module to get a typed,
@@ -692,42 +878,64 @@ var PerSQLDatabase = class {
692
878
  return callback;
693
879
  }
694
880
  /**
695
- * Returns a tool definition for use with Anthropic / OpenAI / function-calling
696
- * agents. Pair it with `runTool` to execute the call.
881
+ * Returns a single SQL-query tool definition in every shape PerSQL
882
+ * supports (Anthropic, OpenAI Chat, Vercel AI SDK, Mastra, LangChain,
883
+ * OpenAI Agents SDK). Same input contract as `asTools()` — the only
884
+ * difference is one fat `query` tool instead of typed-per-table tools.
885
+ *
886
+ * Pair the format-specific fields with `runTool` (or use the bundled
887
+ * `execute`/`invoke` callbacks, which call `runTool` internally).
697
888
  */
698
889
  asTool(name = "persql_query") {
699
- return {
700
- anthropic: {
701
- name,
702
- description: `Run a SQL query against the PerSQL database "${this.namespace}/${this.slug}". Returns up to 1000 rows. Use parameter binding (?) to avoid injection.`,
703
- input_schema: {
704
- type: "object",
705
- properties: {
706
- sql: { type: "string", description: "SQLite SQL statement" },
707
- params: {
708
- type: "array",
709
- items: {},
710
- description: "Positional parameters for the SQL statement"
711
- }
712
- },
713
- required: ["sql"]
890
+ const description = `Run a SQL query against the PerSQL database "${this.namespace}/${this.slug}". A single result is capped at 100,000 rows and the query errors above that \u2014 add a LIMIT for large tables. Use parameter binding (?) to avoid injection.`;
891
+ const inputSchema = {
892
+ type: "object",
893
+ properties: {
894
+ sql: { type: "string", description: "SQLite SQL statement" },
895
+ params: {
896
+ type: "array",
897
+ items: {},
898
+ description: "Positional parameters for the SQL statement"
714
899
  }
715
900
  },
901
+ required: ["sql"],
902
+ // OpenAI strict function-calling rejects a schema without this; the
903
+ // per-table asTools() schemas already set it. Keep them consistent.
904
+ additionalProperties: false
905
+ };
906
+ const execute = (input) => this.runTool({
907
+ sql: String(input.sql ?? ""),
908
+ params: Array.isArray(input.params) ? input.params : []
909
+ });
910
+ return {
911
+ anthropic: { name, description, input_schema: inputSchema },
716
912
  openai: {
717
913
  type: "function",
718
- function: {
914
+ function: { name, description, parameters: inputSchema }
915
+ },
916
+ aiSdk: () => ({
917
+ [name]: { description, inputSchema, execute }
918
+ }),
919
+ mastra: () => ({
920
+ [name]: {
921
+ id: name,
922
+ description,
923
+ inputSchema,
924
+ execute: ({ context }) => execute(context)
925
+ }
926
+ }),
927
+ langchain: () => [
928
+ { name, description, schema: inputSchema, invoke: execute }
929
+ ],
930
+ openaiAgents: () => [
931
+ {
932
+ type: "function",
719
933
  name,
720
- description: `Run a SQL query against the PerSQL database "${this.namespace}/${this.slug}".`,
721
- parameters: {
722
- type: "object",
723
- properties: {
724
- sql: { type: "string" },
725
- params: { type: "array", items: {} }
726
- },
727
- required: ["sql"]
728
- }
934
+ description,
935
+ parameters: inputSchema,
936
+ invoke: execute
729
937
  }
730
- }
938
+ ]
731
939
  };
732
940
  }
733
941
  /**
@@ -1499,106 +1707,148 @@ function sanitizeToolPart(s) {
1499
1707
  function quoteIdent(s) {
1500
1708
  return `"${s.replace(/"/g, '""')}"`;
1501
1709
  }
1502
- var PerSQLBlob = class {
1710
+ var PerSQLApprovals = class {
1503
1711
  constructor(client, namespace, slug) {
1504
1712
  this.client = client;
1505
1713
  this.namespace = namespace;
1506
1714
  this.slug = slug;
1507
1715
  }
1508
- /** Upload a blob. Body can be ArrayBuffer, Blob, ReadableStream, or string. */
1509
- async put(key, body, opts = {}) {
1510
- const path = `/v1/db/${this.namespace}/${this.slug}/blobs/${encodeBlobKey(key)}`;
1511
- return this.client.requestRaw(
1512
- "PUT",
1513
- path,
1514
- body,
1515
- opts.contentType
1716
+ // Tokens this client has seen via subscribe used to dedupe poll
1717
+ // results so onApprovalRequired fires once per token regardless of
1718
+ // how many subscribe ticks observe it.
1719
+ seen = /* @__PURE__ */ new Map();
1720
+ /**
1721
+ * Look up the status of an approval token without consuming it. The
1722
+ * bearer must match the one that minted the original 403; cross-bearer
1723
+ * lookup returns 403.
1724
+ */
1725
+ async get(approvalToken) {
1726
+ return this.client.request(
1727
+ "GET",
1728
+ `/v1/db/${this.namespace}/${this.slug}/approval/${encodeURIComponent(
1729
+ approvalToken
1730
+ )}`
1516
1731
  );
1517
1732
  }
1518
- /** Fetch a blob. Returns null if missing. */
1519
- async get(key) {
1520
- const path = `/v1/db/${this.namespace}/${this.slug}/blobs/${encodeBlobKey(key)}`;
1521
- const res = await this.client.fetchRaw("GET", path);
1522
- if (res.status === 404) return null;
1523
- if (!res.ok) {
1524
- throw new PerSQLError(res.status, `Blob fetch failed (${res.status})`);
1733
+ /**
1734
+ * Poll until a reviewer decides (or the token expires). Resolves with
1735
+ * the final status — `approved` is your green light for `redeem()`.
1736
+ */
1737
+ async poll(approvalToken, opts = {}) {
1738
+ const intervalMs = Math.max(250, opts.intervalMs ?? 2e3);
1739
+ const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60 * 1e3);
1740
+ while (true) {
1741
+ if (opts.signal?.aborted) {
1742
+ throw new Error("approval poll aborted");
1743
+ }
1744
+ const status = await this.get(approvalToken);
1745
+ if (status.status !== "pending") return status;
1746
+ if (Date.now() > deadline) return status;
1747
+ await new Promise((r) => setTimeout(r, intervalMs));
1525
1748
  }
1526
- return res;
1527
1749
  }
1528
- /** Delete a blob. */
1529
- async delete(key) {
1530
- const path = `/v1/db/${this.namespace}/${this.slug}/blobs/${encodeBlobKey(key)}`;
1531
- await this.client.request("DELETE", path);
1750
+ /**
1751
+ * Long-running poll subscription for new approval events on this
1752
+ * database. Returns a `stop()` handle. Webhook delivery is the push
1753
+ * path (events `approval_required` / `approval_resolved`); subscribe
1754
+ * is the pull fallback for clients that can't host a webhook.
1755
+ *
1756
+ * Today this iterates over a known set of tokens supplied by the
1757
+ * caller; an SDK-only client that wants discovery should pair this
1758
+ * with the webhook path. Pass tokens you already hold (e.g. from a
1759
+ * prior `ApprovalRequiredError`).
1760
+ */
1761
+ subscribe(tokens, opts) {
1762
+ const intervalMs = Math.max(1e3, opts.intervalMs ?? 5e3);
1763
+ let cancelled = false;
1764
+ const onAbort = () => {
1765
+ cancelled = true;
1766
+ };
1767
+ opts.signal?.addEventListener("abort", onAbort);
1768
+ const tick = async () => {
1769
+ for (const token of tokens) {
1770
+ if (cancelled) return;
1771
+ try {
1772
+ const status = await this.get(token);
1773
+ const prev = this.seen.get(token);
1774
+ if (status.status !== prev) {
1775
+ this.seen.set(token, status.status);
1776
+ const event = {
1777
+ approvalToken: token,
1778
+ status: status.status,
1779
+ hits: status.hits
1780
+ };
1781
+ if (status.status === "pending") {
1782
+ opts.onApprovalRequired?.(event);
1783
+ } else {
1784
+ opts.onApprovalResolved?.(event);
1785
+ }
1786
+ }
1787
+ } catch (e) {
1788
+ opts.onError?.(e instanceof Error ? e : new Error(String(e)));
1789
+ }
1790
+ }
1791
+ };
1792
+ const id = setInterval(() => {
1793
+ void tick();
1794
+ }, intervalMs);
1795
+ void tick();
1796
+ return {
1797
+ stop: () => {
1798
+ cancelled = true;
1799
+ clearInterval(id);
1800
+ opts.signal?.removeEventListener("abort", onAbort);
1801
+ }
1802
+ };
1532
1803
  }
1533
- /** List blobs by prefix. */
1534
- async list(opts = {}) {
1535
- const params = new URLSearchParams();
1536
- if (opts.prefix) params.set("prefix", opts.prefix);
1537
- if (opts.cursor) params.set("cursor", opts.cursor);
1538
- if (opts.limit) params.set("limit", String(opts.limit));
1539
- const qs = params.toString();
1540
- const path = `/v1/db/${this.namespace}/${this.slug}/blobs` + (qs ? `?${qs}` : "");
1541
- return this.client.request("GET", path);
1804
+ /**
1805
+ * Redeem an approved approvalToken. The bearer must be the same one
1806
+ * that minted it, and the request must target the same database.
1807
+ * Returns the result of the originally-blocked query or batch.
1808
+ */
1809
+ async redeem(approvalToken) {
1810
+ const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
1811
+ approvalToken
1812
+ });
1813
+ if (Array.isArray(raw)) {
1814
+ return raw.map((r) => ({
1815
+ ...r,
1816
+ data: rowsToObjects(r.columns, r.rows)
1817
+ }));
1818
+ }
1819
+ return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
1542
1820
  }
1543
1821
  };
1544
- function encodeBlobKey(key) {
1545
- return key.split("/").map(encodeURIComponent).join("/");
1546
- }
1547
- var PerSQLVectors = class {
1822
+ var PerSQLApprovalRules = class {
1548
1823
  constructor(client, namespace, slug) {
1549
1824
  this.client = client;
1550
1825
  this.namespace = namespace;
1551
1826
  this.slug = slug;
1552
1827
  }
1553
- /** Embed and upsert up to 100 items in one call. */
1554
- async upsert(items) {
1828
+ async list() {
1555
1829
  return this.client.request(
1556
- "POST",
1557
- `/v1/db/${this.namespace}/${this.slug}/vectors`,
1558
- { items }
1830
+ "GET",
1831
+ `/v1/db/${this.namespace}/${this.slug}/approval-rules`
1559
1832
  );
1560
1833
  }
1561
- /** Top-K nearest neighbours by free-text query. */
1562
- async query(text, opts = {}) {
1834
+ /** Requires an admin-role bearer token. */
1835
+ async create(input) {
1563
1836
  return this.client.request(
1564
1837
  "POST",
1565
- `/v1/db/${this.namespace}/${this.slug}/vectors/query`,
1566
- { query: text, topK: opts.topK, filter: opts.filter }
1838
+ `/v1/db/${this.namespace}/${this.slug}/approval-rules`,
1839
+ input
1567
1840
  );
1568
1841
  }
1569
- /** Delete vectors by id. Up to 1000 ids per call. */
1570
- async delete(ids) {
1842
+ /** Requires an admin-role bearer token. */
1843
+ async delete(ruleId) {
1571
1844
  return this.client.request(
1572
1845
  "DELETE",
1573
- `/v1/db/${this.namespace}/${this.slug}/vectors`,
1574
- { ids }
1846
+ `/v1/db/${this.namespace}/${this.slug}/approval-rules/${encodeURIComponent(
1847
+ ruleId
1848
+ )}`
1575
1849
  );
1576
1850
  }
1577
1851
  };
1578
- var PerSQLApprovals = class {
1579
- constructor(client, namespace, slug) {
1580
- this.client = client;
1581
- this.namespace = namespace;
1582
- this.slug = slug;
1583
- }
1584
- /**
1585
- * Redeem an approved approvalToken. The bearer must be the same one
1586
- * that minted it, and the request must target the same database.
1587
- * Returns the result of the originally-blocked query or batch.
1588
- */
1589
- async redeem(approvalToken) {
1590
- const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
1591
- approvalToken
1592
- });
1593
- if (Array.isArray(raw)) {
1594
- return raw.map((r) => ({
1595
- ...r,
1596
- data: rowsToObjects(r.columns, r.rows)
1597
- }));
1598
- }
1599
- return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
1600
- }
1601
- };
1602
1852
  var PerSQLProposals = class {
1603
1853
  constructor(client, namespace, slug) {
1604
1854
  this.client = client;
@@ -1610,6 +1860,11 @@ var PerSQLProposals = class {
1610
1860
  * rows, and returns a single-use `executionToken` valid for 10 min
1611
1861
  * (override with `ttlSec`, max 1 hour). Nothing is mutated until
1612
1862
  * `apply()` is called.
1863
+ *
1864
+ * Local mode keeps the token in-process: single-use still holds, but
1865
+ * `estimatedAffectedRows` is always `null` (no server estimator) and a
1866
+ * stale/unknown token throws a plain `Error` rather than the HTTP
1867
+ * 404/403 of the server path.
1613
1868
  */
1614
1869
  async propose(sql, opts = {}) {
1615
1870
  if (this.client.local) {
@@ -1760,13 +2015,13 @@ function rowsToObjects(columns, rows) {
1760
2015
  0 && (module.exports = {
1761
2016
  ApprovalRequiredError,
1762
2017
  PerSQL,
2018
+ PerSQLApprovalRules,
1763
2019
  PerSQLApprovals,
1764
- PerSQLBlob,
1765
2020
  PerSQLBranches,
1766
2021
  PerSQLDatabase,
1767
2022
  PerSQLError,
1768
2023
  PerSQLProposals,
1769
- PerSQLVectors,
1770
- RateLimitError
2024
+ RateLimitError,
2025
+ SupportClient
1771
2026
  });
1772
2027
  //# sourceMappingURL=index.cjs.map