@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/cli.cjs CHANGED
@@ -5,6 +5,28 @@
5
5
  var import_promises = require("fs/promises");
6
6
 
7
7
  // src/local.ts
8
+ function checkLocalExpect(e, r) {
9
+ const rows = r.rows.length;
10
+ if (e.rows != null && rows !== e.rows) {
11
+ return `expected rows=${e.rows}, got ${rows}`;
12
+ }
13
+ if (e.rowsAtLeast != null && rows < e.rowsAtLeast) {
14
+ return `expected rows>=${e.rowsAtLeast}, got ${rows}`;
15
+ }
16
+ if (e.rowsAtMost != null && rows > e.rowsAtMost) {
17
+ return `expected rows<=${e.rowsAtMost}, got ${rows}`;
18
+ }
19
+ if (e.rowsWritten != null && r.rowsWritten !== e.rowsWritten) {
20
+ return `expected rowsWritten=${e.rowsWritten}, got ${r.rowsWritten}`;
21
+ }
22
+ if (e.rowsWrittenAtLeast != null && r.rowsWritten < e.rowsWrittenAtLeast) {
23
+ return `expected rowsWritten>=${e.rowsWrittenAtLeast}, got ${r.rowsWritten}`;
24
+ }
25
+ if (e.rowsWrittenAtMost != null && r.rowsWritten > e.rowsWrittenAtMost) {
26
+ return `expected rowsWritten<=${e.rowsWrittenAtMost}, got ${r.rowsWritten}`;
27
+ }
28
+ return null;
29
+ }
8
30
  var PROPOSAL_TTL_DEFAULT_SEC = 600;
9
31
  var PROPOSAL_TTL_MAX_SEC = 3600;
