socketduct 0.1.18 → 0.1.20

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 (55) hide show
  1. package/.env.example +17 -5
  2. package/README.md +5 -5
  3. package/changelog.d/20260925143000-reverse-client-process-recovery.md +1 -0
  4. package/changelog.d/20260925170000-disconnect-attribution-compressed-logs.md +1 -0
  5. package/docs/architecture.md +11 -9
  6. package/docs/docker.md +5 -4
  7. package/docs/logging.md +131 -159
  8. package/docs/protocol.md +43 -7
  9. package/docs/reverse-gateway.md +75 -9
  10. package/docs/running.md +16 -13
  11. package/docs/security.md +16 -5
  12. package/docs/session-lifecycle.md +14 -3
  13. package/docs/socket-api.md +54 -20
  14. package/package.json +5 -1
  15. package/scripts/verify-zstd-interoperability.mjs +41 -0
  16. package/src/cli/socketduct.js +143 -0
  17. package/src/client/multiplexed-transport-set.js +12 -5
  18. package/src/client/virtual-socket.js +52 -0
  19. package/src/config/environment.js +56 -7
  20. package/src/index.js +64 -26
  21. package/src/logging/compressed-log-reader.js +256 -0
  22. package/src/logging/compressed-log-worker.js +430 -0
  23. package/src/logging/compressed-log-writer.js +538 -0
  24. package/src/logging/log-batch.js +72 -0
  25. package/src/logging/log-retention.js +241 -0
  26. package/src/logging/log-segment-index.js +176 -0
  27. package/src/logging/structured-logger.js +136 -30
  28. package/src/logging/zstd-frame-codec.js +73 -0
  29. package/src/reverse/client.js +372 -82
  30. package/src/reverse/disconnect-attribution.js +191 -0
  31. package/src/reverse/gateway.js +68 -18
  32. package/src/reverse/hub.js +215 -32
  33. package/src/reverse/runtime.js +5 -3
  34. package/src/spool/spool-file.js +136 -7
  35. package/tensorbuzz.yml +4 -2
  36. package/test/e2e/logger-shutdown-process.test.js +86 -0
  37. package/test/e2e/reverse-client-recovery-process.test.js +170 -0
  38. package/test/e2e/reverse-gateway-process.test.js +15 -14
  39. package/test/fixtures/reverse-client-recovery.mjs +123 -0
  40. package/test/integration/logging-cli.test.js +168 -0
  41. package/test/integration/logging-compressed-recovery.test.js +186 -0
  42. package/test/integration/reverse-gateway.test.js +597 -1
  43. package/test/integration/reverse-route-class.test.js +6 -1
  44. package/test/unit/client/multiplexed-transport-set.test.js +12 -1
  45. package/test/unit/config/docker-gateway-files.test.js +2 -0
  46. package/test/unit/config/environment.test.js +54 -14
  47. package/test/unit/index.test.js +3 -0
  48. package/test/unit/logging/compressed-log-worker.test.js +29 -0
  49. package/test/unit/logging/compressed-log-writer.test.js +183 -0
  50. package/test/unit/logging/log-retention.test.js +86 -0
  51. package/test/unit/logging/log-segment-index.test.js +104 -0
  52. package/test/unit/logging/structured-logger.test.js +14 -1
  53. package/test/unit/logging/zstd-frame-codec.test.js +24 -0
  54. package/test/unit/reverse/disconnect-attribution.test.js +82 -0
  55. package/test/unit/spool/spool-file.test.js +133 -0
package/.env.example CHANGED
@@ -19,16 +19,23 @@ SOCKETDUCT_RESUME_WINDOW_MS=30000
19
19
  # Emit secret-free JSON runtime counters at this interval. Set 0 to disable.
20
20
  SOCKETDUCT_STATUS_INTERVAL_MS=30000
21
21
 
22
- # Optional bounded persistent JSONL logs. Bind mount this directory from host
23
- # storage in Docker. Files are isolated by role/PID; defaults are 10 MiB and 5
24
- # retained files including the active file. Existing stdout/stderr is preserved.
22
+ # Optional durable compressed JSONL logs. Bind mount this directory from host
23
+ # storage in Docker. Standard checksummed zstd frames and their private index
24
+ # are written by an isolated worker. Existing stdout/stderr is preserved.
25
25
  # Host directory mounted by the checked-in production Compose files. Create it
26
26
  # for uid/gid 1000 before deployment. Logging stays disabled unless the
27
27
  # container-side SOCKETDUCT_LOG_DIRECTORY is also set.
