socketduct 0.1.25 → 0.1.27

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.
Files changed (58) hide show
  1. package/AGENTS.md +2 -2
  2. package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
  3. package/changelog.d/20261025070000-reclaim-unreadable-retention-lock.md +14 -0
  4. package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
  5. package/package.json +2 -2
  6. package/scripts/test.js +9 -14
  7. package/src/logging/compressed-log-writer.js +1 -1
  8. package/src/logging/log-retention.js +36 -1
  9. package/src/logging/zstd-frame-codec.js +15 -1
  10. package/test/e2e/gateway-process.test.js +93 -92
  11. package/test/e2e/logger-shutdown-process.test.js +40 -37
  12. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  13. package/test/e2e/reverse-gateway-process.test.js +328 -318
  14. package/test/integration/initial-target-recovery.test.js +333 -334
  15. package/test/integration/logging-cli.test.js +121 -119
  16. package/test/integration/logging-compressed-recovery.test.js +96 -95
  17. package/test/integration/multiplexed-compatibility.test.js +178 -177
  18. package/test/integration/multiplexed-corrections.test.js +360 -359
  19. package/test/integration/multiplexed-failures.test.js +253 -251
  20. package/test/integration/multiplexed-transport-set.test.js +334 -322
  21. package/test/integration/multiplexed-transport.test.js +135 -134
  22. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  23. package/test/integration/reverse-gateway.test.js +2309 -2218
  24. package/test/integration/reverse-resume-continuity.test.js +219 -218
  25. package/test/integration/reverse-route-class.test.js +358 -341
  26. package/test/integration/socketduct-http-agent.test.js +314 -312
  27. package/test/integration/socketduct-https-agent.test.js +516 -485
  28. package/test/integration/stream-compression.test.js +862 -824
  29. package/test/integration/tcp-forwarding.test.js +2550 -2447
  30. package/test/integration/tls-forwarding.test.js +593 -592
  31. package/test/unit/auth/auth.test.js +43 -44
  32. package/test/unit/auth/static-token-store.test.js +9 -8
  33. package/test/unit/client/connect.test.js +41 -40
  34. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  35. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  36. package/test/unit/client/multiplexed-transport-set.test.js +830 -748
  37. package/test/unit/client/socketduct-socket.test.js +768 -771
  38. package/test/unit/client/virtual-socket.test.js +198 -168
  39. package/test/unit/config/docker-gateway-files.test.js +224 -224
  40. package/test/unit/config/environment.test.js +472 -492
  41. package/test/unit/index.test.js +53 -52
  42. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  43. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  44. package/test/unit/logging/log-retention.test.js +71 -48
  45. package/test/unit/logging/log-segment-index.test.js +84 -81
  46. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  47. package/test/unit/protocol/compression.test.js +254 -298
  48. package/test/unit/protocol/frame.test.js +280 -276
  49. package/test/unit/protocol/v3-compression.test.js +21 -23
  50. package/test/unit/protocol/v3-frame.test.js +42 -41
  51. package/test/unit/release-configuration.test.js +15 -12
  52. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  53. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  54. package/test/unit/server/control-handler.test.js +232 -231
  55. package/test/unit/server/primary.test.js +51 -49
  56. package/test/unit/server/relay-runtime.test.js +280 -294
  57. package/test/unit/sessions/session.test.js +48 -62
  58. package/test/unit/spool/spool-file.test.js +401 -385
@@ -1,11 +1,10 @@
1
1
  /* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-returns */
2
- import assert from "node:assert/strict"
2
+ import {describe, expect, it, withTestCleanup} from "@velocious/testing"
3
3
  import {mkdir, mkdtemp, rm, writeFile} from "node:fs/promises"
4
4
  import {createConnection, createServer} from "node:net"
5
5
  import {tmpdir} from "node:os"
6
6
  import {join} from "node:path"
7
7
  import {Duplex} from "node:stream"
8
- import test from "node:test"
9
8
 
10
9
  import {StaticTokenStore} from "../../src/auth/static-token-store.js"
11
10
  import {MultiplexedSocketductConnection} from "../../src/client/multiplexed-connection.js"
@@ -29,7 +28,7 @@ async function listen(server) {
29
28
  server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(undefined) })
30
29
  })
31
30
  const address = server.address()
32
- assert(address !== null && typeof address !== "string")
31
+ if (address === null || typeof address === "string") throw new Error("Expected a bound TCP address")
33
32
  return address.port
34
33
  }
35
34
 
@@ -173,379 +172,381 @@ async function createBlackholeThenReadyServer(blackholeAccepts) {
173
172
  return {server, port, status: () => ({accepts})}
174
173
  }
175
174
 
