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