28
28
  # SOCKETDUCT_LOG_HOST_DIRECTORY=/srv/socketduct/logs
29
29
  # SOCKETDUCT_LOG_DIRECTORY=/var/log/socketduct
30
- # SOCKETDUCT_LOG_MAX_FILE_BYTES=10485760
31
- # SOCKETDUCT_LOG_MAX_FILES=5
30
+ # SOCKETDUCT_LOG_FRAME_MAX_BYTES=1048576
31
+ # SOCKETDUCT_LOG_FLUSH_INTERVAL_MS=1000
32
+ # SOCKETDUCT_LOG_CRITICAL_FLUSH_INTERVAL_MS=250
33
+ # SOCKETDUCT_LOG_SEGMENT_MAX_BYTES=134217728
34
+ # SOCKETDUCT_LOG_RETENTION_DAYS=30
35
+ # SOCKETDUCT_LOG_RETENTION_MAX_BYTES=4294967296
36
+ # SOCKETDUCT_LOG_QUEUE_MAX_BYTES=67108864
37
+ # SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES=8388608
38
+ # SOCKETDUCT_LOG_COMPRESSION_LEVEL=19
32
39
 
33
40
  # Per-session hard byte limit for target-to-client data buffered while the
34
41
  # edge tunnel is detached. When the limit is exceeded the relay terminates
@@ -113,6 +120,11 @@ SOCKETDUCT_TARGET_ALLOWLIST=docker-socket-shim:2375
113
120
  # Server3 hub: change SOCKETDUCT_MODE to reverse-hub, keep the TLS server pair
114
121
  # above, and bind every stable gateway ID to its credential username.
115
122
  # SOCKETDUCT_REVERSE_GATEWAY_BINDINGS=o2:home-o2,zyxel:home-zyxel,starlink:home-starlink
123
+ # Required when a reverse hub enables SOCKETDUCT_LOG_DIRECTORY. Generate 32-64
124
+ # random bytes, encode them as unpadded base64url, keep the key hub-only, and
125
+ # rotate it with the public URL-safe epoch label at least every 30 days.
126
+ # SOCKETDUCT_CONNECTION_FINGERPRINT_KEY=<unpadded-base64url-secret>
127
+ # SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH=2026-09
116
128
  # Authorize exact client username, gateway ID, and home-local target tuples.
117
129
  # SOCKETDUCT_REVERSE_ROUTES=tensorbuzz:o2:docker-server:2375,tensorbuzz:zyxel:docker-server:2375,tensorbuzz:starlink:docker-server:2375
118
130
  # Or bind one client physical connection to a route class and resolve each
package/README.md CHANGED
@@ -35,8 +35,8 @@ Normal TCP sockets cannot magically resume after the underlying internet path di
35
35
  sockets, and deterministic per-socket round-robin across ready members
36
36
  - native reverse-gateway v3 lets authenticated private home members register
37
37
  outbound with a Server3 hub, so CGNAT paths need no inbound forwarding
38
- - optional bounded JSONL lifecycle logs can be written to per-process files in
39
- a Docker bind-mounted directory without replacing existing stdout/stderr
38
+ - optional bounded diagnostic JSONL is stored as checksummed standard zstd
39
+ frames with an owned tail/search CLI, without replacing stdout/stderr
40
40
 
41
41
  ## MVP goals
42
42
 
@@ -91,8 +91,8 @@ and automatically rejoin selection. A socket created before readiness is
91
91
  returned immediately and waits inside Socketduct with normal callback, event,
92
92
  timeout and backpressure behavior; assignment stays FIFO and occurs only on a
93
93
  ready member. A virtual socket never changes members.
94
- Durable process-replacement recovery uses `recoverConnection()` with the
95
- original gateway ID before the replacement set starts.
94
+ Durable process-replacement recovery uses `recoverConnection()` with exactly
95
+ the original gateway ID or reverse route class before the replacement set starts.
96
96
 
97
97
  For home Docker hosts behind O2, Zyxel, and Starlink, run
98
98
  `OutboundReverseGateway` in each router-pinned namespace and
@@ -113,7 +113,7 @@ hub address because `gatewayId` selects the outbound member. Docker remains
113
113
  - [Docker topology](docs/docker-topology.md)
114
114
  - [Node-compatible socket API design](docs/socket-api.md)
115
115
  - [Native reverse gateway](docs/reverse-gateway.md)