176
- test("a full lost client window is credited again before replay", async (t) => {
177
- let received = Buffer.alloc(0)
178
- const target = createServer((socket) => socket.on("data", (chunk) => { received = Buffer.concat([received, chunk]) }))
179
- const targetPort = await listen(target)
180
- t.after(() => closeServer(target))
181
- const relay = await startMultiplexedTcpRelay({tokens: tokens(), authorizeTarget: ({target: requested}) => requested.port === targetPort, initialWindowBytes: 32, maxFrameBytes: 32})
182
- t.after(() => relay.close())
183
- const fault = await createFaultProxy(relay.port, "drop-client-window")
184
- t.after(() => closeServer(fault.proxy))
185
- const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fault.port, token: "valid-token"}, initialWindowBytes: 32, maxFrameBytes: 32, reconnectDelayMs: 10})
186
- t.after(() => connection.close())
187
- const socket = connection.createConnection({host: "127.0.0.1", port: targetPort})
188
- await onceEvent(socket, "connect")
189
- socket.write(Buffer.alloc(32, 0x61))
190
- await waitUntil(() => fault.status().faultTriggered)
191
- await waitUntil(() => received.length === 32)
192
- assert.equal(received.toString(), "a".repeat(32))
193
- assert.equal(fault.status().accepts, 2)
194
- })
175
+ describe("multiplexed corrections", () => {
176
+ it("a full lost client window is credited again before replay", async () => withTestCleanup(async (after) => {
177
+ let received = Buffer.alloc(0)
178
+ const target = createServer((socket) => socket.on("data", (chunk) => { received = Buffer.concat([received, chunk]) }))
179
+ const targetPort = await listen(target)
180
+ after(() => closeServer(target))
181
+ const relay = await startMultiplexedTcpRelay({tokens: tokens(), authorizeTarget: ({target: requested}) => requested.port === targetPort, initialWindowBytes: 32, maxFrameBytes: 32})
182
+ after(() => relay.close())
183
+ const fault = await createFaultProxy(relay.port, "drop-client-window")
184
+ after(() => closeServer(fault.proxy))
185
+ const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fault.port, token: "valid-token"}, initialWindowBytes: 32, maxFrameBytes: 32, reconnectDelayMs: 10})
186
+ after(() => connection.close())
187
+ const socket = connection.createConnection({host: "127.0.0.1", port: targetPort})
188
+ await onceEvent(socket, "connect")
189
+ socket.write(Buffer.alloc(32, 0x61))
190
+ await waitUntil(() => fault.status().faultTriggered)
191
+ await waitUntil(() => received.length === 32)
192
+ expect(received.toString()).toBe("a".repeat(32))
193
+ expect(fault.status().accepts).toBe(2)
194
+ }))
195
195
 
196
- test("partial target replay is delivered exactly once after reconnect", async (t) => {
197
- const payload = Buffer.from("0123456789abcdefABCDEFGHIJKLMNOP")
198
- const target = createServer((socket) => socket.write(payload))
199
- const targetPort = await listen(target)
200
- t.after(() => closeServer(target))
201
- const relay = await startMultiplexedTcpRelay({tokens: tokens(), authorizeTarget: ({target: requested}) => requested.port === targetPort, initialWindowBytes: 32, maxFrameBytes: 16})
202
- t.after(() => relay.close())
203
- const fault = await createFaultProxy(relay.port, "drop-second-relay-data")
204
- t.after(() => closeServer(fault.proxy))
205
- const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fault.port, token: "valid-token"}, initialWindowBytes: 32, maxFrameBytes: 16, reconnectDelayMs: 10})
206
- t.after(() => connection.close())
207
- const socket = connection.createConnection({host: "127.0.0.1", port: targetPort, readableHighWaterMark: 1})
208
- await onceEvent(socket, "connect")
209
- await waitUntil(() => fault.status().faultTriggered)
210
- await waitUntil(() => connection.status().reconnectSuccesses === 1)
211
- await waitUntil(() => relay.status().bufferedBytes === 16)
212
- /** @type {Buffer[]} */
213
- const chunks = []
214
- socket.on("data", (chunk) => chunks.push(Buffer.from(chunk)))
215
- socket.resume()
216
- await waitUntil(() => Buffer.concat(chunks).length === payload.length)
217
- assert.deepEqual(Buffer.concat(chunks), payload)
218
- assert.equal(fault.status().accepts, 2)
219
- await connection.close()
220
- })
196
+ it("partial target replay is delivered exactly once after reconnect", async () => withTestCleanup(async (after) => {
197
+ const payload = Buffer.from("0123456789abcdefABCDEFGHIJKLMNOP")
198
+ const target = createServer((socket) => socket.write(payload))
199
+ const targetPort = await listen(target)
200
+ after(() => closeServer(target))
201
+ const relay = await startMultiplexedTcpRelay({tokens: tokens(), authorizeTarget: ({target: requested}) => requested.port === targetPort, initialWindowBytes: 32, maxFrameBytes: 16})
202
+ after(() => relay.close())
203
+ const fault = await createFaultProxy(relay.port, "drop-second-relay-data")
204
+ after(() => closeServer(fault.proxy))
205
+ const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fault.port, token: "valid-token"}, initialWindowBytes: 32, maxFrameBytes: 16, reconnectDelayMs: 10})
206
+ after(() => connection.close())
207
+ const socket = connection.createConnection({host: "127.0.0.1", port: targetPort, readableHighWaterMark: 1})
208
+ await onceEvent(socket, "connect")
209
+ await waitUntil(() => fault.status().faultTriggered)
210
+ await waitUntil(() => connection.status().reconnectSuccesses === 1)
211
+ await waitUntil(() => relay.status().bufferedBytes === 16)
212
+ /** @type {Buffer[]} */
213
+ const chunks = []
214
+ socket.on("data", (chunk) => chunks.push(Buffer.from(chunk)))
215
+ socket.resume()
216
+ await waitUntil(() => Buffer.concat(chunks).length === payload.length)
217
+ expect(Buffer.concat(chunks).equals(payload)).toBe(true)
218
+ expect(fault.status().accepts).toBe(2)
219
+ await connection.close()
220
+ }))
221
221
 
