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