116
- - [Persistent structured logging](docs/logging.md)
116
+ - [Durable compressed diagnostic logs](docs/logging.md)
117
117
  - [Initial implementation plan](docs/plans/initial-implementation.md)
118
118
  - [Socket + Docker integration plan](docs/plans/socket-docker-integration.md)
119
119
 
@@ -0,0 +1 @@
1
+ Reverse clients can now recover crash-consistent persistent streams in a replacement process with application-consumption ACKs and a quiescent detach handoff, without reopening the surviving gateway target, while unsupported hub, gateway, and target loss fails closed.
@@ -0,0 +1 @@
1
+ Socketduct now preserves reverse disconnect causes across terminal errors and writes bounded, recovery-safe indexed zstd diagnostics with incremental owned tail/search behavior.
@@ -85,12 +85,12 @@ gateway independently for each target `OPEN`, so adding target gateways does not
85
85
  multiply client physical connections. Allocation metadata is `routeClass`;
86
86
  legacy exact-gateway sets keep `gatewayId` and their existing identity derivation.
87
87
 
88
- Replacement recovery is registered before set startup and addresses the
89
- original configured gateway ID directly. The set derives the same member
90
- identity/spool namespace and delegates recovery to that member before any
91
- physical attempt; round-robin is never involved. An orderly set detach preserves
92
- active spools while retiring empty warmed lanes. Ordinary new opens cannot name
93
- a gateway.
88
+ Replacement recovery is registered before set startup and addresses exactly one
89
+ original configured selector: `gatewayId` or `routeClass`. The set derives the
90
+ same member identity/spool namespace and delegates recovery to that member
91
+ before any physical attempt; round-robin is never involved. An orderly set
92
+ detach preserves active spools while retiring empty warmed lanes. Ordinary new
93
+ opens cannot name a member.
94
94
 
95
95
  Protocol v2 retains the original one-session-per-tunnel architecture for
96
96
  rollback. A relay listener selects v2 or v3 explicitly.
@@ -112,9 +112,11 @@ keeps its original target socket and both endpoints durably retain
112
112
  unacknowledged send chunks. Replacement physical generations exchange paired
113
113
  resume manifests; the hub reconciles receiver-committed sequence positions
114
114
  before either side replays. This resumes the same byte stream without reopening
115
- the target or issuing a second application request. Reverse process replacement
116
- remains terminal because the original target/app socket objects cannot move to
117
- a new process.
115
+ the target or issuing a second application request. The Server3 reverse-client
116
+ object/process may be replaced from persistent spool state because the home
117
+ gateway still owns the target socket; the replacement creates a new app-facing
118
+ `VirtualSocket` and resumes from its durable receive ACK. The hub process,
119
+ gateway process, and target socket are not reconstructable by this mechanism.
118
120
 
119
121
  The hub serializes output independently for each physical destination with a
120
122
  bounded queue, so one backpressured target gateway cannot stop client frames
package/docs/docker.md CHANGED
@@ -162,14 +162,15 @@ sudo install -d -o 1000 -g 1000 -m 0700 /srv/socketduct/logs
162
162
  ```text
163
163
  SOCKETDUCT_LOG_HOST_DIRECTORY=/srv/socketduct/logs
164
164
  SOCKETDUCT_LOG_DIRECTORY=/var/log/socketduct
165
- SOCKETDUCT_LOG_MAX_FILE_BYTES=10485760
166
- SOCKETDUCT_LOG_MAX_FILES=5
165
+ SOCKETDUCT_LOG_SEGMENT_MAX_BYTES=134217728
166
+ SOCKETDUCT_LOG_RETENTION_DAYS=30
167
+ SOCKETDUCT_LOG_RETENTION_MAX_BYTES=4294967296
167
168
  ```
168
169
 
169
170
  The production relay and reverse-gateway Compose files mount the host directory
170
171
  at `/var/log/socketduct`. Existing container stdout/stderr remains enabled. See
171
- [Persistent structured logging](logging.md) for file names, rotation, embedded
172
- client configuration, and the redaction contract.
172
+ [durable compressed diagnostic logs](logging.md) for the indexed frame layout,
173
+ owned tail/search CLI, rotation, embedded configuration, and privacy contract.
173
174
 
174
175
  Expected startup log (formatted here for readability; the actual line is JSON):
175
176
 
package/docs/logging.md CHANGED
@@ -1,185 +1,157 @@
1
- # Persistent structured logging
1
+ # Durable compressed diagnostic logs
2
2
 