222
- test("a recreated client recovers durable replay without reopening the target", async (t) => {
223
- const root = await mkdtemp(join(tmpdir(), "socketduct-v3-recovery-"))
224
- let targetAccepts = 0
225
- let received = Buffer.alloc(0)
226
- const target = createServer((socket) => {
227
- targetAccepts += 1
228
- socket.on("data", (chunk) => { received = Buffer.concat([received, chunk]) })
229
- })
230
- const targetPort = await listen(target)
231
- t.after(() => closeServer(target))
232
- const relay = await startMultiplexedTcpRelay({tokens: tokens(), authorizeTarget: ({target: requested}) => requested.port === targetPort, initialWindowBytes: 32, maxFrameBytes: 32, resumeWindowMs: 2_000})
233
- t.after(() => relay.close())
234
- const fault = await createFaultProxy(relay.port, "drop-client-window")
235
- t.after(() => closeServer(fault.proxy))
236
- const shared = {relay: {host: "127.0.0.1", port: fault.port, token: "valid-token"}, connectionId: "stable_connection_01", spoolDirectory: root, initialWindowBytes: 32, maxFrameBytes: 32, reconnectDelayMs: 1_000}
237
- const first = new MultiplexedSocketductConnection(shared)
238
- const initial = first.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_01", sessionName: "durable"})
239
- await onceEvent(initial, "connect")
240
- await new Promise((resolve, reject) => initial.write(Buffer.alloc(32, 0x72), (error) => error ? reject(error) : resolve(undefined)))
241
- await waitUntil(() => fault.status().faultTriggered)
242
- await first.detach()
222
+ it("a recreated client recovers durable replay without reopening the target", async () => withTestCleanup(async (after) => {
223
+ const root = await mkdtemp(join(tmpdir(), "socketduct-v3-recovery-"))
224
+ let targetAccepts = 0
225
+ let received = Buffer.alloc(0)
226
+ const target = createServer((socket) => {
227
+ targetAccepts += 1
228
+ socket.on("data", (chunk) => { received = Buffer.concat([received, chunk]) })
229
+ })
230
+ const targetPort = await listen(target)
231
+ after(() => closeServer(target))
232
+ const relay = await startMultiplexedTcpRelay({tokens: tokens(), authorizeTarget: ({target: requested}) => requested.port === targetPort, initialWindowBytes: 32, maxFrameBytes: 32, resumeWindowMs: 2_000})
233
+ after(() => relay.close())
234
+ const fault = await createFaultProxy(relay.port, "drop-client-window")
235
+ after(() => closeServer(fault.proxy))
236
+ const shared = {relay: {host: "127.0.0.1", port: fault.port, token: "valid-token"}, connectionId: "stable_connection_01", spoolDirectory: root, initialWindowBytes: 32, maxFrameBytes: 32, reconnectDelayMs: 1_000}
237
+ const first = new MultiplexedSocketductConnection(shared)
238
+ const initial = first.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_01", sessionName: "durable"})
239
+ await onceEvent(initial, "connect")
240
+ await new Promise((resolve, reject) => initial.write(Buffer.alloc(32, 0x72), (error) => error ? reject(error) : resolve(undefined)))
241
+ await waitUntil(() => fault.status().faultTriggered)
242
+ await first.detach()
243
243
 
244
- const accidentalFresh = new MultiplexedSocketductConnection(shared)
245
- const duplicate = accidentalFresh.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_01", sessionName: "durable"})
246
- assert.match((await nextError(duplicate)).message, /explicit recovery is required/i)
247
- await accidentalFresh.close()
244
+ const accidentalFresh = new MultiplexedSocketductConnection(shared)
245
+ const duplicate = accidentalFresh.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_01", sessionName: "durable"})
246
+ expect((await nextError(duplicate)).message).toMatch(/explicit recovery is required/i)
247
+ await accidentalFresh.close()
248
248
 
249
- const second = new MultiplexedSocketductConnection(shared)
250
- t.after(() => second.close())
251
- t.after(() => rm(root, {recursive: true, force: true}))
252
- const recovered = second.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_01", sessionName: "durable", recover: true})
253
- await onceEvent(recovered, "connect")
254
- await waitUntil(() => received.length === 32)
255
- assert.equal(received.toString(), "r".repeat(32))
256
- assert.equal(targetAccepts, 1)
257
- })
249
+ const second = new MultiplexedSocketductConnection(shared)
250
+ after(() => second.close())
251
+ after(() => rm(root, {recursive: true, force: true}))
252
+ const recovered = second.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_01", sessionName: "durable", recover: true})
253
+ await onceEvent(recovered, "connect")
254
+ await waitUntil(() => received.length === 32)
255
+ expect(received.toString()).toBe("r".repeat(32))
256
+ expect(targetAccepts).toBe(1)
257
+ }))
258
258
 
259
- test("corrupt durable recovery state fails closed", async (t) => {
260
- const root = await mkdtemp(join(tmpdir(), "socketduct-v3-corrupt-"))
261
- t.after(() => rm(root, {recursive: true, force: true}))
262
- const directory = join(root, "stable_stream_02")
263
- await mkdir(directory, {recursive: true})
264
- await writeFile(join(directory, "metadata.json"), JSON.stringify({sessionId: "stable_stream_02", ownerId: "edge-v3", createdAt: new Date().toISOString()}))
265
- await writeFile(join(directory, "recovery.json"), "{not-json")
266
- const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: 9, token: "valid-token"}, connectionId: "stable_connection_02", spoolDirectory: root})
267
- assert.throws(() => connection.createConnection({host: "127.0.0.1", port: 9, identifier: "stable_stream_02", recover: true}), /recovery/i)
268
- })
259
+ it("corrupt durable recovery state fails closed", async () => withTestCleanup(async (after) => {
260
+ const root = await mkdtemp(join(tmpdir(), "socketduct-v3-corrupt-"))
261
+ after(() => rm(root, {recursive: true, force: true}))
262
+ const directory = join(root, "stable_stream_02")
263
+ await mkdir(directory, {recursive: true})
264
+ await writeFile(join(directory, "metadata.json"), JSON.stringify({sessionId: "stable_stream_02", ownerId: "edge-v3", createdAt: new Date().toISOString()}))
265
+ await writeFile(join(directory, "recovery.json"), "{not-json")
266
+ const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: 9, token: "valid-token"}, connectionId: "stable_connection_02", spoolDirectory: root})
267
+ expect(() => connection.createConnection({host: "127.0.0.1", port: 9, identifier: "stable_stream_02", recover: true})).toThrow(/recovery/i)
268
+ }))
269
269
 
