socketduct 0.1.19 → 0.1.21

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 (59) hide show
  1. package/.env.example +17 -5
  2. package/README.md +3 -3
  3. package/changelog.d/20260925170000-disconnect-attribution-compressed-logs.md +1 -0
  4. package/changelog.d/20260925180000-manual-ack-durable-admission.md +1 -0
  5. package/docs/docker.md +5 -4
  6. package/docs/logging.md +131 -159
  7. package/docs/protocol.md +48 -7
  8. package/docs/reverse-gateway.md +83 -8
  9. package/docs/running.md +17 -13
  10. package/docs/security.md +7 -3
  11. package/docs/session-lifecycle.md +22 -0
  12. package/docs/socket-api.md +22 -17
  13. package/package.json +5 -1
  14. package/scripts/verify-zstd-interoperability.mjs +41 -0
  15. package/src/cli/socketduct.js +143 -0
  16. package/src/client/deferred-virtual-socket.js +42 -1
  17. package/src/client/multiplexed-connection.js +17 -0
  18. package/src/client/multiplexed-transport-set.js +43 -8
  19. package/src/client/multiplexed.js +1 -1
  20. package/src/client/virtual-socket.js +119 -4
  21. package/src/config/environment.js +56 -7
  22. package/src/index.js +65 -27
  23. package/src/logging/compressed-log-reader.js +256 -0
  24. package/src/logging/compressed-log-worker.js +430 -0
  25. package/src/logging/compressed-log-writer.js +538 -0
  26. package/src/logging/log-batch.js +72 -0
  27. package/src/logging/log-retention.js +241 -0
  28. package/src/logging/log-segment-index.js +176 -0
  29. package/src/logging/structured-logger.js +136 -30
  30. package/src/logging/zstd-frame-codec.js +73 -0
  31. package/src/reverse/client.js +245 -34
  32. package/src/reverse/disconnect-attribution.js +191 -0
  33. package/src/reverse/gateway.js +68 -9
  34. package/src/reverse/hub.js +215 -32
  35. package/src/reverse/runtime.js +5 -3
  36. package/src/spool/spool-file.js +92 -6
  37. package/tensorbuzz.yml +2 -1
  38. package/test/e2e/logger-shutdown-process.test.js +86 -0
  39. package/test/e2e/reverse-client-recovery-process.test.js +3 -0
  40. package/test/e2e/reverse-gateway-process.test.js +15 -14
  41. package/test/fixtures/reverse-client-recovery.mjs +14 -2
  42. package/test/integration/logging-cli.test.js +168 -0
  43. package/test/integration/logging-compressed-recovery.test.js +186 -0
  44. package/test/integration/multiplexed-transport-set.test.js +1 -5
  45. package/test/integration/reverse-gateway.test.js +334 -1
  46. package/test/integration/reverse-route-class.test.js +6 -1
  47. package/test/unit/client/multiplexed-transport-set.test.js +50 -3
  48. package/test/unit/client/virtual-socket.test.js +123 -1
  49. package/test/unit/config/docker-gateway-files.test.js +2 -0
  50. package/test/unit/config/environment.test.js +54 -14
  51. package/test/unit/index.test.js +5 -0
  52. package/test/unit/logging/compressed-log-worker.test.js +29 -0
  53. package/test/unit/logging/compressed-log-writer.test.js +183 -0
  54. package/test/unit/logging/log-retention.test.js +86 -0
  55. package/test/unit/logging/log-segment-index.test.js +104 -0
  56. package/test/unit/logging/structured-logger.test.js +14 -1
  57. package/test/unit/logging/zstd-frame-codec.test.js +24 -0
  58. package/test/unit/reverse/disconnect-attribution.test.js +82 -0
  59. package/test/unit/spool/spool-file.test.js +58 -0
