get-db9 0.5.0 → 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/client.cjs CHANGED
@@ -48,7 +48,7 @@ var Db9Error = class _Db9Error extends Error {
48
48
  let message;
49
49
  try {
50
50
  const body = await response.json();
51
- message = body.message || response.statusText;
51
+ message = body.message || body.detail || body.error_description || body.error || response.statusText;
52
52
  } catch {
53
53
  message = response.statusText;
54
54
  }
@@ -219,17 +219,7 @@ var FileCredentialStore = class {
219
219
  const parsed = (0, import_toml.parse)(content);
220
220
  const token = parsed["token"];
221
221
  if (typeof token !== "string") return null;
222
- const creds = { token };
223
- if (typeof parsed["is_anonymous"] === "boolean") {
224
- creds.is_anonymous = parsed["is_anonymous"];
225
- }
226
- if (typeof parsed["anonymous_id"] === "string") {
227
- creds.anonymous_id = parsed["anonymous_id"];
228
- }
229
- if (typeof parsed["anonymous_secret"] === "string") {
230
- creds.anonymous_secret = parsed["anonymous_secret"];
231
- }
232
- return creds;
222
+ return { token };
233
223
  }
234
224
  async save(credentials) {
235
225
  const fs = await import("fs/promises");
@@ -250,15 +240,6 @@ var FileCredentialStore = class {
250
240
  if (err.code !== "ENOENT") throw err;
251
241
  }
252
242
  data["token"] = credentials.token;
253
- if (credentials.is_anonymous !== void 0) {
254
- data["is_anonymous"] = credentials.is_anonymous;
255
- }
256
- if (credentials.anonymous_id !== void 0) {
257
- data["anonymous_id"] = credentials.anonymous_id;
258
- }
259
- if (credentials.anonymous_secret !== void 0) {
260
- data["anonymous_secret"] = credentials.anonymous_secret;
261
- }
262
243
  const toml = (0, import_toml.stringify)(
263
244
  data
264
245
  );
@@ -278,9 +259,303 @@ function defaultCredentialStore() {
278
259
  return new FileCredentialStore();
279
260
  }
280
261
 
262
+ // src/ws.ts
263
+ var import_node_crypto = require("crypto");
264
+ var FsError = class extends Error {
265
+ code;
266
+ constructor(code, message) {
267
+ super(message);
268
+ this.name = "FsError";
269
+ this.code = code;
270
+ }
271
+ };
272
+ var STREAMING_THRESHOLD = 1024 * 1024;
273
+ var DEFAULT_CHUNK_SIZE = 64 * 1024;
274
+ var nextId = 1;
275
+ function nextRequestId() {
276
+ return String(nextId++);
277
+ }
278
+ function toBase64(data) {
279
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
280
+ return Buffer.from(bytes).toString("base64");
281
+ }
282
+ function fromBase64(b64) {
283
+ return new Uint8Array(Buffer.from(b64, "base64"));
284
+ }
285
+ function computeChecksum(data) {
286
+ const hash = (0, import_node_crypto.createHash)("sha256").update(data).digest("hex");
287
+ return `sha256:${hash}`;
288
+ }
289
+ function encodeBinaryFrame(streamId, chunk) {
290
+ const frame = Buffer.alloc(8 + chunk.length);
291
+ frame.writeBigUInt64BE(BigInt(streamId), 0);
292
+ frame.set(chunk, 8);
293
+ return frame;
294
+ }
295
+ var FsClient = class _FsClient {
296
+ ws;
297
+ pending = /* @__PURE__ */ new Map();
298
+ closed = false;
299
+ constructor(ws) {
300
+ this.ws = ws;
301
+ ws.onmessage = (ev) => {
302
+ const text = typeof ev.data === "string" ? ev.data : String(ev.data);
303
+ let resp;
304
+ try {
305
+ resp = JSON.parse(text);
306
+ } catch {
307
+ return;
308
+ }
309
+ const p = this.pending.get(resp.id);
310
+ if (p) {
311
+ this.pending.delete(resp.id);
312
+ p.resolve(resp);
313
+ }
314
+ };
315
+ ws.onclose = () => {
316
+ this.closed = true;
317
+ for (const [, p] of this.pending) {
318
+ p.reject(new FsError("CONNECTION_CLOSED", "WebSocket connection closed"));
319
+ }
320
+ this.pending.clear();
321
+ };
322
+ ws.onerror = () => {
323
+ };
324
+ }
325
+ /**
326
+ * Connect to an fs9 WebSocket server.
327
+ *
328
+ * @param url WebSocket URL, e.g. `wss://host:5480`
329
+ * @param WS WebSocket constructor (native or from `ws` package)
330
+ */
331
+ static connect(url, WS) {
332
+ return new Promise((resolve, reject) => {
333
+ const ws = new WS(url);
334
+ ws.onopen = () => {
335
+ ws.onopen = null;
336
+ ws.onerror = null;
337
+ resolve(new _FsClient(ws));
338
+ };
339
+ ws.onerror = (ev) => {
340
+ ws.onopen = null;
341
+ ws.onerror = null;
342
+ const msg = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket connection failed";
343
+ reject(new FsError("CONNECTION_ERROR", msg));
344
+ };
345
+ });
346
+ }
347
+ /** Authenticate with the server. Must be called first after connect. */
348
+ async authenticate(username, password) {
349
+ const resp = await this.sendAndRecv({
350
+ id: nextRequestId(),
351
+ op: "auth",
352
+ username,
353
+ password
354
+ });
355
+ if (!resp.ok) {
356
+ throw new FsError("AUTH_FAILED", this.errorMessage(resp));
357
+ }
358
+ const data = resp.data;
359
+ return {
360
+ user: String(data?.user ?? ""),
361
+ tenant: String(data?.tenant ?? ""),
362
+ keyspace: String(data?.keyspace ?? "")
363
+ };
364
+ }
365
+ /** Get file or directory metadata. */
366
+ async stat(path) {
367
+ const resp = await this.sendAndRecv({
368
+ id: nextRequestId(),
369
+ op: "stat",
370
+ path
371
+ });
372
+ this.expectOk(resp);
373
+ return resp.data;
374
+ }
375
+ /** List directory contents. */
376
+ async readdir(path) {
377
+ const resp = await this.sendAndRecv({
378
+ id: nextRequestId(),
379
+ op: "readdir",
380
+ path
381
+ });
382
+ this.expectOk(resp);
383
+ const data = resp.data;
384
+ return data?.entries ?? [];
385
+ }
386
+ /** Create a directory. Always recursive (mkdir -p). */
387
+ async mkdir(path, recursive = true) {
388
+ const resp = await this.sendAndRecv({
389
+ id: nextRequestId(),
390
+ op: "mkdir",
391
+ path,
392
+ recursive
393
+ });
394
+ this.expectOk(resp);
395
+ }
396
+ /** Read an entire file, returning raw bytes. */
397
+ async readFile(path) {
398
+ const resp = await this.sendAndRecv({
399
+ id: nextRequestId(),
400
+ op: "read",
401
+ path
402
+ });
403
+ this.expectOk(resp);
404
+ const data = resp.data;
405
+ const content = data?.content;
406
+ if (typeof content !== "string") {
407
+ throw new FsError("PROTOCOL", "missing content field in read response");
408
+ }
409
+ return fromBase64(content);
410
+ }
411
+ /**
412
+ * Write (overwrite) a file. Returns bytes written.
413
+ *
414
+ * Automatically uses streaming mode for files >= 1 MB to avoid base64
415
+ * overhead and bypass the 2 MB JSON frame limit.
416
+ */
417
+ async writeFile(path, data) {
418
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
419
+ if (bytes.byteLength >= STREAMING_THRESHOLD) {
420
+ return this.writeFileStreaming(path, bytes);
421
+ }
422
+ const resp = await this.sendAndRecv({
423
+ id: nextRequestId(),
424
+ op: "write",
425
+ path,
426
+ content: toBase64(bytes),
427
+ encoding: "base64"
428
+ });
429
+ this.expectOk(resp);
430
+ const result = resp.data;
431
+ return result?.written ?? bytes.byteLength;
432
+ }
433
+ /** Append to a file. Returns bytes written. */
434
+ async appendFile(path, data) {
435
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
436
+ const resp = await this.sendAndRecv({
437
+ id: nextRequestId(),
438
+ op: "append",
439
+ path,
440
+ content: toBase64(bytes),
441
+ encoding: "base64"
442
+ });
443
+ this.expectOk(resp);
444
+ const result = resp.data;
445
+ return result?.written ?? bytes.byteLength;
446
+ }
447
+ /** Remove a file (non-recursive) or directory (recursive). */
448
+ async rm(path, recursive = false) {
449
+ const resp = await this.sendAndRecv(
450
+ recursive ? { id: nextRequestId(), op: "rm", path, recursive: true } : { id: nextRequestId(), op: "unlink", path }
451
+ );
452
+ this.expectOk(resp);
453
+ }
454
+ /** Rename (move) a file or directory. */
455
+ async rename(oldPath, newPath) {
456
+ const resp = await this.sendAndRecv({
457
+ id: nextRequestId(),
458
+ op: "rename",
459
+ old_path: oldPath,
460
+ new_path: newPath
461
+ });
462
+ this.expectOk(resp);
463
+ }
464
+ /** Gracefully close the WebSocket connection. */
465
+ async close() {
466
+ if (!this.closed) {
467
+ this.ws.close();
468
+ }
469
+ }
470
+ // ── streaming internals ────────────────────────────────────────
471
+ /**
472
+ * Write a file using streaming mode (binary frames, no base64).
473
+ *
474
+ * Protocol:
475
+ * 1. Send streaming write request with file size
476
+ * 2. Receive ready response with stream_id and chunk_size
477
+ * 3. Send binary frames: [8-byte stream_id BE][chunk_data]
478
+ * 4. Send stream end with checksum
479
+ * 5. Receive final write confirmation
480
+ */
481
+ async writeFileStreaming(path, bytes) {
482
+ const requestId = nextRequestId();
483
+ const readyResp = await this.sendAndRecv({
484
+ id: requestId,
485
+ op: "write",
486
+ path,
487
+ streaming: true,
488
+ size: bytes.byteLength
489
+ });
490
+ this.expectOk(readyResp);
491
+ const ready = readyResp.data;
492
+ if (!ready?.ready) {
493
+ throw new FsError("PROTOCOL", "server not ready for streaming");
494
+ }
495
+ const streamId = ready.stream_id;
496
+ const chunkSize = ready.chunk_size > 0 ? ready.chunk_size : DEFAULT_CHUNK_SIZE;
497
+ try {
498
+ for (let offset = 0; offset < bytes.byteLength; offset += chunkSize) {
499
+ const chunk = bytes.subarray(offset, offset + chunkSize);
500
+ const frame = encodeBinaryFrame(streamId, chunk);
501
+ this.ws.send(frame);
502
+ }
503
+ } catch (err) {
504
+ this.tryAbortStream(streamId);
505
+ throw new FsError("CONNECTION_ERROR", `send chunk: ${err}`);
506
+ }
507
+ const checksum = computeChecksum(bytes);
508
+ const finalResp = await this.sendAndRecv({
509
+ id: requestId,
510
+ stream: "end",
511
+ stream_id: streamId,
512
+ checksum
513
+ });
514
+ this.expectOk(finalResp);
515
+ const result = finalResp.data;
516
+ return result?.written ?? bytes.byteLength;
517
+ }
518
+ /** Best-effort abort of an in-progress stream so the server can clean up. */
519
+ tryAbortStream(streamId) {
520
+ try {
521
+ this.ws.send(JSON.stringify({ stream: "abort", stream_id: streamId }));
522
+ } catch {
523
+ }
524
+ }
525
+ // ── internals ──────────────────────────────────────────────────
526
+ sendAndRecv(request) {
527
+ if (this.closed) {
528
+ return Promise.reject(new FsError("CONNECTION_CLOSED", "WebSocket is closed"));
529
+ }
530
+ return new Promise((resolve, reject) => {
531
+ const id = request.id;
532
+ this.pending.set(id, { resolve, reject });
533
+ try {
534
+ this.ws.send(JSON.stringify(request));
535
+ } catch (err) {
536
+ this.pending.delete(id);
537
+ reject(new FsError("CONNECTION_ERROR", String(err)));
538
+ }
539
+ });
540
+ }
541
+ expectOk(resp) {
542
+ if (!resp.ok) {
543
+ const detail = resp.error;
544
+ throw new FsError(
545
+ detail?.code ?? "UNKNOWN",
546
+ detail?.message ?? "unknown error"
547
+ );
548
+ }
549
+ }
550
+ errorMessage(resp) {
551
+ const detail = resp.error;
552
+ return detail ? `${detail.code}: ${detail.message}` : "unknown error";
553
+ }
554
+ };
555
+
281
556
  // src/client.ts
282
557
  function createDb9Client(options = {}) {
283
- const baseUrl = options.baseUrl ?? "https://db9.shared.aws.tidbcloud.com/api";
558
+ const baseUrl = options.baseUrl ?? "https://db9.ai/api";
284
559
  let token = options.token;
285
560
  let tokenLoaded = !!token;
286
561
  const store = options.credentialStore ?? defaultCredentialStore();
@@ -299,16 +574,10 @@ function createDb9Client(options = {}) {
299
574
  tokenLoaded = true;
300
575
  }
301
576
  if (!token) {
302
- const reg = await publicClient.post(
303
- "/customer/anonymous-register"
577
+ throw new Db9Error(
578
+ "No token available. Run `db9 login` or pass a token via Db9ClientOptions.",
579
+ 401
304
580
  );
305
- token = reg.token;
306
- await store.save({
307
- token: reg.token,
308
- is_anonymous: reg.is_anonymous,
309
- anonymous_id: reg.anonymous_id,
310
- anonymous_secret: reg.anonymous_secret
311
- });
312
581
  }
313
582
  return createHttpClient({
314
583
  baseUrl,
@@ -319,82 +588,46 @@ function createDb9Client(options = {}) {
319
588
  retryDelay: options.retryDelay
320
589
  });
321
590
  }
322
- let refreshPromise = null;
323
- async function refreshAnonymousToken() {
324
- const creds = await store.load();
325
- if (!creds?.anonymous_id || !creds?.anonymous_secret) {
326
- throw new Error("Not an anonymous session");
327
- }
328
- const resp = await publicClient.post(
329
- "/customer/anonymous-refresh",
330
- {
331
- anonymous_id: creds.anonymous_id,
332
- anonymous_secret: creds.anonymous_secret
333
- }
334
- );
335
- token = resp.token;
336
- await store.save({ ...creds, token: resp.token });
337
- }
338
591
  async function withAuthRetry(operation) {
339
592
  const client = await getAuthClient();
340
- try {
341
- return await operation(client);
342
- } catch (err) {
343
- if (!(err instanceof Db9Error) || err.statusCode !== 401) {
344
- throw err;
345
- }
346
- try {
347
- if (!refreshPromise) {
348
- refreshPromise = refreshAnonymousToken();
349
- }
350
- await refreshPromise;
351
- } catch {
352
- throw err;
353
- } finally {
354
- refreshPromise = null;
355
- }
356
- const newClient = await getAuthClient();
357
- return operation(newClient);
358
- }
359
- }
360
- function deriveFs9Url(dbId) {
361
- const origin = baseUrl.replace(/\/api\/?$/, "");
362
- return `${origin}/fs9/${dbId}`;
593
+ return operation(client);
363
594
  }
364
- function getFsClient(dbId) {
365
- const fs9Base = deriveFs9Url(dbId) + "/api/v1";
366
- return createHttpClient({
367
- baseUrl: fs9Base,
368
- fetch: options.fetch,
369
- headers: token ? { Authorization: `Bearer ${token}` } : {},
370
- timeout: options.timeout,
371
- maxRetries: options.maxRetries,
372
- retryDelay: options.retryDelay
373
- });
595
+ const fsWsPort = options.wsPort ?? 5480;
596
+ async function resolveFsConn(dbId) {
597
+ const creds = await withAuthRetry(
598
+ (client) => client.get(
599
+ `/customer/databases/${dbId}/credentials`
600
+ )
601
+ );
602
+ const connStr = creds.connection_string;
603
+ const hostMatch = connStr.match(/@([^:/?]+)/);
604
+ const host = hostMatch?.[1];
605
+ if (!host) {
606
+ throw new Error(`Cannot parse host from connection string for database '${dbId}'`);
607
+ }
608
+ const userMatch = connStr.match(/:\/\/([^:@]+)/);
609
+ const username = userMatch?.[1] ?? creds.admin_user;
610
+ const protocol = host === "localhost" || host === "127.0.0.1" ? "ws" : "wss";
611
+ return {
612
+ wsUrl: `${protocol}://${host}:${fsWsPort}`,
613
+ username,
614
+ password: creds.admin_password
615
+ };
374
616
  }
375
- async function withFsAuthRetry(dbId, operation) {
376
- if (!token && !tokenLoaded) {
377
- await getAuthClient();
617
+ async function withFsClient(dbId, operation) {
618
+ const WS = options.WebSocket ?? (typeof globalThis !== "undefined" ? globalThis.WebSocket : void 0);
619
+ if (!WS) {
620
+ throw new Error(
621
+ "WebSocket constructor not available. Pass `WebSocket` in Db9ClientOptions, or use Node 21+ / a browser environment with native WebSocket support, or install the `ws` package for Node 18\u201320."
622
+ );
378
623
  }
379
- const client = getFsClient(dbId);
624
+ const conn = await resolveFsConn(dbId);
625
+ const client = await FsClient.connect(conn.wsUrl, WS);
380
626
  try {
627
+ await client.authenticate(conn.username, conn.password);
381
628
  return await operation(client);
382
- } catch (err) {
383
- if (!(err instanceof Db9Error) || err.statusCode !== 401) {
384
- throw err;
385
- }
386
- try {
387
- if (!refreshPromise) {
388
- refreshPromise = refreshAnonymousToken();
389
- }
390
- await refreshPromise;
391
- } catch {
392
- throw err;
393
- } finally {
394
- refreshPromise = null;
395
- }
396
- const newClient = getFsClient(dbId);
397
- return operation(newClient);
629
+ } finally {
630
+ await client.close();
398
631
  }
399
632
  }
400
633
  function parseSqlError(raw) {
@@ -415,50 +648,11 @@ function createDb9Client(options = {}) {
415
648
  }
416
649
  return { message: raw };
417
650
  }
418
- async function fsStat(dbId, path) {
419
- return withFsAuthRetry(
420
- dbId,
421
- (client) => client.get("/stat", { path })
422
- );
423
- }
424
- async function fetchAnonymousSecret() {
425
- return withAuthRetry(
426
- (client) => client.post("/customer/anonymous-secret", {})
427
- );
428
- }
429
651
  return {
430
652
  auth: {
431
- // Public endpoints (no token required)
432
- register: (req) => publicClient.post("/customer/register", req),
433
- login: (req) => publicClient.post("/customer/login", req),
434
- anonymousRegister: () => publicClient.post(
435
- "/customer/anonymous-register"
436
- ),
437
- anonymousRefresh: (req) => publicClient.post(
438
- "/customer/anonymous-refresh",
439
- req
440
- ),
441
- // Authenticated endpoints
442
653
  me: async () => withAuthRetry(
443
654
  (client) => client.get("/customer/me")
444
- ),
445
- getAnonymousSecret: () => {
446
- return fetchAnonymousSecret();
447
- },
448
- claim: async (req) => withAuthRetry(
449
- (client) => client.post("/customer/claim", req)
450
- ),
451
- ensureAnonymousSecret: async () => {
452
- const creds = await store.load();
453
- if (!creds?.anonymous_id || creds.anonymous_secret) {
454
- return;
455
- }
456
- const resp = await fetchAnonymousSecret();
457
- await store.save({
458
- ...creds,
459
- anonymous_secret: resp.anonymous_secret
460
- });
461
- }
655
+ )
462
656
  },
463
657
  tokens: {
464
658
  list: async () => withAuthRetry(
@@ -494,6 +688,11 @@ function createDb9Client(options = {}) {
494
688
  `/customer/databases/${databaseId}/reset-password`
495
689
  )
496
690
  ),
691
+ credentials: async (databaseId) => withAuthRetry(
692
+ (client) => client.get(
693
+ `/customer/databases/${databaseId}/credentials`
694
+ )
695
+ ),
497
696
  observability: async (databaseId) => withAuthRetry(
498
697
  (client) => client.get(
499
698
  `/customer/databases/${databaseId}/observability`
@@ -576,69 +775,62 @@ function createDb9Client(options = {}) {
576
775
  }
577
776
  },
578
777
  fs: {
579
- list: async (dbId, path, options2) => {
580
- const params = { path };
581
- if (options2?.recursive) params.recursive = "true";
582
- return withFsAuthRetry(
583
- dbId,
584
- (client) => client.get("/readdir", params)
585
- );
586
- },
587
- read: async (dbId, path) => {
588
- return withFsAuthRetry(dbId, async (client) => {
589
- const resp = await client.getRaw("/download", { path });
590
- return resp.text();
591
- });
592
- },
593
- readBinary: async (dbId, path) => {
594
- return withFsAuthRetry(dbId, async (client) => {
595
- const resp = await client.getRaw("/download", { path });
596
- return resp.arrayBuffer();
597
- });
778
+ /**
779
+ * Open a persistent WebSocket connection for multiple fs operations.
780
+ * Caller is responsible for calling `client.close()` when done.
781
+ */
782
+ connect: async (dbId) => {
783
+ const WS = options.WebSocket ?? (typeof globalThis !== "undefined" ? globalThis.WebSocket : void 0);
784
+ if (!WS) {
785
+ throw new Error(
786
+ "WebSocket constructor not available. Pass `WebSocket` in Db9ClientOptions, or use Node 21+ / a browser environment with native WebSocket support, or install the `ws` package for Node 18\u201320."
787
+ );
788
+ }
789
+ const conn = await resolveFsConn(dbId);
790
+ const client = await FsClient.connect(conn.wsUrl, WS);
791
+ await client.authenticate(conn.username, conn.password);
792
+ return client;
598
793
  },
794
+ /** List directory contents. */
795
+ list: async (dbId, path) => withFsClient(dbId, (client) => client.readdir(path)),
796
+ /** Read a file as text (UTF-8). */
797
+ read: async (dbId, path) => withFsClient(dbId, async (client) => {
798
+ const bytes = await client.readFile(path);
799
+ return new TextDecoder().decode(bytes);
800
+ }),
801
+ /** Read a file as raw bytes. */
802
+ readBinary: async (dbId, path) => withFsClient(dbId, (client) => client.readFile(path)),
803
+ /** Write (overwrite) a file. Accepts string, ArrayBuffer, or Uint8Array. */
599
804
  write: async (dbId, path, content) => {
600
- const contentType = typeof content === "string" ? "text/plain" : "application/octet-stream";
601
- await withFsAuthRetry(
602
- dbId,
603
- (client) => client.putRaw(`/upload?${new URLSearchParams({ path })}`, content, { "Content-Type": contentType })
604
- );
605
- },
606
- stat: (dbId, path) => {
607
- return fsStat(dbId, path);
805
+ await withFsClient(dbId, (client) => client.writeFile(path, content));
608
806
  },
807
+ /** Append to a file. Returns bytes written. */
808
+ append: async (dbId, path, content) => withFsClient(dbId, (client) => client.appendFile(path, content)),
809
+ /** Get file or directory metadata. */
810
+ stat: async (dbId, path) => withFsClient(dbId, (client) => client.stat(path)),
811
+ /** Check if a file or directory exists. */
609
812
  exists: async (dbId, path) => {
610
813
  try {
611
- await fsStat(dbId, path);
814
+ await withFsClient(dbId, (client) => client.stat(path));
612
815
  return true;
613
816
  } catch (err) {
614
- if (err instanceof Db9Error && err.statusCode === 404) {
817
+ if (err instanceof FsError && err.code === "ENOENT") {
615
818
  return false;
616
819
  }
617
820
  throw err;
618
821
  }
619
822
  },
823
+ /** Create a directory (recursive by default). */
620
824
  mkdir: async (dbId, path) => {
621
- await withFsAuthRetry(
622
- dbId,
623
- (client) => client.postRaw(`/mkdir?${new URLSearchParams({ path, recursive: "true" })}`)
624
- );
825
+ await withFsClient(dbId, (client) => client.mkdir(path, true));
625
826
  },
626
- remove: async (dbId, path) => {
627
- await withFsAuthRetry(
628
- dbId,
629
- (client) => client.delRaw("/remove", { path })
630
- );
827
+ /** Remove a file or directory. */
828
+ remove: async (dbId, path, opts) => {
829
+ await withFsClient(dbId, (client) => client.rm(path, opts?.recursive ?? false));
631
830
  },
632
- events: async (dbId, options2) => {
633
- const params = {};
634
- if (options2?.limit !== void 0) params.limit = String(options2.limit);
635
- if (options2?.offset !== void 0) params.offset = String(options2.offset);
636
- if (options2?.path) params.path = options2.path;
637
- if (options2?.type) params.type = options2.type;
638
- return withFsAuthRetry(
639
- dbId,
640
- (client) => client.get("/events", params)
641
- );
831
+ /** Rename (move) a file or directory. */
832
+ rename: async (dbId, oldPath, newPath) => {
833
+ await withFsClient(dbId, (client) => client.rename(oldPath, newPath));
642
834
  }
643
835
  }
644
836
  };