270
- test("relay enforces one process-wide retained buffer ceiling", async (t) => {
271
- const targets = new Map()
272
- const relay = await startMultiplexedTcpRelay({
273
- tokens: tokens(),
274
- authorizeTarget: () => true,
275
- createTargetConnection: (requested) => {
276
- const target = new Duplex({read() {}, write(_chunk, _encoding, callback) { callback() }})
277
- targets.set(requested.port, target)
278
- setImmediate(() => target.emit("connect"))
279
- return /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (target))
280
- },
281
- initialWindowBytes: 64,
282
- maxFrameBytes: 32,
283
- maxStreamBufferBytes: 64,
284
- maxConnectionBufferBytes: 64,
285
- maxTotalBufferBytes: 64
286
- })
287
- t.after(() => relay.close())
288
- const options = {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}, initialWindowBytes: 64, maxFrameBytes: 32}
289
- const firstConnection = new MultiplexedSocketductConnection(options)
290
- const secondConnection = new MultiplexedSocketductConnection(options)
291
- t.after(() => Promise.all([firstConnection.close(), secondConnection.close()]))
292
- const first = firstConnection.createConnection({host: "target.internal", port: 1, readableHighWaterMark: 1})
293
- const second = secondConnection.createConnection({host: "target.internal", port: 2, readableHighWaterMark: 1})
294
- await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
295
- const secondError = nextError(second)
296
- targets.get(1)?.push(Buffer.alloc(40, 0x61))
297
- await waitUntil(() => relay.status().bufferedBytes === 40)
298
- targets.get(2)?.push(Buffer.alloc(40, 0x62))
299
- assert.equal((await secondError).code, "PROCESS_BUFFER_LIMIT")
300
- assert.equal(relay.status().bufferedBytes, 40)
301
- assert.equal(relay.status().bufferLimitBytes, 64)
302
- first.destroy()
303
- await waitUntil(() => relay.status().bufferedBytes === 0)
304
- })
270
+ it("relay enforces one process-wide retained buffer ceiling", async () => withTestCleanup(async (after) => {
271
+ const targets = new Map()
272
+ const relay = await startMultiplexedTcpRelay({
273
+ tokens: tokens(),
274
+ authorizeTarget: () => true,
275
+ createTargetConnection: (requested) => {
276
+ const target = new Duplex({read() {}, write(_chunk, _encoding, callback) { callback() }})
277
+ targets.set(requested.port, target)
278
+ setImmediate(() => target.emit("connect"))
279
+ return /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (target))
280
+ },
281
+ initialWindowBytes: 64,
282
+ maxFrameBytes: 32,
283
+ maxStreamBufferBytes: 64,
284
+ maxConnectionBufferBytes: 64,
285
+ maxTotalBufferBytes: 64
286
+ })
287
+ after(() => relay.close())
288
+ const options = {relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"}, initialWindowBytes: 64, maxFrameBytes: 32}
289
+ const firstConnection = new MultiplexedSocketductConnection(options)
290
+ const secondConnection = new MultiplexedSocketductConnection(options)
291
+ after(async () => { await Promise.all([firstConnection.close(), secondConnection.close()]) })
292
+ const first = firstConnection.createConnection({host: "target.internal", port: 1, readableHighWaterMark: 1})
293
+ const second = secondConnection.createConnection({host: "target.internal", port: 2, readableHighWaterMark: 1})
294
+ await Promise.all([onceEvent(first, "connect"), onceEvent(second, "connect")])
295
+ const secondError = nextError(second)
296
+ targets.get(1)?.push(Buffer.alloc(40, 0x61))
297
+ await waitUntil(() => relay.status().bufferedBytes === 40)
298
+ targets.get(2)?.push(Buffer.alloc(40, 0x62))
299
+ expect((await secondError).code).toBe("PROCESS_BUFFER_LIMIT")
300
+ expect(relay.status().bufferedBytes).toBe(40)
301
+ expect(relay.status().bufferLimitBytes).toBe(64)
302
+ first.destroy()
303
+ await waitUntil(() => relay.status().bufferedBytes === 0)
304
+ }))
305
305
 
306
- test("authentication timeout covers AUTH_OK and permits a later open", async (t) => {
307
- const fixture = await createBlackholeThenReadyServer(1)
308
- t.after(() => closeServer(fixture.server))
309
- const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fixture.port, token: "valid-token"}, connectTimeoutMs: 40})
310
- t.after(() => connection.close())
311
- const first = connection.createConnection({host: "target.internal", port: 1})
312
- const error = await Promise.race([
313
- nextError(first),
314
- new Promise((_, reject) => setTimeout(() => reject(new Error("authentication timeout did not settle")), 250))
315
- ])
316
- assert.equal(error.code, "SOCKETDUCT_AUTH_TIMEOUT")
317
- const second = connection.createConnection({host: "target.internal", port: 1})
318
- await onceEvent(second, "connect")
319
- assert.equal(fixture.status().accepts, 2)
320
- })
306
+ it("authentication timeout covers AUTH_OK and permits a later open", async () => withTestCleanup(async (after) => {
307
+ const fixture = await createBlackholeThenReadyServer(1)
308
+ after(() => closeServer(fixture.server))
309
+ const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fixture.port, token: "valid-token"}, connectTimeoutMs: 40})
310
+ after(() => connection.close())
311
+ const first = connection.createConnection({host: "target.internal", port: 1})
312
+ const error = await Promise.race([
313
+ nextError(first),
314
+ new Promise((_, reject) => setTimeout(() => reject(new Error("authentication timeout did not settle")), 250))
315
+ ])
316
+ expect(error.code).toBe("SOCKETDUCT_AUTH_TIMEOUT")
317
+ const second = connection.createConnection({host: "target.internal", port: 1})
318
+ await onceEvent(second, "connect")
319
+ expect(fixture.status().accepts).toBe(2)
320
+ }))
321
321
 
