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