socketduct 0.1.15
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/.dockerignore +11 -0
- package/.env.example +131 -0
- package/AGENTS.md +168 -0
- package/Dockerfile +30 -0
- package/README.md +134 -0
- package/changelog.d/20260716083144-v2-ack-wire-format.md +3 -0
- package/changelog.d/20260716092044-v2-only-stream-framing.md +1 -0
- package/changelog.d/20260727113000-relay-connect-timeout-destroy.md +1 -0
- package/changelog.d/20260813093000-private-relay-hardening.md +1 -0
- package/changelog.d/20260824183412-initial-target-recovery-https.md +1 -0
- package/changelog.d/20260824230000-tls-relay-session-close.md +1 -0
- package/changelog.d/20260825120000-reconnect-close-target-timeout.md +1 -0
- package/changelog.d/20260829120000-protocol-v3-multiplexing.md +1 -0
- package/changelog.d/20260829160000-v3-review-corrections.md +1 -0
- package/changelog.d/20260830000000-fix-socket-idle-timeout.md +1 -0
- package/changelog.d/20260830090000-multi-gateway-transport-set.md +1 -0
- package/changelog.d/20260830093000-relay-close-settlement.md +1 -0
- package/changelog.d/20260830120000-immutable-gateway-image.md +1 -0
- package/changelog.d/20260902210000-native-reverse-gateway.md +1 -0
- package/changelog.d/20260902230000-relay-resume-terminal-order.md +1 -0
- package/changelog.d/20260902230708-multiplexed-open-ownership.md +1 -0
- package/changelog.d/20260902231541-multiplexed-terminal-close.md +1 -0
- package/changelog.d/20260902231542-reverse-close-position.md +1 -0
- package/changelog.d/20260902231543-reverse-terminal-connect.md +1 -0
- package/changelog.d/20260902233000-reverse-gateway-bounds.md +1 -0
- package/changelog.d/20260903001000-reverse-data-ordering.md +1 -0
- package/changelog.d/20260903003000-reverse-hub-restart-window.md +1 -0
- package/changelog.d/20260903010000-reverse-terminal-lifecycle.md +1 -0
- package/changelog.d/20260904142743-transport-set-readiness-wait.md +1 -0
- package/changelog.d/20260904152855-deferred-open-snapshots-binary-views.md +1 -0
- package/changelog.d/20260904163041-tensorbuzz-container-workflow.md +1 -0
- package/changelog.d/20260904174955-transport-set-terminal-causes.md +1 -0
- package/changelog.d/20260905081000-contain-reverse-auth-write-errors.md +1 -0
- package/changelog.d/20260905100520-reverse-hub-close-race.md +1 -0
- package/changelog.d/20260905120000-persistent-jsonl-logging.md +1 -0
- package/changelog.d/20260908071009-jsonl-partial-append-recovery.md +1 -0
- package/changelog.d/20260908074916-private-log-directory-setup.md +1 -0
- package/changelog.d/20260909093057-reverse-large-write-backpressure.md +1 -0
- package/changelog.d/20260909135830-actionable-error-diagnostics.md +1 -0
- package/changelog.d/20260909150000-transport-set-error-redaction.md +1 -0
- package/docker-compose.dev.yml +19 -0
- package/docker-compose.reverse-gateway.yml +36 -0
- package/docker-compose.yml +73 -0
- package/docs/architecture.md +264 -0
- package/docs/docker-topology.md +164 -0
- package/docs/docker.md +281 -0
- package/docs/logging.md +185 -0
- package/docs/plans/initial-implementation.md +205 -0
- package/docs/plans/socket-docker-integration.md +221 -0
- package/docs/protocol.md +558 -0
- package/docs/releasing.md +18 -0
- package/docs/reverse-gateway.md +186 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +144 -0
- package/docs/running.md +215 -0
- package/docs/security.md +258 -0
- package/docs/session-lifecycle.md +127 -0
- package/docs/socket-api.md +448 -0
- package/eslint.config.js +36 -0
- package/package.json +44 -0
- package/scripts/deploy-gateway.js +194 -0
- package/scripts/test.js +28 -0
- package/src/auth/passwords.js +38 -0
- package/src/auth/static-token-store.js +41 -0
- package/src/auth/user-store.js +139 -0
- package/src/client/connect.js +76 -0
- package/src/client/deferred-virtual-socket.js +312 -0
- package/src/client/http-agent.js +101 -0
- package/src/client/https-agent.js +366 -0
- package/src/client/multiplexed-connection.js +1504 -0
- package/src/client/multiplexed-transport-set.js +917 -0
- package/src/client/multiplexed.js +3 -0
- package/src/client/socketduct-socket.js +1174 -0
- package/src/client/virtual-socket.js +256 -0
- package/src/config/environment.js +400 -0
- package/src/edge/tcp-edge.js +82 -0
- package/src/index.js +101 -0
- package/src/logging/structured-logger.js +745 -0
- package/src/protocol/compression.js +325 -0
- package/src/protocol/frame.js +341 -0
- package/src/protocol/v3-compression.js +44 -0
- package/src/protocol/v3-frame.js +209 -0
- package/src/proxy/multiplexed-relay.js +798 -0
- package/src/proxy/tcp-relay.js +1191 -0
- package/src/relay.js +3 -0
- package/src/reverse/client.js +721 -0
- package/src/reverse/common.js +88 -0
- package/src/reverse/gateway.js +663 -0
- package/src/reverse/hub.js +563 -0
- package/src/reverse/index.js +3 -0
- package/src/reverse/runtime.js +78 -0
- package/src/server/control-handler.js +170 -0
- package/src/server/primary.js +64 -0
- package/src/server/relay-runtime.js +124 -0
- package/src/server/worker.js +39 -0
- package/src/sessions/session.js +212 -0
- package/src/spool/spool-file.js +451 -0
- package/tensorbuzz.yml +36 -0
- package/test/e2e/gateway-process.test.js +262 -0
- package/test/e2e/reverse-gateway-process.test.js +285 -0
- package/test/fixtures/logging/enospc-matrix.mjs +238 -0
- package/test/fixtures/logging/enospc-recovery.mjs +146 -0
- package/test/fixtures/logging/process-writer.mjs +20 -0
- package/test/helpers/tls-certificates.js +115 -0
- package/test/integration/initial-target-recovery.test.js +518 -0
- package/test/integration/logging-recovery.test.js +163 -0
- package/test/integration/multiplexed-compatibility.test.js +254 -0
- package/test/integration/multiplexed-corrections.test.js +551 -0
- package/test/integration/multiplexed-failures.test.js +339 -0
- package/test/integration/multiplexed-transport-set.test.js +456 -0
- package/test/integration/multiplexed-transport.test.js +208 -0
- package/test/integration/reverse-gateway.test.js +1293 -0
- package/test/integration/socketduct-http-agent.test.js +435 -0
- package/test/integration/socketduct-https-agent.test.js +669 -0
- package/test/integration/stream-compression.test.js +1267 -0
- package/test/integration/tcp-forwarding.test.js +2878 -0
- package/test/integration/tls-forwarding.test.js +748 -0
- package/test/unit/auth/auth.test.js +64 -0
- package/test/unit/auth/static-token-store.test.js +14 -0
- package/test/unit/client/connect.test.js +58 -0
- package/test/unit/client/multiplexed-spool-budget.test.js +309 -0
- package/test/unit/client/multiplexed-transport-set.test.js +906 -0
- package/test/unit/client/socketduct-socket.test.js +1124 -0
- package/test/unit/client/virtual-socket.test.js +98 -0
- package/test/unit/config/docker-gateway-files.test.js +274 -0
- package/test/unit/config/environment.test.js +507 -0
- package/test/unit/index.test.js +78 -0
- package/test/unit/logging/structured-logger.test.js +674 -0
- package/test/unit/protocol/compression.test.js +421 -0
- package/test/unit/protocol/frame.test.js +336 -0
- package/test/unit/protocol/v3-compression.test.js +31 -0
- package/test/unit/protocol/v3-frame.test.js +57 -0
- package/test/unit/release-configuration.test.js +21 -0
- package/test/unit/server/control-handler.test.js +291 -0
- package/test/unit/server/primary.test.js +59 -0
- package/test/unit/server/relay-runtime.test.js +434 -0
- package/test/unit/sessions/session.test.js +84 -0
- package/test/unit/spool/spool-file.test.js +295 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import test from "node:test"
|
|
3
|
+
|
|
4
|
+
import {isLoopbackHost, loadEnvironmentConfig} from "../../../src/config/environment.js"
|
|
5
|
+
|
|
6
|
+
test("loadEnvironmentConfig parses relay Docker environment", () => {
|
|
7
|
+
const config = loadEnvironmentConfig({
|
|
8
|
+
SOCKETDUCT_MODE: "relay",
|
|
9
|
+
SOCKETDUCT_RELAY_HOST: "0.0.0.0",
|
|
10
|
+
SOCKETDUCT_RELAY_PORT: "3100",
|
|
11
|
+
SOCKETDUCT_RESUME_WINDOW_MS: "45000",
|
|
12
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one,backup:token-two",
|
|
13
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "tensorbuzz.internal:443,127.0.0.1:8080"
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
assert.deepEqual(config, {
|
|
17
|
+
mode: "relay",
|
|
18
|
+
relayHost: "0.0.0.0",
|
|
19
|
+
relayPort: 3100,
|
|
20
|
+
resumeWindowMs: 45000,
|
|
21
|
+
maxTargetBufferBytes: 1_048_576,
|
|
22
|
+
maxTotalBufferBytes: 16_777_216,
|
|
23
|
+
initialTargetRetryDelayMs: 250,
|
|
24
|
+
initialTargetRetryMaxDelayMs: 5_000,
|
|
25
|
+
initialTargetConnectTimeoutMs: 10_000,
|
|
26
|
+
handshakeTimeoutMs: 10_000,
|
|
27
|
+
maxInputBytesPerConnection: 65_536,
|
|
28
|
+
statusIntervalMs: 30_000,
|
|
29
|
+
compression: "auto",
|
|
30
|
+
protocolVersion: 2,
|
|
31
|
+
minCompressBytes: 256,
|
|
32
|
+
maxDecompressedBytes: 16_777_216,
|
|
33
|
+
maxExpansionRatio: 1000,
|
|
34
|
+
authTokens: [
|
|
35
|
+
{username: "edge", token: "token-one"},
|
|
36
|
+
{username: "backup", token: "token-two"}
|
|
37
|
+
],
|
|
38
|
+
targetAllowlist: [
|
|
39
|
+
{host: "tensorbuzz.internal", port: 443},
|
|
40
|
+
{host: "127.0.0.1", port: 8080}
|
|
41
|
+
],
|
|
42
|
+
allowInsecurePublic: false
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("loadEnvironmentConfig accepts legacy Seamline relay secrets during migration", () => {
|
|
47
|
+
const config = loadEnvironmentConfig({
|
|
48
|
+
SOCKETDUCT_MODE: "relay",
|
|
49
|
+
SEAMLINE_AUTH_TOKENS: "edge:legacy-token",
|
|
50
|
+
SEAMLINE_TARGET_ALLOWLIST: "docker-socket-shim:2375"
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(config.authTokens, [{username: "edge", token: "legacy-token"}])
|
|
54
|
+
assert.deepEqual(config.targetAllowlist, [{host: "docker-socket-shim", port: 2375}])
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("loadEnvironmentConfig prefers Socketduct relay secrets over legacy Seamline aliases", () => {
|
|
58
|
+
const config = loadEnvironmentConfig({
|
|
59
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:new-token",
|
|
60
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "docker-socket-shim:2375",
|
|
61
|
+
SEAMLINE_AUTH_TOKENS: "edge:legacy-token",
|
|
62
|
+
SEAMLINE_TARGET_ALLOWLIST: "legacy-docker:2375"
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
assert.deepEqual(config.authTokens, [{username: "edge", token: "new-token"}])
|
|
66
|
+
assert.deepEqual(config.targetAllowlist, [{host: "docker-socket-shim", port: 2375}])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("loadEnvironmentConfig rejects malformed ports and list entries", () => {
|
|
70
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_RELAY_PORT: "0"}), /positive integer/)
|
|
71
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_RELAY_PORT: "70000"}), /TCP port/)
|
|
72
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_AUTH_TOKENS: "missing-token"}), /username:token/)
|
|
73
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_TARGET_ALLOWLIST: "missing-port"}), /host:port/)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("loadEnvironmentConfig parses or disables the status interval", () => {
|
|
77
|
+
assert.equal(loadEnvironmentConfig({SOCKETDUCT_STATUS_INTERVAL_MS: "45000"}).statusIntervalMs, 45_000)
|
|
78
|
+
assert.equal(loadEnvironmentConfig({SOCKETDUCT_STATUS_INTERVAL_MS: "0"}).statusIntervalMs, 0)
|
|
79
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_STATUS_INTERVAL_MS: "-1"}), /non-negative integer/)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("loadEnvironmentConfig keeps persistent logging disabled by default and parses explicit bounds", () => {
|
|
83
|
+
assert.equal(loadEnvironmentConfig({}).fileLogging, undefined)
|
|
84
|
+
assert.equal(loadEnvironmentConfig({SOCKETDUCT_LOG_DIRECTORY: " "}).fileLogging, undefined)
|
|
85
|
+
assert.deepEqual(loadEnvironmentConfig({
|
|
86
|
+
SOCKETDUCT_LOG_DIRECTORY: " /var/log/socketduct ",
|
|
87
|
+
SOCKETDUCT_LOG_MAX_FILE_BYTES: "2097152",
|
|
88
|
+
SOCKETDUCT_LOG_MAX_FILES: "4"
|
|
89
|
+
}).fileLogging, {
|
|
90
|
+
directory: "/var/log/socketduct",
|
|
91
|
+
maxFileBytes: 2_097_152,
|
|
92
|
+
maxFiles: 4
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("loadEnvironmentConfig rejects unsafe persistent logging bounds", () => {
|
|
97
|
+
assert.throws(() => loadEnvironmentConfig({
|
|
98
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
99
|
+
SOCKETDUCT_LOG_MAX_FILE_BYTES: "100"
|
|
100
|
+
}), /SOCKETDUCT_LOG_MAX_FILE_BYTES/)
|
|
101
|
+
assert.throws(() => loadEnvironmentConfig({
|
|
102
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
103
|
+
SOCKETDUCT_LOG_MAX_FILE_BYTES: "9007199254740992"
|
|
104
|
+
}), /SOCKETDUCT_LOG_MAX_FILE_BYTES/)
|
|
105
|
+
assert.throws(() => loadEnvironmentConfig({
|
|
106
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
107
|
+
SOCKETDUCT_LOG_MAX_FILES: "0"
|
|
108
|
+
}), /SOCKETDUCT_LOG_MAX_FILES/)
|
|
109
|
+
assert.throws(() => loadEnvironmentConfig({
|
|
110
|
+
SOCKETDUCT_LOG_DIRECTORY: "/var/log/socketduct",
|
|
111
|
+
SOCKETDUCT_LOG_MAX_FILES: "101"
|
|
112
|
+
}), /SOCKETDUCT_LOG_MAX_FILES/)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test("loadEnvironmentConfig selects v3 explicitly and keeps v2 as rollback default", () => {
|
|
116
|
+
assert.equal(loadEnvironmentConfig({}).protocolVersion, 2)
|
|
117
|
+
assert.equal(loadEnvironmentConfig({SOCKETDUCT_PROTOCOL_VERSION: "3"}).protocolVersion, 3)
|
|
118
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_PROTOCOL_VERSION: "1"}), /must be 2 or 3/)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test("loadEnvironmentConfig rejects placeholder auth tokens", () => {
|
|
122
|
+
assert.throws(
|
|
123
|
+
() => loadEnvironmentConfig({
|
|
124
|
+
SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:replace-with-generated-token",
|
|
125
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:443"
|
|
126
|
+
}),
|
|
127
|
+
/placeholder/
|
|
128
|
+
)
|
|
129
|
+
assert.throws(
|
|
130
|
+
() => loadEnvironmentConfig({
|
|
131
|
+
SOCKETDUCT_AUTH_TOKENS: "tensorbuzz-edge:paste-generated-token-here",
|
|
132
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:443"
|
|
133
|
+
}),
|
|
134
|
+
/placeholder/
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test("loadEnvironmentConfig parses TLS cert/key as an atomic pair", () => {
|
|
139
|
+
const config = loadEnvironmentConfig({
|
|
140
|
+
SOCKETDUCT_TLS_CERT_FILE: "/certs/relay.crt",
|
|
141
|
+
SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key"
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(config.relayTls, {
|
|
145
|
+
certFile: "/certs/relay.crt",
|
|
146
|
+
keyFile: "/certs/relay.key"
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("loadEnvironmentConfig parses optional client CA for mTLS", () => {
|
|
151
|
+
const config = loadEnvironmentConfig({
|
|
152
|
+
SOCKETDUCT_TLS_CERT_FILE: "/certs/relay.crt",
|
|
153
|
+
SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key",
|
|
154
|
+
SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt"
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
assert.deepEqual(config.relayTls, {
|
|
158
|
+
certFile: "/certs/relay.crt",
|
|
159
|
+
keyFile: "/certs/relay.key",
|
|
160
|
+
clientCaFile: "/certs/client-ca.crt"
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test("loadEnvironmentConfig returns undefined relayTls when no TLS vars are set", () => {
|
|
165
|
+
const config = loadEnvironmentConfig({
|
|
166
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token",
|
|
167
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:443"
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
assert.equal(config.relayTls, undefined)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test("loadEnvironmentConfig rejects TLS cert without key", () => {
|
|
174
|
+
assert.throws(
|
|
175
|
+
() => loadEnvironmentConfig({SOCKETDUCT_TLS_CERT_FILE: "/certs/relay.crt"}),
|
|
176
|
+
/atomic pair/
|
|
177
|
+
)
|
|
178
|
+
assert.throws(
|
|
179
|
+
() => loadEnvironmentConfig({SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key"}),
|
|
180
|
+
/atomic pair/
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test("loadEnvironmentConfig trims whitespace from TLS file paths", () => {
|
|
185
|
+
const config = loadEnvironmentConfig({
|
|
186
|
+
SOCKETDUCT_TLS_CERT_FILE: " /certs/relay.crt ",
|
|
187
|
+
SOCKETDUCT_TLS_KEY_FILE: " /certs/relay.key "
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
assert.deepEqual(config.relayTls, {
|
|
191
|
+
certFile: "/certs/relay.crt",
|
|
192
|
+
keyFile: "/certs/relay.key"
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test("loadEnvironmentConfig parses SOCKETDUCT_ALLOW_INSECURE_PUBLIC valid values", () => {
|
|
197
|
+
assert.equal(loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "1"}).allowInsecurePublic, true)
|
|
198
|
+
assert.equal(loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "true"}).allowInsecurePublic, true)
|
|
199
|
+
assert.equal(loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: " true "}).allowInsecurePublic, true)
|
|
200
|
+
assert.equal(loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: ""}).allowInsecurePublic, false)
|
|
201
|
+
assert.equal(loadEnvironmentConfig({}).allowInsecurePublic, false)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
test("loadEnvironmentConfig rejects invalid SOCKETDUCT_ALLOW_INSECURE_PUBLIC values", () => {
|
|
205
|
+
assert.throws(
|
|
206
|
+
() => loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "yes"}),
|
|
207
|
+
/SOCKETDUCT_ALLOW_INSECURE_PUBLIC/
|
|
208
|
+
)
|
|
209
|
+
assert.throws(
|
|
210
|
+
() => loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "0"}),
|
|
211
|
+
/SOCKETDUCT_ALLOW_INSECURE_PUBLIC/
|
|
212
|
+
)
|
|
213
|
+
assert.throws(
|
|
214
|
+
() => loadEnvironmentConfig({SOCKETDUCT_ALLOW_INSECURE_PUBLIC: "false"}),
|
|
215
|
+
/SOCKETDUCT_ALLOW_INSECURE_PUBLIC/
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test("loadEnvironmentConfig rejects client CA without cert/key pair", () => {
|
|
220
|
+
assert.throws(
|
|
221
|
+
() => loadEnvironmentConfig({SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt"}),
|
|
222
|
+
/SOCKETDUCT_TLS_CLIENT_CA_FILE/
|
|
223
|
+
)
|
|
224
|
+
assert.throws(
|
|
225
|
+
() => loadEnvironmentConfig({
|
|
226
|
+
SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt",
|
|
227
|
+
SOCKETDUCT_TLS_CERT_FILE: "/certs/relay.crt"
|
|
228
|
+
}),
|
|
229
|
+
/SOCKETDUCT_TLS_CLIENT_CA_FILE/
|
|
230
|
+
)
|
|
231
|
+
assert.throws(
|
|
232
|
+
() => loadEnvironmentConfig({
|
|
233
|
+
SOCKETDUCT_TLS_CLIENT_CA_FILE: "/certs/client-ca.crt",
|
|
234
|
+
SOCKETDUCT_TLS_KEY_FILE: "/certs/relay.key"
|
|
235
|
+
}),
|
|
236
|
+
/SOCKETDUCT_TLS_CLIENT_CA_FILE/
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test("isLoopbackHost identifies loopback addresses", () => {
|
|
241
|
+
assert.equal(isLoopbackHost("127.0.0.1"), true)
|
|
242
|
+
assert.equal(isLoopbackHost("127.0.0.2"), true)
|
|
243
|
+
assert.equal(isLoopbackHost("127.255.255.255"), true)
|
|
244
|
+
assert.equal(isLoopbackHost("localhost"), true)
|
|
245
|
+
assert.equal(isLoopbackHost("::1"), true)
|
|
246
|
+
assert.equal(isLoopbackHost("[::1]"), true)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
test("isLoopbackHost rejects non-loopback addresses", () => {
|
|
250
|
+
assert.equal(isLoopbackHost("0.0.0.0"), false)
|
|
251
|
+
assert.equal(isLoopbackHost("192.168.1.1"), false)
|
|
252
|
+
assert.equal(isLoopbackHost("10.0.0.1"), false)
|
|
253
|
+
assert.equal(isLoopbackHost("172.16.0.1"), false)
|
|
254
|
+
assert.equal(isLoopbackHost("::ffff:127.0.0.1"), false)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test("loadEnvironmentConfig parses reverse hub identity bindings and exact routes", () => {
|
|
258
|
+
const config = loadEnvironmentConfig({
|
|
259
|
+
SOCKETDUCT_MODE: "reverse-hub",
|
|
260
|
+
SOCKETDUCT_AUTH_TOKENS: "home-o2:gateway-token,tensorbuzz:client-token",
|
|
261
|
+
SOCKETDUCT_REVERSE_GATEWAY_BINDINGS: "o2:home-o2,zyxel:home-zyxel",
|
|
262
|
+
SOCKETDUCT_REVERSE_ROUTES: "tensorbuzz:o2:docker-server:2375,tensorbuzz:zyxel:docker-server:2375",
|
|
263
|
+
SOCKETDUCT_TLS_CERT_FILE: "/certs/hub.crt",
|
|
264
|
+
SOCKETDUCT_TLS_KEY_FILE: "/certs/hub.key"
|
|
265
|
+
})
|
|
266
|
+
assert.equal(config.mode, "reverse-hub")
|
|
267
|
+
assert.deepEqual(config.reverseGatewayBindings, [{gatewayId: "o2", username: "home-o2"}, {gatewayId: "zyxel", username: "home-zyxel"}])
|
|
268
|
+
assert.deepEqual(config.reverseRoutes, [
|
|
269
|
+
{username: "tensorbuzz", gatewayId: "o2", host: "docker-server", port: 2375},
|
|
270
|
+
{username: "tensorbuzz", gatewayId: "zyxel", host: "docker-server", port: 2375}
|
|
271
|
+
])
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test("loadEnvironmentConfig parses outbound reverse gateway verified TLS settings", () => {
|
|
275
|
+
const config = loadEnvironmentConfig({
|
|
276
|
+
SOCKETDUCT_MODE: "reverse-gateway",
|
|
277
|
+
SOCKETDUCT_REVERSE_GATEWAY_ID: "starlink",
|
|
278
|
+
SOCKETDUCT_REVERSE_CONNECTION_ID: "home_starlink_01",
|
|
279
|
+
SOCKETDUCT_REVERSE_HUB_HOST: "server3.example.test",
|
|
280
|
+
SOCKETDUCT_REVERSE_HUB_PORT: "443",
|
|
281
|
+
SOCKETDUCT_REVERSE_TOKEN: "gateway-token",
|
|
282
|
+
SOCKETDUCT_REVERSE_HUB_CA_FILE: "/certs/ca.crt",
|
|
283
|
+
SOCKETDUCT_REVERSE_HUB_CLIENT_CERT_FILE: "/certs/client.crt",
|
|
284
|
+
SOCKETDUCT_REVERSE_HUB_CLIENT_KEY_FILE: "/certs/client.key",
|
|
285
|
+
SOCKETDUCT_REVERSE_HUB_SERVERNAME: "server3.example.test",
|
|
286
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "docker-server:2375"
|
|
287
|
+
})
|
|
288
|
+
assert.equal(config.mode, "reverse-gateway")
|
|
289
|
+
assert.equal(config.reverseGatewayId, "starlink")
|
|
290
|
+
assert.equal(config.reverseConnectionId, "home_starlink_01")
|
|
291
|
+
assert.deepEqual(config.reverseHub, {host: "server3.example.test", port: 443, token: "gateway-token"})
|
|
292
|
+
assert.deepEqual(config.reverseHubTls, {caFile: "/certs/ca.crt", certFile: "/certs/client.crt", keyFile: "/certs/client.key", servername: "server3.example.test"})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test("loadEnvironmentConfig rejects malformed reverse mode policy and credentials", () => {
|
|
296
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "unknown"}), /Unsupported SOCKETDUCT_MODE/)
|
|
297
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-hub", SOCKETDUCT_REVERSE_GATEWAY_BINDINGS: "missing-user"}), /gatewayId:username/)
|
|
298
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-hub", SOCKETDUCT_REVERSE_ROUTES: "too:few:fields"}), /username:gatewayId:host:port/)
|
|
299
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-gateway", SOCKETDUCT_REVERSE_TOKEN: "replace-me"}), /placeholder/)
|
|
300
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_MODE: "reverse-gateway", SOCKETDUCT_REVERSE_HUB_CLIENT_CERT_FILE: "/certs/client.crt"}), /must both be set/)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test("isLoopbackHost rejects 127-prefixed hostnames", () => {
|
|
304
|
+
assert.equal(isLoopbackHost("127.0.0.1.nip.io"), false)
|
|
305
|
+
assert.equal(isLoopbackHost("127localhost"), false)
|
|
306
|
+
assert.equal(isLoopbackHost("127anything"), false)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test("isLoopbackHost rejects invalid loopback-like IPs", () => {
|
|
310
|
+
assert.equal(isLoopbackHost("127.0.0."), false)
|
|
311
|
+
assert.equal(isLoopbackHost("127.0.0"), false)
|
|
312
|
+
assert.equal(isLoopbackHost("127."), false)
|
|
313
|
+
assert.equal(isLoopbackHost("127.0.0.0.1"), false)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test("isLoopbackHost rejects loopback IPs with invalid octets", () => {
|
|
317
|
+
assert.equal(isLoopbackHost("127.999.999.999"), false)
|
|
318
|
+
assert.equal(isLoopbackHost("127.256.0.1"), false)
|
|
319
|
+
assert.equal(isLoopbackHost("127.-1.0.1"), false)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test("loadEnvironmentConfig parses SOCKETDUCT_MAX_TARGET_BUFFER_BYTES and SOCKETDUCT_MAX_TOTAL_BUFFER_BYTES", () => {
|
|
323
|
+
const config = loadEnvironmentConfig({
|
|
324
|
+
SOCKETDUCT_MODE: "relay",
|
|
325
|
+
SOCKETDUCT_MAX_TARGET_BUFFER_BYTES: "65536",
|
|
326
|
+
SOCKETDUCT_MAX_TOTAL_BUFFER_BYTES: "262144",
|
|
327
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
328
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
assert.equal(config.maxTargetBufferBytes, 65536)
|
|
332
|
+
assert.equal(config.maxTotalBufferBytes, 262144)
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test("loadEnvironmentConfig parses SOCKETDUCT_COMPRESSION and its limits", () => {
|
|
336
|
+
const config = loadEnvironmentConfig({
|
|
337
|
+
SOCKETDUCT_MODE: "relay",
|
|
338
|
+
SOCKETDUCT_COMPRESSION: "off",
|
|
339
|
+
SOCKETDUCT_MIN_COMPRESS_BYTES: "1024",
|
|
340
|
+
SOCKETDUCT_MAX_DECOMPRESSED_BYTES: "65536",
|
|
341
|
+
SOCKETDUCT_MAX_EXPANSION_RATIO: "50",
|
|
342
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
343
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
assert.equal(config.compression, "off")
|
|
347
|
+
assert.equal(config.minCompressBytes, 1024)
|
|
348
|
+
assert.equal(config.maxDecompressedBytes, 65536)
|
|
349
|
+
assert.equal(config.maxExpansionRatio, 50)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test("loadEnvironmentConfig rejects an unknown SOCKETDUCT_COMPRESSION instead of silently defaulting", () => {
|
|
353
|
+
assert.throws(
|
|
354
|
+
() => loadEnvironmentConfig({
|
|
355
|
+
SOCKETDUCT_MODE: "relay",
|
|
356
|
+
SOCKETDUCT_COMPRESSION: "zstd",
|
|
357
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
358
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
359
|
+
}),
|
|
360
|
+
/Unsupported SOCKETDUCT_COMPRESSION/
|
|
361
|
+
)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
test("loadEnvironmentConfig defaults buffer limits when env vars are absent", () => {
|
|
365
|
+
const config = loadEnvironmentConfig({
|
|
366
|
+
SOCKETDUCT_MODE: "relay",
|
|
367
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
368
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
assert.equal(config.maxTargetBufferBytes, 1_048_576)
|
|
372
|
+
assert.equal(config.maxTotalBufferBytes, 16_777_216)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test("loadEnvironmentConfig rejects non-positive buffer limit values", () => {
|
|
376
|
+
assert.throws(
|
|
377
|
+
() => loadEnvironmentConfig({
|
|
378
|
+
SOCKETDUCT_MAX_TARGET_BUFFER_BYTES: "0",
|
|
379
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
380
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
381
|
+
}),
|
|
382
|
+
/positive integer/
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
assert.throws(
|
|
386
|
+
() => loadEnvironmentConfig({
|
|
387
|
+
SOCKETDUCT_MAX_TARGET_BUFFER_BYTES: "-1",
|
|
388
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
389
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
390
|
+
}),
|
|
391
|
+
/positive integer/
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
assert.throws(
|
|
395
|
+
() => loadEnvironmentConfig({
|
|
396
|
+
SOCKETDUCT_MAX_TOTAL_BUFFER_BYTES: "not-a-number",
|
|
397
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
398
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
399
|
+
}),
|
|
400
|
+
/positive integer/
|
|
401
|
+
)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
test("loadEnvironmentConfig parses handshakeTimeoutMs and maxInputBytesPerConnection", () => {
|
|
405
|
+
const config = loadEnvironmentConfig({
|
|
406
|
+
SOCKETDUCT_MODE: "relay",
|
|
407
|
+
SOCKETDUCT_HANDSHAKE_TIMEOUT_MS: "5000",
|
|
408
|
+
SOCKETDUCT_MAX_INPUT_BYTES_PER_CONNECTION: "32768",
|
|
409
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
410
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
assert.equal(config.handshakeTimeoutMs, 5000)
|
|
414
|
+
assert.equal(config.maxInputBytesPerConnection, 32768)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
test("loadEnvironmentConfig parses and validates initial target retry delays", () => {
|
|
418
|
+
const config = loadEnvironmentConfig({
|
|
419
|
+
SOCKETDUCT_INITIAL_TARGET_RETRY_DELAY_MS: "25",
|
|
420
|
+
SOCKETDUCT_INITIAL_TARGET_RETRY_MAX_DELAY_MS: "200"
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
assert.equal(config.initialTargetRetryDelayMs, 25)
|
|
424
|
+
assert.equal(config.initialTargetRetryMaxDelayMs, 200)
|
|
425
|
+
assert.throws(() => loadEnvironmentConfig({SOCKETDUCT_INITIAL_TARGET_RETRY_DELAY_MS: "0"}), /positive integer/)
|
|
426
|
+
assert.throws(() => loadEnvironmentConfig({
|
|
427
|
+
SOCKETDUCT_INITIAL_TARGET_RETRY_DELAY_MS: "500",
|
|
428
|
+
SOCKETDUCT_INITIAL_TARGET_RETRY_MAX_DELAY_MS: "250"
|
|
429
|
+
}), /greater than or equal/i)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
test("loadEnvironmentConfig parses and validates the initial target connect timeout", () => {
|
|
433
|
+
assert.equal(loadEnvironmentConfig({SOCKETDUCT_INITIAL_TARGET_CONNECT_TIMEOUT_MS: "750"}).initialTargetConnectTimeoutMs, 750)
|
|
434
|
+
assert.throws(
|
|
435
|
+
() => loadEnvironmentConfig({SOCKETDUCT_INITIAL_TARGET_CONNECT_TIMEOUT_MS: "0"}),
|
|
436
|
+
/SOCKETDUCT_INITIAL_TARGET_CONNECT_TIMEOUT_MS.*positive integer/
|
|
437
|
+
)
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
test("loadEnvironmentConfig defaults handshakeTimeoutMs and maxInputBytesPerConnection when env vars are absent", () => {
|
|
441
|
+
const config = loadEnvironmentConfig({
|
|
442
|
+
SOCKETDUCT_MODE: "relay",
|
|
443
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
444
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
assert.equal(config.handshakeTimeoutMs, 10_000)
|
|
448
|
+
assert.equal(config.maxInputBytesPerConnection, 65_536)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test("loadEnvironmentConfig rejects non-positive handshakeTimeoutMs and maxInputBytesPerConnection", () => {
|
|
452
|
+
assert.throws(
|
|
453
|
+
() => loadEnvironmentConfig({
|
|
454
|
+
SOCKETDUCT_HANDSHAKE_TIMEOUT_MS: "0",
|
|
455
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
456
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
457
|
+
}),
|
|
458
|
+
/positive integer/
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
assert.throws(
|
|
462
|
+
() => loadEnvironmentConfig({
|
|
463
|
+
SOCKETDUCT_MAX_INPUT_BYTES_PER_CONNECTION: "-1",
|
|
464
|
+
SOCKETDUCT_AUTH_TOKENS: "edge:token-one",
|
|
465
|
+
SOCKETDUCT_TARGET_ALLOWLIST: "127.0.0.1:8080"
|
|
466
|
+
}),
|
|
467
|
+
/positive integer/
|
|
468
|
+
)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
test("loadEnvironmentConfig mode validation", async (t) => {
|
|
472
|
+
await t.test("defaults to summary when SOCKETDUCT_MODE is not set", () => {
|
|
473
|
+
const config = loadEnvironmentConfig({})
|
|
474
|
+
assert.equal(config.mode, "summary")
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
await t.test("returns relay when SOCKETDUCT_MODE is relay", () => {
|
|
478
|
+
const config = loadEnvironmentConfig({SOCKETDUCT_MODE: "relay"})
|
|
479
|
+
assert.equal(config.mode, "relay")
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
await t.test("returns summary when SOCKETDUCT_MODE is summary", () => {
|
|
483
|
+
const config = loadEnvironmentConfig({SOCKETDUCT_MODE: "summary"})
|
|
484
|
+
assert.equal(config.mode, "summary")
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
await t.test("throws on invalid SOCKETDUCT_MODE", () => {
|
|
488
|
+
assert.throws(
|
|
489
|
+
() => loadEnvironmentConfig({SOCKETDUCT_MODE: "relya"}),
|
|
490
|
+
/Unsupported SOCKETDUCT_MODE/
|
|
491
|
+
)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
await t.test("throws on arbitrary SOCKETDUCT_MODE value", () => {
|
|
495
|
+
assert.throws(
|
|
496
|
+
() => loadEnvironmentConfig({SOCKETDUCT_MODE: "garbage"}),
|
|
497
|
+
/Unsupported SOCKETDUCT_MODE/
|
|
498
|
+
)
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
await t.test("mode validation is case-sensitive", () => {
|
|
502
|
+
assert.throws(
|
|
503
|
+
() => loadEnvironmentConfig({SOCKETDUCT_MODE: "RELAY"}),
|
|
504
|
+
/Unsupported SOCKETDUCT_MODE/
|
|
505
|
+
)
|
|
506
|
+
})
|
|
507
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import test from "node:test"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
connectSocketduct,
|
|
6
|
+
createJsonlFileLogger,
|
|
7
|
+
createMultiplexedSocketductConnection,
|
|
8
|
+
createMultiplexedSocketductTransportSet,
|
|
9
|
+
createSocketductConnectionFactory,
|
|
10
|
+
getStartupSummary,
|
|
11
|
+
MultiplexedSocketductConnection,
|
|
12
|
+
MultiplexedSocketductTransportSet,
|
|
13
|
+
OutboundReverseGateway,
|
|
14
|
+
ReverseGatewayConnection,
|
|
15
|
+
SocketductHttpAgent,
|
|
16
|
+
SocketductSocket
|
|
17
|
+
} from "../../src/index.js"
|
|
18
|
+
import {createSocketductConnectionFactory as directCreateSocketductConnectionFactory} from "../../src/client/connect.js"
|
|
19
|
+
import {SocketductHttpAgent as DirectSocketductHttpAgent} from "../../src/client/http-agent.js"
|
|
20
|
+
import {connectSocketduct as directConnectSocketduct, SocketductSocket as DirectSocketductSocket} from "../../src/client/socketduct-socket.js"
|
|
21
|
+
import {createJsonlFileLogger as directCreateJsonlFileLogger} from "../../src/logging/structured-logger.js"
|
|
22
|
+
|
|
23
|
+
test("getStartupSummary returns the Socketduct service identity and default mode", () => {
|
|
24
|
+
assert.deepEqual(getStartupSummary(), {
|
|
25
|
+
name: "Socketduct",
|
|
26
|
+
runtime: "node",
|
|
27
|
+
mode: "single-worker"
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("index exports the public SocketductSocket client API", () => {
|
|
32
|
+
assert.equal(SocketductSocket, DirectSocketductSocket)
|
|
33
|
+
assert.equal(connectSocketduct, directConnectSocketduct)
|
|
34
|
+
assert.equal(createSocketductConnectionFactory, directCreateSocketductConnectionFactory)
|
|
35
|
+
assert.equal(createJsonlFileLogger, directCreateJsonlFileLogger)
|
|
36
|
+
assert.equal(SocketductHttpAgent, DirectSocketductHttpAgent)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("package exports expose dedicated HTTP and HTTPS adapter entrypoints without an HTTPS barrel export", async () => {
|
|
40
|
+
const socketduct = await import("socketduct")
|
|
41
|
+
const socketductHttp = await import("socketduct/http")
|
|
42
|
+
const socketductHttps = await import("socketduct/https")
|
|
43
|
+
const {SocketductHttpsAgent} = await import("../../src/client/https-agent.js")
|
|
44
|
+
|
|
45
|
+
assert.equal(socketduct.SocketductHttpAgent, DirectSocketductHttpAgent)
|
|
46
|
+
assert.equal(socketductHttp.SocketductHttpAgent, DirectSocketductHttpAgent)
|
|
47
|
+
assert.equal(socketductHttps.SocketductHttpsAgent, SocketductHttpsAgent)
|
|
48
|
+
assert.equal("SocketductHttpsAgent" in socketduct, false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("package exports the protocol-v3 pooled connection and relay entrypoints", async () => {
|
|
52
|
+
const multiplexed = await import("socketduct/multiplexed")
|
|
53
|
+
const relay = await import("socketduct/relay")
|
|
54
|
+
|
|
55
|
+
assert.equal(multiplexed.MultiplexedSocketductConnection, MultiplexedSocketductConnection)
|
|
56
|
+
assert.equal(multiplexed.MultiplexedSocketductTransportSet, MultiplexedSocketductTransportSet)
|
|
57
|
+
assert.equal(createMultiplexedSocketductConnection({
|
|
58
|
+
relay: {host: "127.0.0.1", port: 3100, token: "test-token"}
|
|
59
|
+
}) instanceof MultiplexedSocketductConnection, true)
|
|
60
|
+
assert.equal(createMultiplexedSocketductTransportSet({
|
|
61
|
+
connectionId: "process_identity_01",
|
|
62
|
+
spoolDirectory: "/tmp/socketduct-index-test",
|
|
63
|
+
maxTotalBufferBytes: 1024,
|
|
64
|
+
maxTotalSpoolBytes: 1024,
|
|
65
|
+
maxTotalQueuedBytes: 2048,
|
|
66
|
+
gateways: [{gatewayId: "starlink", endpoint: {relay: {host: "127.0.0.1", port: 3100, token: "test-token"}}}]
|
|
67
|
+
}) instanceof MultiplexedSocketductTransportSet, true)
|
|
68
|
+
assert.equal(typeof relay.startMultiplexedTcpRelay, "function")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("package exports native reverse hub, gateway, and client entrypoints", async () => {
|
|
72
|
+
const reverse = await import("socketduct/reverse")
|
|
73
|
+
const relay = await import("socketduct/relay")
|
|
74
|
+
assert.equal(reverse.OutboundReverseGateway, OutboundReverseGateway)
|
|
75
|
+
assert.equal(reverse.ReverseGatewayConnection, ReverseGatewayConnection)
|
|
76
|
+
assert.equal(typeof reverse.startReverseGatewayHub, "function")
|
|
77
|
+
assert.equal(relay.startReverseGatewayHub, reverse.startReverseGatewayHub)
|
|
78
|
+
})
|