322
- test("slow spool initialization does not consume the authentication timeout", async (t) => {
323
- const originalCreateNew = SessionSpool.prototype.createNew
324
- /** @type {() => void} */
325
- let releaseCreateNew = () => {}
326
- /** @type {Promise<void>} */
327
- const createNewGate = new Promise((resolve) => { releaseCreateNew = () => resolve() })
328
- SessionSpool.prototype.createNew = async function (/** @type {{sessionId: string, ownerId: string}} */ options) {
329
- await createNewGate
330
- return await originalCreateNew.call(this, options)
331
- }
332
- t.after(() => { SessionSpool.prototype.createNew = originalCreateNew })
333
- const fixture = await createBlackholeThenReadyServer(0)
334
- t.after(() => closeServer(fixture.server))
335
- let physicalAttempts = 0
336
- const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fixture.port, token: "valid-token"}, connectTimeoutMs: 20, createPhysicalSocket: (options) => {
337
- physicalAttempts += 1
338
- return createConnection(options)
339
- }})
340
- t.after(() => connection.close())
341
- const socket = connection.createConnection({host: "target.internal", port: 1})
342
- void nextError(socket)
343
- await new Promise((resolve) => setTimeout(resolve, 50))
344
- const attemptsBeforeSpoolReady = physicalAttempts
345
- releaseCreateNew()
346
- assert.equal(attemptsBeforeSpoolReady, 0)
347
- await onceEvent(socket, "connect")
348
- assert.equal(physicalAttempts, 1)
349
- })
322
+ it("slow spool initialization does not consume the authentication timeout", async () => withTestCleanup(async (after) => {
323
+ const originalCreateNew = SessionSpool.prototype.createNew
324
+ /** @type {() => void} */
325
+ let releaseCreateNew = () => {}
326
+ /** @type {Promise<void>} */
327
+ const createNewGate = new Promise((resolve) => { releaseCreateNew = () => resolve() })
328
+ SessionSpool.prototype.createNew = async function (/** @type {{sessionId: string, ownerId: string}} */ options) {
329
+ await createNewGate
330
+ return await originalCreateNew.call(this, options)
331
+ }
332
+ after(() => { SessionSpool.prototype.createNew = originalCreateNew })
333
+ const fixture = await createBlackholeThenReadyServer(0)
334
+ after(() => closeServer(fixture.server))
335
+ let physicalAttempts = 0
336
+ const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fixture.port, token: "valid-token"}, connectTimeoutMs: 20, createPhysicalSocket: (options) => {
337
+ physicalAttempts += 1
338
+ return createConnection(options)
339
+ }})
340
+ after(() => connection.close())
341
+ const socket = connection.createConnection({host: "target.internal", port: 1})
342
+ void nextError(socket)
343
+ await new Promise((resolve) => setTimeout(resolve, 50))
344
+ const attemptsBeforeSpoolReady = physicalAttempts
345
+ releaseCreateNew()
346
+ expect(attemptsBeforeSpoolReady).toBe(0)
347
+ await onceEvent(socket, "connect")
348
+ expect(physicalAttempts).toBe(1)
349
+ }))
350
350
 
351
- test("cancelling the only opening stream clears physical attempt ownership", async (t) => {
352
- const originalCreateNew = SessionSpool.prototype.createNew
353
- /** @type {() => void} */
354
- let releaseCreateNew = () => {}
355
- /** @type {Promise<void>} */
356
- const createNewGate = new Promise((resolve) => { releaseCreateNew = () => resolve() })
357
- /** @type {() => void} */
358
- let markFirstCreateNewDone = () => {}
359
- /** @type {Promise<void>} */
360
- const firstCreateNewDone = new Promise((resolve) => { markFirstCreateNewDone = () => resolve() })
361
- let createNewCalls = 0
362
- SessionSpool.prototype.createNew = async function (/** @type {{sessionId: string, ownerId: string}} */ options) {
363
- createNewCalls += 1
364
- if (createNewCalls !== 1) return await originalCreateNew.call(this, options)
365
- await createNewGate
366
- const created = await originalCreateNew.call(this, options)
367
- markFirstCreateNewDone()
368
- return created
369
- }
370
- t.after(() => { SessionSpool.prototype.createNew = originalCreateNew })
371
- const fixture = await createBlackholeThenReadyServer(0)
372
- t.after(() => closeServer(fixture.server))
373
- let physicalAttempts = 0
374
- const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fixture.port, token: "valid-token"}, connectTimeoutMs: 1_000, createPhysicalSocket: (options) => {
375
- physicalAttempts += 1
376
- return createConnection(options)
377
- }})
378
- t.after(() => connection.close())
379
- const first = connection.createConnection({host: "target.internal", port: 1})
380
- first.destroy()
381
- releaseCreateNew()
382
- await firstCreateNewDone
383
- await new Promise((resolve) => setImmediate(resolve))
384
- assert.equal(physicalAttempts, 0)
385
- const second = connection.createConnection({host: "target.internal", port: 1})
386
- await Promise.race([
387
- onceEvent(second, "connect"),
388
- new Promise((_, reject) => setTimeout(() => reject(new Error("later open reused a stale connect promise")), 250))
389
- ])
390
- assert.equal(physicalAttempts, 1)
391
- assert.equal(fixture.status().accepts, 1)
392
- })
351
+ it("cancelling the only opening stream clears physical attempt ownership", async () => withTestCleanup(async (after) => {
352
+ const originalCreateNew = SessionSpool.prototype.createNew
353
+ /** @type {() => void} */
354
+ let releaseCreateNew = () => {}
355
+ /** @type {Promise<void>} */
356
+ const createNewGate = new Promise((resolve) => { releaseCreateNew = () => resolve() })
357
+ /** @type {() => void} */
358
+ let markFirstCreateNewDone = () => {}
359
+ /** @type {Promise<void>} */
360
+ const firstCreateNewDone = new Promise((resolve) => { markFirstCreateNewDone = () => resolve() })
361
+ let createNewCalls = 0
362
+ SessionSpool.prototype.createNew = async function (/** @type {{sessionId: string, ownerId: string}} */ options) {
363
+ createNewCalls += 1
364
+ if (createNewCalls !== 1) return await originalCreateNew.call(this, options)
365
+ await createNewGate
366
+ const created = await originalCreateNew.call(this, options)
367
+ markFirstCreateNewDone()
368
+ return created
369
+ }
370
+ after(() => { SessionSpool.prototype.createNew = originalCreateNew })
371
+ const fixture = await createBlackholeThenReadyServer(0)
372
+ after(() => closeServer(fixture.server))
373
+ let physicalAttempts = 0
374
+ const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: fixture.port, token: "valid-token"}, connectTimeoutMs: 1_000, createPhysicalSocket: (options) => {
375
+ physicalAttempts += 1
376
+ return createConnection(options)
377
+ }})
378
+ after(() => connection.close())
379
+ const first = connection.createConnection({host: "target.internal", port: 1})
380
+ first.destroy()
381
+ releaseCreateNew()
382
+ await firstCreateNewDone
383
+ await new Promise((resolve) => setImmediate(resolve))
384
+ expect(physicalAttempts).toBe(0)
385
+ const second = connection.createConnection({host: "target.internal", port: 1})
386
+ await Promise.race([
387
+ onceEvent(second, "connect"),
388
+ new Promise((_, reject) => setTimeout(() => reject(new Error("later open reused a stale connect promise")), 250))
389
+ ])
390
+ expect(physicalAttempts).toBe(1)
391
+ expect(fixture.status().accepts).toBe(1)
392
+ }))
393
393
 