10
32
  function newLocalProposalId() {
@@ -43,7 +65,16 @@ var LocalDriver = class {
43
65
  }
44
66
  async batch(statements, transaction) {
45
67
  const db = await this.open();
46
- const exec = () => statements.map((s) => runOne(db, s.sql, s.params ?? []));
68
+ const exec = () => statements.map((s, i) => {
69
+ const r = runOne(db, s.sql, s.params ?? []);
70
+ if (s.expect) {
71
+ const violation = checkLocalExpect(s.expect, r);
72
+ if (violation) {
73
+ throw new Error(`Assertion failed on statement ${i}: ${violation}`);
74
+ }
75
+ }
76
+ return r;
77
+ });
47
78
  if (transaction) return db.transaction(exec)();
48
79
  return exec();
49
80
  }
@@ -205,6 +236,19 @@ var PerSQL = class _PerSQL {
205
236
  close() {
206
237
  this.local?.close();
207
238
  }
239
+ /**
240
+ * Resolve the bearer token's namespace + live usage. Use this once
241
+ * per agent run so you know the slug to put in URLs and how much
242
+ * monthly budget / prepaid balance remains before a 402.
243
+ *
244
+ * Throws in local mode — no server, nothing to ask.
245
+ */
246
+ async me() {
247
+ if (this.local) {
248
+ throw new Error("PerSQL: me() is not available in local mode.");
249
+ }
250
+ return this.request("GET", "/v1/me");
251
+ }
208
252
  /**
209
253
  * Redeem a handoff token (`phand_…`) for a regular PerSQL client
210
254
  * scoped to the handed-off (database, branch, role). Single use:
@@ -278,40 +322,74 @@ var PerSQL = class _PerSQL {
278
322
  }
279
323
  return envelope.data;
280
324
  }
281
- /** @internal — raw upload (e.g. blob PUT) returning the ApiResponse data. */
282
- async requestRaw(method, path, body, contentType) {
283
- const res = await this._fetch(`${this.baseURL}${path}`, {
284
- method,
285
- headers: {
286
- Authorization: `Bearer ${this.token}`,
287
- "Content-Type": contentType ?? "application/octet-stream"
288
- },
289
- body,
290
- // @ts-expect-error — duplex is required by Node fetch for streaming bodies
291
- duplex: "half"
292
- });
293
- if (res.status === 429) {
294
- const retryAfter = Number(res.headers.get("retry-after") ?? "60");
295
- throw new RateLimitError(retryAfter);
296
- }
297
- let envelope = null;
298
- try {
299
- envelope = await res.json();
300
- } catch {
301
- }
302
- if (!res.ok || !envelope?.success) {
303
- const message = envelope?.error ?? `Request failed (${res.status})`;
304
- throw new PerSQLError(res.status, message, envelope?.errorDetail);
305
- }
306
- return envelope.data;
307
- }
308
- /** @internal — raw fetch returning the underlying Response (used by blob GET). */
325
+ /** @internal — raw fetch returning the underlying Response. */
309
326
  fetchRaw(method, path) {
310
327
  return this._fetch(`${this.baseURL}${path}`, {
311
328
  method,
312
329
  headers: { Authorization: `Bearer ${this.token}` }
313
330
  });
314
331
  }
332
+ /**
333
+ * File a support ticket directly from your SDK code. Tickets land in
334
+ * the customer console at `/support`; staff reply from the admin
335
+ * app. Useful for agents and background jobs that hit a PerSQL
336
+ * error and want a human to see it.
337
+ *
338
+ * ```ts
339
+ * try {
340
+ * await db.query("UPDATE ...");
341
+ * } catch (err) {
342
+ * await persql.support.createTicket({
343
+ * subject: "Branch merge keeps 500ing",
344
+ * body: "Repro: ...",
345
+ * error: err,
346
+ * });
347
+ * }
348
+ * ```
349
+ */
350
+ get support() {
351
+ return new SupportClient(this);
352
+ }
353
+ };
354
+ function errorContextFor(err) {
355
+ if (err instanceof PerSQLError) {
356
+ return {
357
+ errorName: err.name,
358
+ errorMessage: err.message,
359
+ status: err.status,
360
+ detail: err.detail ?? null
361
+ };
362
+ }
363
+ if (err instanceof Error) {
364
+ return { errorName: err.name, errorMessage: err.message };
365
+ }
366
+ if (err !== void 0 && err !== null) {
367
+ return { errorMessage: String(err) };
368
+ }
369
+ return {};
370
+ }
371
+ var SupportClient = class {
372
+ constructor(client) {
373
+ this.client = client;
374
+ }
375
+ async createTicket(opts) {
376
+ if (this.client.local) {
377
+ throw new Error("support.createTicket is not supported in local mode");
378
+ }
379
+ const errorContext = {
380
+ ...errorContextFor(opts.error),
381
+ ...opts.errorContext ?? {}
382
+ };
383
+ return this.client.request(
384
+ "POST",
385
+ "/v1/support/tickets",
386
+ {
387
+ subject: opts.subject,
388
+ body: opts.body,
389
+ errorContext: Object.keys(errorContext).length > 0 ? errorContext : void 0
390
+ }
391
+ );
392
+ }
315
393
  };
316
394
  var PerSQLDatabase = class {
317
395
  constructor(client, namespace, slug) {
@@ -365,6 +443,25 @@ var PerSQLDatabase = class {
365
443
  transaction(statements, options = {}) {
366
444
  return this.batch(statements, { ...options, transaction: true });
367
445
  }
446
+ /**
447
+ * Mint a short-lived session JWT for an end user. The server (which
448
+ * holds the bearer token) calls this and hands the resulting token
449
+ * to an untrusted client (browser, mobile app, agent run). The
450
+ * client uses the JWT to call this database's published `/p/*`
451
+ * endpoints on behalf of `userId`. TTL defaults to 1 hour; max 24h.
452
+ */
453
+ async createSession(opts) {
454
+ if (this.client.local) {
455
+ throw new Error("createSession is not supported in local mode");
456
+ }
457
+ return this.client.request("POST", "/v1/sessions", {
458
+ namespaceSlug: this.namespace,
459
+ databaseSlug: this.slug,
460
+ userId: opts.userId,
461
+ email: opts.email,
462
+ expiresIn: opts.expiresIn
463
+ });
464
+ }
368
465
  /**
369
466
  * Engine telemetry — recent /v1/query and /v1/batch calls issued
370
467
  * against this database. Backed by the in-DO `_persql_meta_query_log`
@@ -486,6 +583,61 @@ var PerSQLDatabase = class {
486
583
  if (this.client.local) return this.client.local.explain(sql, params);
487
584
  return this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/explain`, { sql, params });
488
585
  }
586
+ /**
587
+ * Parse + bind-check a SQL statement without executing it. Faster
588
+ * and cheaper than running the query just to catch unknown columns
589
+ * or wrong param counts.
590
+ *
591
+ * Returns `{ ok: true }` on success or `{ ok: false, error, errorDetail }`
592
+ * with the same `errorDetail` envelope as `PerSQLError.detail`.
593
+ */
594
+ async validate(sql, params = []) {
595
+ if (this.client.local) {
596
+ throw new Error("PerSQL: validate is not available in local mode.");
597
+ }
598
+ const data = await this.client.request(
599
+ "POST",
600
+ `/v1/db/${this.namespace}/${this.slug}/validate`,
601
+ { sql, params }
602
+ );
603
+ return data;
604
+ }
605
+ /**
606
+ * Compact text rendering of the database's schema — one line per
607
+ * table with columns, types, PKs, and FK arrows inlined. Designed
608
+ * to stuff into an LLM prompt with minimum token overhead. See
609
+ * `db.describe()` for the structured form.
610
+ */
611
+ async describeCompact() {
612
+ if (this.client.local) {
613
+ throw new Error("PerSQL: describeCompact is not available in local mode.");
614
+ }
615
+ const data = await this.client.request(
616
+ "GET",
617
+ `/v1/db/${this.namespace}/${this.slug}/describe?format=compact`
618
+ );
619
+ return data.text;
620
+ }
621
+ /**
622
+ * First N rows of a table plus per-column stats (null count,
623
+ * distinct count, min, max). One call to size up an unfamiliar
624
+ * table — replaces a hand-rolled `SELECT *`, `COUNT(*)`, and
625
+ * `PRAGMA table_info` sequence.
626
+ *
627
+ * `n` defaults to 10 and is capped at 100.
628
+ */
629
+ async sampleTable(table, opts = {}) {
630
+ if (this.client.local) {
631
+ throw new Error("PerSQL: sampleTable is not available in local mode.");
632
+ }
633
+ const qs = opts.n ? `?n=${Math.max(1, Math.min(100, opts.n | 0))}` : "";
634
+ return this.client.request(
635
+ "GET",
636
+ `/v1/db/${this.namespace}/${this.slug}/tables/${encodeURIComponent(
637
+ table
638
+ )}/sample${qs}`
639
+ );
640
+ }
489
641
  /**
490
642
  * Introspect the database schema. Returns one entry per user table
491
643
  * with column definitions, suitable for codegen tools (the
@@ -509,20 +661,6 @@ var PerSQLDatabase = class {
509
661
  }
510
662
  return out;
511
663
  }
512
- /**
513
- * Per-database semantic search via Cloudflare Vectorize. Use
514
- * `vectors.upsert` to embed and store rows; `vectors.query` to
515
- * retrieve the most similar by free text. Embeddings are computed
516
- * server-side using bge-base-en-v1.5 (768 dim, cosine distance).
517
- */
518
- get vectors() {
519
- if (this.client.local) {
520
- throw new Error(
521
- "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."
522
- );
523
- }
524
- return new PerSQLVectors(this.client, this.namespace, this.slug);
525
- }
526
664
  /**
527
665
  * Manage preview/PR-style branches of this database. Each branch is
528
666
  * its own DO with its own SQLite file; create-or-reset by ref is
@@ -551,6 +689,19 @@ var PerSQLDatabase = class {
551
689
  }
552
690
  return new PerSQLApprovals(this.client, this.namespace, this.slug);
553
691
  }
692
+ /**
693
+ * Manage the require_approval / deny rules for this database. Reads
694
+ * are open to any bearer with read access; create + delete require
695
+ * an admin-role bearer token.
696
+ */
697
+ get approvalRules() {
698
+ if (this.client.local) {
699
+ throw new Error(
700
+ "PerSQL: approval rules live on the server; local mode does not enforce them."
701
+ );
702
+ }
703
+ return new PerSQLApprovalRules(this.client, this.namespace, this.slug);
704
+ }
554
705
  /**
555
706
  * Pre-flight a write before running it. `propose()` validates the
556
707
  * SQL via EXPLAIN, estimates affected rows, and returns a single-use
@@ -562,20 +713,6 @@ var PerSQLDatabase = class {
562
713
  get proposals() {
563
714
  return new PerSQLProposals(this.client, this.namespace, this.slug);
564
715
  }
565
- /**
566
- * Per-database BLOB storage backed by R2. Use this for anything
567
- * larger than a SQLite cell (images, PDFs, model weights). Each
568
- * database has its own private namespace; keys may be hierarchical
569
- * (`avatars/2025/foo.jpg`) but never start with `/`.
570
- */
571
- get blob() {
572
- if (this.client.local) {
573
- throw new Error(
574
- "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."
575
- );
576
- }
577
- return new PerSQLBlob(this.client, this.namespace, this.slug);
578
- }
579
716
  /**
580
717
  * Subscribe to row-changes via WebSocket — the SQL equivalent of
581
718
  * Postgres `LISTEN`. The callback fires once per write that
@@ -636,6 +773,55 @@ var PerSQLDatabase = class {
636
773
  }
637
774
  };
638
775
  }
776
+ /**
777
+ * Long-poll for row-changes on this database. Returns immediately
778
+ * if changes newer than `since` are already buffered; otherwise
779
+ * blocks server-side for up to `waitMs` (default 25s, max 25s).
780
+ *
781
+ * Most callers want `changes()` instead — an async iterator that
782
+ * loops `waitForChanges` forever, surfacing each batch as it
783
+ * arrives.
784
+ */
785
+ async waitForChanges(opts = {}) {
786
+ if (this.client.local) {
787
+ throw new Error(
788
+ "PerSQL: waitForChanges requires the DO change ring \u2014 not available in local mode."
789
+ );
790
+ }
791
+ const qs = new URLSearchParams();
792
+ if (typeof opts.since === "number") qs.set("since", String(opts.since));
793
+ if (typeof opts.waitMs === "number") qs.set("waitMs", String(opts.waitMs));
794
+ if (opts.tables && opts.tables.length > 0) qs.set("tables", opts.tables.join(","));
795
+ const q = qs.toString();
796
+ return this.client.request(
797
+ "GET",
798
+ `/v1/db/${this.namespace}/${this.slug}/changes${q ? `?${q}` : ""}`
799
+ );
800
+ }
801
+ /**
802
+ * Async-iterator change feed. Loops `waitForChanges` forever and
803
+ * yields one batch per server response. Pass an `AbortSignal` to
804
+ * stop the loop cleanly.
805
+ *
806
+ * ```ts
807
+ * const ctl = new AbortController();
808
+ * for await (const batch of db.changes({ signal: ctl.signal })) {
809
+ * for (const c of batch.changes) console.log(c.kind, c.table);
810
+ * }
811
+ * ```
812
+ */
813
+ async *changes(opts = {}) {
814
+ let cursor = opts.since ?? 0;
815
+ while (!opts.signal?.aborted) {
816
+ const batch = await this.waitForChanges({
817
+ since: cursor,
818
+ waitMs: opts.waitMs,
819
+ tables: opts.tables
820
+ });
821
+ cursor = batch.cursor;
822
+ yield batch;
823
+ }
824
+ }
639
825
  /**
640
826
  * Returns the callback shape `drizzle-orm/sqlite-proxy` expects.
641
827
  * Pair with `drizzle()` from that module to get a typed,
@@ -663,42 +849,64 @@ var PerSQLDatabase = class {
663
849
  return callback;
664
850
  }
665
851
  /**
666
- * Returns a tool definition for use with Anthropic / OpenAI / function-calling
667
- * agents. Pair it with `runTool` to execute the call.
852
+ * Returns a single SQL-query tool definition in every shape PerSQL
853
+ * supports (Anthropic, OpenAI Chat, Vercel AI SDK, Mastra, LangChain,
854
+ * OpenAI Agents SDK). Same input contract as `asTools()` — the only
855
+ * difference is one fat `query` tool instead of typed-per-table tools.
856
+ *
857
+ * Pair the format-specific fields with `runTool` (or use the bundled
858
+ * `execute`/`invoke` callbacks, which call `runTool` internally).
668
859
  */
669
860
  asTool(name = "persql_query") {
670
- return {
671
- anthropic: {
672
- name,
673
- description: `Run a SQL query against the PerSQL database "${this.namespace}/${this.slug}". Returns up to 1000 rows. Use parameter binding (?) to avoid injection.`,
674
- input_schema: {
675
- type: "object",
676
- properties: {
677
- sql: { type: "string", description: "SQLite SQL statement" },
678
- params: {
679
- type: "array",
680
- items: {},
681
- description: "Positional parameters for the SQL statement"
682
- }
683
- },
684
- required: ["sql"]
861
+ 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.`;
862
+ const inputSchema = {
863
+ type: "object",
864
+ properties: {
865
+ sql: { type: "string", description: "SQLite SQL statement" },
866
+ params: {
867
+ type: "array",
868
+ items: {},
869
+ description: "Positional parameters for the SQL statement"
685
870
  }
686
871
  },
872
+ required: ["sql"],
873
+ // OpenAI strict function-calling rejects a schema without this; the
874
+ // per-table asTools() schemas already set it. Keep them consistent.
875
+ additionalProperties: false
876
+ };
877
+ const execute = (input) => this.runTool({
878
+ sql: String(input.sql ?? ""),
879
+ params: Array.isArray(input.params) ? input.params : []
880
+ });
881
+ return {
882
+ anthropic: { name, description, input_schema: inputSchema },
687
883
  openai: {
688
884
  type: "function",
689
- function: {
885
+ function: { name, description, parameters: inputSchema }
886
+ },
887
+ aiSdk: () => ({
888
+ [name]: { description, inputSchema, execute }
889
+ }),
890
+ mastra: () => ({
891
+ [name]: {
892
+ id: name,
893
+ description,
894
+ inputSchema,
895
+ execute: ({ context }) => execute(context)
896
+ }
897
+ }),
898
+ langchain: () => [
899
+ { name, description, schema: inputSchema, invoke: execute }
900
+ ],
901
+ openaiAgents: () => [
902
+ {
903
+ type: "function",
690
904
  name,
691
- description: `Run a SQL query against the PerSQL database "${this.namespace}/${this.slug}".`,
692
- parameters: {
693
- type: "object",
694
- properties: {
695
- sql: { type: "string" },
696
- params: { type: "array", items: {} }
697
- },
698
- required: ["sql"]
699
- }
905
+ description,
906
+ parameters: inputSchema,
907
+ invoke: execute
700
908
  }
701
- }
909
+ ]
702
910
  };
703
911
  }
704
912
  /**
@@ -1470,106 +1678,148 @@ function sanitizeToolPart(s) {
1470
1678
  function quoteIdent(s) {
1471
1679
  return `"${s.replace(/"/g, '""')}"`;
1472
1680
  }
1473
- var PerSQLBlob = class {
1681
+ var PerSQLApprovals = class {
1474
1682
  constructor(client, namespace, slug) {
1475
1683
  this.client = client;
1476
1684
  this.namespace = namespace;
1477
1685
  this.slug = slug;
1478
1686
  }
1479
- /** Upload a blob. Body can be ArrayBuffer, Blob, ReadableStream, or string. */
1480
- async put(key, body, opts = {}) {
1481
- const path = `/v1/db/${this.namespace}/${this.slug}/blobs/${encodeBlobKey(key)}`;
1482
- return this.client.requestRaw(
1483
- "PUT",
1484
- path,
1485
- body,
1486
- opts.contentType
1687
+ // Tokens this client has seen via subscribe used to dedupe poll
1688
+ // results so onApprovalRequired fires once per token regardless of
1689
+ // how many subscribe ticks observe it.
1690
+ seen = /* @__PURE__ */ new Map();
1691
+ /**
1692
+ * Look up the status of an approval token without consuming it. The
1693
+ * bearer must match the one that minted the original 403; cross-bearer
1694
+ * lookup returns 403.
1695
+ */
1696
+ async get(approvalToken) {
1697
+ return this.client.request(
1698
+ "GET",
1699
+ `/v1/db/${this.namespace}/${this.slug}/approval/${encodeURIComponent(
1700
+ approvalToken
1701
+ )}`
1487
1702
  );
1488
1703
  }
1489
- /** Fetch a blob. Returns null if missing. */
1490
- async get(key) {
1491
- const path = `/v1/db/${this.namespace}/${this.slug}/blobs/${encodeBlobKey(key)}`;
1492
- const res = await this.client.fetchRaw("GET", path);
1493
- if (res.status === 404) return null;
1494
- if (!res.ok) {
1495
- throw new PerSQLError(res.status, `Blob fetch failed (${res.status})`);
1704
+ /**
1705
+ * Poll until a reviewer decides (or the token expires). Resolves with
1706
+ * the final status — `approved` is your green light for `redeem()`.
1707
+ */
1708
+ async poll(approvalToken, opts = {}) {
1709
+ const intervalMs = Math.max(250, opts.intervalMs ?? 2e3);
1710
+ const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60 * 1e3);
1711
+ while (true) {
1712
+ if (opts.signal?.aborted) {
1713
+ throw new Error("approval poll aborted");
1714
+ }
1715
+ const status = await this.get(approvalToken);
1716
+ if (status.status !== "pending") return status;
1717
+ if (Date.now() > deadline) return status;
1718
+ await new Promise((r) => setTimeout(r, intervalMs));
1496
1719
  }
1497
- return res;
1498
1720
  }
1499
- /** Delete a blob. */
1500
- async delete(key) {
1501
- const path = `/v1/db/${this.namespace}/${this.slug}/blobs/${encodeBlobKey(key)}`;
1502
- await this.client.request("DELETE", path);
1721
+ /**
1722
+ * Long-running poll subscription for new approval events on this
1723
+ * database. Returns a `stop()` handle. Webhook delivery is the push
1724
+ * path (events `approval_required` / `approval_resolved`); subscribe
1725
+ * is the pull fallback for clients that can't host a webhook.
1726
+ *
1727
+ * Today this iterates over a known set of tokens supplied by the
1728
+ * caller; an SDK-only client that wants discovery should pair this
1729
+ * with the webhook path. Pass tokens you already hold (e.g. from a
1730
+ * prior `ApprovalRequiredError`).
1731
+ */
1732
+ subscribe(tokens, opts) {
1733
+ const intervalMs = Math.max(1e3, opts.intervalMs ?? 5e3);
1734
+ let cancelled = false;
1735
+ const onAbort = () => {
1736
+ cancelled = true;
1737
+ };
1738
+ opts.signal?.addEventListener("abort", onAbort);
1739
+ const tick = async () => {
1740
+ for (const token of tokens) {
1741
+ if (cancelled) return;
1742
+ try {
1743
+ const status = await this.get(token);
1744
+ const prev = this.seen.get(token);
1745
+ if (status.status !== prev) {
1746
+ this.seen.set(token, status.status);
1747
+ const event = {
1748
+ approvalToken: token,
1749
+ status: status.status,
1750
+ hits: status.hits
1751
+ };
1752
+ if (status.status === "pending") {
1753
+ opts.onApprovalRequired?.(event);
1754
+ } else {
1755
+ opts.onApprovalResolved?.(event);
1756
+ }
1757
+ }
1758
+ } catch (e) {
1759
+ opts.onError?.(e instanceof Error ? e : new Error(String(e)));
1760
+ }
1761
+ }
1762
+ };
1763
+ const id = setInterval(() => {
1764
+ void tick();
1765
+ }, intervalMs);
1766
+ void tick();
1767
+ return {
1768
+ stop: () => {
1769
+ cancelled = true;
1770
+ clearInterval(id);
1771
+ opts.signal?.removeEventListener("abort", onAbort);
1772
+ }
1773
+ };
1503
1774
  }
1504
- /** List blobs by prefix. */
1505
- async list(opts = {}) {
1506
- const params = new URLSearchParams();
1507
- if (opts.prefix) params.set("prefix", opts.prefix);
1508
- if (opts.cursor) params.set("cursor", opts.cursor);
1509
- if (opts.limit) params.set("limit", String(opts.limit));
1510
- const qs = params.toString();
1511
- const path = `/v1/db/${this.namespace}/${this.slug}/blobs` + (qs ? `?${qs}` : "");
1512
- return this.client.request("GET", path);
1775
+ /**
1776
+ * Redeem an approved approvalToken. The bearer must be the same one
1777
+ * that minted it, and the request must target the same database.
1778
+ * Returns the result of the originally-blocked query or batch.
1779
+ */
1780
+ async redeem(approvalToken) {
1781
+ const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
1782
+ approvalToken
1783
+ });
1784
+ if (Array.isArray(raw)) {
1785
+ return raw.map((r) => ({
1786
+ ...r,
1787
+ data: rowsToObjects(r.columns, r.rows)
1788
+ }));
1789
+ }
1790
+ return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
1513
1791
  }
1514
1792
  };
1515
- function encodeBlobKey(key) {
1516
- return key.split("/").map(encodeURIComponent).join("/");
1517
- }
1518
- var PerSQLVectors = class {
1793
+ var PerSQLApprovalRules = class {
1519
1794
  constructor(client, namespace, slug) {
1520
1795
  this.client = client;
1521
1796
  this.namespace = namespace;
1522
1797
  this.slug = slug;
1523
1798
  }
1524
- /** Embed and upsert up to 100 items in one call. */
1525
- async upsert(items) {
1799
+ async list() {
1526
1800
  return this.client.request(
1527
- "POST",
1528
- `/v1/db/${this.namespace}/${this.slug}/vectors`,
1529
- { items }
1801
+ "GET",
1802
+ `/v1/db/${this.namespace}/${this.slug}/approval-rules`
1530
1803
  );
1531
1804
  }
1532
- /** Top-K nearest neighbours by free-text query. */
1533
- async query(text, opts = {}) {
1805
+ /** Requires an admin-role bearer token. */
1806
+ async create(input) {
1534
1807
  return this.client.request(
1535
1808
  "POST",
1536
- `/v1/db/${this.namespace}/${this.slug}/vectors/query`,
1537
- { query: text, topK: opts.topK, filter: opts.filter }
1809
+ `/v1/db/${this.namespace}/${this.slug}/approval-rules`,
1810
+ input
1538
1811
  );
1539
1812
  }
1540
- /** Delete vectors by id. Up to 1000 ids per call. */
1541
- async delete(ids) {
1813
+ /** Requires an admin-role bearer token. */
1814
+ async delete(ruleId) {
1542
1815
  return this.client.request(
1543
1816
  "DELETE",
1544
- `/v1/db/${this.namespace}/${this.slug}/vectors`,
1545
- { ids }
1817
+ `/v1/db/${this.namespace}/${this.slug}/approval-rules/${encodeURIComponent(
1818
+ ruleId
1819
+ )}`
1546
1820
  );
1547
1821
  }
1548
1822
  };
1549
- var PerSQLApprovals = class {
1550
- constructor(client, namespace, slug) {
1551
- this.client = client;
1552
- this.namespace = namespace;
1553
- this.slug = slug;
1554
- }
1555
- /**
1556
- * Redeem an approved approvalToken. The bearer must be the same one
1557
- * that minted it, and the request must target the same database.
1558
- * Returns the result of the originally-blocked query or batch.
1559
- */
1560
- async redeem(approvalToken) {
1561
- const raw = await this.client.request("POST", `/v1/db/${this.namespace}/${this.slug}/redeem_approval`, {
1562
- approvalToken
1563
- });
1564
- if (Array.isArray(raw)) {
1565
- return raw.map((r) => ({
1566
- ...r,
1567
- data: rowsToObjects(r.columns, r.rows)
1568
- }));
1569
- }
1570
- return { ...raw, data: rowsToObjects(raw.columns, raw.rows) };
1571
- }
1572
- };
1573
1823
  var PerSQLProposals = class {
1574
1824
  constructor(client, namespace, slug) {
1575
1825
  this.client = client;
@@ -1581,6 +1831,11 @@ var PerSQLProposals = class {
1581
1831
  * rows, and returns a single-use `executionToken` valid for 10 min
1582
1832
  * (override with `ttlSec`, max 1 hour). Nothing is mutated until
1583
1833
  * `apply()` is called.
1834
+ *
1835
+ * Local mode keeps the token in-process: single-use still holds, but
1836
+ * `estimatedAffectedRows` is always `null` (no server estimator) and a
1837
+ * stale/unknown token throws a plain `Error` rather than the HTTP
1838
+ * 404/403 of the server path.
1584
1839
  */
1585
1840
  async propose(sql, opts = {}) {
1586
1841
  if (this.client.local) {