3
- Socketduct can write lifecycle diagnostics as newline-delimited JSON (JSONL) to
4
- a directory mounted from persistent host storage. File logging is opt-in.
5
- Existing startup/status output on stdout and errors on stderr remain unchanged
6
- when file logging is enabled.
3
+ Socketduct’s managed runtime can write sanitized lifecycle diagnostics as
4
+ newline-delimited JSON records inside standard Zstandard frames. Logging remains
5
+ opt-in: set `SOCKETDUCT_LOG_DIRECTORY` for a `relay`, `reverse-hub`,
6
+ `reverse-gateway`, or summary process. Existing stdout/stderr output is unchanged.
7
7
 
8
- ## Standalone runtime configuration
8
+ The managed sink never falls back to plaintext. The older
9
+ `createJsonlFileLogger()` API remains available only for embedded callers that
10
+ explicitly select its legacy plaintext files; managed runtimes use
11
+ `createCompressedJsonlFileLogger()`.
9
12
 
10
- Set `SOCKETDUCT_LOG_DIRECTORY` for a `relay`, `reverse-hub`, or
11
- `reverse-gateway` process:
13
+ ## Configuration
12
14
 
13
15
  ```sh
14
16
  SOCKETDUCT_LOG_DIRECTORY=/var/log/socketduct
15
- SOCKETDUCT_LOG_MAX_FILE_BYTES=10485760
16
- SOCKETDUCT_LOG_MAX_FILES=5
17
+ SOCKETDUCT_LOG_FRAME_MAX_BYTES=1048576
18
+ SOCKETDUCT_LOG_FLUSH_INTERVAL_MS=1000
19
+ SOCKETDUCT_LOG_CRITICAL_FLUSH_INTERVAL_MS=250
20
+ SOCKETDUCT_LOG_SEGMENT_MAX_BYTES=134217728
21
+ SOCKETDUCT_LOG_RETENTION_DAYS=30
22
+ SOCKETDUCT_LOG_RETENTION_MAX_BYTES=4294967296
23
+ SOCKETDUCT_LOG_QUEUE_MAX_BYTES=67108864
24
+ SOCKETDUCT_LOG_CRITICAL_RESERVE_BYTES=8388608
25
+ SOCKETDUCT_LOG_COMPRESSION_LEVEL=19
17
26
  ```
18
27
 
19
- `SOCKETDUCT_LOG_MAX_FILE_BYTES` defaults to 10 MiB and must be at least 1024.
20
- `SOCKETDUCT_LOG_MAX_FILES` defaults to 5, includes the active file, and must be
21
- between 1 and 100. The size/count settings are ignored unless a non-empty log
22
- directory enables the sink.
23
-
24
- Each process writes its own `<role>-<pid>.jsonl` file, with older files renamed
25
- to `.1`, `.2`, and so on. A writer never rotates another live process's active
26
- file, which avoids cross-process rename races. Each file in the current
27
- process/role family is bounded by the configured byte limit and the family
28
- retains at most the configured file count. A reused PID continues the same
29
- bounded filename family.
30
-
31
- On its first write, a logger also caps files left by definitely exited
32
- same-role process instances to the configured file count in total, retaining
33
- the newest files. Repeated signal-0 PID probes must report `ESRCH`, and the file
34
- must remain unchanged, before it can be deleted. A live/reused PID, permission
35
- failure, unsupported probe, changed file, or other indeterminate result
36
- preserves every file in that family. Files are created with mode `0600`; a newly
37
- created log directory uses mode `0700`.
38
-
39
- For Docker, bind mount a persistent host directory and point Socketduct at the
40
- container path:
41
-
42
- ```yaml
43
- services:
44
- socketduct:
45
- environment:
46
- SOCKETDUCT_LOG_DIRECTORY: /var/log/socketduct
47
- SOCKETDUCT_LOG_MAX_FILE_BYTES: "10485760"
48
- SOCKETDUCT_LOG_MAX_FILES: "5"
49
- volumes:
50
- - /srv/socketduct/logs:/var/log/socketduct
51
- ```
28
+ Those values are the defaults. Reaching the uncompressed frame limit starts a
29
+ flush immediately. Flush deadlines are measured from the oldest queued work and
30
+ are never extended by later records; critical lifecycle, security, and resume
31
+ events use the shorter deadline and may use the reserved queue capacity.
32
+ Segment size, age, and directory quota are bounded independently. Invalid or mutually
33
+ inconsistent values stop startup.
34
+ The former `SOCKETDUCT_LOG_MAX_FILE_BYTES` and `SOCKETDUCT_LOG_MAX_FILES`
35
+ plaintext settings are rejected instead of being silently applied to the new
36
+ format.
52
37
 