394
- test("close-delimited streams release relay capacity after CLOSE delivery", async (t) => {
395
- const originalWriteRecoveryState = SessionSpool.prototype.writeRecoveryState
396
- /** @type {() => void} */
397
- let releaseReceiveAck = () => {}
398
- /** @type {Promise<void>} */
399
- const receiveAckGate = new Promise((resolve) => { releaseReceiveAck = resolve })
400
- /** @type {() => void} */
401
- let markReceiveAckStarted = () => {}
402
- /** @type {Promise<void>} */
403
- const receiveAckStarted = new Promise((resolve) => { markReceiveAckStarted = resolve })
404
- SessionSpool.prototype.writeRecoveryState = async function (/** @type {string} */ sessionId, /** @type {Record<string, unknown>} */ state) {
405
- if (sessionId === "close_stream_1" && typeof state.receiveAck === "number" && state.receiveAck > 0) {
406
- markReceiveAckStarted()
407
- await receiveAckGate
394
+ it("close-delimited streams release relay capacity after CLOSE delivery", async () => withTestCleanup(async (after) => {
395
+ const originalWriteRecoveryState = SessionSpool.prototype.writeRecoveryState
396
+ /** @type {() => void} */
397
+ let releaseReceiveAck = () => {}
398
+ /** @type {Promise<void>} */
399
+ const receiveAckGate = new Promise((resolve) => { releaseReceiveAck = resolve })
400
+ /** @type {() => void} */
401
+ let markReceiveAckStarted = () => {}
402
+ /** @type {Promise<void>} */
403
+ const receiveAckStarted = new Promise((resolve) => { markReceiveAckStarted = resolve })
404
+ SessionSpool.prototype.writeRecoveryState = async function (/** @type {string} */ sessionId, /** @type {Record<string, unknown>} */ state) {
405
+ if (sessionId === "close_stream_1" && typeof state.receiveAck === "number" && state.receiveAck > 0) {
406
+ markReceiveAckStarted()
407
+ await receiveAckGate
408
+ }
409
+ return await originalWriteRecoveryState.call(this, sessionId, state)
408
410
  }
409
- return await originalWriteRecoveryState.call(this, sessionId, state)
410
- }
411
- t.after(() => { SessionSpool.prototype.writeRecoveryState = originalWriteRecoveryState })
412
- let targetAccepts = 0
413
- const target = createServer((socket) => {
414
- targetAccepts += 1
415
- socket.end(`response-${targetAccepts}`)
416
- })
417
- const targetPort = await listen(target)
418
- t.after(() => closeServer(target))
419
- const relay = await startMultiplexedTcpRelay({
420
- tokens: tokens(),
421
- authorizeTarget: ({target: requested}) => requested.port === targetPort,
422
- maxStreams: 1
423
- })
424
- t.after(() => relay.close())
425
- const observer = await createFaultProxy(relay.port, "observe-client")
426
- t.after(() => closeServer(observer.proxy))
427
- const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: observer.port, token: "valid-token"}})
428
- t.after(() => connection.close())
429
- /** @type {number[]} */
430
- const streamIds = []
411
+ after(() => { SessionSpool.prototype.writeRecoveryState = originalWriteRecoveryState })
412
+ let targetAccepts = 0
413
+ const target = createServer((socket) => {
414
+ targetAccepts += 1
415
+ socket.end(`response-${targetAccepts}`)
416
+ })
417
+ const targetPort = await listen(target)
418
+ after(() => closeServer(target))
419
+ const relay = await startMultiplexedTcpRelay({
420
+ tokens: tokens(),
421
+ authorizeTarget: ({target: requested}) => requested.port === targetPort,
422
+ maxStreams: 1
423
+ })
424
+ after(() => relay.close())
425
+ const observer = await createFaultProxy(relay.port, "observe-client")
426
+ after(() => closeServer(observer.proxy))
427
+ const connection = new MultiplexedSocketductConnection({relay: {host: "127.0.0.1", port: observer.port, token: "valid-token"}})
428
+ after(() => connection.close())
429
+ /** @type {number[]} */
430
+ const streamIds = []
431
431
 
432
- for (const [index, expected] of ["response-1", "response-2"].entries()) {
433
- const socket = connection.createConnection({host: "127.0.0.1", port: targetPort, identifier: `close_stream_${index + 1}`, allowHalfOpen: true})
434
- streamIds.push(socket.streamId)
435
- /** @type {Buffer[]} */
436
- const chunks = []
437
- socket.on("data", (chunk) => chunks.push(Buffer.from(chunk)))
438
- const closed = onceEvent(socket, "close")
439
- const ended = onceEvent(socket, "end")
440
- await onceEvent(socket, "connect")
441
- await waitUntil(() => observer.status().clientFrames.some((frame) => frame.streamId === socket.streamId && frame.type === V3FrameType.CLOSE))
442
- if (index === 0) {
443
- await receiveAckStarted
444
- releaseReceiveAck()
432
+ for (const [index, expected] of ["response-1", "response-2"].entries()) {
433
+ const socket = connection.createConnection({host: "127.0.0.1", port: targetPort, identifier: `close_stream_${index + 1}`, allowHalfOpen: true})
434
+ streamIds.push(socket.streamId)
435
+ /** @type {Buffer[]} */
436
+ const chunks = []
437
+ socket.on("data", (chunk) => chunks.push(Buffer.from(chunk)))
438
+ const closed = onceEvent(socket, "close")
439
+ const ended = onceEvent(socket, "end")
440
+ await onceEvent(socket, "connect")
441
+ await waitUntil(() => observer.status().clientFrames.some((frame) => frame.streamId === socket.streamId && frame.type === V3FrameType.CLOSE))
442
+ if (index === 0) {
443
+ await receiveAckStarted
444
+ releaseReceiveAck()
445
+ }
446
+ await ended
447
+ socket.end()
448
+ await closed
449
+ expect(Buffer.concat(chunks).toString()).toBe(expected)
450
+ await waitUntil(() => /** @type {{virtualStreams: number}} */ (relay.status()).virtualStreams === 0)
445
451
  }
446
- await ended
447
- socket.end()
448
- await closed
449
- assert.equal(Buffer.concat(chunks).toString(), expected)
450
- await waitUntil(() => /** @type {{virtualStreams: number}} */ (relay.status()).virtualStreams === 0)
451
- }
452
452
 
453
- const firstCloseIndex = observer.status().clientFrames.findIndex((frame) => frame.streamId === streamIds[0] && frame.type === V3FrameType.CLOSE)
454
- assert.deepEqual(observer.status().clientFrames.slice(firstCloseIndex).filter((frame) => frame.streamId === streamIds[0]).map((frame) => frame.type), [V3FrameType.CLOSE])
455
- assert.equal(targetAccepts, 2)
456
- assert.equal(/** @type {{physicalAccepts: number}} */ (relay.status()).physicalAccepts, 1)
457
- })
453
+ const firstCloseIndex = observer.status().clientFrames.findIndex((frame) => frame.streamId === streamIds[0] && frame.type === V3FrameType.CLOSE)
454
+ expect(observer.status().clientFrames.slice(firstCloseIndex).filter((frame) => frame.streamId === streamIds[0]).map((frame) => frame.type)).toEqual([V3FrameType.CLOSE])
455
+ expect(targetAccepts).toBe(2)
456
+ expect(/** @type {{physicalAccepts: number}} */ (relay.status()).physicalAccepts).toBe(1)
457
+ }))
458
458
 
