@glion/mllp-client 0.16.0

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.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Default entry point of `@glion/mllp-client`.
3
+ *
4
+ * `import { MllpClient } from "@glion/mllp-client"` resolves here on
5
+ * Node and Bun. This module is a thin re-export of
6
+ * `@glion/mllp-client/node` so the common case stays a single
7
+ * import line.
8
+ *
9
+ * For other runtimes:
10
+ *
11
+ * - Deno: `@glion/mllp-client/deno`
12
+ * - Cloudflare Workers: `@glion/mllp-client/workers`
13
+ * - Custom transport / browser via a bridge: `@glion/mllp-client/core`
14
+ *
15
+ * Bundlers that honour the `workerd` and `deno` keys in this
16
+ * package's `exports` map will automatically pick the right entry
17
+ * for the target runtime — the per-runtime subpaths are also
18
+ * available as explicit imports for clarity.
19
+ *
20
+ * @module
21
+ */
22
+ export type { Acknowledgment } from "./core/acknowledgment";
23
+ export type { MllpConnect, MllpConnectParams, MllpDuplexStream, } from "./core/connect";
24
+ export { MllpClientError, MllpClientErrorCode } from "./core/errors";
25
+ export { MllpClient, nodeConnect } from "./runtimes/node";
26
+ export type { BoundMllpClientOptions, MllpClientOptions, MllpClientTlsOptions, SendMode, SendOptions, } from "./runtimes/node";
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC1D,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,QAAQ,EACR,WAAW,GACZ,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { i as MllpClientErrorCode, r as MllpClientError } from "./client.js";
2
+ import { n as nodeConnect, t as MllpClient } from "./node.js";
3
+ export { MllpClient, MllpClientError, MllpClientErrorCode, nodeConnect };
package/dist/node.js ADDED
@@ -0,0 +1,214 @@
1
+ import { i as MllpClientErrorCode, n as subscribeAbort, r as MllpClientError, t as MllpClient$1 } from "./client.js";
2
+ import { connect } from "node:net";
3
+ import { Readable, Writable } from "node:stream";
4
+ import { connect as connect$1 } from "node:tls";
5
+ //#region src/runtimes/internal/node-error-mapping.ts
6
+ /**
7
+ * Translation layer between Node's socket error surface and
8
+ * `MllpClientError`. Lives in `runtimes/internal/` so it is unreachable
9
+ * via the package's `exports` map (and therefore safe to evolve without
10
+ * a major bump) while remaining importable from the per-runtime test
11
+ * suite.
12
+ *
13
+ * **Drift contract.** Node does not document `error.code` as a stable
14
+ * enum — values come from libuv (system errors) and OpenSSL (TLS
15
+ * verification errors), and new codes can appear when Node bumps
16
+ * either dependency. The mapping here is curated against:
17
+ *
18
+ * - System errors (`ECONN*`, `ENOTFOUND`, `ETIMEDOUT`, …) —
19
+ * {@link https://nodejs.org/api/errors.html#common-system-errors | Node — Common System Errors}.
20
+ * - TLS X.509 verification errors (`CERT_HAS_EXPIRED`,
21
+ * `DEPTH_ZERO_SELF_SIGNED_CERT`, `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, …) —
22
+ * {@link https://nodejs.org/api/tls.html#x509-certificate-error-codes | Node — X.509 Certificate Error Codes}
23
+ * (Node surfaces the upstream OpenSSL `X509_V_ERR_*` names).
24
+ * - Node-specific TLS errors (`ERR_TLS_*`) —
25
+ * {@link https://nodejs.org/api/errors.html#nodejs-error-codes | Node — Node.js Error Codes}.
26
+ *
27
+ * **The list does not aim to be exhaustive.** Two fallbacks absorb
28
+ * the long tail without leaking native errors to callers:
29
+ *
30
+ * 1. Any unrecognised `error.code` whose `error.message` looks TLS-flavoured
31
+ * ({@link looksLikeTlsHandshakeError}) routes to `TLS_HANDSHAKE_FAILED`.
32
+ * This catches new OpenSSL X.509 codes, `ERR_SSL_*` codes that arrive
33
+ * without a stable name, and bare `Error("alert handshake failure")`-style
34
+ * throws from Node's TLS layer.
35
+ * 2. Anything else routes to `CONNECTION_CLOSED`. Caller still gets a typed
36
+ * `MllpClientError` with the original Node error chained as `cause`; the
37
+ * caller's `error.code` discriminator is just less specific than ideal.
38
+ *
39
+ * **When to expand the explicit list.** Add a `case` only when a
40
+ * real-world report (a user issue, a CI failure, a production
41
+ * incident) shows a code consistently misclassified by the
42
+ * fallbacks. Don't pre-emptively enumerate from upstream changelogs —
43
+ * the fallback layer is doing its job for unknowns.
44
+ *
45
+ * @module
46
+ */
47
+ /**
48
+ * Translate a Node.js socket error into the matching
49
+ * {@link MllpClientError} so callers see one consistent error shape
50
+ * regardless of which native code path failed.
51
+ *
52
+ * See the module-level JSDoc for the full drift contract.
53
+ */
54
+ function mapSocketError(error, host, port) {
55
+ if (error instanceof MllpClientError) return error;
56
+ const target = `${host}:${port}`;
57
+ switch (error.code) {
58
+ case "ECONNREFUSED":
59
+ case "ENOTFOUND":
60
+ case "EHOSTUNREACH":
61
+ case "ENETUNREACH": return new MllpClientError(MllpClientErrorCode.CONNECTION_REFUSED, `Could not connect to ${target}: ${error.message}`, { cause: error });
62
+ case "ETIMEDOUT": return new MllpClientError(MllpClientErrorCode.TIMEOUT, `Connection to ${target} timed out: ${error.message}`, { cause: error });
63
+ case "CERT_HAS_EXPIRED":
64
+ case "DEPTH_ZERO_SELF_SIGNED_CERT":
65
+ case "ERR_TLS_CERT_ALTNAME_INVALID":
66
+ case "ERR_TLS_HANDSHAKE_TIMEOUT":
67
+ case "SELF_SIGNED_CERT_IN_CHAIN":
68
+ case "UNABLE_TO_GET_ISSUER_CERT":
69
+ case "UNABLE_TO_GET_ISSUER_CERT_LOCALLY":
70
+ case "UNABLE_TO_VERIFY_LEAF_SIGNATURE": return new MllpClientError(MllpClientErrorCode.TLS_HANDSHAKE_FAILED, `TLS handshake to ${target} failed: ${error.message}`, { cause: error });
71
+ default:
72
+ if (looksLikeTlsHandshakeError(error)) return new MllpClientError(MllpClientErrorCode.TLS_HANDSHAKE_FAILED, `TLS handshake to ${target} failed: ${error.message}`, { cause: error });
73
+ return new MllpClientError(MllpClientErrorCode.CONNECTION_CLOSED, `Connection error: ${error.message}`, { cause: error });
74
+ }
75
+ }
76
+ /**
77
+ * Best-effort detector for TLS handshake errors that don't carry a
78
+ * stable `error.code`. This is the drift-protection mechanism: when
79
+ * Node or OpenSSL adds a new TLS error code we haven't enumerated,
80
+ * a TLS-flavoured `error.message` keeps it routed to the typed
81
+ * {@link MllpClientErrorCode.TLS_HANDSHAKE_FAILED} bucket rather than
82
+ * falling through to `CONNECTION_CLOSED`.
83
+ *
84
+ * Triggers on: `ssl`, `tls`, `handshake`, `certificate` (case-insensitive).
85
+ * Tested in `test/node.test.ts` so a future change here cannot
86
+ * silently widen or narrow the routing.
87
+ */
88
+ function looksLikeTlsHandshakeError(error) {
89
+ const msg = error.message.toLowerCase();
90
+ return msg.includes("ssl") || msg.includes("tls") || msg.includes("handshake") || msg.includes("certificate");
91
+ }
92
+ //#endregion
93
+ //#region src/runtimes/node.ts
94
+ /**
95
+ * Node.js (and Bun) runtime adapter for `@glion/mllp-client`.
96
+ *
97
+ * Re-exports the core `MllpClient` pre-wired with a `connect`
98
+ * implementation that uses Node's `node:net` and `node:tls` modules.
99
+ * Application code on Node or Bun imports from this entry and never
100
+ * has to think about transport plumbing:
101
+ *
102
+ * ```ts
103
+ * import { MllpClient } from "@glion/mllp-client/node";
104
+ *
105
+ * const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
106
+ * const ack = await client.send(rawHl7Message);
107
+ * ```
108
+ *
109
+ * Bun ships a `node:net` shim that is API-compatible for our needs,
110
+ * so this same adapter covers both runtimes.
111
+ *
112
+ * @module
113
+ */
114
+ /**
115
+ * MLLP client pre-wired with the Node/Bun connector. API-identical to
116
+ * the core class — the only difference is that callers do not need
117
+ * to supply `connect`.
118
+ */
119
+ var MllpClient = class extends MllpClient$1 {
120
+ constructor(options) {
121
+ super({
122
+ ...options,
123
+ connect: nodeConnect
124
+ });
125
+ }
126
+ };
127
+ /**
128
+ * `MllpConnect` implementation backed by `node:net` (TCP) or
129
+ * `node:tls` (TLS). Returns a {@link MllpDuplexStream} whose readable
130
+ * and writable sides are the Web-Streams views of the underlying
131
+ * Node socket.
132
+ *
133
+ * Exported alongside the class so advanced callers can compose it
134
+ * with the core `MllpClient` directly (e.g., to wrap it with logging
135
+ * or fault injection in tests).
136
+ */
137
+ const nodeConnect = (params) => new Promise((resolve, reject) => {
138
+ const socket = openSocket(params);
139
+ const readyEvent = params.tls ? "secureConnect" : "connect";
140
+ const cleanupAbort = params.signal ? subscribeAbort(params.signal, () => {
141
+ socket.destroy();
142
+ reject(toAbortError(params.signal.reason, params.host, params.port));
143
+ }) : noop;
144
+ socket.once(readyEvent, () => {
145
+ cleanupAbort();
146
+ socket.setNoDelay(true);
147
+ resolve(toDuplex(socket));
148
+ });
149
+ socket.once("error", (error) => {
150
+ cleanupAbort();
151
+ socket.destroy();
152
+ reject(mapSocketError(error, params.host, params.port));
153
+ });
154
+ });
155
+ /** No-op disposer used when no abort signal was supplied. */
156
+ function noop() {}
157
+ /**
158
+ * Translate an abort `reason` from the connect-phase signal into a typed
159
+ * {@link MllpClientError}. Mirrors the precedence in `normaliseSendError` in
160
+ * `core/client.ts`: typed reasons pass through, anything else maps to TIMEOUT.
161
+ */
162
+ function toAbortError(reason, host, port) {
163
+ if (reason instanceof MllpClientError) return reason;
164
+ return new MllpClientError(MllpClientErrorCode.TIMEOUT, `Connect to ${host}:${port} aborted`, { cause: reason instanceof Error ? reason : void 0 });
165
+ }
166
+ /**
167
+ * Open a raw TCP or TLS socket using Node's built-ins. Splits TLS
168
+ * material into the shape `tls.connect()` expects and drops the
169
+ * `insecure` flag into `rejectUnauthorized` (the only opt-out form).
170
+ */
171
+ function openSocket(params) {
172
+ if (!params.tls) return connect({
173
+ host: params.host,
174
+ port: params.port
175
+ });
176
+ return connect$1({
177
+ ca: toBufferOrString(params.tls.ca),
178
+ cert: toBufferOrString(params.tls.cert),
179
+ host: params.host,
180
+ key: toBufferOrString(params.tls.key),
181
+ passphrase: params.tls.passphrase,
182
+ port: params.port,
183
+ rejectUnauthorized: params.tls.insecure !== true,
184
+ servername: params.tls.servername ?? params.host
185
+ });
186
+ }
187
+ /**
188
+ * Coerce TLS material from the cross-runtime `string | Uint8Array`
189
+ * shape into Node's expected `string | Buffer`. `Buffer.from(view)`
190
+ * is a zero-copy wrap when the input is already a `Buffer`.
191
+ */
192
+ function toBufferOrString(input) {
193
+ if (input === void 0) return;
194
+ return typeof input === "string" ? input : Buffer.from(input);
195
+ }
196
+ /**
197
+ * Wrap a Node `Socket` in the {@link MllpDuplexStream} contract.
198
+ * Bridges Node's stream model into Web Streams via `Readable.toWeb`
199
+ * and `Writable.toWeb` (Node 18+).
200
+ */
201
+ function toDuplex(socket) {
202
+ return {
203
+ close() {
204
+ if (!socket.destroyed) socket.destroy();
205
+ return Promise.resolve();
206
+ },
207
+ readable: Readable.toWeb(socket),
208
+ writable: Writable.toWeb(socket)
209
+ };
210
+ }
211
+ //#endregion
212
+ export { nodeConnect as n, MllpClient as t };
213
+
214
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.js","names":["CoreMllpClient","netConnect","tlsConnect"],"sources":["../src/runtimes/internal/node-error-mapping.ts","../src/runtimes/node.ts"],"sourcesContent":["/**\n * Translation layer between Node's socket error surface and\n * `MllpClientError`. Lives in `runtimes/internal/` so it is unreachable\n * via the package's `exports` map (and therefore safe to evolve without\n * a major bump) while remaining importable from the per-runtime test\n * suite.\n *\n * **Drift contract.** Node does not document `error.code` as a stable\n * enum — values come from libuv (system errors) and OpenSSL (TLS\n * verification errors), and new codes can appear when Node bumps\n * either dependency. The mapping here is curated against:\n *\n * - System errors (`ECONN*`, `ENOTFOUND`, `ETIMEDOUT`, …) —\n * {@link https://nodejs.org/api/errors.html#common-system-errors | Node — Common System Errors}.\n * - TLS X.509 verification errors (`CERT_HAS_EXPIRED`,\n * `DEPTH_ZERO_SELF_SIGNED_CERT`, `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, …) —\n * {@link https://nodejs.org/api/tls.html#x509-certificate-error-codes | Node — X.509 Certificate Error Codes}\n * (Node surfaces the upstream OpenSSL `X509_V_ERR_*` names).\n * - Node-specific TLS errors (`ERR_TLS_*`) —\n * {@link https://nodejs.org/api/errors.html#nodejs-error-codes | Node — Node.js Error Codes}.\n *\n * **The list does not aim to be exhaustive.** Two fallbacks absorb\n * the long tail without leaking native errors to callers:\n *\n * 1. Any unrecognised `error.code` whose `error.message` looks TLS-flavoured\n * ({@link looksLikeTlsHandshakeError}) routes to `TLS_HANDSHAKE_FAILED`.\n * This catches new OpenSSL X.509 codes, `ERR_SSL_*` codes that arrive\n * without a stable name, and bare `Error(\"alert handshake failure\")`-style\n * throws from Node's TLS layer.\n * 2. Anything else routes to `CONNECTION_CLOSED`. Caller still gets a typed\n * `MllpClientError` with the original Node error chained as `cause`; the\n * caller's `error.code` discriminator is just less specific than ideal.\n *\n * **When to expand the explicit list.** Add a `case` only when a\n * real-world report (a user issue, a CI failure, a production\n * incident) shows a code consistently misclassified by the\n * fallbacks. Don't pre-emptively enumerate from upstream changelogs —\n * the fallback layer is doing its job for unknowns.\n *\n * @module\n */\n\nimport { MllpClientError, MllpClientErrorCode } from \"../../core/errors\";\n\n/** Node socket errors carry an optional system code (e.g. `ECONNREFUSED`). */\nexport type NodeError = Error & { code?: string };\n\n/**\n * Translate a Node.js socket error into the matching\n * {@link MllpClientError} so callers see one consistent error shape\n * regardless of which native code path failed.\n *\n * See the module-level JSDoc for the full drift contract.\n */\nexport function mapSocketError(\n error: NodeError,\n host: string,\n port: number\n): MllpClientError {\n if (error instanceof MllpClientError) {\n return error;\n }\n const target = `${host}:${port}`;\n switch (error.code) {\n case \"ECONNREFUSED\":\n case \"ENOTFOUND\":\n case \"EHOSTUNREACH\":\n case \"ENETUNREACH\": {\n return new MllpClientError(\n MllpClientErrorCode.CONNECTION_REFUSED,\n `Could not connect to ${target}: ${error.message}`,\n { cause: error }\n );\n }\n case \"ETIMEDOUT\": {\n return new MllpClientError(\n MllpClientErrorCode.TIMEOUT,\n `Connection to ${target} timed out: ${error.message}`,\n { cause: error }\n );\n }\n case \"CERT_HAS_EXPIRED\":\n case \"DEPTH_ZERO_SELF_SIGNED_CERT\":\n case \"ERR_TLS_CERT_ALTNAME_INVALID\":\n case \"ERR_TLS_HANDSHAKE_TIMEOUT\":\n case \"SELF_SIGNED_CERT_IN_CHAIN\":\n case \"UNABLE_TO_GET_ISSUER_CERT\":\n case \"UNABLE_TO_GET_ISSUER_CERT_LOCALLY\":\n case \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\": {\n return new MllpClientError(\n MllpClientErrorCode.TLS_HANDSHAKE_FAILED,\n `TLS handshake to ${target} failed: ${error.message}`,\n { cause: error }\n );\n }\n default: {\n if (looksLikeTlsHandshakeError(error)) {\n return new MllpClientError(\n MllpClientErrorCode.TLS_HANDSHAKE_FAILED,\n `TLS handshake to ${target} failed: ${error.message}`,\n { cause: error }\n );\n }\n return new MllpClientError(\n MllpClientErrorCode.CONNECTION_CLOSED,\n `Connection error: ${error.message}`,\n { cause: error }\n );\n }\n }\n}\n\n/**\n * Best-effort detector for TLS handshake errors that don't carry a\n * stable `error.code`. This is the drift-protection mechanism: when\n * Node or OpenSSL adds a new TLS error code we haven't enumerated,\n * a TLS-flavoured `error.message` keeps it routed to the typed\n * {@link MllpClientErrorCode.TLS_HANDSHAKE_FAILED} bucket rather than\n * falling through to `CONNECTION_CLOSED`.\n *\n * Triggers on: `ssl`, `tls`, `handshake`, `certificate` (case-insensitive).\n * Tested in `test/node.test.ts` so a future change here cannot\n * silently widen or narrow the routing.\n */\nexport function looksLikeTlsHandshakeError(error: NodeError): boolean {\n const msg = error.message.toLowerCase();\n return (\n msg.includes(\"ssl\") ||\n msg.includes(\"tls\") ||\n msg.includes(\"handshake\") ||\n msg.includes(\"certificate\")\n );\n}\n","/**\n * Node.js (and Bun) runtime adapter for `@glion/mllp-client`.\n *\n * Re-exports the core `MllpClient` pre-wired with a `connect`\n * implementation that uses Node's `node:net` and `node:tls` modules.\n * Application code on Node or Bun imports from this entry and never\n * has to think about transport plumbing:\n *\n * ```ts\n * import { MllpClient } from \"@glion/mllp-client/node\";\n *\n * const client = new MllpClient({ host: \"127.0.0.1\", port: 2575 });\n * const ack = await client.send(rawHl7Message);\n * ```\n *\n * Bun ships a `node:net` shim that is API-compatible for our needs,\n * so this same adapter covers both runtimes.\n *\n * @module\n */\n\nimport { connect as netConnect } from \"node:net\";\nimport type { Socket } from \"node:net\";\nimport { Readable, Writable } from \"node:stream\";\nimport { connect as tlsConnect } from \"node:tls\";\n\nimport { MllpClient as CoreMllpClient } from \"../core/client\";\nimport type {\n BoundMllpClientOptions,\n MllpClientOptions,\n MllpClientTlsOptions,\n} from \"../core/client\";\nimport type {\n MllpConnect,\n MllpConnectParams,\n MllpDuplexStream,\n} from \"../core/connect\";\nimport { MllpClientError, MllpClientErrorCode } from \"../core/errors\";\nimport { subscribeAbort } from \"../core/internal/subscribe-abort\";\nimport type { NodeError } from \"./internal/node-error-mapping\";\nimport { mapSocketError } from \"./internal/node-error-mapping\";\n\n// ---------------------------------------------------------------------------\n// Public class\n// ---------------------------------------------------------------------------\n\n/**\n * MLLP client pre-wired with the Node/Bun connector. API-identical to\n * the core class — the only difference is that callers do not need\n * to supply `connect`.\n */\nexport class MllpClient extends CoreMllpClient {\n constructor(options: BoundMllpClientOptions) {\n super({ ...options, connect: nodeConnect });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { Acknowledgment } from \"../core/acknowledgment\";\nexport type {\n MllpConnect,\n MllpConnectParams,\n MllpDuplexStream,\n} from \"../core/connect\";\nexport type {\n BoundMllpClientOptions,\n MllpClientOptions,\n MllpClientTlsOptions,\n SendMode,\n SendOptions,\n} from \"../core/client\";\nexport { MllpClientError, MllpClientErrorCode } from \"../core/errors\";\n\n// ---------------------------------------------------------------------------\n// Node connector\n// ---------------------------------------------------------------------------\n\n/**\n * `MllpConnect` implementation backed by `node:net` (TCP) or\n * `node:tls` (TLS). Returns a {@link MllpDuplexStream} whose readable\n * and writable sides are the Web-Streams views of the underlying\n * Node socket.\n *\n * Exported alongside the class so advanced callers can compose it\n * with the core `MllpClient` directly (e.g., to wrap it with logging\n * or fault injection in tests).\n */\nexport const nodeConnect: MllpConnect = (params) =>\n // oxlint-disable-next-line promise/avoid-new\n new Promise<MllpDuplexStream>((resolve, reject) => {\n const socket = openSocket(params);\n const readyEvent = params.tls ? \"secureConnect\" : \"connect\";\n\n const cleanupAbort = params.signal\n ? subscribeAbort(params.signal, () => {\n socket.destroy();\n reject(toAbortError(params.signal!.reason, params.host, params.port));\n })\n : noop;\n\n socket.once(readyEvent, () => {\n cleanupAbort();\n // Disable Nagle so small MLLP frames flush immediately.\n socket.setNoDelay(true);\n resolve(toDuplex(socket));\n });\n\n socket.once(\"error\", (error: NodeError) => {\n cleanupAbort();\n socket.destroy();\n reject(mapSocketError(error, params.host, params.port));\n });\n });\n\n/** No-op disposer used when no abort signal was supplied. */\nfunction noop(): void {\n /* nothing to dispose */\n}\n\n/**\n * Translate an abort `reason` from the connect-phase signal into a typed\n * {@link MllpClientError}. Mirrors the precedence in `normaliseSendError` in\n * `core/client.ts`: typed reasons pass through, anything else maps to TIMEOUT.\n */\nfunction toAbortError(\n reason: unknown,\n host: string,\n port: number\n): MllpClientError {\n if (reason instanceof MllpClientError) {\n return reason;\n }\n return new MllpClientError(\n MllpClientErrorCode.TIMEOUT,\n `Connect to ${host}:${port} aborted`,\n { cause: reason instanceof Error ? reason : undefined }\n );\n}\n\n/**\n * Open a raw TCP or TLS socket using Node's built-ins. Splits TLS\n * material into the shape `tls.connect()` expects and drops the\n * `insecure` flag into `rejectUnauthorized` (the only opt-out form).\n */\nfunction openSocket(params: MllpConnectParams): Socket {\n if (!params.tls) {\n return netConnect({ host: params.host, port: params.port });\n }\n return tlsConnect({\n ca: toBufferOrString(params.tls.ca),\n cert: toBufferOrString(params.tls.cert),\n host: params.host,\n key: toBufferOrString(params.tls.key),\n passphrase: params.tls.passphrase,\n port: params.port,\n // Verification is on by default; the only opt-out is the\n // explicit `insecure: true`. Spelling the mapping out protects\n // against future Node default changes.\n rejectUnauthorized: params.tls.insecure !== true,\n servername: params.tls.servername ?? params.host,\n });\n}\n\n/**\n * Coerce TLS material from the cross-runtime `string | Uint8Array`\n * shape into Node's expected `string | Buffer`. `Buffer.from(view)`\n * is a zero-copy wrap when the input is already a `Buffer`.\n */\nfunction toBufferOrString(\n input: string | Uint8Array | undefined\n): string | Buffer | undefined {\n if (input === undefined) {\n return;\n }\n return typeof input === \"string\" ? input : Buffer.from(input);\n}\n\n/**\n * Wrap a Node `Socket` in the {@link MllpDuplexStream} contract.\n * Bridges Node's stream model into Web Streams via `Readable.toWeb`\n * and `Writable.toWeb` (Node 18+).\n */\nfunction toDuplex(socket: Socket): MllpDuplexStream {\n return {\n // `socket.destroy()` is sync and non-throwing — no work to await,\n // so we resolve immediately. The async signature is for the\n // contract (Workers' `socket.close()` is genuinely async).\n close() {\n if (!socket.destroyed) {\n socket.destroy();\n }\n return Promise.resolve();\n },\n readable: Readable.toWeb(socket) as ReadableStream<Uint8Array>,\n writable: Writable.toWeb(socket) as WritableStream<Uint8Array>,\n };\n}\n\n// Node socket-error translation lives in\n// `./internal/node-error-mapping` so the per-runtime test suite can\n// pin the drift-protection contract directly. See that module's\n// JSDoc for the curated list and fallback behaviour.\n"],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Translation layer between Node's socket error surface and
3
+ * `MllpClientError`. Lives in `runtimes/internal/` so it is unreachable
4
+ * via the package's `exports` map (and therefore safe to evolve without
5
+ * a major bump) while remaining importable from the per-runtime test
6
+ * suite.
7
+ *
8
+ * **Drift contract.** Node does not document `error.code` as a stable
9
+ * enum — values come from libuv (system errors) and OpenSSL (TLS
10
+ * verification errors), and new codes can appear when Node bumps
11
+ * either dependency. The mapping here is curated against:
12
+ *
13
+ * - System errors (`ECONN*`, `ENOTFOUND`, `ETIMEDOUT`, …) —
14
+ * {@link https://nodejs.org/api/errors.html#common-system-errors | Node — Common System Errors}.
15
+ * - TLS X.509 verification errors (`CERT_HAS_EXPIRED`,
16
+ * `DEPTH_ZERO_SELF_SIGNED_CERT`, `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, …) —
17
+ * {@link https://nodejs.org/api/tls.html#x509-certificate-error-codes | Node — X.509 Certificate Error Codes}
18
+ * (Node surfaces the upstream OpenSSL `X509_V_ERR_*` names).
19
+ * - Node-specific TLS errors (`ERR_TLS_*`) —
20
+ * {@link https://nodejs.org/api/errors.html#nodejs-error-codes | Node — Node.js Error Codes}.
21
+ *
22
+ * **The list does not aim to be exhaustive.** Two fallbacks absorb
23
+ * the long tail without leaking native errors to callers:
24
+ *
25
+ * 1. Any unrecognised `error.code` whose `error.message` looks TLS-flavoured
26
+ * ({@link looksLikeTlsHandshakeError}) routes to `TLS_HANDSHAKE_FAILED`.
27
+ * This catches new OpenSSL X.509 codes, `ERR_SSL_*` codes that arrive
28
+ * without a stable name, and bare `Error("alert handshake failure")`-style
29
+ * throws from Node's TLS layer.
30
+ * 2. Anything else routes to `CONNECTION_CLOSED`. Caller still gets a typed
31
+ * `MllpClientError` with the original Node error chained as `cause`; the
32
+ * caller's `error.code` discriminator is just less specific than ideal.
33
+ *
34
+ * **When to expand the explicit list.** Add a `case` only when a
35
+ * real-world report (a user issue, a CI failure, a production
36
+ * incident) shows a code consistently misclassified by the
37
+ * fallbacks. Don't pre-emptively enumerate from upstream changelogs —
38
+ * the fallback layer is doing its job for unknowns.
39
+ *
40
+ * @module
41
+ */
42
+ import { MllpClientError } from "../../core/errors";
43
+ /** Node socket errors carry an optional system code (e.g. `ECONNREFUSED`). */
44
+ export type NodeError = Error & {
45
+ code?: string;
46
+ };
47
+ /**
48
+ * Translate a Node.js socket error into the matching
49
+ * {@link MllpClientError} so callers see one consistent error shape
50
+ * regardless of which native code path failed.
51
+ *
52
+ * See the module-level JSDoc for the full drift contract.
53
+ */
54
+ export declare function mapSocketError(error: NodeError, host: string, port: number): MllpClientError;
55
+ /**
56
+ * Best-effort detector for TLS handshake errors that don't carry a
57
+ * stable `error.code`. This is the drift-protection mechanism: when
58
+ * Node or OpenSSL adds a new TLS error code we haven't enumerated,
59
+ * a TLS-flavoured `error.message` keeps it routed to the typed
60
+ * {@link MllpClientErrorCode.TLS_HANDSHAKE_FAILED} bucket rather than
61
+ * falling through to `CONNECTION_CLOSED`.
62
+ *
63
+ * Triggers on: `ssl`, `tls`, `handshake`, `certificate` (case-insensitive).
64
+ * Tested in `test/node.test.ts` so a future change here cannot
65
+ * silently widen or narrow the routing.
66
+ */
67
+ export declare function looksLikeTlsHandshakeError(error: NodeError): boolean;
68
+ //# sourceMappingURL=node-error-mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-error-mapping.d.ts","sourceRoot":"","sources":["../../../src/runtimes/internal/node-error-mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAAE,eAAe,EAAuB,MAAM,mBAAmB,CAAC;AAEzE,8EAA8E;AAC9E,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,eAAe,CAoDjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAQpE"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Node.js (and Bun) runtime adapter for `@glion/mllp-client`.
3
+ *
4
+ * Re-exports the core `MllpClient` pre-wired with a `connect`
5
+ * implementation that uses Node's `node:net` and `node:tls` modules.
6
+ * Application code on Node or Bun imports from this entry and never
7
+ * has to think about transport plumbing:
8
+ *
9
+ * ```ts
10
+ * import { MllpClient } from "@glion/mllp-client/node";
11
+ *
12
+ * const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
13
+ * const ack = await client.send(rawHl7Message);
14
+ * ```
15
+ *
16
+ * Bun ships a `node:net` shim that is API-compatible for our needs,
17
+ * so this same adapter covers both runtimes.
18
+ *
19
+ * @module
20
+ */
21
+ import { MllpClient as CoreMllpClient } from "../core/client";
22
+ import type { BoundMllpClientOptions } from "../core/client";
23
+ import type { MllpConnect } from "../core/connect";
24
+ /**
25
+ * MLLP client pre-wired with the Node/Bun connector. API-identical to
26
+ * the core class — the only difference is that callers do not need
27
+ * to supply `connect`.
28
+ */
29
+ export declare class MllpClient extends CoreMllpClient {
30
+ constructor(options: BoundMllpClientOptions);
31
+ }
32
+ export type { Acknowledgment } from "../core/acknowledgment";
33
+ export type { MllpConnect, MllpConnectParams, MllpDuplexStream, } from "../core/connect";
34
+ export type { BoundMllpClientOptions, MllpClientOptions, MllpClientTlsOptions, SendMode, SendOptions, } from "../core/client";
35
+ export { MllpClientError, MllpClientErrorCode } from "../core/errors";
36
+ /**
37
+ * `MllpConnect` implementation backed by `node:net` (TCP) or
38
+ * `node:tls` (TLS). Returns a {@link MllpDuplexStream} whose readable
39
+ * and writable sides are the Web-Streams views of the underlying
40
+ * Node socket.
41
+ *
42
+ * Exported alongside the class so advanced callers can compose it
43
+ * with the core `MllpClient` directly (e.g., to wrap it with logging
44
+ * or fault injection in tests).
45
+ */
46
+ export declare const nodeConnect: MllpConnect;
47
+ //# sourceMappingURL=node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/runtimes/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,KAAK,EACV,sBAAsB,EAGvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EACV,WAAW,EAGZ,MAAM,iBAAiB,CAAC;AAUzB;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,cAAc;gBAChC,OAAO,EAAE,sBAAsB;CAG5C;AAMD,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,QAAQ,EACR,WAAW,GACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAMtE;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,EAAE,WAyBtB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { i as MllpClientErrorCode, r as MllpClientError } from "../client.js";
2
+ import { n as nodeConnect, t as MllpClient } from "../node.js";
3
+ export { MllpClient, MllpClientError, MllpClientErrorCode, nodeConnect };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Cloudflare Workers runtime adapter for `@glion/mllp-client`.
3
+ *
4
+ * Re-exports the core `MllpClient` pre-wired with a `connect`
5
+ * implementation that uses the `cloudflare:sockets` API. Application
6
+ * code on Workers imports from this entry:
7
+ *
8
+ * ```ts
9
+ * import { MllpClient } from "@glion/mllp-client/workers";
10
+ *
11
+ * const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
12
+ * const ack = await client.send(rawHl7Message);
13
+ * ```
14
+ *
15
+ * The `cloudflare:sockets` module is only available inside the
16
+ * Workers (`workerd`) runtime. Importing this entry from a Node or
17
+ * Deno build will fail at module-resolution time — that's the
18
+ * intended behaviour, since each runtime should pick its own
19
+ * adapter.
20
+ *
21
+ * @module
22
+ */
23
+ import { MllpClient as CoreMllpClient } from "../core/client";
24
+ import type { BoundMllpClientOptions } from "../core/client";
25
+ import type { MllpConnect } from "../core/connect";
26
+ /**
27
+ * MLLP client pre-wired with the Cloudflare Workers connector.
28
+ * API-identical to the core class — the only difference is that
29
+ * callers do not need to supply `connect`.
30
+ */
31
+ export declare class MllpClient extends CoreMllpClient {
32
+ constructor(options: BoundMllpClientOptions);
33
+ }
34
+ export type { Acknowledgment } from "../core/acknowledgment";
35
+ export type { MllpConnect, MllpConnectParams, MllpDuplexStream, } from "../core/connect";
36
+ export type { BoundMllpClientOptions, MllpClientOptions, MllpClientTlsOptions, SendMode, SendOptions, } from "../core/client";
37
+ export { MllpClientError, MllpClientErrorCode } from "../core/errors";
38
+ /**
39
+ * `MllpConnect` implementation backed by Cloudflare's
40
+ * `cloudflare:sockets` module. Returns a {@link MllpDuplexStream} that
41
+ * mirrors the Worker socket's already-Web-Streams shape.
42
+ *
43
+ * TLS is on iff `params.tls` is set (the core normalises `tls: true`
44
+ * to `tls: {}` before reaching the adapter). Workers exposes a small
45
+ * subset of TLS options compared to Node — `ca`, `cert`, `key`,
46
+ * `passphrase`, and `servername` cannot be supplied programmatically
47
+ * and are rejected with `INVALID_INPUT`. `insecure: true` is also
48
+ * rejected: on Node it means "TLS on, certificate verification off",
49
+ * but Workers does not expose a way to disable verification, so
50
+ * silently downgrading to plain TCP would be a security regression.
51
+ * Callers wanting plain TCP must pass `tls: false` (the constructor
52
+ * defaults `tls` to `true`).
53
+ *
54
+ * **Error-code divergence vs. the Node adapter.** `cloudflare:sockets`
55
+ * does not surface enough detail to distinguish a TLS handshake
56
+ * failure from a TCP connect failure. Both surface here as
57
+ * `CONNECTION_REFUSED`. The Node adapter, by contrast, sniffs the
58
+ * underlying error and routes handshake failures to
59
+ * `TLS_HANDSHAKE_FAILED`. Callers writing cross-runtime code that
60
+ * switches on `error.code` must accept this asymmetry, or treat
61
+ * `CONNECTION_REFUSED` as the supertype.
62
+ */
63
+ export declare const workersConnect: MllpConnect;
64
+ //# sourceMappingURL=workers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workers.d.ts","sourceRoot":"","sources":["../../src/runtimes/workers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AASH,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC9D,OAAO,KAAK,EACV,sBAAsB,EAEvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAoB,MAAM,iBAAiB,CAAC;AAQrE;;;;GAIG;AACH,qBAAa,UAAW,SAAQ,cAAc;gBAChC,OAAO,EAAE,sBAAsB;CAG5C;AAMD,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,QAAQ,EACR,WAAW,GACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAMtE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,cAAc,EAAE,WAsD5B,CAAC"}
@@ -0,0 +1,143 @@
1
+ import { i as MllpClientErrorCode, n as subscribeAbort, r as MllpClientError, t as MllpClient$1 } from "../client.js";
2
+ import { connect } from "cloudflare:sockets";
3
+ //#region src/runtimes/workers.ts
4
+ /**
5
+ * Cloudflare Workers runtime adapter for `@glion/mllp-client`.
6
+ *
7
+ * Re-exports the core `MllpClient` pre-wired with a `connect`
8
+ * implementation that uses the `cloudflare:sockets` API. Application
9
+ * code on Workers imports from this entry:
10
+ *
11
+ * ```ts
12
+ * import { MllpClient } from "@glion/mllp-client/workers";
13
+ *
14
+ * const client = new MllpClient({ host: "127.0.0.1", port: 2575 });
15
+ * const ack = await client.send(rawHl7Message);
16
+ * ```
17
+ *
18
+ * The `cloudflare:sockets` module is only available inside the
19
+ * Workers (`workerd`) runtime. Importing this entry from a Node or
20
+ * Deno build will fail at module-resolution time — that's the
21
+ * intended behaviour, since each runtime should pick its own
22
+ * adapter.
23
+ *
24
+ * @module
25
+ */
26
+ /**
27
+ * MLLP client pre-wired with the Cloudflare Workers connector.
28
+ * API-identical to the core class — the only difference is that
29
+ * callers do not need to supply `connect`.
30
+ */
31
+ var MllpClient = class extends MllpClient$1 {
32
+ constructor(options) {
33
+ super({
34
+ ...options,
35
+ connect: workersConnect
36
+ });
37
+ }
38
+ };
39
+ /**
40
+ * `MllpConnect` implementation backed by Cloudflare's
41
+ * `cloudflare:sockets` module. Returns a {@link MllpDuplexStream} that
42
+ * mirrors the Worker socket's already-Web-Streams shape.
43
+ *
44
+ * TLS is on iff `params.tls` is set (the core normalises `tls: true`
45
+ * to `tls: {}` before reaching the adapter). Workers exposes a small
46
+ * subset of TLS options compared to Node — `ca`, `cert`, `key`,
47
+ * `passphrase`, and `servername` cannot be supplied programmatically
48
+ * and are rejected with `INVALID_INPUT`. `insecure: true` is also
49
+ * rejected: on Node it means "TLS on, certificate verification off",
50
+ * but Workers does not expose a way to disable verification, so
51
+ * silently downgrading to plain TCP would be a security regression.
52
+ * Callers wanting plain TCP must pass `tls: false` (the constructor
53
+ * defaults `tls` to `true`).
54
+ *
55
+ * **Error-code divergence vs. the Node adapter.** `cloudflare:sockets`
56
+ * does not surface enough detail to distinguish a TLS handshake
57
+ * failure from a TCP connect failure. Both surface here as
58
+ * `CONNECTION_REFUSED`. The Node adapter, by contrast, sniffs the
59
+ * underlying error and routes handshake failures to
60
+ * `TLS_HANDSHAKE_FAILED`. Callers writing cross-runtime code that
61
+ * switches on `error.code` must accept this asymmetry, or treat
62
+ * `CONNECTION_REFUSED` as the supertype.
63
+ */
64
+ const workersConnect = async (params) => {
65
+ rejectUnsupportedTlsMaterial(params.tls);
66
+ const socket = connect({
67
+ hostname: params.host,
68
+ port: params.port
69
+ }, {
70
+ allowHalfOpen: true,
71
+ secureTransport: params.tls ? "on" : "off"
72
+ });
73
+ const dispose = params.signal ? subscribeAbort(params.signal, () => {
74
+ socket.close().catch(() => {});
75
+ }) : null;
76
+ try {
77
+ await socket.opened;
78
+ } catch (error) {
79
+ if (params.signal?.aborted) throw toAbortError(params.signal.reason, params.host, params.port, error);
80
+ const wrapped = error instanceof Error ? error : new Error(String(error));
81
+ throw new MllpClientError(MllpClientErrorCode.CONNECTION_REFUSED, `Could not connect to ${params.host}:${params.port}: ${wrapped.message}`, { cause: wrapped });
82
+ } finally {
83
+ dispose?.();
84
+ }
85
+ return {
86
+ async close() {
87
+ try {
88
+ await socket.close();
89
+ } catch {}
90
+ },
91
+ readable: socket.readable,
92
+ writable: socket.writable
93
+ };
94
+ };
95
+ /**
96
+ * Translate an abort `reason` from the connect-phase signal into a typed
97
+ * {@link MllpClientError}. Mirrors the precedence in `normaliseSendError` in
98
+ * `core/client.ts`: typed reasons pass through, anything else maps to TIMEOUT
99
+ * with the reason chained as `cause`.
100
+ */
101
+ function toAbortError(reason, host, port, fallbackCause) {
102
+ if (reason instanceof MllpClientError) return reason;
103
+ let cause;
104
+ if (reason instanceof Error) cause = reason;
105
+ else if (fallbackCause instanceof Error) cause = fallbackCause;
106
+ return new MllpClientError(MllpClientErrorCode.TIMEOUT, `Connect to ${host}:${port} aborted`, { cause });
107
+ }
108
+ /**
109
+ * Reject TLS configuration the Workers runtime cannot honour.
110
+ * Surfacing the mismatch as `INVALID_INPUT` is more honest than
111
+ * silently dropping the material — operators who supply a custom CA,
112
+ * client cert, key, passphrase, or servername expect it to take
113
+ * effect.
114
+ *
115
+ * `insecure: true` is rejected separately because the rationale is
116
+ * different: Workers cannot disable certificate verification
117
+ * independently, and silently dropping to plain TCP would downgrade an
118
+ * explicitly TLS-typed configuration to plaintext — a runtime-dependent
119
+ * security regression vs. the Node adapter.
120
+ *
121
+ * For all rejected fields the error message names the field, points to
122
+ * the Cloudflare TCP-sockets docs, and lists the platform-side
123
+ * mechanisms (Hyperdrive, Worker bindings, upstream TLS termination)
124
+ * so an operator porting from Node has a concrete next step.
125
+ */
126
+ function rejectUnsupportedTlsMaterial(tls) {
127
+ if (!tls) return;
128
+ const unsupported = [];
129
+ if (tls.ca) unsupported.push("ca");
130
+ if (tls.cert) unsupported.push("cert");
131
+ if (tls.key) unsupported.push("key");
132
+ if (tls.passphrase !== void 0) unsupported.push("passphrase");
133
+ if (tls.servername !== void 0) unsupported.push("servername");
134
+ if (unsupported.length > 0) {
135
+ const fields = unsupported.map((f) => `\`tls.${f}\``).join(", ");
136
+ throw new MllpClientError(MllpClientErrorCode.INVALID_INPUT, `Cloudflare Workers does not accept programmatic TLS material (${fields}). Configure TLS via the Cloudflare platform — Hyperdrive, Worker bindings, or terminate TLS upstream of the receiver. See https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/`);
137
+ }
138
+ if (tls.insecure === true) throw new MllpClientError(MllpClientErrorCode.INVALID_INPUT, "Cloudflare Workers does not support disabling TLS certificate verification (`tls.insecure: true`). Pass `tls: false` for plain TCP (caller takes the security trade-off explicitly), or terminate TLS upstream of the receiver via the Cloudflare platform. See https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/");
139
+ }
140
+ //#endregion
141
+ export { MllpClient, MllpClientError, MllpClientErrorCode, workersConnect };
142
+
143
+ //# sourceMappingURL=workers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workers.js","names":["CoreMllpClient","workerSocketConnect"],"sources":["../../src/runtimes/workers.ts"],"sourcesContent":["/**\n * Cloudflare Workers runtime adapter for `@glion/mllp-client`.\n *\n * Re-exports the core `MllpClient` pre-wired with a `connect`\n * implementation that uses the `cloudflare:sockets` API. Application\n * code on Workers imports from this entry:\n *\n * ```ts\n * import { MllpClient } from \"@glion/mllp-client/workers\";\n *\n * const client = new MllpClient({ host: \"127.0.0.1\", port: 2575 });\n * const ack = await client.send(rawHl7Message);\n * ```\n *\n * The `cloudflare:sockets` module is only available inside the\n * Workers (`workerd`) runtime. Importing this entry from a Node or\n * Deno build will fail at module-resolution time — that's the\n * intended behaviour, since each runtime should pick its own\n * adapter.\n *\n * @module\n */\n\n// `cloudflare:sockets` is a runtime-provided module — its shape is\n// declared in `./cloudflare-sockets.d.ts`. See that file for why we\n// do not use `@cloudflare/workers-types` (it pollutes Web Streams\n// globals project-wide, which breaks the Node adapter's\n// `node:stream/web` types).\nimport { connect as workerSocketConnect } from \"cloudflare:sockets\";\n\nimport { MllpClient as CoreMllpClient } from \"../core/client\";\nimport type {\n BoundMllpClientOptions,\n MllpClientTlsOptions,\n} from \"../core/client\";\nimport type { MllpConnect, MllpDuplexStream } from \"../core/connect\";\nimport { MllpClientError, MllpClientErrorCode } from \"../core/errors\";\nimport { subscribeAbort } from \"../core/internal/subscribe-abort\";\n\n// ---------------------------------------------------------------------------\n// Public class\n// ---------------------------------------------------------------------------\n\n/**\n * MLLP client pre-wired with the Cloudflare Workers connector.\n * API-identical to the core class — the only difference is that\n * callers do not need to supply `connect`.\n */\nexport class MllpClient extends CoreMllpClient {\n constructor(options: BoundMllpClientOptions) {\n super({ ...options, connect: workersConnect });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport type { Acknowledgment } from \"../core/acknowledgment\";\nexport type {\n MllpConnect,\n MllpConnectParams,\n MllpDuplexStream,\n} from \"../core/connect\";\nexport type {\n BoundMllpClientOptions,\n MllpClientOptions,\n MllpClientTlsOptions,\n SendMode,\n SendOptions,\n} from \"../core/client\";\nexport { MllpClientError, MllpClientErrorCode } from \"../core/errors\";\n\n// ---------------------------------------------------------------------------\n// Workers connector\n// ---------------------------------------------------------------------------\n\n/**\n * `MllpConnect` implementation backed by Cloudflare's\n * `cloudflare:sockets` module. Returns a {@link MllpDuplexStream} that\n * mirrors the Worker socket's already-Web-Streams shape.\n *\n * TLS is on iff `params.tls` is set (the core normalises `tls: true`\n * to `tls: {}` before reaching the adapter). Workers exposes a small\n * subset of TLS options compared to Node — `ca`, `cert`, `key`,\n * `passphrase`, and `servername` cannot be supplied programmatically\n * and are rejected with `INVALID_INPUT`. `insecure: true` is also\n * rejected: on Node it means \"TLS on, certificate verification off\",\n * but Workers does not expose a way to disable verification, so\n * silently downgrading to plain TCP would be a security regression.\n * Callers wanting plain TCP must pass `tls: false` (the constructor\n * defaults `tls` to `true`).\n *\n * **Error-code divergence vs. the Node adapter.** `cloudflare:sockets`\n * does not surface enough detail to distinguish a TLS handshake\n * failure from a TCP connect failure. Both surface here as\n * `CONNECTION_REFUSED`. The Node adapter, by contrast, sniffs the\n * underlying error and routes handshake failures to\n * `TLS_HANDSHAKE_FAILED`. Callers writing cross-runtime code that\n * switches on `error.code` must accept this asymmetry, or treat\n * `CONNECTION_REFUSED` as the supertype.\n */\nexport const workersConnect: MllpConnect = async (params) => {\n rejectUnsupportedTlsMaterial(params.tls);\n\n const socket = workerSocketConnect(\n { hostname: params.host, port: params.port },\n {\n allowHalfOpen: true,\n secureTransport: params.tls ? \"on\" : \"off\",\n }\n );\n\n // Honour the caller's abort signal — when it fires we close the\n // pending socket so `socket.opened` rejects with the abort.\n const dispose = params.signal\n ? subscribeAbort(params.signal, () => {\n // oxlint-disable-next-line promise/prefer-await-to-then\n socket.close().catch(() => {\n /* socket already closing — non-actionable */\n });\n })\n : null;\n\n try {\n await socket.opened;\n } catch (error) {\n if (params.signal?.aborted) {\n throw toAbortError(params.signal.reason, params.host, params.port, error);\n }\n const wrapped = error instanceof Error ? error : new Error(String(error));\n throw new MllpClientError(\n MllpClientErrorCode.CONNECTION_REFUSED,\n `Could not connect to ${params.host}:${params.port}: ${wrapped.message}`,\n { cause: wrapped }\n );\n } finally {\n dispose?.();\n }\n\n return {\n // The contract requires `close()` to resolve (never reject) and to\n // be idempotent. workerd's `socket.close()` is genuinely async; we\n // await it and swallow any rejection so the contract holds.\n // Teardown errors at this point are non-actionable — the request\n // lifecycle is already ending.\n async close() {\n try {\n await socket.close();\n } catch {\n /* idempotent close or close-time error — non-actionable */\n }\n },\n readable: socket.readable,\n writable: socket.writable,\n };\n};\n\n/**\n * Translate an abort `reason` from the connect-phase signal into a typed\n * {@link MllpClientError}. Mirrors the precedence in `normaliseSendError` in\n * `core/client.ts`: typed reasons pass through, anything else maps to TIMEOUT\n * with the reason chained as `cause`.\n */\nfunction toAbortError(\n reason: unknown,\n host: string,\n port: number,\n fallbackCause: unknown\n): MllpClientError {\n if (reason instanceof MllpClientError) {\n return reason;\n }\n let cause: Error | undefined;\n if (reason instanceof Error) {\n cause = reason;\n } else if (fallbackCause instanceof Error) {\n cause = fallbackCause;\n }\n return new MllpClientError(\n MllpClientErrorCode.TIMEOUT,\n `Connect to ${host}:${port} aborted`,\n { cause }\n );\n}\n\n/**\n * Reject TLS configuration the Workers runtime cannot honour.\n * Surfacing the mismatch as `INVALID_INPUT` is more honest than\n * silently dropping the material — operators who supply a custom CA,\n * client cert, key, passphrase, or servername expect it to take\n * effect.\n *\n * `insecure: true` is rejected separately because the rationale is\n * different: Workers cannot disable certificate verification\n * independently, and silently dropping to plain TCP would downgrade an\n * explicitly TLS-typed configuration to plaintext — a runtime-dependent\n * security regression vs. the Node adapter.\n *\n * For all rejected fields the error message names the field, points to\n * the Cloudflare TCP-sockets docs, and lists the platform-side\n * mechanisms (Hyperdrive, Worker bindings, upstream TLS termination)\n * so an operator porting from Node has a concrete next step.\n */\nfunction rejectUnsupportedTlsMaterial(\n tls: MllpClientTlsOptions | undefined\n): void {\n if (!tls) {\n return;\n }\n\n const unsupported: string[] = [];\n if (tls.ca) {\n unsupported.push(\"ca\");\n }\n if (tls.cert) {\n unsupported.push(\"cert\");\n }\n if (tls.key) {\n unsupported.push(\"key\");\n }\n if (tls.passphrase !== undefined) {\n unsupported.push(\"passphrase\");\n }\n if (tls.servername !== undefined) {\n unsupported.push(\"servername\");\n }\n\n if (unsupported.length > 0) {\n const fields = unsupported.map((f) => `\\`tls.${f}\\``).join(\", \");\n throw new MllpClientError(\n MllpClientErrorCode.INVALID_INPUT,\n `Cloudflare Workers does not accept programmatic TLS material (${fields}). Configure TLS via the Cloudflare platform — Hyperdrive, Worker bindings, or terminate TLS upstream of the receiver. See https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/`\n );\n }\n\n if (tls.insecure === true) {\n throw new MllpClientError(\n MllpClientErrorCode.INVALID_INPUT,\n \"Cloudflare Workers does not support disabling TLS certificate verification (`tls.insecure: true`). Pass `tls: false` for plain TCP (caller takes the security trade-off explicitly), or terminate TLS upstream of the receiver via the Cloudflare platform. See https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/\"\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,aAAb,cAAgCA,aAAe;CAC7C,YAAY,SAAiC;AAC3C,QAAM;GAAE,GAAG;GAAS,SAAS;GAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDlD,MAAa,iBAA8B,OAAO,WAAW;AAC3D,8BAA6B,OAAO,IAAI;CAExC,MAAM,SAASC,QACb;EAAE,UAAU,OAAO;EAAM,MAAM,OAAO;EAAM,EAC5C;EACE,eAAe;EACf,iBAAiB,OAAO,MAAM,OAAO;EACtC,CACF;CAID,MAAM,UAAU,OAAO,SACnB,eAAe,OAAO,cAAc;AAElC,SAAO,OAAO,CAAC,YAAY,GAEzB;GACF,GACF;AAEJ,KAAI;AACF,QAAM,OAAO;UACN,OAAO;AACd,MAAI,OAAO,QAAQ,QACjB,OAAM,aAAa,OAAO,OAAO,QAAQ,OAAO,MAAM,OAAO,MAAM,MAAM;EAE3E,MAAM,UAAU,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC;AACzE,QAAM,IAAI,gBACR,oBAAoB,oBACpB,wBAAwB,OAAO,KAAK,GAAG,OAAO,KAAK,IAAI,QAAQ,WAC/D,EAAE,OAAO,SAAS,CACnB;WACO;AACR,aAAW;;AAGb,QAAO;EAML,MAAM,QAAQ;AACZ,OAAI;AACF,UAAM,OAAO,OAAO;WACd;;EAIV,UAAU,OAAO;EACjB,UAAU,OAAO;EAClB;;;;;;;;AASH,SAAS,aACP,QACA,MACA,MACA,eACiB;AACjB,KAAI,kBAAkB,gBACpB,QAAO;CAET,IAAI;AACJ,KAAI,kBAAkB,MACpB,SAAQ;UACC,yBAAyB,MAClC,SAAQ;AAEV,QAAO,IAAI,gBACT,oBAAoB,SACpB,cAAc,KAAK,GAAG,KAAK,WAC3B,EAAE,OAAO,CACV;;;;;;;;;;;;;;;;;;;;AAqBH,SAAS,6BACP,KACM;AACN,KAAI,CAAC,IACH;CAGF,MAAM,cAAwB,EAAE;AAChC,KAAI,IAAI,GACN,aAAY,KAAK,KAAK;AAExB,KAAI,IAAI,KACN,aAAY,KAAK,OAAO;AAE1B,KAAI,IAAI,IACN,aAAY,KAAK,MAAM;AAEzB,KAAI,IAAI,eAAe,KAAA,EACrB,aAAY,KAAK,aAAa;AAEhC,KAAI,IAAI,eAAe,KAAA,EACrB,aAAY,KAAK,aAAa;AAGhC,KAAI,YAAY,SAAS,GAAG;EAC1B,MAAM,SAAS,YAAY,KAAK,MAAM,SAAS,EAAE,IAAI,CAAC,KAAK,KAAK;AAChE,QAAM,IAAI,gBACR,oBAAoB,eACpB,iEAAiE,OAAO,gMACzE;;AAGH,KAAI,IAAI,aAAa,KACnB,OAAM,IAAI,gBACR,oBAAoB,eACpB,sUACD"}