53
- Create `/srv/socketduct/logs` with mode `0700` and ownership that permits the
54
- container user to write it; for uid/gid 1000, use
55
- `sudo install -d -o 1000 -g 1000 -m 0700 /srv/socketduct/logs`.
56
- A permission, disk, or rotation error does not change tunnel,
57
- session, target, or reconnect behavior. Socketduct reports only
58
- `Socketduct logger failure (LOGGER_IO_ERROR)` to stderr, at most once per
59
- logger, without including the path or underlying error text.
60
-
61
- ## Interrupted writes and retained evidence
62
-
63
- Before appending, the file sink checks the last byte of a nonempty active file.
64
- This bounded check also runs for a fresh logger or restarted process reusing
65
- the same filename; it does not scan or parse historical records. A failed
66
- append may have written a prefix even when the filesystem reports an error.
67
-
68
- If the active file does not end in a newline, Socketduct moves the entire file
69
- byte-for-byte into the existing numbered retention family and writes subsequent
70
- events to a clean active file. The retained segment may still contain an
71
- incomplete final record: it is incident evidence, not repaired JSONL. Normal
72
- bounded retention eventually ages it out, so preserve incident evidence outside
73
- the managed family before rollout or forensic work. New complete records must
74
- parse independently; ignoring malformed historical lines is not proof of recovery.
75
-
76
- If tail inspection, rotation, clean-file creation, or mode verification fails,
77
- that logging call writes no new record. Later ordinary calls recheck the
78
- filesystem; there is no background retry loop or replay of missed diagnostics.
79
- If an append itself fails, it may leave another incomplete prefix that the next
80
- call must separate. Logging failures still do not interrupt tunnel traffic.
81
-
82
- With `maxFiles: 1`, there is no retained slot: an incomplete active file is left
83
- untouched and file logging pauses. An incomplete active file already larger
84
- than a newly lowered size limit also pauses logging **before startup pruning**;
85
- it is not deleted to satisfy that limit. These pre-existing bytes can remain
86
- outside the newly configured bound, but Socketduct will not grow them. An
87
- operator must preserve the evidence separately and provide a clean path before
88
- logging can continue. Socketduct never truncates or rewrites damaged records.
89
- Already newline-terminated malformed historical records are not repaired.
90
-
91
- The sink requires a regular active file with mode `0600` and a directory with
92
- mode `0700`, including on reused paths. It checks the newly created file's
93
- actual mode before writing and does not follow an active-file symlink. Fix
94
- ownership/modes on an operator-preserved path rather than making logs public.
95
-
96
- See [the private rollout runbook](runbooks/jsonl-integrity-rollout.md) for the
97
- isolated fault commands and evidence-preserving Apoc-only rollout gates.
98
-
99
- The checked-in production relay and reverse-gateway Compose files already mount
100
- `${SOCKETDUCT_LOG_HOST_DIRECTORY:-./logs}` at `/var/log/socketduct`. Enable the
101
- sink in `.env` after creating the host directory for the container's uid/gid
102
- 1000:
38
+ A reverse hub that enables managed logging must also configure a hub-only
39
+ correlation key:
103
40
 
104
41
  ```sh
105
- SOCKETDUCT_LOG_HOST_DIRECTORY=/srv/socketduct/logs
106
- SOCKETDUCT_LOG_DIRECTORY=/var/log/socketduct
42
+ SOCKETDUCT_CONNECTION_FINGERPRINT_KEY='<unpadded-base64url-at-least-32-bytes>'
43
+ SOCKETDUCT_CONNECTION_FINGERPRINT_KEY_EPOCH='2026-09'
107
44
  ```
108
45
 
109
- `SOCKETDUCT_STATUS_INTERVAL_MS` controls both console and file-backed periodic
110
- status events. Its default remains 30000 milliseconds; `0` disables periodic
111
- status only, not lifecycle logging.
46
+ Generate the key from random bytes, keep it out of gateways and clients, rotate
47
+ it at least every 30 days and on suspected exposure, and remove the previous
48
+ key after at most 24 hours of rollout overlap. The epoch is a public 1–32
49
+ character URL-safe label. Socketduct logs only the role-separated first 128 bits
50
+ of an HMAC-SHA-256 fingerprint; it never emits the key or raw connection ID.
112
51
 
113
- ## Embedded client configuration
52
+ ## Storage and durability
114
53
 