459
- test("replacement recovery replays target bytes not durably acknowledged", async (t) => {
460
- const root = await mkdtemp(join(tmpdir(), "socketduct-v3-receive-recovery-"))
461
- const payload = Buffer.from("unread-target-payload")
462
- let targetAccepts = 0
463
- const target = createServer((socket) => {
464
- socket.write(payload)
465
- })
466
- const targetPort = await listen(target)
467
- t.after(() => closeServer(target))
468
- // Prove target-open accounting is owned by the relay, not arbitrary clients of the fixture listener.
469
- const unrelated = createConnection({host: "127.0.0.1", port: targetPort})
470
- const unrelatedData = onceEvent(unrelated, "data")
471
- const unrelatedClosed = onceEvent(unrelated, "close")
472
- await onceEvent(unrelated, "connect")
473
- await unrelatedData
474
- unrelated.end()
475
- await unrelatedClosed
476
- const relay = await startMultiplexedTcpRelay({
477
- tokens: tokens(),
478
- authorizeTarget: ({target: requested}) => requested.port === targetPort,
479
- createTargetConnection: (requested) => {
480
- targetAccepts += 1
481
- return createConnection(requested)
482
- },
483
- initialWindowBytes: 32,
484
- maxFrameBytes: 32,
485
- resumeWindowMs: 2_000
486
- })
487
- t.after(() => relay.close())
488
- const shared = {
489
- relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
490
- connectionId: "stable_connection_receive_01",
491
- spoolDirectory: root,
492
- initialWindowBytes: 32,
493
- maxFrameBytes: 32
494
- }
495
- const first = new MultiplexedSocketductConnection(shared)
496
- const initial = first.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_receive_01", readableHighWaterMark: 1})
497
- await onceEvent(initial, "connect")
498
- await waitUntil(() => initial.bytesRead === payload.length)
499
- await waitUntil(() => /** @type {{bufferedBytes: number}} */ (relay.status()).bufferedBytes === payload.length)
500
- await first.detach()
459
+ it("replacement recovery replays target bytes not durably acknowledged", async () => withTestCleanup(async (after) => {
460
+ const root = await mkdtemp(join(tmpdir(), "socketduct-v3-receive-recovery-"))
461
+ const payload = Buffer.from("unread-target-payload")
462
+ let targetAccepts = 0
463
+ const target = createServer((socket) => {
464
+ socket.write(payload)
465
+ })
466
+ const targetPort = await listen(target)
467
+ after(() => closeServer(target))
468
+ // Prove target-open accounting is owned by the relay, not arbitrary clients of the fixture listener.
469
+ const unrelated = createConnection({host: "127.0.0.1", port: targetPort})
470
+ const unrelatedData = onceEvent(unrelated, "data")
471
+ const unrelatedClosed = onceEvent(unrelated, "close")
472
+ await onceEvent(unrelated, "connect")
473
+ await unrelatedData
474
+ unrelated.end()
475
+ await unrelatedClosed
476
+ const relay = await startMultiplexedTcpRelay({
477
+ tokens: tokens(),
478
+ authorizeTarget: ({target: requested}) => requested.port === targetPort,
479
+ createTargetConnection: (requested) => {
480
+ targetAccepts += 1
481
+ return createConnection(requested)
482
+ },
483
+ initialWindowBytes: 32,
484
+ maxFrameBytes: 32,
485
+ resumeWindowMs: 2_000
486
+ })
487
+ after(() => relay.close())
488
+ const shared = {
489
+ relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
490
+ connectionId: "stable_connection_receive_01",
491
+ spoolDirectory: root,
492
+ initialWindowBytes: 32,
493
+ maxFrameBytes: 32
494
+ }
495
+ const first = new MultiplexedSocketductConnection(shared)
496
+ const initial = first.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_receive_01", readableHighWaterMark: 1})
497
+ await onceEvent(initial, "connect")
498
+ await waitUntil(() => initial.bytesRead === payload.length)
499
+ await waitUntil(() => /** @type {{bufferedBytes: number}} */ (relay.status()).bufferedBytes === payload.length)
500
+ await first.detach()
501
501
 
502
- const second = new MultiplexedSocketductConnection(shared)
503
- t.after(() => second.close())
504
- t.after(() => rm(root, {recursive: true, force: true}))
505
- const recovered = second.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_receive_01", readableHighWaterMark: 1, recover: true})
506
- /** @type {Buffer[]} */
507
- const chunks = []
508
- recovered.on("data", (chunk) => chunks.push(Buffer.from(chunk)))
509
- recovered.resume()
510
- await onceEvent(recovered, "connect")
511
- await waitUntil(() => Buffer.concat(chunks).length === payload.length)
512
- assert.deepEqual(Buffer.concat(chunks), payload)
513
- assert.equal(targetAccepts, 1)
514
- })
502
+ const second = new MultiplexedSocketductConnection(shared)
503
+ after(() => second.close())
504
+ after(() => rm(root, {recursive: true, force: true}))
505
+ const recovered = second.createConnection({host: "127.0.0.1", port: targetPort, identifier: "stable_stream_receive_01", readableHighWaterMark: 1, recover: true})
506
+ /** @type {Buffer[]} */
507
+ const chunks = []
508
+ recovered.on("data", (chunk) => chunks.push(Buffer.from(chunk)))
509
+ recovered.resume()
510
+ await onceEvent(recovered, "connect")
511
+ await waitUntil(() => Buffer.concat(chunks).length === payload.length)
512
+ expect(Buffer.concat(chunks).equals(payload)).toBe(true)
513
+ expect(targetAccepts).toBe(1)
514
+ }))
515
515
 
