@compforge/harness-toolbox 0.5.3 → 0.5.4

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
@@ -430,3 +430,14 @@ timeouts remain separate, and failed operations are never replayed on another tr
430
430
  DataLoader accepts an optional `logger.debug(message)` and a non-sensitive `label`.
431
431
  Diagnostics distinguish miss, wait, hit, bypass, not-retained and close using scope/read
432
432
  identifiers, never keys, values or errors. Logging does not change retention or retry policy.
433
+
434
+ ### Network connectivity
435
+
436
+ `@compforge/harness-toolbox/network` exports `probeConnectivity({host, port, tls?}, execute, {timeoutMs, signal?})`.
437
+ The caller supplies an executor in the consumer environment (host, Pod or sandbox),
438
+ which runs Python 3 with the supplied argv and stdin and enforces timeout/cancellation.
439
+ DNS, TCP and optional verified TLS have separate results and timings. Domain names,
440
+ IP literals and Kubernetes `service.namespace` names use the same API. There is no
441
+ implicit execution on the caller host or ICMP requirement. Network failure returns
442
+ evidence; missing Python, execution failure and incomplete output throw. TLS success
443
+ does not establish HTTP authorization, object access or application download policy.
@@ -1,3 +1,6 @@
1
+ import {
2
+ withReadScope
3
+ } from "../chunk-4sqvppz7.js";
1
4
  import {
2
5
  MysqlClient,
3
6
  MysqlDatabase,
@@ -15,9 +18,6 @@ import {
15
18
  } from "../chunk-1gyshqd9.js";
16
19
  import"../chunk-835fssgz.js";
17
20
  import"../chunk-pfar9cm3.js";
18
- import {
19
- withReadScope
20
- } from "../chunk-4sqvppz7.js";
21
21
  import"../chunk-18gcp0ck.js";
22
22
  import"../chunk-kap6ka3n.js";
23
23
  import"../chunk-v3hxvnrc.js";
@@ -0,0 +1,35 @@
1
+ /** A probe runs in the caller-selected environment; it never falls back to the Doctor/CLI host. */
2
+ export interface ConnectivityTarget {
3
+ host: string;
4
+ port: number;
5
+ tls?: boolean;
6
+ }
7
+ export interface ConnectivityStage {
8
+ stage: "dns" | "tcp" | "tls";
9
+ status: "passed" | "failed";
10
+ elapsedMs: number;
11
+ addresses?: string[];
12
+ address?: string;
13
+ error?: string;
14
+ }
15
+ export interface ConnectivityResult {
16
+ target: ConnectivityTarget;
17
+ stages: ConnectivityStage[];
18
+ status: "passed" | "failed";
19
+ }
20
+ export interface ConnectivityExecutor {
21
+ (command: readonly string[], options: {
22
+ stdin: string;
23
+ timeoutMs: number;
24
+ signal?: AbortSignal;
25
+ }): Promise<string>;
26
+ }
27
+ /**
28
+ * @spec DNS, TCP and optional verified TLS are observed from the supplied executor's network context.
29
+ * @rule A hostname (including service.namespace) and an IP use the same path; connectivity requires an explicit port.
30
+ * @rule Tool failure/timeout throws; it is not evidence that the target is unhealthy. No ICMP, credentials or HTTP policy is implied.
31
+ */
32
+ export declare function probeConnectivity(target: ConnectivityTarget, execute: ConnectivityExecutor, options: {
33
+ timeoutMs: number;
34
+ signal?: AbortSignal;
35
+ }): Promise<ConnectivityResult>;
@@ -0,0 +1,63 @@
1
+ // src/network/connectivity.ts
2
+ var SCRIPT = `import json,socket,ssl,sys,time
3
+ q=json.load(sys.stdin)
4
+ start=time.monotonic()
5
+ deadline=start+q["timeoutMs"]/1000
6
+ stages=[]
7
+ def remaining():
8
+ t=deadline-time.monotonic()
9
+ if t<=0: raise TimeoutError()
10
+ return t
11
+ def add(stage,t,**values):
12
+ stages.append(dict(stage=stage,elapsedMs=round((time.monotonic()-t)*1000,3),**values))
13
+ phase="dns"
14
+ t=time.monotonic()
15
+ sock=None
16
+ try:
17
+ addresses=socket.getaddrinfo(q["host"],q["port"],type=socket.SOCK_STREAM)
18
+ if not addresses: raise socket.gaierror()
19
+ add(phase,t,status="passed",addresses=list(dict.fromkeys(a[4][0] for a in addresses))[:8])
20
+ phase="tcp"
21
+ t=time.monotonic()
22
+ failure=None
23
+ for family,kind,proto,_,address in addresses[:8]:
24
+ candidate=socket.socket(family,kind,proto)
25
+ try:
26
+ candidate.settimeout(remaining())
27
+ candidate.connect(address)
28
+ sock=candidate
29
+ break
30
+ except OSError as e:
31
+ failure=e
32
+ candidate.close()
33
+ if sock is None: raise failure or OSError()
34
+ add(phase,t,status="passed",address=sock.getpeername()[0])
35
+ if q.get("tls"):
36
+ phase="tls"
37
+ t=time.monotonic()
38
+ sock.settimeout(remaining())
39
+ sock=ssl.create_default_context().wrap_socket(sock,server_hostname=q["host"])
40
+ add(phase,t,status="passed")
41
+ except (OSError,ValueError) as e:
42
+ add(phase,t,status="failed",error=type(e).__name__)
43
+ finally:
44
+ if sock is not None: sock.close()
45
+ print(json.dumps(dict(stages=stages,status="failed" if any(s["status"]=="failed" for s in stages) else "passed")))`;
46
+ async function probeConnectivity(target, execute, options) {
47
+ if (!target.host.trim() || /[\s/\\?#@]/.test(target.host) || !Number.isInteger(target.port) || target.port < 1 || target.port > 65535) {
48
+ throw new Error("Connectivity requires a hostname/IP and a port between 1 and 65535");
49
+ }
50
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0)
51
+ throw new Error("Connectivity timeout must be positive");
52
+ options.signal?.throwIfAborted();
53
+ const raw = await execute(["python3", "-c", SCRIPT], { ...options, stdin: JSON.stringify({ ...target, timeoutMs: options.timeoutMs }) });
54
+ options.signal?.throwIfAborted();
55
+ const value = JSON.parse(raw);
56
+ const expected = ["dns", "tcp", ...target.tls ? ["tls"] : []];
57
+ if (!value || !Array.isArray(value.stages) || !value.stages.length || value.stages.length > expected.length || !["passed", "failed"].includes(value.status) || value.stages.some((stage, index) => !stage || stage.stage !== expected[index] || !["passed", "failed"].includes(stage.status) || !Number.isFinite(stage.elapsedMs) || stage.elapsedMs < 0 || index < value.stages.length - 1 && stage.status !== "passed") || (value.status === "passed" ? value.stages.length !== expected.length || value.stages.some((stage) => stage.status !== "passed") : value.stages.at(-1)?.status !== "failed"))
58
+ throw new Error("Invalid connectivity probe result");
59
+ return { target: { ...target }, stages: value.stages, status: value.status };
60
+ }
61
+ export {
62
+ probeConnectivity
63
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compforge/harness-toolbox",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Shared infrastructure clients, transports and bounded data capture",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -96,6 +96,10 @@
96
96
  "./http": {
97
97
  "types": "./dist/http/index.d.ts",
98
98
  "import": "./dist/http/index.js"
99
+ },
100
+ "./network": {
101
+ "types": "./dist/network/connectivity.d.ts",
102
+ "import": "./dist/network/connectivity.js"
99
103
  }
100
104
  },
101
105
  "files": [
@@ -122,9 +126,6 @@
122
126
  "@types/node": "^22",
123
127
  "@types/ws": "^8.18.1"
124
128
  },
125
- "overrides": {
126
- "@compforge/harness-common": "file:../common"
127
- },
128
129
  "publishConfig": {
129
130
  "access": "public"
130
131
  },