115
- Create one file logger per process/role and pass it to the client, agent, or
116
- transport set through its `logger` option:
54
+ The directory must be a real `0700` directory. Socketduct creates exclusive
55
+ `0600` files without following symlinks:
117
56
 
118
- ```js
119
- import {createJsonlFileLogger} from "socketduct"
120
- import {MultiplexedSocketductTransportSet} from "socketduct/multiplexed"
57
+ ```text
58
+ <role>-<writerEpoch>-<segmentUuid>.jsonl.zst
59
+ <role>-<writerEpoch>-<segmentUuid>.jsonl.zst.idx
60
+ <role>-<writerEpoch>-<segmentUuid>.jsonl.zst.sealed
61
+ ```
121
62
 
122
- const logger = createJsonlFileLogger({
123
- directory: "/var/log/socketduct",
124
- role: "tensorbuzz-client",
125
- maxFileBytes: 10 * 1024 * 1024,
126
- maxFiles: 5
127
- })
63
+ Every `.jsonl.zst` segment is a concatenation of independently decodable,
64
+ checksummed, standard zstd frames at level 19. Each frame contains complete
65
+ newline-terminated records only. Compression and filesystem sync run in a
66
+ dedicated worker thread, off the protocol event loop.
67
+
68
+ The versioned big-endian sidecar index contains structural identities, sequence
69
+ ranges, offsets, lengths, timestamps, frame SHA-256, and CRC32C checks. It does
70
+ not contain gateways, routes, incidents, reasons, connection IDs, or search
71
+ digests. A batch becomes durable only after Socketduct writes and `fdatasync`s
72
+ the data frame, appends and `fdatasync`s its index entry, then acknowledges the
73
+ exact deterministic batch identity to the producer.
74
+
75
+ On worker restart, Socketduct validates every retained segment, accepts a
76
+ retention-deleted sequence prefix, and still rejects gaps or duplicates inside
77
+ the retained range. It rebuilds one fully decodable data-synced orphan entry,
78
+ truncates only a torn final data or index tail, and replays only a batch not
79
+ proven durable by the high-water mark plus the single latest batch identity;
80
+ worker memory and acknowledgement size do not grow with history. Interior
81
+ corruption is preserved and marked quarantined; readers report incomplete
82
+ evidence. Retention deletes only oldest sealed, non-quarantined segments under
83
+ an exclusive process/thread-instance lock. A crashed owner is reclaimed using
84
+ PID/TID plus kernel start identities, while a live or unprovable owner remains fail-closed.
85
+ Active and quarantined bytes count toward the quota and are never deleted to
86
+ hide a failure.
87
+
88
+ `log()` is nonblocking and reports `accepted`, `coalesced`, or `dropped`.
89
+ `flush()` is a caller-time sequence barrier: it resolves after every record
90
+ accepted through that call is data-and-index durable, without waiting for later
91
+ producers. Idempotent async `close()` stops acceptance, drains all records and
92
+ bounded sink lifecycle records, seals, syncs, and terminates the worker. `status()` exposes writer and
93
+ durable high-water marks, allocated bytes, health, restart count, and exact drop
94
+ counters. Orderly SIGINT/SIGTERM closes the network runtime first, records the
95
+ final process lifecycle event, then awaits logger flush and close before exit.
96
+ The startup record includes the effective numeric logger settings but never the
97
+ configured directory or key material.
98
+
99
+ ## Owned reader and CLI
100
+
101
+ Do not use `tail -f` on compressed bytes. Socketduct reads each indexed frame
102
+ independently and verifies the full integrity chain:
128
103
 
129
- const transports = new MultiplexedSocketductTransportSet({
130
- connectionId: "runner_01_stable_identity",
131
- spoolDirectory: "/tmp/socketduct-v3-set",
132
- gateways,
133
- logger
134
- })
104
+ ```sh
105
+ socketduct logs tail --directory /var/log/socketduct
106
+ socketduct logs tail --directory /var/log/socketduct --follow
107
+ socketduct logs search --directory /var/log/socketduct --incident '<22-char-id>'
108
+ socketduct logs search --directory /var/log/socketduct --event reverse_hub.physical_detached
135
109
  ```
136
110
 
137
- The same `logger` option is supported by `MultiplexedSocketductConnection`,
138
- `ReverseGatewayConnection`, `OutboundReverseGateway`, `connectSocketduct`,
139
- `createSocketductConnectionFactory`, `SocketductHttpAgent`, and
140
- `SocketductHttpsAgent`. A transport set owns logger propagation to its members;
141
- do not put `logger` inside `memberOptions`.
142
-
143
- For an existing logging system, use the exported line-writer adapter:
111
+ Search filters include `--event`, `--incident`, `--gateway`, `--route-class`,
112
+ `--client-generation`, `--gateway-generation`, `--reason`, `--since`, and
113
+ `--until`. Records from all writers are merged deterministically by timestamp,
114
+ raw writer epoch, and numeric event sequence; that ordering does not imply
115
+ cross-process causality. Search exits `0` for one or more matches, `1` for no
116
+ matches in complete evidence, and `2` for invalid arguments, I/O failure, or
117
+ any selected corrupt/quarantined/incomplete segment. Tail returns `2` on the
118
+ same evidence failures. Closed segments also remain compatible with ordinary
119
+ `zstdcat` and `zstdgrep`. Follow mode advances sidecar-index cursors and decodes
120
+ only newly committed frames. Cursor state is limited to currently retained
121
+ segments and is pruned as retention removes closed segments, so rotation does
122
+ not cause duplicate output or repeated historical decompression.
123
+
124
+ ## Privacy boundary
125
+
126
+ Records retain the existing 1,024-byte closed event/field allowlist. Disconnect
127
+ events may include a random incident ID, first local cause, origin and physical
128
+ role, explicitly named client/gateway generations, fan-out/resume/terminal
129
+ counts, safe gateway or route identity, safe socket code, and the hub-only keyed
130
+ fingerprint. Public terminal codes classify the outcome; they do not overwrite
131
+ the physical cause.
132
+
133
+ Socketduct never persists payload bytes, Docker requests or targets, tokens,
134
+ passwords, certificates or keys, raw connection IDs, HMAC keys, unkeyed identity
135
+ digests, peer-controlled reserved fields, spool paths/data, or arbitrary caller
136
+ objects. Logger health reports use fixed secret-free classifications.
137
+
138
+ ## Embedded use
144
139
 
145
140
  ```js