516
- test("replacement recovery reconciles a durable opening stream without a duplicate OPEN", async (t) => {
517
- const root = await mkdtemp(join(tmpdir(), "socketduct-v3-opening-recovery-"))
518
- let targetAttempts = 0
519
- /** @type {Duplex | undefined} */
520
- let pendingTarget
521
- const relay = await startMultiplexedTcpRelay({
522
- tokens: tokens(),
523
- authorizeTarget: () => true,
524
- resumeWindowMs: 2_000,
525
- createTargetConnection: () => {
526
- targetAttempts += 1
527
- pendingTarget = new Duplex({read() {}, write(_chunk, _encoding, callback) { callback() }})
528
- return /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (pendingTarget))
516
+ it("replacement recovery reconciles a durable opening stream without a duplicate OPEN", async () => withTestCleanup(async (after) => {
517
+ const root = await mkdtemp(join(tmpdir(), "socketduct-v3-opening-recovery-"))
518
+ let targetAttempts = 0
519
+ /** @type {Duplex | undefined} */
520
+ let pendingTarget
521
+ const relay = await startMultiplexedTcpRelay({
522
+ tokens: tokens(),
523
+ authorizeTarget: () => true,
524
+ resumeWindowMs: 2_000,
525
+ createTargetConnection: () => {
526
+ targetAttempts += 1
527
+ pendingTarget = new Duplex({read() {}, write(_chunk, _encoding, callback) { callback() }})
528
+ return /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (pendingTarget))
529
+ }
530
+ })
531
+ after(() => relay.close())
532
+ const shared = {
533
+ relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
534
+ connectionId: "stable_connection_opening_01",
535
+ spoolDirectory: root
529
536
  }
530
- })
531
- t.after(() => relay.close())
532
- const shared = {
533
- relay: {host: "127.0.0.1", port: relay.port, token: "valid-token"},
534
- connectionId: "stable_connection_opening_01",
535
- spoolDirectory: root
536
- }
537
- const first = new MultiplexedSocketductConnection(shared)
538
- first.createConnection({host: "target.internal", port: 443, identifier: "stable_stream_opening_01"})
539
- await waitUntil(() => pendingTarget !== undefined)
540
- await first.detach()
537
+ const first = new MultiplexedSocketductConnection(shared)
538
+ first.createConnection({host: "target.internal", port: 443, identifier: "stable_stream_opening_01"})
539
+ await waitUntil(() => pendingTarget !== undefined)
540
+ await first.detach()
541
541
 
542
- const second = new MultiplexedSocketductConnection(shared)
543
- t.after(() => second.close())
544
- t.after(() => rm(root, {recursive: true, force: true}))
545
- const recovered = second.createConnection({host: "target.internal", port: 443, identifier: "stable_stream_opening_01", recover: true})
546
- const connected = onceEvent(recovered, "connect")
547
- pendingTarget?.emit("connect")
548
- await connected
549
- assert.equal(targetAttempts, 1)
550
- await second.detach()
542
+ const second = new MultiplexedSocketductConnection(shared)
543
+ after(() => second.close())
544
+ after(() => rm(root, {recursive: true, force: true}))
545
+ const recovered = second.createConnection({host: "target.internal", port: 443, identifier: "stable_stream_opening_01", recover: true})
546
+ const connected = onceEvent(recovered, "connect")
547
+ pendingTarget?.emit("connect")
548
+ await connected
549
+ expect(targetAttempts).toBe(1)
550
+ await second.detach()
551
+ }))
551
552
  })