@compforge/harness-toolbox 0.3.9 → 0.3.11

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 CHANGED
@@ -16,6 +16,48 @@ claiming snapshot consistency; callers supply cancellation and decide how to per
16
16
  Protocol adapters have separate entry points; importing lifecycle or transport contracts does
17
17
  not load database or Kubernetes drivers.
18
18
 
19
+ ## File sources
20
+
21
+ Use `LocalFileDataSource` for a local file or `PodFileDataSource` for a file in an explicit
22
+ namespace/Pod/container. Both extend Common's `FileDataSource`, so a Service can store them directly
23
+ as DataSources and consumers can recognize files with `instanceof FileDataSource`. No additional
24
+ `id` or `kind` wrapper is required; consumer-owned selection names remain separate from client identity.
25
+
26
+ ```ts
27
+ import { ClientManager, FileDataSource } from "@compforge/harness-common";
28
+ import { LocalFileDataSource, PodFileDataSource } from "@compforge/harness-toolbox/file";
29
+
30
+ const source = new LocalFileDataSource("./config.yaml", { concurrency: 2 });
31
+ const clients = new ClientManager();
32
+ try {
33
+ if (source instanceof FileDataSource) {
34
+ const file = await clients.get(source);
35
+ const bytes = await file.read({ maxBytes: 1024 * 1024, timeoutMs: 15_000 });
36
+ console.log(source.path, new TextDecoder("utf-8", { fatal: true }).decode(bytes));
37
+ }
38
+ } finally {
39
+ await clients.dispose();
40
+ }
41
+
42
+ // `environment` is a KubernetesEnvironment (or an equivalent ClientProvider).
43
+ // Resolve Workloads first and create one source per chosen Pod/container.
44
+ const podSource = new PodFileDataSource(environment,
45
+ { namespace: "app", pod: "worker-abc", container: "worker" },
46
+ "/etc/app/config.yaml", { concurrency: 2 });
47
+ ```
48
+
49
+ Each read reopens the file; the manager shares clients, not previous file contents. The byte limit
50
+ rejects oversized reads, including a zero-byte limit on a nonempty file. Timeouts include queue time;
51
+ caller cancellation and disposal cancel active reads. Keep the source path and Pod target with any
52
+ persisted evidence. A successful read describes bytes at that location, not the application's parsed
53
+ or effective configuration and not an atomic filesystem snapshot.
54
+
55
+ Local files may be symlinks to regular files. Pod reads require `pods/exec` access and `head -c` in the
56
+ selected container; paths are passed as argv without a shell. A Pod name is an access location, not
57
+ a UID fence; callers needing incarnation guarantees must verify discovery around collection. Pod
58
+ sources borrow Kubernetes access from the root manager and never close it themselves. Unsupported
59
+ commands, missing paths and denied reads fail explicitly; they are not empty file results.
60
+
19
61
  ## Workload discovery
20
62
 
21
63
  Borrow a KubernetesClient through KubernetesEnvironment and the shared ClientManager lifecycle, then call
@@ -250,3 +292,13 @@ Disposal closes forwards and the exec stream; EOF closes remote listeners/connec
250
292
  connection is lost without EOF, a 30-second heartbeat lease bounds remote process lifetime.
251
293
  A dead relay invalidates its routes and fails callers; no SQL or other protocol operation is replayed.
252
294
  The root should finish or cancel protocol work before disposing the transport.
295
+
296
+ ### OpenSearch connection preparation
297
+
298
+ `openOpenSearch` probes the resolved endpoint before returning. It tries the supplied TCP transports
299
+ in order only for connection failures; HTTP authorization/server errors stop preparation, and failed
300
+ reads are never replayed on another route. `onRoute` reports the selected transport and safe fallback
301
+ reason. The caller owns shared transports such as `PodRelayTransport` and their cleanup.
302
+
303
+ Routed HTTPS preserves the original TLS server name. Bun disables HTTP keep-alive because its reused
304
+ socket recovery can replay a failed request below the driver; Node retains connection pooling.
@@ -12,6 +12,9 @@ class ToolboxError extends Error {
12
12
 
13
13
  class KubernetesError extends ToolboxError {
14
14
  }
15
+
16
+ class FileError extends ToolboxError {
17
+ }
15
18
  function kubernetesHttpError(status) {
16
19
  const kind = {
17
20
  401: "authentication_failed",
@@ -38,4 +41,4 @@ function kubernetesCommandError(result) {
38
41
  return status ? kubernetesHttpError(status) : new KubernetesError("Kubernetes resource request failed", { kind: "operation_failed" });
39
42
  }
40
43
 
41
- export { ToolboxError, KubernetesError, kubernetesHttpError, kubernetesCommandError };
44
+ export { ToolboxError, KubernetesError, FileError, kubernetesHttpError, kubernetesCommandError };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  KubernetesError
3
- } from "./chunk-zhkrydeg.js";
3
+ } from "./chunk-20kv4wc3.js";
4
4
 
5
5
  // src/kubernetes/workload.ts
6
6
  function failure(message, kind) {
@@ -1,15 +1,30 @@
1
+ import {
2
+ ToolboxError
3
+ } from "./chunk-20kv4wc3.js";
4
+ import {
5
+ isConnectionNetworkError
6
+ } from "./chunk-h8fwx0k7.js";
7
+
1
8
  // src/opensearch/client.ts
2
- import { Client } from "@opensearch-project/opensearch";
9
+ import { Client, Connection, errors } from "@opensearch-project/opensearch";
3
10
  function isOpenSearchReadApi(search) {
4
11
  return typeof search.request === "function";
5
12
  }
13
+
14
+ class OpenSearchConnection extends Connection {
15
+ buildRequestObject(params) {
16
+ return { ...super.buildRequestObject(params), servername: this.ssl?.servername };
17
+ }
18
+ }
6
19
  function createClient(options) {
7
20
  return new Client({
8
21
  node: options.node,
22
+ Connection: OpenSearchConnection,
9
23
  auth: options.auth.username && options.auth.password ? { username: options.auth.username, password: options.auth.password } : undefined,
10
- ssl: { rejectUnauthorized: false },
24
+ ssl: { rejectUnauthorized: false, servername: options.servername },
11
25
  requestTimeout: options.requestTimeoutMs ?? 60000,
12
- maxRetries: 0
26
+ maxRetries: 0,
27
+ agent: { keepAlive: !process.versions.bun }
13
28
  });
14
29
  }
15
30
 
@@ -36,8 +51,18 @@ class OpenSearchEngine {
36
51
  });
37
52
  return response.body;
38
53
  }