146
- import {createStructuredLogger} from "socketduct"
141
+ import {createCompressedJsonlFileLogger} from "socketduct"
147
142
 
148
- const logger = createStructuredLogger({
149
- role: "tensorbuzz-client",
150
- writeLine(line) {
151
- destination.append(line)
152
- }
143
+ const logger = createCompressedJsonlFileLogger({
144
+ directory: "/var/log/socketduct",
145
+ role: "tensorbuzz-client"
153
146
  })
147
+
148
+ transportOptions.logger = logger
149
+ // At shutdown, after stopping producers:
150
+ await logger.flush()
151
+ await logger.close()
154
152
  ```
155
153
 
156
- Alternatively, pass an object with `log(event, fields)` directly. Socketduct
157
- calls it only with a known event and already-sanitized fields. A direct custom
158
- logger is responsible for adding its own timestamp/process/role envelope and
159
- for preserving one-object-per-line framing. Synchronous throws and rejected
160
- promises from custom loggers/writers are isolated from connection behavior.
161
-
162
- ## Event and data contract
163
-
164
- The built-in logger emits one JSON object per line, capped at 1024 bytes. Every
165
- entry contains an ISO-8601 `timestamp`, an allowlisted `event`, numeric
166
- `processId`, and validated `role`. Depending on the component, events cover:
167
-
168
- - process startup and periodic status counters
169
- - physical transport connect, disconnect, retry scheduling, recovery, and
170
- terminal failure
171
- - transport-set readiness, member state/generation replacement, and terminal
172
- loss
173
- - reverse-gateway target open success, open failure, and close
174
- - virtual-stream opening, opened, closed, and error transitions
175
-
176
- Only finite allowlisted booleans, non-negative integer counters, validated
177
- gateway IDs, states, and error classifications can be included. Unknown events
178
- are dropped. Unknown error codes become `UNKNOWN`. Errors supplied through
179
- `errorDiagnostics()` retain their name, message, and stack so terminal
180
- transport and virtual-stream failures are actionable.
181
-
182
- The diagnostic path never serializes protocol payloads or arbitrary caller
183
- fields. It serializes error messages and stack traces produced by
184
- `errorDiagnostics()` after redacting bearer credentials, URL userinfo, and
185
- password/token/authorization values.
154
+ The `logger` option remains supported by Socketduct client, agent, transport-set,
155
+ and reverse-gateway APIs. A direct custom logger remains responsible for its own
156
+ durability. The legacy plaintext factory does not gain compressed recovery or
157
+ CLI guarantees and does not rewrite existing `.jsonl` files.
package/docs/protocol.md CHANGED
@@ -141,9 +141,22 @@ Reverse send replay is persisted before transmission on both the Server3 client
141
141
  and outbound gateway. ACKs truncate those private temporary spools. Missing
142
142
  targets, expired retention, identity or manifest mismatch, and configured
143
143
  buffer/spool/output bounds produce one terminal stream error with the causal
144
- code. Process-replacement recovery remains unsupported for reverse streams
145
- because a replacement gateway process cannot inherit the original target TCP
146
- socket.
144
+ code. A replacement reverse client may restore its generation, selector,
145
+ lifecycle, directional sequences/ACKs, END state, and contiguous send replay
146
+ from persistent spool state before making a physical attempt. It presents the
147
+ durable receive ACK, not the dead object's readable-buffer high-water mark, so
148
+ the surviving gateway conservatively replays any target bytes that were not
149
+ durably acknowledged. A reverse client advances that ACK only after bytes are
150
+ returned to the application; merely buffering them below the readable
151
+ high-water mark is process-local and does not make them safe across replacement.
152
+ It never emits `OPEN`.
153
+
154
+ Only the reverse client is replaceable in this protocol boundary. The same hub
155
+ process and retained bridge, outbound gateway process, and original target TCP
156
+ socket must survive. Hub restart reconstruction, gateway process/socket-keeper
157
+ or FD transfer, target reconnection, and replay after target loss are not
158
+ defined. A fresh gateway that encounters retained bridges submits an empty
159
+ manifest; normal omitted-stream resets retire them without target creation.
147
160
 
148
161
  Hub output has one ordered owner per physical destination. Frames for a
149
162
  backpressured gateway remain in that gateway's bounded queue while frames for
@@ -168,6 +181,28 @@ payloads terminate the physical connection. TLS certificate verification is
168
181
  mandatory for every reverse client and gateway transport. The complete role
169
182
  contract is in [Native reverse gateway](reverse-gateway.md).
170
183
 
184
+ ### Reverse disconnect attribution
185
+
186
+ Authenticated reverse GOAWAY and terminal RESET controls may carry the hub’s
187
+ closed attribution object: a 22-character random `disconnectIncidentId`, fixed
188
+ `disconnectReason`, `incidentOrigin`, `physicalRole`, applicable
189
+ `clientConnectionGeneration` / `gatewayConnectionGeneration`, and nonnegative
190
+ `fanOutCount`, `resumeSuccessCount`, and `terminalStreamCount`. A valid hub
191
+ incident takes precedence over a locally observed close. Endpoints preserve it
192
+ on public typed errors. The `code` remains a terminal classification and is not
193
+ interpreted as the transport cause.
194
+
195
+ The hub creates attribution before every retained or terminal fan-out. This
196
+ includes protocol/output-limit termination, replacement, peer GOAWAY, orderly
197
+ hub shutdown, direct transport loss, and resume expiry. A syntactically safe
198
+ peer GOAWAY `code` may classify resulting terminal RESETs, but peer attribution
199
+ fields are never trusted and cannot replace the hub-observed physical cause.
200
+
201
+ These names are hub-reserved. Attribution, fingerprint, and epoch fields in a
202
+ gateway-forwarded RESET are stripped before forwarding; malformed or unknown
203
+ values never become trusted telemetry. No control carries raw connection IDs,
204
+ HMAC keys, targets, credentials, certificates, or payload data.
205
+
171
206
  ## Legacy protocol version 2
172
207
 
173
208
  V2 starts every connection with the length-prefixed JSON envelope
@@ -503,10 +538,11 @@ it, because every frame declares its own encoding.
503
538
 
504
539
  ### Runtime support
505
540
 
506
- zstd is provided by `node:zlib`, which only exposes it from Node 22.15 onward,
507
- while `engines` accepts Node >=22.0. Support is therefore feature-detected at
508
- runtime: a peer whose Node lacks zstd advertises no codec and the tunnel runs
509
- raw. No runtime dependency is added.
541
+ zstd is provided by Node 24's `node:zlib`; the package requires Node 24 or
542
+ newer. Stream-data support is still negotiated, so an explicitly disabled
543
+ codec runs raw. Durable diagnostic storage separately requires checksummed
544
+ standard zstd frames and never falls back to plaintext. No runtime dependency
545
+ is added.
510
546
 
511
547
  ## Acknowledgements
512
548