@compforge/harness-toolbox 0.1.0 → 0.1.2
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 +55 -1
- package/dist/chunk-94anqtxh.js +35 -0
- package/dist/{chunk-mkhyty75.js → chunk-9x1apyp4.js} +20 -3
- package/dist/chunk-fnc291vk.js +4 -0
- package/dist/chunk-kbewz29a.js +95 -0
- package/dist/{chunk-1at7zs0z.js → chunk-vjsr2jb3.js} +1 -1
- package/dist/chunk-x4ntb898.js +46 -0
- package/dist/chunk-zp1s833x.js +200 -0
- package/dist/client-manager.d.ts +1 -14
- package/dist/client-manager.js +2 -3
- package/dist/client.d.ts +1 -5
- package/dist/datasource.d.ts +1 -11
- package/dist/datasource.js +2 -4
- package/dist/kubernetes/client.js +2 -2
- package/dist/kubernetes/pod-log-client.js +2 -3
- package/dist/kubernetes/pod-log-datasource.js +2 -3
- package/dist/kubernetes/service-port-forward.js +2 -2
- package/dist/mysql/client.d.ts +4 -2
- package/dist/mysql/client.js +3 -2
- package/dist/mysql/index.js +7 -6
- package/dist/mysql/readonly.d.ts +9 -0
- package/dist/mysql/readonly.js +8 -0
- package/dist/mysql/types.d.ts +13 -0
- package/dist/redis/index.js +7 -7
- package/dist/s3/client.d.ts +40 -0
- package/dist/s3/client.js +8 -0
- package/dist/s3/datasource.d.ts +14 -0
- package/dist/s3/datasource.js +11 -0
- package/dist/s3/handler.d.ts +13 -0
- package/dist/s3/handler.js +6 -0
- package/dist/s3/index.d.ts +3 -0
- package/dist/s3/index.js +14 -0
- package/dist/s3/types.d.ts +51 -0
- package/dist/s3/types.js +0 -0
- package/package.json +15 -4
- package/dist/chunk-3dt25p98.js +0 -59
- package/dist/chunk-hqx7j9xe.js +0 -14
package/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# Harness Toolbox
|
|
2
2
|
|
|
3
|
+
Client, DataSource and ClientManager are owned by the sibling TypeScript package
|
|
4
|
+
`@compforge/harness-common`. Toolbox provides protocol implementations and transports;
|
|
5
|
+
its lifecycle entrypoints re-export the same common symbols.
|
|
6
|
+
|
|
3
7
|
Shared infrastructure for diagnostic tools, test runners and business adapters. Use the same
|
|
4
|
-
clients to access Kubernetes, MySQL, Redis and
|
|
8
|
+
clients to access Kubernetes, MySQL, Redis, OpenSearch and S3-compatible object storage without depending on a particular
|
|
5
9
|
command, plugin protocol, test model or verdict.
|
|
6
10
|
|
|
7
11
|
The package supports Node.js 22+ and Bun. Install with `npm install @compforge/harness-toolbox`.
|
|
@@ -45,3 +49,53 @@ capture policy, shared concurrency pool and byte budget; the package does not ch
|
|
|
45
49
|
limits. Temporary capture files live until client disposal.
|
|
46
50
|
|
|
47
51
|
See [toolbox contracts](../../../docs/toolbox.md) for ownership and cross-language semantics.
|
|
52
|
+
|
|
53
|
+
## Read S3 objects
|
|
54
|
+
|
|
55
|
+
S3 uses the official AWS SDK behind the same managed lifecycle. Supply resolved connection
|
|
56
|
+
settings and explicit limits; the package does not discover business configuration or credentials.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { ClientManager } from "@compforge/harness-toolbox";
|
|
60
|
+
import { S3DataSource } from "@compforge/harness-toolbox/s3";
|
|
61
|
+
|
|
62
|
+
const clients = new ClientManager();
|
|
63
|
+
try {
|
|
64
|
+
const s3 = await clients.get(new S3DataSource({
|
|
65
|
+
endpoint: "https://s3.example.com", region: "us-east-1",
|
|
66
|
+
credentials: { accessKeyId: "...", secretAccessKey: "..." },
|
|
67
|
+
}, { concurrency: 4, connectTimeoutMs: 5_000, requestTimeoutMs: 15_000 }));
|
|
68
|
+
const object = await s3.headObject("artifacts", "runs/example.json");
|
|
69
|
+
const prefix = await s3.readObject("artifacts", "runs/example.json", { maxBytes: 65_536 });
|
|
70
|
+
// prefix.truncated distinguishes a bounded prefix from the complete object.
|
|
71
|
+
} finally {
|
|
72
|
+
await clients.dispose();
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`listObjects` and `listBuckets` return one explicitly bounded page with continuation tokens;
|
|
77
|
+
callers own traversal and total scan budgets. Bucket HEAD and versioning are also available.
|
|
78
|
+
Errors preserve SDK status/code: a failed HEAD is not converted to `exists=false`. Reads stop at
|
|
79
|
+
the byte cap and close the body; request deadlines include queueing and body consumption.
|
|
80
|
+
No write operations, automatic retries or implicit region redirects are exposed.
|
|
81
|
+
|
|
82
|
+
For a mapped TCP transport, pass `{ key: "cluster/context identity", transport }` as the third
|
|
83
|
+
DataSource argument. The caller owns that transport's lifetime. Path-style addressing is required
|
|
84
|
+
for mapped routes; direct endpoints also support virtual-hosted buckets. Signed Host and TLS
|
|
85
|
+
identity remain the logical endpoint, not the forwarded address. Private CAs may be supplied
|
|
86
|
+
explicitly with `ca`; certificate verification stays enabled. Initialization prepares the client
|
|
87
|
+
and route without listing buckets or claiming that credentials have been validated.
|
|
88
|
+
|
|
89
|
+
## Bounded MySQL reads
|
|
90
|
+
|
|
91
|
+
`MysqlClient.database.queryReadonly(target, sql, values, { timeoutMs, maxRows, maxBytes })` uses
|
|
92
|
+
the shared query slot and a disposable native session with `NO_BACKSLASH_ESCAPES`, a SELECT deadline,
|
|
93
|
+
and a READ ONLY transaction. Prepared execution binds values and retains rows incrementally,
|
|
94
|
+
returning `rows`, `columns`, `bytes`, `truncated`, and `truncation`. An extra row proves row truncation;
|
|
95
|
+
a row exceeding the remaining byte budget is not retained.
|
|
96
|
+
|
|
97
|
+
Callers own SQL classification, function policy, and least-privilege identities: READ ONLY alone
|
|
98
|
+
is not an arbitrary-SQL sandbox. This API requires MySQL 5.7.8+ `max_execution_time` and does not
|
|
99
|
+
fall back to Pod Python. Completion, truncation, timeout, and cancellation destroy the disposable
|
|
100
|
+
session without replay or changes to ordinary borrowed sessions. Disconnect does not prove immediate
|
|
101
|
+
server cancellation. The byte budget limits retained JSON rows, not individual wire packets or fields.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
dataSourceKey
|
|
3
|
+
} from "./chunk-fnc291vk.js";
|
|
4
|
+
import {
|
|
5
|
+
S3Client
|
|
6
|
+
} from "./chunk-zp1s833x.js";
|
|
7
|
+
import {
|
|
8
|
+
DirectTransport
|
|
9
|
+
} from "./chunk-xy4g56g4.js";
|
|
10
|
+
|
|
11
|
+
// src/s3/datasource.ts
|
|
12
|
+
class S3DataSource {
|
|
13
|
+
key;
|
|
14
|
+
#target;
|
|
15
|
+
#limits;
|
|
16
|
+
#transport;
|
|
17
|
+
constructor(target, limits, route) {
|
|
18
|
+
this.#target = structuredClone(target);
|
|
19
|
+
this.#limits = { ...limits };
|
|
20
|
+
this.#transport = route?.transport ?? new DirectTransport;
|
|
21
|
+
this.key = dataSourceKey("s3", {
|
|
22
|
+
target: this.#target,
|
|
23
|
+
limits: this.#limits,
|
|
24
|
+
route: route ? { key: route.key, transport: route.transport.name } : "direct"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
createClient(signal) {
|
|
28
|
+
return new S3Client({
|
|
29
|
+
resolve: async () => this.#target,
|
|
30
|
+
transports: [this.#transport]
|
|
31
|
+
}, this.#limits, { signal });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { S3DataSource };
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ConcurrencyPool
|
|
3
|
-
} from "./chunk-v3hxvnrc.js";
|
|
4
1
|
import {
|
|
5
2
|
queryMysqlViaPod
|
|
6
3
|
} from "./chunk-s06hs7a8.js";
|
|
4
|
+
import {
|
|
5
|
+
queryReadonlySession,
|
|
6
|
+
validateQueryLimits
|
|
7
|
+
} from "./chunk-kbewz29a.js";
|
|
8
|
+
import {
|
|
9
|
+
ConcurrencyPool
|
|
10
|
+
} from "./chunk-v3hxvnrc.js";
|
|
7
11
|
import {
|
|
8
12
|
isConnectionNetworkError
|
|
9
13
|
} from "./chunk-xy4g56g4.js";
|
|
@@ -24,6 +28,19 @@ class MysqlDatabase {
|
|
|
24
28
|
query(target, sql, values) {
|
|
25
29
|
return this.#queries.run(() => this.#query(target, sql, values), this.options.signal);
|
|
26
30
|
}
|
|
31
|
+
queryReadonly(target, sql, values, limits) {
|
|
32
|
+
validateQueryLimits(limits);
|
|
33
|
+
return this.#queries.run(async () => {
|
|
34
|
+
const session = await this.#open(target);
|
|
35
|
+
if (session.kind !== "tcp")
|
|
36
|
+
throw new Error("Bounded readonly MySQL queries require a native TCP transport");
|
|
37
|
+
try {
|
|
38
|
+
return await queryReadonlySession(session.connection, sql, values, limits, this.options.signal);
|
|
39
|
+
} finally {
|
|
40
|
+
session.connection.destroy();
|
|
41
|
+
}
|
|
42
|
+
}, this.options.signal);
|
|
43
|
+
}
|
|
27
44
|
initialize(target) {
|
|
28
45
|
return this.#queries.run(async () => {
|
|
29
46
|
await this.#session(target);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/mysql/readonly.ts
|
|
2
|
+
function validateQueryLimits(limits) {
|
|
3
|
+
for (const name of ["timeoutMs", "maxRows", "maxBytes"]) {
|
|
4
|
+
const value = limits[name];
|
|
5
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647) {
|
|
6
|
+
throw new Error(`${name} must be a positive integer <= 2147483647`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
async function queryReadonlySession(connection, sql, values, limits, signal) {
|
|
11
|
+
validateQueryLimits(limits);
|
|
12
|
+
signal?.throwIfAborted();
|
|
13
|
+
let timer;
|
|
14
|
+
let abort;
|
|
15
|
+
try {
|
|
16
|
+
return await Promise.race([
|
|
17
|
+
(async () => {
|
|
18
|
+
await connection.query("SET SESSION sql_mode = 'STRICT_TRANS_TABLES,NO_BACKSLASH_ESCAPES'");
|
|
19
|
+
await connection.query(`SET SESSION max_execution_time = ${limits.timeoutMs}`);
|
|
20
|
+
await connection.query("START TRANSACTION READ ONLY");
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
return await collectRows(connection, sql, values, limits);
|
|
23
|
+
})(),
|
|
24
|
+
new Promise((_resolve, reject) => {
|
|
25
|
+
const fail = (reason) => {
|
|
26
|
+
connection.destroy();
|
|
27
|
+
reject(reason);
|
|
28
|
+
};
|
|
29
|
+
abort = () => fail(signal?.reason ?? new Error("MySQL query cancelled"));
|
|
30
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
31
|
+
timer = setTimeout(() => fail(new Error("MySQL readonly query timed out")), limits.timeoutMs);
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
abort();
|
|
34
|
+
})
|
|
35
|
+
]);
|
|
36
|
+
} finally {
|
|
37
|
+
if (timer)
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
if (abort)
|
|
40
|
+
signal?.removeEventListener("abort", abort);
|
|
41
|
+
connection.destroy();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function collectRows(connection, sql, values, limits) {
|
|
45
|
+
const native = connection.connection;
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const result = { rows: [], columns: [], bytes: 0, truncated: false };
|
|
48
|
+
let settled = false;
|
|
49
|
+
const finish = (truncation) => {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
52
|
+
settled = true;
|
|
53
|
+
result.truncated = !!truncation;
|
|
54
|
+
if (truncation)
|
|
55
|
+
result.truncation = truncation;
|
|
56
|
+
resolve(result);
|
|
57
|
+
};
|
|
58
|
+
const query = native.execute({ sql, values: [...values], timeout: limits.timeoutMs });
|
|
59
|
+
query.on("fields", (fields) => {
|
|
60
|
+
result.columns = fields?.map((field) => field.name) ?? [];
|
|
61
|
+
});
|
|
62
|
+
query.on("result", (row) => {
|
|
63
|
+
if (settled)
|
|
64
|
+
return;
|
|
65
|
+
if (result.rows.length >= limits.maxRows) {
|
|
66
|
+
finish("rows");
|
|
67
|
+
connection.destroy();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const bytes = Buffer.byteLength(JSON.stringify(row));
|
|
72
|
+
if (result.bytes + bytes > limits.maxBytes) {
|
|
73
|
+
finish("bytes");
|
|
74
|
+
connection.destroy();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
result.rows.push(row);
|
|
78
|
+
result.bytes += bytes;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
settled = true;
|
|
81
|
+
connection.destroy();
|
|
82
|
+
reject(error);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
query.on("error", (error) => {
|
|
86
|
+
if (!settled) {
|
|
87
|
+
settled = true;
|
|
88
|
+
reject(error);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
query.on("end", () => finish());
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export { validateQueryLimits, queryReadonlySession };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/s3/handler.ts
|
|
2
|
+
import { Agent as HttpAgent } from "node:http";
|
|
3
|
+
import { Agent as HttpsAgent } from "node:https";
|
|
4
|
+
import { NodeHttpHandler } from "@smithy/node-http-handler";
|
|
5
|
+
function s3Handler(target, route, limits) {
|
|
6
|
+
const endpoint = new URL(target.endpoint);
|
|
7
|
+
const mapped = route.host !== endpoint.hostname || route.port !== Number(endpoint.port || (endpoint.protocol === "https:" ? 443 : 80));
|
|
8
|
+
if (mapped && target.forcePathStyle === false) {
|
|
9
|
+
throw new Error("S3 mapped transports require path-style addressing to preserve bucket and TLS identity");
|
|
10
|
+
}
|
|
11
|
+
const pool = { keepAlive: true, maxSockets: limits.concurrency, maxTotalSockets: limits.concurrency };
|
|
12
|
+
const httpAgent = new HttpAgent(pool);
|
|
13
|
+
const httpsAgent = new HttpsAgent({
|
|
14
|
+
...pool,
|
|
15
|
+
ca: target.ca,
|
|
16
|
+
rejectUnauthorized: true,
|
|
17
|
+
...mapped ? { servername: endpoint.hostname } : {}
|
|
18
|
+
});
|
|
19
|
+
const transport = new NodeHttpHandler({
|
|
20
|
+
httpAgent,
|
|
21
|
+
httpsAgent,
|
|
22
|
+
connectionTimeout: limits.connectTimeoutMs,
|
|
23
|
+
requestTimeout: limits.requestTimeoutMs,
|
|
24
|
+
throwOnRequestTimeout: true
|
|
25
|
+
});
|
|
26
|
+
const handler = {
|
|
27
|
+
handle(request, options) {
|
|
28
|
+
if (!mapped)
|
|
29
|
+
return transport.handle(request, options);
|
|
30
|
+
const routed = request.clone();
|
|
31
|
+
routed.hostname = route.host;
|
|
32
|
+
routed.port = route.port;
|
|
33
|
+
return transport.handle(routed, options);
|
|
34
|
+
},
|
|
35
|
+
destroy() {
|
|
36
|
+
transport.destroy();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return { handler, destroy: () => {
|
|
40
|
+
handler.destroy();
|
|
41
|
+
httpAgent.destroy();
|
|
42
|
+
httpsAgent.destroy();
|
|
43
|
+
} };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { s3Handler };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConcurrencyPool
|
|
3
|
+
} from "./chunk-v3hxvnrc.js";
|
|
4
|
+
import {
|
|
5
|
+
s3Handler
|
|
6
|
+
} from "./chunk-x4ntb898.js";
|
|
7
|
+
|
|
8
|
+
// src/s3/client.ts
|
|
9
|
+
import {
|
|
10
|
+
S3Client as AwsS3Client,
|
|
11
|
+
HeadBucketCommand,
|
|
12
|
+
HeadObjectCommand,
|
|
13
|
+
ListBucketsCommand,
|
|
14
|
+
ListObjectsV2Command,
|
|
15
|
+
GetBucketVersioningCommand,
|
|
16
|
+
GetObjectCommand
|
|
17
|
+
} from "@aws-sdk/client-s3";
|
|
18
|
+
import { Readable } from "node:stream";
|
|
19
|
+
function positive(value, name, maximum = Number.MAX_SAFE_INTEGER) {
|
|
20
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
21
|
+
throw new Error(`${name} must be an integer between 1 and ${maximum}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function metadata(value) {
|
|
25
|
+
return {
|
|
26
|
+
size: value.ContentLength,
|
|
27
|
+
etag: value.ETag,
|
|
28
|
+
lastModified: value.LastModified,
|
|
29
|
+
contentType: value.ContentType,
|
|
30
|
+
versionId: value.VersionId,
|
|
31
|
+
metadata: value.Metadata ?? {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class S3Client {
|
|
36
|
+
source;
|
|
37
|
+
limits;
|
|
38
|
+
lifecycle;
|
|
39
|
+
#controller = new AbortController;
|
|
40
|
+
signal;
|
|
41
|
+
#pool;
|
|
42
|
+
#operations = new Set;
|
|
43
|
+
#initialization;
|
|
44
|
+
#disposal;
|
|
45
|
+
#sdk;
|
|
46
|
+
#http;
|
|
47
|
+
constructor(source, limits, lifecycle = {}) {
|
|
48
|
+
this.source = source;
|
|
49
|
+
this.limits = limits;
|
|
50
|
+
this.lifecycle = lifecycle;
|
|
51
|
+
positive(limits.concurrency, "concurrency");
|
|
52
|
+
positive(limits.connectTimeoutMs, "connectTimeoutMs", 2147483647);
|
|
53
|
+
positive(limits.requestTimeoutMs, "requestTimeoutMs", 2147483647);
|
|
54
|
+
this.#pool = new ConcurrencyPool(limits.concurrency);
|
|
55
|
+
this.signal = lifecycle.signal ? AbortSignal.any([lifecycle.signal, this.#controller.signal]) : this.#controller.signal;
|
|
56
|
+
}
|
|
57
|
+
initialize() {
|
|
58
|
+
this.signal.throwIfAborted();
|
|
59
|
+
return this.#initialization ??= this.#initialize();
|
|
60
|
+
}
|
|
61
|
+
async#initialize() {
|
|
62
|
+
const target = await this.source.resolve();
|
|
63
|
+
this.signal.throwIfAborted();
|
|
64
|
+
const url = new URL(target.endpoint);
|
|
65
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
66
|
+
throw new Error("S3 endpoint must be an HTTP(S) URL without embedded credentials, query or fragment");
|
|
67
|
+
}
|
|
68
|
+
const transport = this.source.transports[0];
|
|
69
|
+
if (!transport || transport.kind !== "tcp")
|
|
70
|
+
throw new Error("S3 requires a TCP transport");
|
|
71
|
+
const route = await transport.connect({
|
|
72
|
+
host: url.hostname,
|
|
73
|
+
port: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
|
|
74
|
+
servername: url.hostname
|
|
75
|
+
});
|
|
76
|
+
this.signal.throwIfAborted();
|
|
77
|
+
this.#http = s3Handler(target, route, this.limits);
|
|
78
|
+
this.#sdk = new AwsS3Client({
|
|
79
|
+
endpoint: target.endpoint,
|
|
80
|
+
region: target.region,
|
|
81
|
+
credentials: target.credentials,
|
|
82
|
+
forcePathStyle: target.forcePathStyle ?? true,
|
|
83
|
+
requestHandler: this.#http.handler,
|
|
84
|
+
maxAttempts: 1,
|
|
85
|
+
followRegionRedirects: false,
|
|
86
|
+
responseChecksumValidation: "WHEN_REQUIRED"
|
|
87
|
+
});
|
|
88
|
+
this.lifecycle.onRoute?.({ transport: transport.name });
|
|
89
|
+
}
|
|
90
|
+
#run(options, operation) {
|
|
91
|
+
this.signal.throwIfAborted();
|
|
92
|
+
const sdk = this.#sdk;
|
|
93
|
+
if (!sdk)
|
|
94
|
+
throw new Error("S3 client is not initialized");
|
|
95
|
+
const timeout = new AbortController;
|
|
96
|
+
const timer = setTimeout(() => timeout.abort(new Error("S3 request deadline exceeded")), this.limits.requestTimeoutMs);
|
|
97
|
+
timer.unref();
|
|
98
|
+
const signal = AbortSignal.any([this.signal, timeout.signal, ...options.signal ? [options.signal] : []]);
|
|
99
|
+
const pending = this.#pool.run(() => operation(sdk, signal), signal).finally(() => clearTimeout(timer));
|
|
100
|
+
this.#operations.add(pending);
|
|
101
|
+
pending.then(() => this.#operations.delete(pending), () => this.#operations.delete(pending));
|
|
102
|
+
return pending;
|
|
103
|
+
}
|
|
104
|
+
headBucket(bucket, options = {}) {
|
|
105
|
+
return this.#run(options, async (sdk, signal) => {
|
|
106
|
+
await sdk.send(new HeadBucketCommand({ Bucket: bucket }), { abortSignal: signal });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
headObject(bucket, key, options = {}) {
|
|
110
|
+
return this.#run(options, async (sdk, signal) => metadata(await sdk.send(new HeadObjectCommand({ Bucket: bucket, Key: key, VersionId: options.versionId }), { abortSignal: signal })));
|
|
111
|
+
}
|
|
112
|
+
listObjects(bucket, options) {
|
|
113
|
+
positive(options.maxKeys, "maxKeys", 1000);
|
|
114
|
+
return this.#run(options, async (sdk, signal) => {
|
|
115
|
+
const page = await sdk.send(new ListObjectsV2Command({
|
|
116
|
+
Bucket: bucket,
|
|
117
|
+
MaxKeys: options.maxKeys,
|
|
118
|
+
Prefix: options.prefix,
|
|
119
|
+
Delimiter: options.delimiter,
|
|
120
|
+
ContinuationToken: options.continuationToken
|
|
121
|
+
}), { abortSignal: signal });
|
|
122
|
+
return {
|
|
123
|
+
objects: (page.Contents ?? []).filter((item) => item.Key !== undefined).map((item) => ({
|
|
124
|
+
key: item.Key,
|
|
125
|
+
size: item.Size,
|
|
126
|
+
etag: item.ETag,
|
|
127
|
+
lastModified: item.LastModified
|
|
128
|
+
})),
|
|
129
|
+
prefixes: (page.CommonPrefixes ?? []).flatMap((item) => item.Prefix === undefined ? [] : [item.Prefix]),
|
|
130
|
+
truncated: page.IsTruncated ?? false,
|
|
131
|
+
continuationToken: page.NextContinuationToken
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
listBuckets(options) {
|
|
136
|
+
positive(options.maxBuckets, "maxBuckets", 1e4);
|
|
137
|
+
return this.#run(options, async (sdk, signal) => {
|
|
138
|
+
const page = await sdk.send(new ListBucketsCommand({
|
|
139
|
+
MaxBuckets: options.maxBuckets,
|
|
140
|
+
ContinuationToken: options.continuationToken
|
|
141
|
+
}), { abortSignal: signal });
|
|
142
|
+
return { buckets: (page.Buckets ?? []).filter((item) => item.Name !== undefined).map((item) => ({ name: item.Name, creationDate: item.CreationDate })), continuationToken: page.ContinuationToken };
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
getBucketVersioning(bucket, options = {}) {
|
|
146
|
+
return this.#run(options, async (sdk, signal) => {
|
|
147
|
+
const result = await sdk.send(new GetBucketVersioningCommand({ Bucket: bucket }), { abortSignal: signal });
|
|
148
|
+
if (result.Status === "Enabled")
|
|
149
|
+
return "enabled";
|
|
150
|
+
return result.Status === "Suspended" ? "suspended" : "disabled";
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
readObject(bucket, key, options) {
|
|
154
|
+
positive(options.maxBytes, "maxBytes");
|
|
155
|
+
return this.#run(options, async (sdk, signal) => {
|
|
156
|
+
const result = await sdk.send(new GetObjectCommand({
|
|
157
|
+
Bucket: bucket,
|
|
158
|
+
Key: key,
|
|
159
|
+
VersionId: options.versionId
|
|
160
|
+
}), { abortSignal: signal });
|
|
161
|
+
const body = result.Body;
|
|
162
|
+
if (!(body instanceof Readable))
|
|
163
|
+
throw new Error("S3 response is missing a Node readable body");
|
|
164
|
+
const abort = () => body.destroy(new Error("S3 object read aborted", { cause: signal.reason }));
|
|
165
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
166
|
+
const chunks = [];
|
|
167
|
+
let size = 0;
|
|
168
|
+
let truncated = false;
|
|
169
|
+
try {
|
|
170
|
+
signal.throwIfAborted();
|
|
171
|
+
for await (const chunk of body) {
|
|
172
|
+
signal.throwIfAborted();
|
|
173
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
174
|
+
const remaining = options.maxBytes - size;
|
|
175
|
+
chunks.push(Buffer.from(bytes.subarray(0, remaining)));
|
|
176
|
+
size += Math.min(remaining, bytes.length);
|
|
177
|
+
if (bytes.length > remaining || size === options.maxBytes && result.ContentLength !== undefined && result.ContentLength > size) {
|
|
178
|
+
truncated = true;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { ...metadata(result), bytes: Buffer.concat(chunks, size), truncated };
|
|
183
|
+
} finally {
|
|
184
|
+
signal.removeEventListener("abort", abort);
|
|
185
|
+
body.destroy();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
dispose() {
|
|
190
|
+
return this.#disposal ??= (async () => {
|
|
191
|
+
this.#controller.abort(new Error("S3 client disposed"));
|
|
192
|
+
await this.#initialization?.catch(() => {});
|
|
193
|
+
await Promise.allSettled(this.#operations);
|
|
194
|
+
this.#sdk?.destroy();
|
|
195
|
+
this.#http?.destroy();
|
|
196
|
+
})();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { S3Client };
|
package/dist/client-manager.d.ts
CHANGED
|
@@ -1,14 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { DataSource } from "./datasource";
|
|
3
|
-
/** Consumers borrow clients; only the root owner receives the disposal capability. */
|
|
4
|
-
export interface ClientProvider {
|
|
5
|
-
get<C extends Client>(source: DataSource<C>): Promise<C>;
|
|
6
|
-
}
|
|
7
|
-
/** @spec One root execution shares successful initialization per identity and closes clients at finalize. */
|
|
8
|
-
export declare class ClientManager implements ClientProvider {
|
|
9
|
-
#private;
|
|
10
|
-
readonly signal: AbortSignal;
|
|
11
|
-
constructor(signal?: AbortSignal);
|
|
12
|
-
get<C extends Client>(source: DataSource<C>): Promise<C>;
|
|
13
|
-
dispose(): Promise<void>;
|
|
14
|
-
}
|
|
1
|
+
export { ClientManager, type ClientProvider } from "@compforge/harness-common";
|
package/dist/client-manager.js
CHANGED
package/dist/client.d.ts
CHANGED
package/dist/datasource.d.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
export type
|
|
2
|
-
export { ClientManager, type ClientProvider } from "./client-manager";
|
|
3
|
-
import type { Client } from "./client";
|
|
1
|
+
export { dataSourceKey, ClientManager, type Client, type ClientProvider, type DataSource, type ServiceDataSource } from "@compforge/harness-common";
|
|
4
2
|
import type { Transport } from "./transport";
|
|
5
|
-
/** Identity must include protocol, target, configuration and credentials; never an object address. */
|
|
6
|
-
export interface DataSource<C extends Client> {
|
|
7
|
-
readonly key: string;
|
|
8
|
-
/** Construct only; Client.initialize owns external work. */
|
|
9
|
-
createClient(signal: AbortSignal): C;
|
|
10
|
-
}
|
|
11
3
|
/** Resolution owns configuration semantics; transports preserve the resolved target and identity. */
|
|
12
4
|
export interface ConnectionSource<Target> {
|
|
13
5
|
resolve(): Promise<Target>;
|
|
@@ -22,5 +14,3 @@ export interface ClientLifecycle {
|
|
|
22
14
|
reason?: string;
|
|
23
15
|
}) => void;
|
|
24
16
|
}
|
|
25
|
-
/** Stable in-memory identity; connection credentials must not appear in observable cache keys. */
|
|
26
|
-
export declare function dataSourceKey(protocol: string, configuration: unknown): string;
|
package/dist/datasource.js
CHANGED
|
@@ -2,12 +2,12 @@ import {
|
|
|
2
2
|
ServicePortForwarder
|
|
3
3
|
} from "../chunk-205a4vma.js";
|
|
4
4
|
import"../chunk-hv2kx47y.js";
|
|
5
|
-
import"../chunk-j46x7j7c.js";
|
|
6
|
-
import"../chunk-6fnhzhdb.js";
|
|
7
5
|
import {
|
|
8
6
|
KubectlExecutor
|
|
9
7
|
} from "../chunk-0rs1aacd.js";
|
|
10
8
|
import"../chunk-ztenxgjv.js";
|
|
9
|
+
import"../chunk-j46x7j7c.js";
|
|
10
|
+
import"../chunk-6fnhzhdb.js";
|
|
11
11
|
|
|
12
12
|
// src/kubernetes/client.ts
|
|
13
13
|
class KubernetesClient {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PodLogClient
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-vjsr2jb3.js";
|
|
4
4
|
import"../chunk-8nm9f0hh.js";
|
|
5
5
|
import"../chunk-mp88s7r7.js";
|
|
6
6
|
import"../chunk-1vfwvgkj.js";
|
|
7
|
-
import"../chunk-
|
|
8
|
-
import"../chunk-3dt25p98.js";
|
|
7
|
+
import"../chunk-fnc291vk.js";
|
|
9
8
|
import"../chunk-v3hxvnrc.js";
|
|
10
9
|
export {
|
|
11
10
|
PodLogClient
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PodLogClient
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-vjsr2jb3.js";
|
|
4
4
|
import"../chunk-8nm9f0hh.js";
|
|
5
5
|
import"../chunk-mp88s7r7.js";
|
|
6
6
|
import"../chunk-1vfwvgkj.js";
|
|
7
7
|
import {
|
|
8
8
|
dataSourceKey
|
|
9
|
-
} from "../chunk-
|
|
10
|
-
import"../chunk-3dt25p98.js";
|
|
9
|
+
} from "../chunk-fnc291vk.js";
|
|
11
10
|
import"../chunk-v3hxvnrc.js";
|
|
12
11
|
|
|
13
12
|
// src/kubernetes/pod-log-datasource.ts
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
ServicePortForwarder
|
|
3
3
|
} from "../chunk-205a4vma.js";
|
|
4
4
|
import"../chunk-hv2kx47y.js";
|
|
5
|
-
import"../chunk-j46x7j7c.js";
|
|
6
|
-
import"../chunk-6fnhzhdb.js";
|
|
7
5
|
import"../chunk-0rs1aacd.js";
|
|
8
6
|
import"../chunk-ztenxgjv.js";
|
|
7
|
+
import"../chunk-j46x7j7c.js";
|
|
8
|
+
import"../chunk-6fnhzhdb.js";
|
|
9
9
|
export {
|
|
10
10
|
ServicePortForwarder
|
|
11
11
|
};
|
package/dist/mysql/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Client } from "../client";
|
|
2
2
|
import { type Connection, type ConnectionOptions } from "mysql2/promise";
|
|
3
|
-
import type { Database, DatabaseTarget, DatabaseRow } from "./types";
|
|
3
|
+
import type { Database, DatabaseTarget, DatabaseRow, DatabaseQueryLimits, DatabaseQueryResult } from "./types";
|
|
4
4
|
import type { ConnectionSource, ClientLifecycle } from "../datasource";
|
|
5
5
|
import { type Transport } from "../transport";
|
|
6
6
|
export interface MysqlDatabaseOptions extends ClientLifecycle {
|
|
@@ -16,6 +16,8 @@ export declare class MysqlDatabase implements Database {
|
|
|
16
16
|
private readonly connect;
|
|
17
17
|
constructor(transports: readonly Transport[], options: MysqlDatabaseOptions, connect?: ConnectionFactory);
|
|
18
18
|
query(target: DatabaseTarget, sql: string, values: readonly unknown[]): Promise<DatabaseRow[]>;
|
|
19
|
+
/** Bounded, single-statement prepared execution on an isolated READ ONLY session (native only). */
|
|
20
|
+
queryReadonly(target: DatabaseTarget, sql: string, values: readonly unknown[], limits: DatabaseQueryLimits): Promise<DatabaseQueryResult>;
|
|
19
21
|
initialize(target: DatabaseTarget): Promise<void>;
|
|
20
22
|
queryOne(target: DatabaseTarget, sql: string, values: readonly unknown[]): Promise<DatabaseRow | undefined>;
|
|
21
23
|
close(): Promise<void>;
|
|
@@ -28,7 +30,7 @@ export declare class MysqlClient<Target extends DatabaseTarget = DatabaseTarget>
|
|
|
28
30
|
readonly signal: AbortSignal;
|
|
29
31
|
constructor(source: ConnectionSource<Target>, options: MysqlDatabaseOptions);
|
|
30
32
|
initialize(): Promise<void>;
|
|
31
|
-
get database():
|
|
33
|
+
get database(): MysqlDatabase;
|
|
32
34
|
get target(): Target;
|
|
33
35
|
dispose(): Promise<void>;
|
|
34
36
|
}
|
package/dist/mysql/client.js
CHANGED
|
@@ -2,9 +2,10 @@ import {
|
|
|
2
2
|
MysqlClient,
|
|
3
3
|
MysqlDatabase,
|
|
4
4
|
openMysql
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import"../chunk-v3hxvnrc.js";
|
|
5
|
+
} from "../chunk-9x1apyp4.js";
|
|
7
6
|
import"../chunk-s06hs7a8.js";
|
|
7
|
+
import"../chunk-kbewz29a.js";
|
|
8
|
+
import"../chunk-v3hxvnrc.js";
|
|
8
9
|
import"../chunk-xy4g56g4.js";
|
|
9
10
|
export {
|
|
10
11
|
openMysql,
|
package/dist/mysql/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MysqlClient,
|
|
3
|
-
MysqlDatabase,
|
|
4
|
-
openMysql
|
|
5
|
-
} from "../chunk-mkhyty75.js";
|
|
6
|
-
import"../chunk-v3hxvnrc.js";
|
|
7
1
|
import {
|
|
8
2
|
parseMysqlEnvTarget
|
|
9
3
|
} from "../chunk-qq77x9ek.js";
|
|
10
4
|
import"../chunk-pfar9cm3.js";
|
|
5
|
+
import {
|
|
6
|
+
MysqlClient,
|
|
7
|
+
MysqlDatabase,
|
|
8
|
+
openMysql
|
|
9
|
+
} from "../chunk-9x1apyp4.js";
|
|
11
10
|
import"../chunk-s06hs7a8.js";
|
|
11
|
+
import"../chunk-kbewz29a.js";
|
|
12
|
+
import"../chunk-v3hxvnrc.js";
|
|
12
13
|
import"../chunk-xy4g56g4.js";
|
|
13
14
|
export {
|
|
14
15
|
parseMysqlEnvTarget,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Connection } from "mysql2/promise";
|
|
2
|
+
import type { DatabaseQueryLimits, DatabaseQueryResult } from "./types";
|
|
3
|
+
export declare function validateQueryLimits(limits: DatabaseQueryLimits): void;
|
|
4
|
+
/**
|
|
5
|
+
* Owns a disposable session: setup, execution and cancellation cannot affect another borrower.
|
|
6
|
+
* Caller must classify SQL and use a least-privilege identity. READ ONLY is not a SQL sandbox
|
|
7
|
+
* (for example routines and temporary tables need additional caller policy).
|
|
8
|
+
*/
|
|
9
|
+
export declare function queryReadonlySession(connection: Connection, sql: string, values: readonly unknown[], limits: DatabaseQueryLimits, signal?: AbortSignal): Promise<DatabaseQueryResult>;
|
package/dist/mysql/types.d.ts
CHANGED
|
@@ -8,6 +8,19 @@ export interface DatabaseTarget extends DatabaseIdentity {
|
|
|
8
8
|
database: string;
|
|
9
9
|
}
|
|
10
10
|
export type DatabaseRow = Record<string, unknown>;
|
|
11
|
+
export interface DatabaseQueryLimits {
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
maxRows: number;
|
|
14
|
+
/** Maximum retained UTF-8 JSON row bytes, not a limit on an individual wire packet. */
|
|
15
|
+
maxBytes: number;
|
|
16
|
+
}
|
|
17
|
+
export interface DatabaseQueryResult {
|
|
18
|
+
rows: DatabaseRow[];
|
|
19
|
+
columns: string[];
|
|
20
|
+
bytes: number;
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
truncation?: "rows" | "bytes";
|
|
23
|
+
}
|
|
11
24
|
/** Storage-neutral operations shared by infrastructure consumers. */
|
|
12
25
|
export interface Database {
|
|
13
26
|
query(target: DatabaseTarget, sql: string, values: readonly unknown[]): Promise<DatabaseRow[]>;
|
package/dist/redis/index.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collectRedisKeyStats,
|
|
3
|
-
redisGroupedSampleSizes
|
|
4
|
-
} from "../chunk-jr9y0ghr.js";
|
|
5
|
-
import {
|
|
6
|
-
discoverRedisTopology
|
|
7
|
-
} from "../chunk-c6z919qb.js";
|
|
8
1
|
import {
|
|
9
2
|
RedisAccess,
|
|
10
3
|
RedisClient,
|
|
@@ -12,6 +5,13 @@ import {
|
|
|
12
5
|
openRedis,
|
|
13
6
|
parseRedisInfo
|
|
14
7
|
} from "../chunk-ajhdd4hp.js";
|
|
8
|
+
import {
|
|
9
|
+
collectRedisKeyStats,
|
|
10
|
+
redisGroupedSampleSizes
|
|
11
|
+
} from "../chunk-jr9y0ghr.js";
|
|
12
|
+
import {
|
|
13
|
+
discoverRedisTopology
|
|
14
|
+
} from "../chunk-c6z919qb.js";
|
|
15
15
|
export {
|
|
16
16
|
redisGroupedSampleSizes,
|
|
17
17
|
parseRedisInfo,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Client } from "../client";
|
|
2
|
+
import type { ClientLifecycle, ConnectionSource } from "../datasource";
|
|
3
|
+
import type { S3Target, S3Limits, S3RequestOptions, S3ObjectMetadata, S3ObjectPage, S3BucketPage, S3ObjectRead } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* @spec Read-only S3 operations share a bounded pool; no failed protocol operation is replayed.
|
|
6
|
+
* @spec HTTP errors retain SDK status/code, including ambiguous HEAD 403/404 responses.
|
|
7
|
+
* @why Initialization prepares a route without requiring account-wide bucket-list permission.
|
|
8
|
+
*/
|
|
9
|
+
export declare class S3Client implements Client {
|
|
10
|
+
#private;
|
|
11
|
+
private readonly source;
|
|
12
|
+
private readonly limits;
|
|
13
|
+
private readonly lifecycle;
|
|
14
|
+
readonly signal: AbortSignal;
|
|
15
|
+
constructor(source: ConnectionSource<S3Target>, limits: S3Limits, lifecycle?: ClientLifecycle);
|
|
16
|
+
initialize(): Promise<void>;
|
|
17
|
+
/** A failed HEAD remains an error, not a misleading exists=false. */
|
|
18
|
+
headBucket(bucket: string, options?: S3RequestOptions): Promise<void>;
|
|
19
|
+
headObject(bucket: string, key: string, options?: S3RequestOptions & {
|
|
20
|
+
versionId?: string;
|
|
21
|
+
}): Promise<S3ObjectMetadata>;
|
|
22
|
+
/** One bounded page; callers own traversal and total scan budgets. */
|
|
23
|
+
listObjects(bucket: string, options: S3RequestOptions & {
|
|
24
|
+
maxKeys: number;
|
|
25
|
+
prefix?: string;
|
|
26
|
+
delimiter?: string;
|
|
27
|
+
continuationToken?: string;
|
|
28
|
+
}): Promise<S3ObjectPage>;
|
|
29
|
+
listBuckets(options: S3RequestOptions & {
|
|
30
|
+
maxBuckets: number;
|
|
31
|
+
continuationToken?: string;
|
|
32
|
+
}): Promise<S3BucketPage>;
|
|
33
|
+
getBucketVersioning(bucket: string, options?: S3RequestOptions): Promise<"enabled" | "suspended" | "disabled">;
|
|
34
|
+
/** Read a bounded prefix. Own and close the response stream before returning the pool slot. */
|
|
35
|
+
readObject(bucket: string, key: string, options: S3RequestOptions & {
|
|
36
|
+
maxBytes: number;
|
|
37
|
+
versionId?: string;
|
|
38
|
+
}): Promise<S3ObjectRead>;
|
|
39
|
+
dispose(): Promise<void>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DataSource } from "../datasource";
|
|
2
|
+
import { type TcpTransport } from "../transport";
|
|
3
|
+
import { S3Client } from "./client";
|
|
4
|
+
import type { S3Limits, S3Target } from "./types";
|
|
5
|
+
/** Connection details are resolved by the caller; route identity includes the cluster/context, not just a transport name. */
|
|
6
|
+
export declare class S3DataSource implements DataSource<S3Client> {
|
|
7
|
+
#private;
|
|
8
|
+
readonly key: string;
|
|
9
|
+
constructor(target: S3Target, limits: S3Limits, route?: {
|
|
10
|
+
key: string;
|
|
11
|
+
transport: TcpTransport;
|
|
12
|
+
});
|
|
13
|
+
createClient(signal: AbortSignal): S3Client;
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NodeHttpHandler } from "@smithy/node-http-handler";
|
|
2
|
+
import type { Endpoint } from "../transport";
|
|
3
|
+
import type { S3Limits, S3Target } from "./types";
|
|
4
|
+
/** Routing changes only the socket destination, never the signed HTTP Host or TLS identity. */
|
|
5
|
+
export declare function s3Handler(target: S3Target, route: Endpoint, limits: S3Limits): {
|
|
6
|
+
handler: {
|
|
7
|
+
handle(request: Parameters<NodeHttpHandler["handle"]>[0], options: Parameters<NodeHttpHandler["handle"]>[1]): Promise<{
|
|
8
|
+
response: import("@smithy/core/transport").HttpResponse;
|
|
9
|
+
}>;
|
|
10
|
+
destroy(): void;
|
|
11
|
+
};
|
|
12
|
+
destroy: () => void;
|
|
13
|
+
};
|
package/dist/s3/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
S3DataSource
|
|
3
|
+
} from "../chunk-94anqtxh.js";
|
|
4
|
+
import"../chunk-fnc291vk.js";
|
|
5
|
+
import {
|
|
6
|
+
S3Client
|
|
7
|
+
} from "../chunk-zp1s833x.js";
|
|
8
|
+
import"../chunk-v3hxvnrc.js";
|
|
9
|
+
import"../chunk-x4ntb898.js";
|
|
10
|
+
import"../chunk-xy4g56g4.js";
|
|
11
|
+
export {
|
|
12
|
+
S3DataSource,
|
|
13
|
+
S3Client
|
|
14
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface S3Target {
|
|
2
|
+
endpoint: string;
|
|
3
|
+
region: string;
|
|
4
|
+
credentials: {
|
|
5
|
+
accessKeyId: string;
|
|
6
|
+
secretAccessKey: string;
|
|
7
|
+
sessionToken?: string;
|
|
8
|
+
};
|
|
9
|
+
forcePathStyle?: boolean;
|
|
10
|
+
/** Additional trust is explicit; TLS verification is never disabled for a tunnel. */
|
|
11
|
+
ca?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Root-owned policy, not product defaults. Request time includes queueing and body consumption. */
|
|
14
|
+
export interface S3Limits {
|
|
15
|
+
concurrency: number;
|
|
16
|
+
connectTimeoutMs: number;
|
|
17
|
+
requestTimeoutMs: number;
|
|
18
|
+
}
|
|
19
|
+
export interface S3RequestOptions {
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
export interface S3ObjectMetadata {
|
|
23
|
+
size?: number;
|
|
24
|
+
etag?: string;
|
|
25
|
+
lastModified?: Date;
|
|
26
|
+
contentType?: string;
|
|
27
|
+
versionId?: string;
|
|
28
|
+
metadata: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
export interface S3ObjectPage {
|
|
31
|
+
objects: Array<{
|
|
32
|
+
key: string;
|
|
33
|
+
size?: number;
|
|
34
|
+
etag?: string;
|
|
35
|
+
lastModified?: Date;
|
|
36
|
+
}>;
|
|
37
|
+
prefixes: string[];
|
|
38
|
+
truncated: boolean;
|
|
39
|
+
continuationToken?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface S3BucketPage {
|
|
42
|
+
buckets: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
creationDate?: Date;
|
|
45
|
+
}>;
|
|
46
|
+
continuationToken?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface S3ObjectRead extends S3ObjectMetadata {
|
|
49
|
+
bytes: Uint8Array;
|
|
50
|
+
truncated: boolean;
|
|
51
|
+
}
|
package/dist/s3/types.js
ADDED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@compforge/harness-toolbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Shared infrastructure clients, transports and bounded data capture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -57,6 +57,14 @@
|
|
|
57
57
|
"types": "./dist/opensearch/*.d.ts",
|
|
58
58
|
"import": "./dist/opensearch/*.js"
|
|
59
59
|
},
|
|
60
|
+
"./s3": {
|
|
61
|
+
"types": "./dist/s3/index.d.ts",
|
|
62
|
+
"import": "./dist/s3/index.js"
|
|
63
|
+
},
|
|
64
|
+
"./s3/*": {
|
|
65
|
+
"types": "./dist/s3/*.d.ts",
|
|
66
|
+
"import": "./dist/s3/*.js"
|
|
67
|
+
},
|
|
60
68
|
"./client": {
|
|
61
69
|
"types": "./dist/client.d.ts",
|
|
62
70
|
"import": "./dist/client.js"
|
|
@@ -75,10 +83,13 @@
|
|
|
75
83
|
"README.md"
|
|
76
84
|
],
|
|
77
85
|
"dependencies": {
|
|
78
|
-
"
|
|
79
|
-
"@
|
|
80
|
-
"@opensearch-project/opensearch": "3.6.0",
|
|
86
|
+
"@compforge/harness-common": "0.0.1",
|
|
87
|
+
"@aws-sdk/client-s3": "3.1133.0",
|
|
81
88
|
"@kubernetes/client-node": "1.4.0",
|
|
89
|
+
"@opensearch-project/opensearch": "3.6.0",
|
|
90
|
+
"@redis/client": "6.1.0",
|
|
91
|
+
"@smithy/node-http-handler": "4.12.1",
|
|
92
|
+
"mysql2": "3.23.1",
|
|
82
93
|
"node-fetch": "2.7.0"
|
|
83
94
|
},
|
|
84
95
|
"devDependencies": {
|
package/dist/chunk-3dt25p98.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// src/client-manager.ts
|
|
2
|
-
class ClientManager {
|
|
3
|
-
#controller = new AbortController;
|
|
4
|
-
signal;
|
|
5
|
-
#clients = new Map;
|
|
6
|
-
#ready = [];
|
|
7
|
-
#disposal;
|
|
8
|
-
constructor(signal) {
|
|
9
|
-
this.signal = signal ? AbortSignal.any([signal, this.#controller.signal]) : this.#controller.signal;
|
|
10
|
-
}
|
|
11
|
-
get(source) {
|
|
12
|
-
this.signal.throwIfAborted();
|
|
13
|
-
const existing = this.#clients.get(source.key);
|
|
14
|
-
if (existing)
|
|
15
|
-
return existing;
|
|
16
|
-
const pending = Promise.resolve().then(async () => {
|
|
17
|
-
this.signal.throwIfAborted();
|
|
18
|
-
const client = source.createClient(this.signal);
|
|
19
|
-
try {
|
|
20
|
-
await client.initialize();
|
|
21
|
-
this.signal.throwIfAborted();
|
|
22
|
-
this.#ready.push(client);
|
|
23
|
-
return client;
|
|
24
|
-
} catch (error) {
|
|
25
|
-
try {
|
|
26
|
-
await client.dispose();
|
|
27
|
-
} catch (cleanup) {
|
|
28
|
-
throw new AggregateError([error, cleanup], "Client initialization and cleanup failed");
|
|
29
|
-
}
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
this.#clients.set(source.key, pending);
|
|
34
|
-
pending.catch(() => {
|
|
35
|
-
if (this.#clients.get(source.key) === pending)
|
|
36
|
-
this.#clients.delete(source.key);
|
|
37
|
-
});
|
|
38
|
-
return pending;
|
|
39
|
-
}
|
|
40
|
-
dispose() {
|
|
41
|
-
return this.#disposal ??= (async () => {
|
|
42
|
-
this.#controller.abort(new Error("Client manager disposed"));
|
|
43
|
-
const settled = await Promise.allSettled(this.#clients.values());
|
|
44
|
-
this.#clients.clear();
|
|
45
|
-
const errors = settled.flatMap((result) => result.status === "rejected" && result.reason instanceof AggregateError ? [result.reason] : []);
|
|
46
|
-
for (const client of this.#ready.splice(0).reverse()) {
|
|
47
|
-
try {
|
|
48
|
-
await client.dispose();
|
|
49
|
-
} catch (error) {
|
|
50
|
-
errors.push(error);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (errors.length)
|
|
54
|
-
throw new AggregateError(errors, "Client cleanup failed");
|
|
55
|
-
})();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export { ClientManager };
|
package/dist/chunk-hqx7j9xe.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
// src/datasource.ts
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
function dataSourceKey(protocol, configuration) {
|
|
4
|
-
const canonical = (value) => {
|
|
5
|
-
if (Array.isArray(value))
|
|
6
|
-
return value.map(canonical);
|
|
7
|
-
if (value && typeof value === "object")
|
|
8
|
-
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonical(item)]));
|
|
9
|
-
return value;
|
|
10
|
-
};
|
|
11
|
-
return `${protocol}:${createHash("sha256").update(JSON.stringify(canonical(configuration))).digest("hex")}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export { dataSourceKey };
|