39
- ping() {
40
- return this.client.ping();
54
+ async ping(signal, timeoutMs) {
55
+ signal?.throwIfAborted();
56
+ const request = this.client.ping({}, timeoutMs === undefined ? undefined : { requestTimeout: timeoutMs });
57
+ const abort = () => {
58
+ request.abort?.();
59
+ };
60
+ signal?.addEventListener("abort", abort, { once: true });
61
+ try {
62
+ return await request;
63
+ } finally {
64
+ signal?.removeEventListener("abort", abort);
65
+ }
41
66
  }
42
67
  close() {
43
68
  return this.client.close();
@@ -46,6 +71,7 @@ class OpenSearchEngine {
46
71
 
47
72
  class OpenSearchClient {
48
73
  source;
74
+ lifecycle;
49
75
  #controller = new AbortController;
50
76
  signal;
51
77
  #initialization;
@@ -54,6 +80,7 @@ class OpenSearchClient {
54
80
  #operations = new Set;
55
81
  constructor(source, lifecycle = {}) {
56
82
  this.source = source;
83
+ this.lifecycle = lifecycle;
57
84
  this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
58
85
  }
59
86
  initialize() {
@@ -61,15 +88,42 @@ class OpenSearchClient {
61
88
  return this.#initialization ??= (async () => {
62
89
  const target = await this.source.resolve();
63
90
  this.signal.throwIfAborted();
64
- const transport = this.source.transports[0];
65
- if (!transport || transport.kind !== "tcp")
66
- throw new Error("OpenSearch requires a TCP transport");
67
- const url = new URL(target.node);
68
- const endpoint = await transport.connect({ host: url.hostname, port: Number(url.port || (url.protocol === "https:" ? 443 : 80)) });
69
- this.signal.throwIfAborted();
70
- url.hostname = endpoint.host;
71
- url.port = String(endpoint.port);
72
- this.#engine = new OpenSearchEngine({ ...target, node: url.toString() });
91
+ const original = new URL(target.node);
92
+ let reason;
93
+ for (const [index, transport] of this.source.transports.entries()) {
94
+ this.signal.throwIfAborted();
95
+ if (transport.kind !== "tcp")
96
+ throw new Error("OpenSearch requires a TCP transport");
97
+ let engine;
98
+ try {
99
+ const endpoint = await transport.connect({
100
+ host: original.hostname,
101
+ port: Number(original.port || (original.protocol === "https:" ? 443 : 80)),
102
+ servername: target.servername ?? original.hostname
103
+ });
104
+ this.signal.throwIfAborted();
105
+ const url = new URL(original);
106
+ url.hostname = endpoint.host;
107
+ url.port = String(endpoint.port);
108
+ engine = new OpenSearchEngine({ ...target, node: url.toString(), servername: endpoint.servername ?? target.servername ?? original.hostname });
109
+ await engine.ping(this.signal, target.connectTimeoutMs ?? target.requestTimeoutMs ?? 1e4);
110
+ this.signal.throwIfAborted();
111
+ this.lifecycle.onRoute?.({ transport: transport.name, reason });
112
+ this.#engine = engine;
113
+ return;
114
+ } catch (error) {
115
+ await engine?.close();
116
+ this.signal.throwIfAborted();
117
+ const network = isConnectionNetworkError(error);
118
+ if (!network && !(error instanceof errors.ConnectionError) && !(error instanceof errors.TimeoutError))
119
+ throw error;
120
+ reason = network ? String(error.code) : error.name;
121
+ if (index === this.source.transports.length - 1) {
122
+ throw new ToolboxError(`OpenSearch connect failed (${reason})`, { kind: "connection_failed", code: reason, cause: error });
123
+ }
124
+ }
125
+ }
126
+ throw new Error("OpenSearch DataSource has no transport");
73
127
  })();
74
128
  }
75
129
  #ready() {
@@ -2,7 +2,7 @@ import {
2
2
  KubernetesError,
3
3
  ToolboxError,
4
4
  kubernetesHttpError
5
- } from "./chunk-zhkrydeg.js";
5
+ } from "./chunk-20kv4wc3.js";
6
6
  import {
7
7
  ConcurrencyPool
8
8
  } from "./chunk-v3hxvnrc.js";
@@ -1,15 +1,15 @@
1
+ import {
2
+ resolveWorkload
3
+ } from "./chunk-8axd2y2m.js";
1
4
  import {
2
5
  ServicePortForwarder
3
6
  } from "./chunk-2171tajs.js";
4
7
  import {
5
8
  resourceAccess
6
- } from "./chunk-fhtbvj0s.js";
7
- import {
8
- resolveWorkload
9
- } from "./chunk-p6k59m3q.js";
9
+ } from "./chunk-9n9z2pty.js";
10
10
  import {
11
11
  KubernetesError
12
- } from "./chunk-zhkrydeg.js";
12
+ } from "./chunk-20kv4wc3.js";
13
13
  import {
14
14
  KubectlExecutor
15
15
  } from "./chunk-hmah9t8e.js";
@@ -0,0 +1,60 @@
1
+ import {
2
+ BoundedFileClient,
3
+ fileClientOptions
4
+ } from "./chunk-mzdz6wwc.js";
5
+ import {
6
+ FileError
7
+ } from "./chunk-20kv4wc3.js";
8
+
9
+ // src/file/local.ts
10
+ import { constants } from "node:fs";
11
+ import { open } from "node:fs/promises";
12
+ import { resolve } from "node:path";
13
+ import { clientKey, FileDataSource } from "@compforge/harness-common";
14
+ class LocalFileClient extends BoundedFileClient {
15
+ async capture(signal, limits, receive) {
16
+ try {
17
+ const file = await open(this.path, constants.O_RDONLY | constants.O_NONBLOCK);
18
+ try {
19
+ signal.throwIfAborted();
20
+ if (!(await file.stat()).isFile()) {
21
+ throw new FileError(`Not a regular file: ${this.path}`, { kind: "unsupported_operation" });
22
+ }
23
+ const stream = file.createReadStream({ autoClose: false, signal, highWaterMark: Math.min(limits.maxBytes + 1, 64 * 1024) });
24
+ for await (const bytes of stream)
25
+ receive(bytes);
26
+ } finally {
27
+ await file.close();
28
+ }
29
+ } catch (cause) {
30
+ signal.throwIfAborted();
31
+ if (cause instanceof FileError)
32
+ throw cause;
33
+ const code = cause?.code;
34
+ throw new FileError(`Cannot read local file: ${this.path}`, {
35
+ kind: code === "ENOENT" || code === "ENOTDIR" ? "resource_not_found" : code === "EACCES" || code === "EPERM" ? "permission_denied" : "operation_failed",
36
+ code,
37
+ cause
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ class LocalFileDataSource extends FileDataSource {
44
+ path;
45
+ clientKey;
46
+ #options;
47
+ constructor(path, options) {
48
+ super();
49
+ if (!path || path.includes("\x00"))
50
+ throw new FileError("File path must be nonempty and contain no NUL", { kind: "invalid_argument" });
51
+ this.path = resolve(path);
52
+ this.#options = fileClientOptions(options);
53
+ this.clientKey = clientKey("local-file", { path: this.path, options: this.#options });
54
+ }
55
+ createClient(_clients, signal) {
56
+ return new LocalFileClient(this.path, this.#options, signal);
57
+ }
58
+ }
59
+
60
+ export { LocalFileClient, LocalFileDataSource };
@@ -1,3 +1,7 @@
1
+ import {
2
+ queryMysqlViaPod,
3
+ queryMysqlViaPodBatch
4
+ } from "./chunk-m91t44jh.js";
1
5
  import {
2
6
  queryReadonlySession,
3
7
  validateQueryLimits
@@ -5,13 +9,9 @@ import {
5
9
  import {
6
10
  destroyMysqlConnection
7
11
  } from "./chunk-136egyff.js";
8
- import {
9
- queryMysqlViaPod,
10
- queryMysqlViaPodBatch
11
- } from "./chunk-m91t44jh.js";
12
12
  import {
13
13
  ToolboxError
14
- } from "./chunk-zhkrydeg.js";
14
+ } from "./chunk-20kv4wc3.js";
15
15
  import {
16
16
  ConcurrencyPool
17
17
  } from "./chunk-v3hxvnrc.js";
@@ -0,0 +1,62 @@
1
+ import {
2
+ BoundedFileClient,
3
+ fileClientOptions
4
+ } from "./chunk-mzdz6wwc.js";
5
+ import {
6
+ FileError
7
+ } from "./chunk-20kv4wc3.js";
8
+
9
+ // src/file/pod.ts
10
+ import { posix } from "node:path";
11
+ import { clientKey, FileDataSource } from "@compforge/harness-common";
12
+ class PodFileClient extends BoundedFileClient {
13
+ target;
14
+ acquire;
15
+ #kubernetes;
16
+ constructor(path, target, options, signal, acquire) {
17
+ super(path, options, signal);
18
+ this.target = target;
19
+ this.acquire = acquire;
20
+ }
21
+ async initializeAccess() {
22
+ this.#kubernetes = await this.acquire();
23
+ }
24
+ async capture(signal, limits, receive) {
25
+ const result = await this.#kubernetes.exec(this.target.namespace, this.target, ["head", "-c", String(limits.maxBytes + 1), this.path], { signal, timeoutMs: limits.timeoutMs, collectStdout: false, onStdoutBytes: receive });
26
+ signal.throwIfAborted();
27
+ if (!result.ok)
28
+ throw new FileError(`Cannot read Pod file: ${this.target.namespace}/${this.target.pod}/${this.target.container} ${this.path}`, { kind: result.timedOut ? "timeout" : "operation_failed", code: result.exitCode ?? undefined });
29
+ }
30
+ }
31
+
32
+ class PodFileDataSource extends FileDataSource {
33
+ kubernetes;
34
+ path;
35
+ target;
36
+ clientKey;
37
+ #options;
38
+ constructor(kubernetes, target, path, options) {
39
+ super();
40
+ this.kubernetes = kubernetes;
41
+ if (!posix.isAbsolute(path) || path.includes("\x00")) {
42
+ throw new FileError("Pod file path must be absolute and contain no NUL", { kind: "invalid_argument" });
43
+ }
44
+ if ([target.namespace, target.pod, target.container].some((value) => !value.trim())) {
45
+ throw new FileError("Pod file namespace, Pod and Container are required", { kind: "invalid_argument" });
46
+ }
47
+ this.path = path;
48
+ this.target = Object.freeze({ namespace: target.namespace, pod: target.pod, container: target.container });
49
+ this.#options = fileClientOptions(options);
50
+ this.clientKey = clientKey("pod-file", {
51
+ access: kubernetes.clientKey,
52
+ target: this.target,
53
+ path: this.path,
54
+ options: this.#options
55
+ });
56
+ }
57
+ createClient(clients, signal) {
58
+ return new PodFileClient(this.path, this.target, this.#options, signal, () => clients.get(this.kubernetes));
59
+ }
60
+ }
61
+
62
+ export { PodFileClient, PodFileDataSource };
@@ -0,0 +1,91 @@
1
+ import {
2
+ FileError,
3
+ ToolboxError
4
+ } from "./chunk-20kv4wc3.js";
5
+ import {
6
+ ConcurrencyPool
7
+ } from "./chunk-v3hxvnrc.js";
8
+
9
+ // src/file/client.ts
10
+ function fileClientOptions(options) {
11
+ if (!Number.isSafeInteger(options.concurrency) || options.concurrency < 1) {
12
+ throw new FileError("File concurrency must be a positive integer", { kind: "invalid_argument" });
13
+ }
14
+ return Object.freeze({ concurrency: options.concurrency });
15
+ }
16
+
17
+ class BoundedFileClient {
18
+ path;
19
+ #controller = new AbortController;
20
+ #signal;
21
+ #pool;
22
+ #operations = new Set;
23
+ #ready = false;
24
+ #disposal;
25
+ constructor(path, options, signal) {
26
+ this.path = path;
27
+ this.#pool = new ConcurrencyPool(fileClientOptions(options).concurrency);
28
+ this.#signal = AbortSignal.any([signal, this.#controller.signal]);
29
+ }
30
+ async initialize() {
31
+ this.#signal.throwIfAborted();
32
+ await this.initializeAccess();
33
+ this.#signal.throwIfAborted();
34
+ this.#ready = true;
35
+ }
36
+ async initializeAccess() {}
37
+ async read(options) {
38
+ this.#signal.throwIfAborted();
39
+ if (!this.#ready)
40
+ throw new FileError(`File client is not initialized: ${this.path}`, { kind: "operation_failed" });
41
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0 || options.maxBytes === Number.MAX_SAFE_INTEGER || !Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647) {
42
+ throw new FileError(`Invalid file read limits: ${this.path}`, { kind: "invalid_argument" });
43
+ }
44
+ const { maxBytes, timeoutMs } = options;
45
+ const controller = new AbortController;
46
+ const signal = AbortSignal.any([this.#signal, controller.signal, ...options.signal ? [options.signal] : []]);
47
+ const timer = setTimeout(() => controller.abort(new FileError(`File read timed out: ${this.path}`, { kind: "timeout" })), timeoutMs);
48
+ const operation = this.#pool.run(async () => {
49
+ const chunks = [];
50
+ let size = 0;
51
+ await this.capture(signal, { maxBytes, timeoutMs }, (bytes) => {
52
+ if (signal.aborted)
53
+ return;
54
+ if (bytes.byteLength > maxBytes - size) {
55
+ controller.abort(new FileError(`File exceeds ${maxBytes} bytes: ${this.path}`, { kind: "limit_exceeded" }));
56
+ return;
57
+ }
58
+ chunks.push(Uint8Array.from(bytes));
59
+ size += bytes.byteLength;
60
+ });
61
+ signal.throwIfAborted();
62
+ const result = new Uint8Array(size);
63
+ let offset = 0;
64
+ for (const chunk of chunks) {
65
+ result.set(chunk, offset);
66
+ offset += chunk.byteLength;
67
+ }
68
+ return result;
69
+ }, signal);
70
+ this.#operations.add(operation);
71
+ try {
72
+ return await operation;
73
+ } catch (cause) {
74
+ signal.throwIfAborted();
75
+ if (cause instanceof ToolboxError)
76
+ throw cause;
77
+ throw new FileError(`Cannot read file: ${this.path}`, { kind: "operation_failed", cause });
78
+ } finally {
79
+ clearTimeout(timer);
80
+ this.#operations.delete(operation);
81
+ }
82
+ }
83
+ dispose() {
84
+ return this.#disposal ??= (async () => {
85
+ this.#controller.abort(new Error(`File client disposed: ${this.path}`));
86
+ await Promise.allSettled(this.#operations);
87
+ })();
88
+ }
89
+ }
90
+
91
+ export { fileClientOptions, BoundedFileClient };
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  KubernetesError,
8
8
  ToolboxError
9
- } from "./chunk-zhkrydeg.js";
9
+ } from "./chunk-20kv4wc3.js";
10
10
  export {
11
11
  clientKey,
12
12
  ToolboxError,
package/dist/errors.d.ts CHANGED
@@ -11,6 +11,8 @@ export declare class ToolboxError extends Error {
11
11
  }
12
12
  export declare class KubernetesError extends ToolboxError {
13
13
  }
14
+ export declare class FileError extends ToolboxError {
15
+ }
14
16
  export declare function kubernetesHttpError(status: number): KubernetesError;
15
17
  /**
16
18
  * Normalize a failed kubectl resource request at the transport boundary.
package/dist/errors.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import {
2
+ FileError,
2
3
  KubernetesError,
3
4
  ToolboxError,
4
5
  kubernetesCommandError,
5
6
  kubernetesHttpError
6
- } from "./chunk-zhkrydeg.js";
7
+ } from "./chunk-20kv4wc3.js";
7
8
  export {
8
9
  kubernetesHttpError,
9
10
  kubernetesCommandError,
10
11
  ToolboxError,
11
- KubernetesError
12
+ KubernetesError,
13
+ FileError
12
14
  };
@@ -0,0 +1,17 @@
1
+ import type { FileClient, FileReadOptions } from "@compforge/harness-common";
2
+ export interface FileClientOptions {
3
+ concurrency: number;
4
+ }
5
+ export declare function fileClientOptions(options: FileClientOptions): Readonly<FileClientOptions>;
6
+ /** Shared limits and lifecycle; transports stream bytes and own per-read resource cleanup. */
7
+ export declare abstract class BoundedFileClient implements FileClient {
8
+ #private;
9
+ readonly path: string;
10
+ constructor(path: string, options: FileClientOptions, signal: AbortSignal);
11
+ initialize(): Promise<void>;
12
+ protected initializeAccess(): Promise<void>;
13
+ protected abstract capture(signal: AbortSignal, limits: Pick<FileReadOptions, "maxBytes" | "timeoutMs">, receive: (bytes: Uint8Array) => void): Promise<void>;
14
+ /** @spec Limits apply while streaming; a failed read never publishes partial bytes. */
15
+ read(options: FileReadOptions): Promise<Uint8Array>;
16
+ dispose(): Promise<void>;
17
+ }
@@ -0,0 +1,10 @@
1
+ import {
2
+ BoundedFileClient,
3
+ fileClientOptions
4
+ } from "../chunk-mzdz6wwc.js";
5
+ import"../chunk-20kv4wc3.js";
6
+ import"../chunk-v3hxvnrc.js";
7
+ export {
8
+ fileClientOptions,
9
+ BoundedFileClient
10
+ };
@@ -0,0 +1,5 @@
1
+ export { FileDataSource, type FileClient, type FileReadOptions } from "@compforge/harness-common";
2
+ export { FileError } from "../errors";
3
+ export type { FileClientOptions } from "./client";
4
+ export { LocalFileDataSource, LocalFileClient } from "./local";
5
+ export { PodFileDataSource, PodFileClient, type PodFileTarget } from "./pod";
@@ -0,0 +1,24 @@
1
+ import {
2
+ PodFileClient,
3
+ PodFileDataSource
4
+ } from "../chunk-mxyvpy79.js";
5
+ import {
6
+ LocalFileClient,
7
+ LocalFileDataSource
8
+ } from "../chunk-ba883sdp.js";
9
+ import"../chunk-mzdz6wwc.js";
10
+ import {
11
+ FileError
12
+ } from "../chunk-20kv4wc3.js";
13
+ import"../chunk-v3hxvnrc.js";
14
+
15
+ // src/file/index.ts
16
+ import { FileDataSource } from "@compforge/harness-common";
17
+ export {
18
+ PodFileDataSource,
19
+ PodFileClient,
20
+ LocalFileDataSource,
21
+ LocalFileClient,
22
+ FileError,
23
+ FileDataSource
24
+ };
@@ -0,0 +1,13 @@
1
+ import { FileDataSource, type ClientManager, type FileReadOptions } from "@compforge/harness-common";
2
+ import { BoundedFileClient, type FileClientOptions } from "./client";
3
+ export declare class LocalFileClient extends BoundedFileClient {
4
+ protected capture(signal: AbortSignal, limits: Pick<FileReadOptions, "maxBytes" | "timeoutMs">, receive: (bytes: Uint8Array) => void): Promise<void>;
5
+ }
6
+ /** @spec Resolve relative paths once at declaration; reads reopen the path to observe replaced mounted files. */
7
+ export declare class LocalFileDataSource extends FileDataSource<LocalFileClient> {
8
+ #private;
9
+ readonly path: string;
10
+ readonly clientKey: string;
11
+ constructor(path: string, options: FileClientOptions);
12
+ createClient(_clients: Pick<ClientManager, "get">, signal: AbortSignal): LocalFileClient;
13
+ }
@@ -0,0 +1,11 @@
1
+ import {
2
+ LocalFileClient,
3
+ LocalFileDataSource
4
+ } from "../chunk-ba883sdp.js";
5
+ import"../chunk-mzdz6wwc.js";
6
+ import"../chunk-20kv4wc3.js";
7
+ import"../chunk-v3hxvnrc.js";
8
+ export {
9
+ LocalFileDataSource,
10
+ LocalFileClient
11
+ };
@@ -0,0 +1,31 @@
1
+ import { FileDataSource, type ClientManager, type ClientProvider, type FileReadOptions } from "@compforge/harness-common";
2
+ import type { KubernetesClient } from "../kubernetes/client";
3
+ import { BoundedFileClient, type FileClientOptions } from "./client";
4
+ export interface PodFileTarget {
5
+ namespace: string;
6
+ pod: string;
7
+ container: string;
8
+ }
9
+ export type PodFileAccess = Pick<KubernetesClient, "initialize" | "dispose" | "exec">;
10
+ export declare class PodFileClient extends BoundedFileClient {
11
+ #private;
12
+ readonly target: Readonly<PodFileTarget>;
13
+ private readonly acquire;
14
+ constructor(path: string, target: Readonly<PodFileTarget>, options: FileClientOptions, signal: AbortSignal, acquire: () => Promise<PodFileAccess>);
15
+ protected initializeAccess(): Promise<void>;
16
+ protected capture(signal: AbortSignal, limits: Pick<FileReadOptions, "maxBytes" | "timeoutMs">, receive: (bytes: Uint8Array) => void): Promise<void>;
17
+ }
18
+ /**
19
+ * @spec One explicit Pod/container/path per source; workload discovery and replica traversal belong to callers.
20
+ * @rule Borrow Kubernetes access from the root manager; file clients never dispose that shared dependency.
21
+ * @see {@link component://../../../docs/toolbox.md}
22
+ */
23
+ export declare class PodFileDataSource extends FileDataSource<PodFileClient> {
24
+ #private;
25
+ private readonly kubernetes;
26
+ readonly path: string;
27
+ readonly target: Readonly<PodFileTarget>;
28
+ readonly clientKey: string;
29
+ constructor(kubernetes: ClientProvider<PodFileAccess>, target: PodFileTarget, path: string, options: FileClientOptions);
30
+ createClient(clients: Pick<ClientManager, "get">, signal: AbortSignal): PodFileClient;
31
+ }
@@ -0,0 +1,11 @@
1
+ import {
2
+ PodFileClient,
3
+ PodFileDataSource
4
+ } from "../chunk-mxyvpy79.js";
5
+ import"../chunk-mzdz6wwc.js";
6
+ import"../chunk-20kv4wc3.js";
7
+ import"../chunk-v3hxvnrc.js";
8
+ export {
9
+ PodFileDataSource,
10
+ PodFileClient
11
+ };
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  KubernetesClient
3
- } from "../chunk-y94xq8b4.js";
3
+ } from "../chunk-a1cb44ga.js";
4
+ import"../chunk-8axd2y2m.js";
4
5
  import"../chunk-2171tajs.js";
5
6
  import"../chunk-j46x7j7c.js";
6
7
  import"../chunk-6fnhzhdb.js";
7
- import"../chunk-fhtbvj0s.js";
8
- import"../chunk-p6k59m3q.js";
9
- import"../chunk-zhkrydeg.js";
8
+ import"../chunk-9n9z2pty.js";
9
+ import"../chunk-20kv4wc3.js";
10
10
  import"../chunk-v3hxvnrc.js";
11
11
  import"../chunk-631vce5z.js";
12
12
  import"../chunk-hmah9t8e.js";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  KubernetesClient
3
- } from "../chunk-y94xq8b4.js";
3
+ } from "../chunk-a1cb44ga.js";
4
+ import"../chunk-8axd2y2m.js";
4
5
  import"../chunk-2171tajs.js";
5
6
  import"../chunk-j46x7j7c.js";
6
7
  import"../chunk-6fnhzhdb.js";
7
- import"../chunk-fhtbvj0s.js";
8
- import"../chunk-p6k59m3q.js";
8
+ import"../chunk-9n9z2pty.js";
9
9
  import {
10
10
  clientKey
11
11
  } from "../chunk-a9htb6cr.js";
12
- import"../chunk-zhkrydeg.js";
12
+ import"../chunk-20kv4wc3.js";
13
13
  import"../chunk-v3hxvnrc.js";
14
14
  import"../chunk-631vce5z.js";
15
15
  import"../chunk-hmah9t8e.js";
@@ -8,7 +8,7 @@ import"../chunk-a9htb6cr.js";
8
8
  import {
9
9
  clientKey
10
10
  } from "../chunk-z5pm2emz.js";
11
- import"../chunk-zhkrydeg.js";
11
+ import"../chunk-20kv4wc3.js";
12
12
  import"../chunk-v3hxvnrc.js";
13
13
 
14
14
  // src/kubernetes/pod-log-datasource.ts
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  resourceAccess
3
- } from "../chunk-fhtbvj0s.js";
4
- import"../chunk-zhkrydeg.js";
3
+ } from "../chunk-9n9z2pty.js";
4
+ import"../chunk-20kv4wc3.js";
5
5
  import"../chunk-v3hxvnrc.js";
6
6
  export {
7
7
  resourceAccess
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  resolveWorkload
3
- } from "../chunk-p6k59m3q.js";
4
- import"../chunk-zhkrydeg.js";
3
+ } from "../chunk-8axd2y2m.js";
4
+ import"../chunk-20kv4wc3.js";
5
5
  export {
6
6
  resolveWorkload
7
7
  };
@@ -2,11 +2,11 @@ import {
2
2
  MysqlClient,
3
3
  MysqlDatabase,
4
4
  openMysql
5
- } from "../chunk-ahc00qs5.js";
5
+ } from "../chunk-h73h2b7y.js";
6
+ import"../chunk-m91t44jh.js";
6
7
  import"../chunk-age65q4n.js";
7
8
  import"../chunk-136egyff.js";
8
- import"../chunk-m91t44jh.js";
9
- import"../chunk-zhkrydeg.js";
9
+ import"../chunk-20kv4wc3.js";
10
10
  import"../chunk-v3hxvnrc.js";
11
11
  import"../chunk-h8fwx0k7.js";
12
12
  import"../chunk-wv4jwhvj.js";
@@ -1,12 +1,3 @@
1
- import {
2
- parseMysqlEnvTarget
3
- } from "../chunk-qq77x9ek.js";
4
- import {
5
- limitedRows,
6
- limitedStatement,
7
- queryLimited
8
- } from "../chunk-1gyshqd9.js";
9
- import"../chunk-pfar9cm3.js";
10
1
  import {
11
2
  withReadScope
12
3
  } from "../chunk-4sqvppz7.js";
@@ -14,11 +5,20 @@ import {
14
5
  MysqlClient,
15
6
  MysqlDatabase,
16
7
  openMysql
17
- } from "../chunk-ahc00qs5.js";
8
+ } from "../chunk-h73h2b7y.js";
9
+ import"../chunk-m91t44jh.js";
18
10
  import"../chunk-age65q4n.js";