@@ -178,6 +178,7 @@ class HeldWriteTarget extends Duplex {
178
178
  /** @param {Buffer} chunk - Written bytes. @param {BufferEncoding} _encoding - Ignored encoding. @param {(error?: Error | null) => void} callback - Completion. @override */
179
179
  _write(chunk, _encoding, callback) {
180
180
  this.chunks.push(Buffer.from(chunk))
181
+ this.emit("writeObserved")
181
182
  if (this.#held) { callback(); return }
182
183
  this.#held = true
183
184
  this.releaseFirstWrite = callback
@@ -191,6 +192,19 @@ class HeldWriteTarget extends Duplex {
191
192
  }
192
193
  }
193
194
 
195
+ /** @param {HeldWriteTarget} target - Observed target. @param {number} bytes - Required bytes. */
196
+ async function waitForTargetBytes(target, bytes) {
197
+ if (Buffer.concat(target.chunks).length >= bytes) return
198
+ await new Promise((resolve) => {
199
+ const observe = () => {
200
+ if (Buffer.concat(target.chunks).length < bytes) return
201
+ target.off("writeObserved", observe)
202
+ resolve(undefined)
203
+ }
204
+ target.on("writeObserved", observe)
205
+ })
206
+ }
207
+
194
208
  class ObservedMemoryBudget {
195
209
  /** @type {number} */ bytes = 0
196
210
  /** @type {number} */ spoolBytes = 0
@@ -307,11 +321,50 @@ test("reverse client contains a TLS AUTH write race inside the transport", async
307
321
  await assert.rejects(connecting, /closed/i)
308
322
  })
309
323
 
324
+ test("fatal hub GOAWAY preserves validated attribution on the public client error", async (t) => {
325
+ /** @type {AuthWriteSocket | undefined} */
326
+ let physical
327
+ const client = new ReverseGatewayConnection({
328
+ gatewayId: "o2",
329
+ connectionId: "fatal_goaway_attribution_client",
330
+ hub: {host: "127.0.0.1", port: 3100, token: "client-token"},
331
+ tls: {servername: "localhost"},
332
+ continuousReconnect: false,
333
+ createPhysicalSocket: () => {
334
+ physical = new AuthWriteSocket()
335
+ return /** @type {import('node:tls').TLSSocket} */ (/** @type {unknown} */ (physical))
336
+ }
337
+ })
338
+ t.after(() => client.close())
339
+ const connecting = client.connect()
340
+ physical?.emit("secureConnect")
341
+ physical?.emit("data", encodeV3Frame({type: V3FrameType.GOAWAY, streamId: 0, sequence: 0}, encodeV3Control({
342
+ code: "PROTOCOL_VIOLATION",
343
+ disconnectIncidentId: "AAAAAAAAAAAAAAAAAAAAAA",
344
+ disconnectReason: "protocol_violation",
345
+ incidentOrigin: "reverse_hub",
346
+ physicalRole: "client",
347
+ clientConnectionGeneration: 1,
348
+ fanOutCount: 0,
349
+ resumeSuccessCount: 0,
350
+ terminalStreamCount: 0
351
+ })))
352
+ /** @type {Error & Record<string, unknown> | undefined} */
353
+ let rejection
354
+ await assert.rejects(withTimeout(connecting, "fatal attributed GOAWAY"), (error) => {
355
+ rejection = /** @type {Error & Record<string, unknown>} */ (error)
356
+ return rejection.code === "PROTOCOL_VIOLATION"
357
+ })
358
+ assert.equal(rejection?.disconnectIncidentId, "AAAAAAAAAAAAAAAAAAAAAA")
359
+ assert.equal(rejection?.disconnectReason, "protocol_violation")
360
+ assert.equal(rejection?.incidentOrigin, "reverse_hub")
361
+ })
362
+
310
363
  test("recovered reverse clients require explicit cold durable identity before any physical attempt", async (t) => {
311
364
  const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-recovery-contract-"))
312
365
  t.after(() => rm(root, {recursive: true, force: true}))
313
366
  const spoolDirectory = join(root, "client-spool")
314
- await seedReverseClientRecovery(spoolDirectory, {sendAck: 2}, [
367
+ await seedReverseClientRecovery(spoolDirectory, {sendAck: 2, manualReceiveAck: true}, [
315
368
  {sequence: 2, data: Buffer.from("aa")},
316
369
  {sequence: 4, data: Buffer.from("bb")}
317
370
  ])
@@ -334,10 +387,17 @@ test("recovered reverse clients require explicit cold durable identity before an
334
387
 
335
388
  const client = new ReverseGatewayConnection(base)
336
389
  t.after(() => client.close())
390
+ assert.deepEqual(client._inspectRecoveredConnection("recovered_stream_01"), {
391
+ identifier: "recovered_stream_01",
392
+ target: {host: "docker-server", port: 2375},
393
+ sessionName: "durable-reverse-stream",
394
+ manualReceiveAck: true
395
+ })
337
396
  assert.throws(() => client.createConnection({recover: true, identifier: "recovered_stream_01", host: "docker-server", port: 2375}), /must use registerRecoveredConnection/i)
338
397
  assert.throws(() => client.registerRecoveredConnection(/** @type {import('../../src/client/multiplexed-connection.js').VirtualConnectionOptions} */ ({identifier: "recovered_stream_01", host: "docker-server", port: 2375})), /recover: true/i)
339
398
  const recovered = client.registerRecoveredConnection({recover: true, identifier: "recovered_stream_01", sessionName: "durable-reverse-stream", host: "docker-server", port: 2375})
340
399
  assert.equal(recovered.streamId, 1)
400
+ assert.equal(recovered.manualReceiveAck, true)
341
401
  assert.equal(client.status().physicalState, "configured")
342
402
  assert.equal(client.status().connectionGeneration, 41)
343
403
  assert.equal(client.status().spoolBytes, 2)
@@ -1271,6 +1331,63 @@ test("reverse client backpressures a write larger than its retained-byte limit",
1271
1331
  socket.destroy()
1272
1332
  })
1273
1333
 
1334
+ test("reverse client durable write rejects atomically at limits and remains retryable", async (t) => {
1335
+ const {budget, client, socket, target} = await createBackpressuredClient(t, "durable_write_limit")
1336
+ const oversized = Buffer.alloc(33, 0x6f)
1337
+
1338
+ await assert.rejects(
1339
+ socket.writeDurable(oversized),
1340
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_DURABLE_WRITE_LIMIT"
1341
+ )
1342
+ assert.deepEqual(target.chunks, [])
1343
+ assert.equal(client.status().queuedBytes, 0)
1344
+ assert.equal(client.status().spoolBytes, 0)
1345
+ assert.equal(budget.bytes, 0)
1346
+
1347
+ const request = Buffer.alloc(32, 0x72)
1348
+ await socket.writeDurable(request)
1349
+ assert.equal(client.status().queuedBytes, request.length)
1350
+ assert.equal(client.status().spoolBytes, request.length)
1351
+ assert.equal(budget.bytes, request.length)
1352
+ await withTimeout(target.firstWrite, "first atomically admitted request frame")
1353
+ target.release()
1354
+ await withTimeout(waitForTargetBytes(target, request.length), "complete atomically admitted request")
1355
+ assert.deepEqual(Buffer.concat(target.chunks), request)
1356
+ socket.destroy()
1357
+ })
1358
+
1359
+ test("reverse client durable write exposes no bytes after a partial persistence failure and can retry", async (t) => {
1360
+ const {budget, client, socket, target} = await createBackpressuredClient(t, "durable_write_retry")
1361
+ const originalWriteRecoveryState = SessionSpool.prototype.writeRecoveryState
1362
+ let failAdmissionCommit = true
1363
+ SessionSpool.prototype.writeRecoveryState = async function (/** @type {string} */ identifier, /** @type {Record<string, unknown>} */ state) {
1364
+ if (failAdmissionCommit && identifier.length >= 8 && state.sendSequence === 16) {
1365
+ failAdmissionCommit = false
1366
+ throw Object.assign(new Error("injected recovery commit failure"), {code: "EIO"})
1367
+ }
1368
+ await originalWriteRecoveryState.call(this, identifier, state)
1369
+ }
1370
+ t.after(() => { SessionSpool.prototype.writeRecoveryState = originalWriteRecoveryState })
1371
+
1372
+ const request = Buffer.alloc(16, 0x64)
1373
+ await assert.rejects(
1374
+ socket.writeDurable(request),
1375
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_DURABLE_WRITE_PERSISTENCE"
1376
+ )
1377
+ assert.deepEqual(target.chunks, [])
1378
+ assert.equal(client.status().queuedBytes, 0)
1379
+ assert.equal(client.status().spoolBytes, 0)
1380
+ assert.equal(budget.bytes, 0)
1381
+ assert.equal(budget.spoolBytes, 0)
1382
+
1383
+ await socket.writeDurable(request)
1384
+ await withTimeout(target.firstWrite, "retried durable request")
1385
+ target.release()
1386
+ await withTimeout(waitForTargetBytes(target, request.length), "complete retried durable request")
1387
+ assert.deepEqual(Buffer.concat(target.chunks), request)
1388
+ socket.destroy()
1389
+ })
1390
+
1274
1391
  test("reverse client chunks a large write to the negotiated stream window", async (t) => {
1275
1392
  const {budget, client, socket, target} = await createBackpressuredClient(t, "negotiated_window", {
1276
1393
  hubWindowBytes: 8,
@@ -1385,6 +1502,85 @@ test("reverse gateway honors the smaller hub receive window", async (t) => {
1385
1502
  socket.destroy()
1386
1503
  })
1387
1504
 
1505
+ test("reverse client manual receive ACK advances only after the application commits bytes", async (t) => {
1506
+ const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-manual-ack-"))
1507
+ t.after(() => rm(root, {recursive: true, force: true}))
1508
+ const certificates = generateTestCertificates(root)
1509
+ const tokens = new StaticTokenStore([{username: "home", token: "gateway-token"}, {username: "app", token: "client-token"}])
1510
+ const hub = await startReverseGatewayHub({tokens, tls: {certFile: certificates.serverCertFile, keyFile: certificates.serverKeyFile}, authorizeGateway: () => true, authorizeClientGateway: () => true, authorizeOpen: () => true, initialWindowBytes: 8, maxFrameBytes: 8})
1511
+ t.after(() => hub.close())
1512
+ const tls = {caFile: certificates.caCertFile, servername: "localhost"}
1513
+ /** @type {import('node:net').Socket | undefined} */
1514
+ let targetSocket
1515
+ const targetSockets = new Set()
1516
+ const target = createServer((socket) => {
1517
+ targetSocket = socket
1518
+ targetSockets.add(socket)
1519
+ socket.once("close", () => targetSockets.delete(socket))
1520
+ })
1521
+ const targetPort = await listen(target)
1522
+ t.after(() => { for (const socket of targetSockets) socket.destroy(); return new Promise((resolve) => target.close(() => resolve(undefined))) })
1523
+ const gateway = new OutboundReverseGateway({gatewayId: "o2", connectionId: "manual_ack_gateway", hub: {host: "127.0.0.1", port: hub.port, token: "gateway-token"}, tls, authorizeTarget: () => true, initialWindowBytes: 8, maxFrameBytes: 8})
1524
+ t.after(() => gateway.close())
1525
+ await gateway.start()
1526
+
1527
+ /** @type {number[]} */
1528
+ const ackSequences = []
1529
+ const spoolDirectory = join(root, "client-spool")
1530
+ const client = new ReverseGatewayConnection({
1531
+ gatewayId: "o2",
1532
+ connectionId: "manual_ack_client",
1533
+ hub: {host: "127.0.0.1", port: hub.port, token: "client-token"},
1534
+ tls,
1535
+ spoolDirectory,
1536
+ initialWindowBytes: 8,
1537
+ maxFrameBytes: 8,
1538
+ createPhysicalSocket: (options) => {
1539
+ const physical = connectTls({host: options.host, port: options.port, ...options.tls})
1540
+ const write = physical.write.bind(physical)
1541
+ physical.write = ((chunk, ...args) => {
1542
+ if (Buffer.isBuffer(chunk) && chunk.length >= 24 && chunk[5] === V3FrameType.ACK) ackSequences.push(Number(chunk.readBigUInt64BE(12)))
1543
+ return Reflect.apply(write, physical, [chunk, ...args])
1544
+ })
1545
+ return physical
1546
+ }
1547
+ })
1548
+ t.after(() => client.close())
1549
+ await client.connect()
1550
+ const socket = client.createConnection({identifier: "manual_ack_stream", host: "127.0.0.1", port: targetPort, manualReceiveAck: true})
1551
+ await onceEvent(socket, "connect")
1552
+ assert(targetSocket !== undefined)
1553
+
1554
+ const received = readBytes(socket, 8)
1555
+ targetSocket.write("response")
1556
+ assert.equal((await withTimeout(received, "manual ACK response")).toString(), "response")
1557
+ assert.deepEqual(ackSequences, [])
1558
+
1559
+ assert.equal(await socket.acknowledgeReceive(3), 3)
1560
+ assert.equal(await socket.acknowledgeReceive(5), 8)
1561
+ assert.deepEqual(ackSequences, [3, 8])
1562
+ assert.throws(
1563
+ () => socket.acknowledgeReceive(1),
1564
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_INVALID_RECEIVE_ACK"
1565
+ )
1566
+ const recovery = JSON.parse(await readFile(join(spoolDirectory, "manual_ack_stream", "recovery.json"), "utf8"))
1567
+ assert.equal(recovery.receiveAck, 8)
1568
+ assert.equal(recovery.manualReceiveAck, true)
1569
+
1570
+ const finalByte = readBytes(socket, 1)
1571
+ targetSocket.write("x")
1572
+ await withTimeout(finalByte, "manual ACK final byte")
1573
+ await client.detach()
1574
+ await assert.rejects(
1575
+ socket.acknowledgeReceive(1),
1576
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_CONNECTION_DETACHED"
1577
+ )
1578
+ await assert.rejects(
1579
+ socket.writeDurable("late-request"),
1580
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_CONNECTION_DETACHED"
1581
+ )
1582
+ })
1583
+
1388
1584
  test("reverse peers reconnect after graceful hub shutdown", async (t) => {
1389
1585
  const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-hub-restart-"))
1390
1586
  t.after(() => rm(root, {recursive: true, force: true}))
@@ -2168,6 +2364,143 @@ test("an expired reverse resume window fails the logical stream once and release
2168
2364
  assert.equal(targets[0]?.destroyed, true)
2169
2365
  })
2170
2366
 
2367
+ test("one gateway disconnect incident preserves physical cause across fan-out and terminal resume errors", async (t) => {
2368
+ const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-attribution-"))
2369
+ t.after(() => rm(root, {recursive: true, force: true}))
2370
+ const certificates = generateTestCertificates(root)
2371
+ const tokens = new StaticTokenStore([{username: "home", token: "gateway-attribution-token"}, {username: "app", token: "client-attribution-token"}])
2372
+ /** @type {Array<Record<string, unknown> & {event: string}>} */
2373
+ const hubEvents = []
2374
+ /** @type {Array<Record<string, unknown> & {event: string}>} */
2375
+ const gatewayEvents = []
2376
+ /** @type {Array<Record<string, unknown> & {event: string}>} */
2377
+ const clientEvents = []
2378
+ const hub = await startReverseGatewayHub({
2379
+ tokens,
2380
+ tls: {certFile: certificates.serverCertFile, keyFile: certificates.serverKeyFile},
2381
+ authorizeGateway: () => true,
2382
+ authorizeClientGateway: () => true,
2383
+ authorizeOpen: () => true,
2384
+ resumeWindowMs: 40,
2385
+ logger: {log: (/** @type {string} */ event, /** @type {Record<string, unknown>} */ fields = {}) => { hubEvents.push({event, ...fields}) }},
2386
+ connectionFingerprint: {key: Buffer.alloc(32, 0x5a), keyEpoch: "test-epoch"}
2387
+ })
2388
+ t.after(() => hub.close())
2389
+ const tls = {caFile: certificates.caCertFile, servername: "localhost"}
2390
+ const targets = []
2391
+ const gateway = new OutboundReverseGateway({
2392
+ gatewayId: "o2",
2393
+ connectionId: "low_entropy_gateway_connection",
2394
+ hub: {host: "127.0.0.1", port: hub.port, token: "gateway-attribution-token"},
2395
+ tls,
2396
+ authorizeTarget: () => true,
2397
+ reconnectDelayMs: 150,
2398
+ maxReconnectDelayMs: 150,
2399
+ logger: {log: (/** @type {string} */ event, /** @type {Record<string, unknown>} */ fields = {}) => { gatewayEvents.push({event, ...fields}) }},
2400
+ createTargetConnection: () => {
2401
+ const target = new InterruptibleTarget()
2402
+ targets.push(target)
2403
+ return /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (target))
2404
+ }
2405
+ })
2406
+ t.after(() => gateway.close())
2407
+ await gateway.start()
2408
+ const client = new ReverseGatewayConnection({
2409
+ gatewayId: "o2",
2410
+ connectionId: "low_entropy_client_connection",
2411
+ hub: {host: "127.0.0.1", port: hub.port, token: "client-attribution-token"},
2412
+ tls,
2413
+ reconnectDelayMs: 5,
2414
+ maxReconnectDelayMs: 10,
2415
+ logger: {log: (/** @type {string} */ event, /** @type {Record<string, unknown>} */ fields = {}) => { clientEvents.push({event, ...fields}) }}
2416
+ })
2417
+ t.after(() => client.close())
2418
+ await client.connect()
2419
+ const sockets = [client.createConnection({host: "target", port: 2375}), client.createConnection({host: "target", port: 2375})]
2420
+ await Promise.all(sockets.map((socket) => onceEvent(socket, "connect")))
2421
+ const terminalErrors = sockets.map((socket) => nextError(socket))
2422
+
2423
+ await hub.closeActiveGateway("o2")
2424
+ const errors = /** @type {Array<Error & Record<string, unknown>>} */ (await Promise.all(terminalErrors))
2425
+ assert.deepEqual(errors.map((error) => error.code), ["RESUME_WINDOW_EXPIRED", "RESUME_WINDOW_EXPIRED"])
2426
+ assert.equal(new Set(errors.map((error) => error.disconnectIncidentId)).size, 1)
2427
+ for (const error of errors) {
2428
+ assert.match(/** @type {string} */ (error.disconnectIncidentId), /^[A-Za-z0-9_-]{22}$/)
2429
+ assert.equal(error.disconnectReason, "unknown_transport_close")
2430
+ assert.equal(error.incidentOrigin, "reverse_hub")
2431
+ assert.equal(error.physicalRole, "gateway")
2432
+ assert.equal(typeof error.clientConnectionGeneration, "number")
2433
+ assert.equal(typeof error.gatewayConnectionGeneration, "number")
2434
+ assert.equal(error.fanOutCount, 2)
2435
+ }
2436
+ const detached = hubEvents.find(({event, physicalRole}) => event === "reverse_hub.physical_detached" && physicalRole === "gateway")
2437
+ assert(detached !== undefined)
2438
+ assert.equal(detached.disconnectIncidentId, /** @type {Error & Record<string, unknown>} */ (errors[0]).disconnectIncidentId)
2439
+ assert.match(/** @type {string} */ (detached.connectionFingerprint), /^[a-f0-9]{32}$/)
2440
+ assert.equal(detached.fingerprintKeyEpoch, "test-epoch")
2441
+ assert(hubEvents.some(({event, fanOutCount}) => event === "reverse_hub.streams_interrupted" && fanOutCount === 2))
2442
+ assert(hubEvents.some(({event, terminalStreamCount}) => event === "reverse_hub.streams_reset" && terminalStreamCount === 2))
2443
+ assert(clientEvents.some(({event}) => event === "reverse_client.physical_detached"))
2444
+ assert(gatewayEvents.some(({event}) => event === "reverse_gateway.physical_detached"))
2445
+ assert.equal(targets.length, 2)
2446
+ assert.doesNotMatch(JSON.stringify([hubEvents, gatewayEvents, clientEvents]), /low_entropy_(?:gateway|client)_connection|attribution-token/)
2447
+ })
2448
+
2449
+ test("peer GOAWAY classification and hub attribution reach terminal non-resumable RESET", async (t) => {
2450
+ const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-terminal-attribution-"))
2451
+ t.after(() => rm(root, {recursive: true, force: true}))
2452
+ const certificates = generateTestCertificates(root)
2453
+ const tokens = new StaticTokenStore([{username: "home", token: "gateway-token"}, {username: "app", token: "client-token"}])
2454
+ /** @type {Array<Record<string, unknown> & {event: string}>} */
2455
+ const hubEvents = []
2456
+ const hub = await startReverseGatewayHub({
2457
+ tokens,
2458
+ tls: {certFile: certificates.serverCertFile, keyFile: certificates.serverKeyFile},
2459
+ authorizeGateway: () => true,
2460
+ authorizeClientGateway: () => true,
2461
+ authorizeOpen: () => true,
2462
+ logger: {log: (/** @type {string} */ event, /** @type {Record<string, unknown>} */ fields = {}) => { hubEvents.push({event, ...fields}) }}
2463
+ })
2464
+ t.after(() => hub.close())
2465
+ const tls = {caFile: certificates.caCertFile, servername: "localhost"}
2466
+ const gateway = new OutboundReverseGateway({
2467
+ gatewayId: "o2",
2468
+ connectionId: "terminal_attribution_gateway",
2469
+ hub: {host: "127.0.0.1", port: hub.port, token: "gateway-token"},
2470
+ tls,
2471
+ authorizeTarget: () => true,
2472
+ createTargetConnection: () => /** @type {import('node:net').Socket} */ (/** @type {unknown} */ (new InterruptibleTarget())),
2473
+ createPhysicalSocket: (options) => {
2474
+ const physical = connectTls({host: options.host, port: options.port, ...options.tls})
2475
+ const originalWrite = physical.write.bind(physical)
2476
+ physical.write = /** @type {typeof physical.write} */ ((chunk, ...args) => {
2477
+ if (!Buffer.isBuffer(chunk) || chunk[5] !== V3FrameType.GOAWAY) return Reflect.apply(originalWrite, physical, [chunk, ...args])
2478
+ const safeGoaway = encodeV3Frame({type: V3FrameType.GOAWAY, streamId: 0, sequence: 0}, encodeV3Control({code: "GATEWAY_MAINTENANCE", disconnectIncidentId: "BBBBBBBBBBBBBBBBBBBBBB"}))
2479
+ return Reflect.apply(originalWrite, physical, [safeGoaway, ...args])
2480
+ })
2481
+ return physical
2482
+ }
2483
+ })
2484
+ t.after(() => gateway.close())
2485
+ await gateway.start()
2486
+ const client = new ReverseGatewayConnection({gatewayId: "o2", connectionId: "terminal_attribution_client", hub: {host: "127.0.0.1", port: hub.port, token: "client-token"}, tls, continuousReconnect: false})
2487
+ t.after(() => client.close())
2488
+ await client.connect()
2489
+ const socket = client.createConnection({host: "target", port: 2375})
2490
+ await onceEvent(socket, "connect")
2491
+ const terminal = nextError(socket)
2492
+ await gateway.close()
2493
+ const error = /** @type {Error & Record<string, unknown>} */ (await withTimeout(terminal, "terminal peer GOAWAY reset"))
2494
+ assert.equal(error.code, "GATEWAY_MAINTENANCE")
2495
+ assert.equal(error.disconnectReason, "peer_goaway")
2496
+ assert.equal(error.incidentOrigin, "reverse_hub")
2497
+ assert.equal(error.physicalRole, "gateway")
2498
+ assert.equal(error.fanOutCount, 1)
2499
+ assert.equal(error.terminalStreamCount, 1)
2500
+ assert.notEqual(error.disconnectIncidentId, "BBBBBBBBBBBBBBBBBBBBBB")
2501
+ assert(hubEvents.some(({event, terminalStreamCount}) => event === "reverse_hub.streams_reset" && terminalStreamCount === 1), JSON.stringify(hubEvents))
2502
+ })
2503
+
2171
2504
  test("reverse mode enforces verified TLS and stream resource bounds", async (t) => {
2172
2505
  const root = await mkdtemp(join(tmpdir(), "socketduct-reverse-limits-"))
2173
2506
  const wrongRoot = await mkdtemp(join(tmpdir(), "socketduct-reverse-wrong-ca-"))
@@ -364,7 +364,12 @@ test("a backpressured gateway does not block DATA or a new OPEN for another rout
364
364
  t.after(() => socket.destroy())
365
365
  return nextError(socket)
366
366
  })
367
- assert.equal((await withTimeout(streamAError, "gateway A output limit")).code, "OUTPUT_BACKPRESSURE_LIMIT")
367
+ const outputError = /** @type {Error & Record<string, unknown>} */ (await withTimeout(streamAError, "gateway A output limit"))
368
+ assert.equal(outputError.code, "OUTPUT_BACKPRESSURE_LIMIT")
369
+ assert.equal(outputError.disconnectReason, "output_backpressure_limit")
370
+ assert.equal(outputError.incidentOrigin, "reverse_hub")
371
+ assert.equal(outputError.physicalRole, "gateway")
372
+ assert.match(/** @type {string} */ (outputError.disconnectIncidentId), /^[A-Za-z0-9_-]{22}$/)
368
373
  await withTimeout(Promise.all(queuedAErrors), "queued gateway A stream cleanup")
369
374
  assert.equal(await exchange(streamB, "after-limit"), "b:after-limit")
370
375
  assert.equal(hub.status().healthyGateways, 1)
@@ -66,6 +66,8 @@ class FakeMember extends EventEmitter {
66
66
  this.createConnectionCalls = []
67
67
  /** @type {Record<string, unknown>[]} */
68
68
  this.recoveryConnectionCalls = []
69
+ /** @type {Map<string, {identifier: string, target: {host: string, port: number}, sessionName: string, manualReceiveAck: boolean}>} */
70
+ this.recoveryDescriptors = new Map()
69
71
  this.state = "configured"
70
72
  this.generation = 0
71
73
  this.physicalConnects = 0
@@ -125,6 +127,11 @@ class FakeMember extends EventEmitter {
125
127
  return /** @type {import('../../../src/client/virtual-socket.js').VirtualSocket} */ (/** @type {unknown} */ (socket))
126
128
  }
127
129
 
130
+ /** @param {string} identifier - Stable stream identifier. */
131
+ _inspectRecoveredConnection(identifier) {
132
+ return this.recoveryDescriptors.get(identifier)
133
+ }
134
+
128
135
  status() {
129
136
  return {
130
137
  physicalState: this.state,
@@ -289,8 +296,8 @@ test("route-class members are first-class reverse transports with route-class al
289
296
  assert(connectionIds.every((connectionId) => typeof connectionId === "string"))
290
297
  assert.equal(new Set(connectionIds).size, 2)
291
298
 
292
- assert.throws(() => set.recoverConnection(/** @type {Parameters<MultiplexedSocketductTransportSet['recoverConnection']>[0]} */ (/** @type {unknown} */ ({host: "target.internal", port: 443, identifier: "route_stream_01"}))), /exactly one.*gatewayId.*routeClass/i)
293
- assert.throws(() => set.recoverConnection(/** @type {Parameters<MultiplexedSocketductTransportSet['recoverConnection']>[0]} */ (/** @type {unknown} */ ({gatewayId: "starlink", routeClass: "zyxel", host: "target.internal", port: 443, identifier: "route_stream_01"}))), /exactly one.*gatewayId.*routeClass/i)
299
+ assert.throws(() => set.recoverConnection(/** @type {Parameters<MultiplexedSocketductTransportSet['recoverConnection']>[0]} */ (/** @type {unknown} */ ({host: "target.internal", port: 443, identifier: "route_stream_01"}))), (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_RECOVERY_NOT_FOUND")
300
+ assert.throws(() => set.recoverConnection(/** @type {Parameters<MultiplexedSocketductTransportSet['recoverConnection']>[0]} */ (/** @type {unknown} */ ({gatewayId: "starlink", routeClass: "zyxel", host: "target.internal", port: 443, identifier: "route_stream_01"}))), /at most one.*gatewayId.*routeClass/i)
294
301
  assert.throws(() => set.recoverConnection({routeClass: "missing", host: "target.internal", port: 443, identifier: "route_stream_01"}), /unknown routeClass/i)
295
302
  const recovered = set.recoverConnection({routeClass: "zyxel", host: "target.internal", port: 443, identifier: "route_stream_01", sessionName: "durable-route"})
296
303
  assert.equal(/** @type {{gatewayId?: string}} */ (recovered).gatewayId, undefined)
@@ -858,7 +865,7 @@ test("pending socket end reserves and sends DataView bytes", async () => {
858
865
 
859
866
  test("recovery requires one original configured selector and registration before start", async () => {
860
867
  const {set, members} = fixture()
861
- assert.throws(() => set.recoverConnection(/** @type {Parameters<MultiplexedSocketductTransportSet['recoverConnection']>[0]} */ (/** @type {unknown} */ ({host: "target.internal", port: 443, identifier: "stable_stream_01"}))), /gatewayId/i)
868
+ assert.throws(() => set.recoverConnection(/** @type {Parameters<MultiplexedSocketductTransportSet['recoverConnection']>[0]} */ (/** @type {unknown} */ ({host: "target.internal", port: 443, identifier: "stable_stream_01"}))), (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_RECOVERY_NOT_FOUND")
862
869
  assert.throws(() => set.recoverConnection({gatewayId: "unknown", host: "target.internal", port: 443, identifier: "stable_stream_01"}), /unknown gatewayId/i)
863
870
  assert.throws(() => set.createConnection(/** @type {import('../../../src/client/multiplexed-connection.js').VirtualConnectionOptions} */ (/** @type {unknown} */ ({gatewayId: "zyxel", host: "target.internal", port: 443}))), /recoverConnection/i)
864
871
  assert.throws(() => set.createConnection({recover: true, host: "target.internal", port: 443, identifier: "stable_stream_01"}), /recoverConnection/i)
@@ -895,6 +902,46 @@ test("recovery requires one original configured selector and registration before
895
902
  await set.close()
896
903
  })
897
904
 
905
+ test("identifier-only recovery finds exactly one durable member and restores stored options", async () => {
906
+ const unique = fixture()
907
+ at(unique.members, 1).recoveryDescriptors.set("stable_stream_05", {
908
+ identifier: "stable_stream_05",
909
+ target: {host: "target.internal", port: 443},
910
+ sessionName: "durable-request",
911
+ manualReceiveAck: true
912
+ })
913
+
914
+ const recovered = unique.set.recoverConnection({identifier: "stable_stream_05"})
915
+ assert.equal(gatewayOf(recovered), "zyxel")
916
+ assert.deepEqual(at(unique.members, 1).recoveryConnectionCalls, [{
917
+ identifier: "stable_stream_05",
918
+ target: {host: "target.internal", port: 443},
919
+ sessionName: "durable-request",
920
+ manualReceiveAck: true,
921
+ recover: true
922
+ }])
923
+ assert.throws(
924
+ () => unique.set.recoverConnection({identifier: "missing_stream_01"}),
925
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_RECOVERY_NOT_FOUND"
926
+ )
927
+ await unique.set.close()
928
+
929
+ const ambiguous = fixture()
930
+ const descriptor = {
931
+ identifier: "duplicate_stream_01",
932
+ target: {host: "target.internal", port: 443},
933
+ sessionName: "duplicate",
934
+ manualReceiveAck: false
935
+ }
936
+ at(ambiguous.members, 0).recoveryDescriptors.set(descriptor.identifier, descriptor)
937
+ at(ambiguous.members, 2).recoveryDescriptors.set(descriptor.identifier, descriptor)
938
+ assert.throws(
939
+ () => ambiguous.set.recoverConnection({identifier: descriptor.identifier}),
940
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_RECOVERY_AMBIGUOUS"
941
+ )
942
+ await ambiguous.set.close()
943
+ })
944
+
898
945
  test("returns the stable selected gatewayId on ordinary and recovered sockets", async () => {
899
946
  const {set, members} = fixture()
900
947
  const started = set.start()
@@ -8,7 +8,7 @@ import {VirtualSocket} from "../../../src/client/virtual-socket.js"
8
8
  *
9
9
  */
10
10
  function fakeConnection() {
11
- const calls = {destroyed: 0, ended: 0, resumed: 0, refs: 0, unrefs: 0}
11
+ const calls = {destroyed: 0, ended: 0, resumed: 0, refs: 0, unrefs: 0, delivered: 0, acknowledged: 0, durableWrites: /** @type {Buffer[]} */ ([])}
12
12
  return {
13
13
  calls,
14
14
  connection: /** @type {import('../../../src/client/multiplexed-connection.js').MultiplexedSocketductConnection} */ (/** @type {unknown} */ ({
@@ -25,6 +25,16 @@ function fakeConnection() {
25
25
  _endStream: (_id, callback) => { calls.ended += 1; callback() },
26
26
  _destroyStream: () => { calls.destroyed += 1 },
27
27
  _resumeReadable: () => { calls.resumed += 1 },
28
+ _deliverReadable: (/** @type {number} */ _id, /** @type {number} */ bytes) => { calls.delivered += bytes },
29
+ _acknowledgeReadable: (/** @type {number} */ _id, /** @type {number} */ bytes) => {
30
+ assert(bytes <= calls.delivered - calls.acknowledged)
31
+ calls.acknowledged += bytes
32
+ return Promise.resolve(calls.acknowledged)
33
+ },
34
+ _writeDurableStream: (/** @type {number} */ _id, /** @type {Buffer} */ data, /** @type {(error?: Error | null) => void} */ callback) => {
35
+ calls.durableWrites.push(Buffer.from(data))
36
+ callback()
37
+ },
28
38
  physicalAddress: () => ({address: "127.0.0.1", port: 43210}),
29
39
  ref: () => { calls.refs += 1 },
30
40
  unref: () => { calls.unrefs += 1 }
@@ -96,3 +106,115 @@ test("VirtualSocket pause/resume, timeout, data and half-close remain stream-loc
96
106
  assert(fake.calls.resumed > 0)
97
107
  socket.destroy()
98
108
  })
109
+
110
+ test("VirtualSocket exposes opt-in manual receive ACK and durable write contracts", async () => {
111
+ const fake = fakeConnection()
112
+ const socket = new VirtualSocket(fake.connection, 7, {
113
+ host: "127.0.0.1",
114
+ port: 80,
115
+ manualReceiveAck: true
116
+ })
117
+ socket._markConnected()
118
+
119
+ const acknowledged = new Promise((resolve, reject) => {
120
+ socket.once("data", (chunk) => {
121
+ socket.acknowledgeReceive(chunk.length).then(resolve, reject)
122
+ })
123
+ })
124
+ socket._receive(Buffer.from("response"))
125
+ assert.equal(await acknowledged, 8)
126
+ assert.equal(fake.calls.delivered, 8)
127
+ assert.equal(fake.calls.acknowledged, 8)
128
+
129
+ await socket.writeDurable(new Uint8Array(Buffer.from("request")))
130
+ assert.deepEqual(fake.calls.durableWrites, [Buffer.from("request")])
131
+ assert.equal(socket.bytesWritten, 7)
132
+ assert.throws(
133
+ () => socket.acknowledgeReceive(1),
134
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_INVALID_RECEIVE_ACK"
135
+ )
136
+ const closed = new Promise((resolve) => socket.once("close", resolve))
137
+ socket.destroy()
138
+ await closed
139
+ await assert.rejects(
140
+ socket.acknowledgeReceive(1),
141
+ (error) => /** @type {{code?: string}} */ (error).code === "ERR_STREAM_DESTROYED"
142
+ )
143
+ })
144
+
145
+ test("VirtualSocket orders durable writes behind queued ordinary writes", async () => {
146
+ const fake = fakeConnection()
147
+ /** @type {string[]} */
148
+ const delivered = []
149
+ /** @type {(() => void) | undefined} */
150
+ let releaseFirst
151
+ const connection = /** @type {{_writeStream: (id: number, data: Buffer, callback: (error?: Error | null) => void) => void, _writeDurableStream: (id: number, data: Buffer, callback: (error?: Error | null) => void) => void}} */ (/** @type {unknown} */ (fake.connection))
152
+ connection._writeStream = (_id, data, callback) => {
153
+ delivered.push(data.toString())
154
+ if (data.toString() === "A") releaseFirst = callback
155
+ else callback()
156
+ }
157
+ connection._writeDurableStream = (_id, data, callback) => {
158
+ delivered.push(data.toString())
159
+ callback()
160
+ }
161
+ const socket = new VirtualSocket(fake.connection, 9, {host: "127.0.0.1", port: 80})
162
+
163
+ const first = new Promise((resolve, reject) => socket.write("A", (error) => error ? reject(error) : resolve(undefined)))
164
+ const second = new Promise((resolve, reject) => socket.write("B", (error) => error ? reject(error) : resolve(undefined)))
165
+ const durable = socket.writeDurable("C")
166
+ assert(releaseFirst !== undefined)
167
+ releaseFirst()
168
+ await Promise.all([first, second, durable])
169
+
170
+ assert.deepEqual(delivered, ["A", "B", "C"])
171
+ assert.equal(socket.bytesWritten, 3)
172
+ socket.destroy()
173
+ })
174
+
175
+ test("VirtualSocket rejects durable writes after end while ordinary writes remain queued", async () => {
176
+ const fake = fakeConnection()
177
+ /** @type {string[]} */
178
+ const delivered = []
179
+ /** @type {(() => void) | undefined} */
180
+ let releaseFirst
181
+ const connection = /** @type {{_writeStream: (id: number, data: Buffer, callback: (error?: Error | null) => void) => void}} */ (/** @type {unknown} */ (fake.connection))
182
+ connection._writeStream = (_id, data, callback) => {
183
+ delivered.push(data.toString())
184
+ if (data.toString() === "A") releaseFirst = callback
185
+ else callback()
186
+ }
187
+ const socket = new VirtualSocket(fake.connection, 11, {host: "127.0.0.1", port: 80})
188
+
189
+ const first = new Promise((resolve, reject) => socket.write("A", (error) => error ? reject(error) : resolve(undefined)))
190
+ const second = new Promise((resolve, reject) => socket.write("B", (error) => error ? reject(error) : resolve(undefined)))
191
+ const finished = new Promise((resolve) => socket.once("finish", resolve))
192
+ socket.end()
193
+ const rejected = assert.rejects(
194
+ socket.writeDurable("late"),
195
+ (error) => /** @type {{code?: string}} */ (error).code === "ERR_STREAM_WRITE_AFTER_END"
196
+ )
197
+
198
+ assert(releaseFirst !== undefined)
199
+ releaseFirst()
200
+ await Promise.all([first, second, finished, rejected])
201
+ assert.deepEqual(delivered, ["A", "B"])
202
+ assert.deepEqual(fake.calls.durableWrites, [])
203
+ socket.destroy()
204
+ })
205
+
206
+ test("VirtualSocket keeps manual ACK opt-in and reports unsupported durable admission", async () => {
207
+ const fake = fakeConnection()
208
+ delete /** @type {{_writeDurableStream?: unknown}} */ (fake.connection)._writeDurableStream
209
+ const socket = new VirtualSocket(fake.connection, 9, {host: "127.0.0.1", port: 80})
210
+
211
+ assert.throws(
212
+ () => socket.acknowledgeReceive(1),
213
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_MANUAL_ACK_DISABLED"
214
+ )
215
+ await assert.rejects(
216
+ socket.writeDurable("request"),
217
+ (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_DURABLE_WRITE_UNSUPPORTED"
218
+ )
219
+ socket.destroy()
220
+ })
@@ -36,6 +36,8 @@ test("TensorBuzz exclusively owns verification and container build validation",
36
36
  const packageMetadata = JSON.parse(packageJson)
37
37
 
38
38
  assert.deepEqual(workflowNames, [])
39
+ assert.match(tensorbuzz, /^dependency_roots:\n {2}- path: \.$/m)
40
+ assert.doesNotMatch(tensorbuzz, /^\s*- npm ci$/m)
39
41
  assert.match(tensorbuzz, /DOCKER_HOST: "tcp:\/\/docker_server:2375"/)
40
42
  assert.match(tensorbuzz, /docker_server:\n\s+image: "docker:[^"]+-dind"/)
41
43
  assert.match(tensorbuzz, /privileged: true/)