11
+ import {
12
+ parseMysqlEnvTarget
13
+ } from "../chunk-qq77x9ek.js";
14
+ import {
15
+ limitedRows,
16
+ limitedStatement,
17
+ queryLimited
18
+ } from "../chunk-1gyshqd9.js";
19
19
  import"../chunk-136egyff.js";
20
- import"../chunk-m91t44jh.js";
21
- import"../chunk-zhkrydeg.js";
20
+ import"../chunk-pfar9cm3.js";
21
+ import"../chunk-20kv4wc3.js";
22
22
  import"../chunk-v3hxvnrc.js";
23
23
  import"../chunk-h8fwx0k7.js";
24
24
  import"../chunk-wv4jwhvj.js";
@@ -9,6 +9,10 @@ export interface OpenSearchOptions {
9
9
  node: string;
10
10
  auth: OpenSearchAuth;
11
11
  requestTimeoutMs?: number;
12
+ /** Deadline for the initialization probe; query deadlines remain independent. */
13
+ connectTimeoutMs?: number;
14
+ /** Original TLS identity when a transport maps the socket to localhost. */
15
+ servername?: string;
12
16
  }
13
17
  export interface OpenSearchClientApi {
14
18
  count(params: Record<string, unknown>): Promise<{
@@ -19,7 +23,11 @@ export interface OpenSearchClientApi {
19
23
  search(params: Record<string, unknown>): Promise<{
20
24
  body: SearchResult;
21
25
  }>;
22
- ping(params?: Record<string, unknown>): Promise<unknown>;
26
+ ping(params?: Record<string, unknown>, options?: {
27
+ requestTimeout?: number;
28
+ }): Promise<unknown> & {
29
+ abort?(): void;
30
+ };
23
31
  transport?: {
24
32
  request(params: {
25
33
  method: string;
@@ -43,12 +51,18 @@ export declare class OpenSearchEngine implements OpenSearchReadApi {
43
51
  count(index: string, query: SearchQuery): Promise<number>;
44
52
  search(index: string, body: SearchQuery): Promise<SearchResult>;
45
53
  request(path: string, query?: SearchQuery): Promise<unknown>;
46
- ping(): Promise<unknown>;
54
+ ping(signal?: AbortSignal, timeoutMs?: number): Promise<unknown>;
47
55
  close(): Promise<void>;
48
56
  }
57
+ /**
58
+ * @spec Only connection preparation may advance to another Transport; reads are never replayed.
59
+ * @why The official client connects lazily and wraps socket errors, so creating it does not prove reachability.
60
+ * @case id=opensearch_connect_fallback,desc=`Host cannot reach target`,expect=`Probe next TCP route`,forbid=`Replay a failed search`
61
+ */
49
62
  export declare class OpenSearchClient implements ManagedClient, OpenSearchReadApi {
50
63
  #private;
51
64
  private readonly source;
65
+ private readonly lifecycle;
52
66
  readonly signal: AbortSignal;
53
67
  constructor(source: ConnectionSource<OpenSearchOptions>, lifecycle?: ClientLifecycle);
54
68
  initialize(): Promise<void>;
@@ -3,7 +3,14 @@ import {
3
3
  OpenSearchEngine,
4
4
  isOpenSearchReadApi,
5
5
  openOpenSearch
6
- } from "../chunk-w3a4p6jm.js";
6
+ } from "../chunk-8mb6natq.js";
7
+ import"../chunk-20kv4wc3.js";
8
+ import"../chunk-h8fwx0k7.js";
9
+ import"../chunk-wv4jwhvj.js";
10
+ import"../chunk-631vce5z.js";
11
+ import"../chunk-hmah9t8e.js";
12
+ import"../chunk-mn6dwsvh.js";
13
+ import"../chunk-ssj93brx.js";
7
14
  export {
8
15
  openOpenSearch,
9
16
  isOpenSearchReadApi,
@@ -1,9 +1,3 @@
1
- import {
2
- OpenSearchClient,
3
- OpenSearchEngine,
4
- isOpenSearchReadApi,
5
- openOpenSearch
6
- } from "../chunk-w3a4p6jm.js";
7
1
  import {
8
2
  searchAfterPages
9
3
  } from "../chunk-99p0zt4f.js";
@@ -11,6 +5,19 @@ import {
11
5
  parseOpenSearchEndpoint
12
6
  } from "../chunk-s1sjyem7.js";
13
7
  import"../chunk-djk4kmxb.js";
8
+ import {
9
+ OpenSearchClient,
10
+ OpenSearchEngine,
11
+ isOpenSearchReadApi,
12
+ openOpenSearch
13
+ } from "../chunk-8mb6natq.js";
14
+ import"../chunk-20kv4wc3.js";
15
+ import"../chunk-h8fwx0k7.js";
16
+ import"../chunk-wv4jwhvj.js";
17
+ import"../chunk-631vce5z.js";
18
+ import"../chunk-hmah9t8e.js";
19
+ import"../chunk-mn6dwsvh.js";
20
+ import"../chunk-ssj93brx.js";
14
21
  export {
15
22
  searchAfterPages,
16
23
  parseOpenSearchEndpoint,
@@ -2,7 +2,7 @@ import {
2
2
  S3DataSource
3
3
  } from "../chunk-35cjec9z.js";
4
4
  import"../chunk-z5pm2emz.js";
5
- import"../chunk-zhkrydeg.js";
5
+ import"../chunk-20kv4wc3.js";
6
6
  import"../chunk-zp1s833x.js";
7
7
  import"../chunk-v3hxvnrc.js";
8
8
  import"../chunk-x4ntb898.js";
package/dist/s3/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  S3DataSource
3
3
  } from "../chunk-35cjec9z.js";
4
4
  import"../chunk-z5pm2emz.js";
5
- import"../chunk-zhkrydeg.js";
5
+ import"../chunk-20kv4wc3.js";
6
6
  import {
7
7
  S3Client
8
8
  } from "../chunk-zp1s833x.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-toolbox",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Shared infrastructure clients, transports and bounded data capture",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -88,6 +88,10 @@
88
88
  "./data-loader": {
89
89
  "types": "./dist/data-loader.d.ts",
90
90
  "import": "./dist/data-loader.js"
91
+ },
92
+ "./file": {
93
+ "types": "./dist/file/index.d.ts",
94
+ "import": "./dist/file/index.js"
91
95
  }
92
96
  },
93
97
  "files": [
@@ -95,7 +99,7 @@
95
99
  "README.md"
96
100
  ],
97
101
  "dependencies": {
98
- "@compforge/harness-common": "0.3.0",
102
+ "@compforge/harness-common": "0.5.3",
99
103
  "@aws-sdk/client-s3": "3.1133.0",
100
104
  "@kubernetes/client-node": "1.4.0",
101
105
  "@opensearch-project/opensearch": "3.6.0",
@@ -110,9 +114,6 @@
110
114
  "@types/node-fetch": "2.6.13",
111
115
  "@types/node": "^22"
112
116
  },
113
- "overrides": {
114
- "@compforge/harness-common": "file:../common"
115
- },
116
117
  "publishConfig": {
117
118
  "access": "public"
118
119
  },