@moqtap/trace 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 moqtap
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @moqtap/trace
2
+
3
+ Record, serialize, and analyze MoQT (Media over QUIC Transport) sessions using the `.moqtrace` binary format.
4
+
5
+ - Binary `.moqtrace` format with CBOR encoding (compact, streamable, cross-language)
6
+ - Configurable detail levels from control-only to full wire captures
7
+ - Session recorder that wraps `@moqtap/codec` session state machines
8
+ - Human-readable JSON export for debugging
9
+ - Zero-copy streaming writer for large traces
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @moqtap/trace @moqtap/codec
15
+ ```
16
+
17
+ `@moqtap/codec` is a peer dependency — install it alongside `@moqtap/trace`.
18
+
19
+ ## Quick Start
20
+
21
+ ### Recording a session
22
+
23
+ ```typescript
24
+ import { createRecorder, writeMoqtrace } from '@moqtap/trace';
25
+ import { createDraft14SessionState } from '@moqtap/codec/draft14/session';
26
+
27
+ const recorder = createRecorder({
28
+ protocol: 'moq-transport-14',
29
+ perspective: 'client',
30
+ detail: 'control',
31
+ });
32
+
33
+ // Wrap a session to auto-capture control messages and state transitions
34
+ const session = createDraft14SessionState({ codec: { draft: 'draft-ietf-moq-transport-14' }, role: 'client' });
35
+ const traced = recorder.wrapSession(session);
36
+
37
+ // Use `traced` instead of `session` — all send/receive calls are recorded
38
+ traced.send(clientSetupMessage);
39
+ traced.receive(serverSetupMessage);
40
+
41
+ // Manually record events the session layer doesn't see
42
+ recorder.recordStreamOpened(4n, 0, 0); // outgoing subgroup stream
43
+ recorder.annotate('connected', { relay: 'cdn.example.com' });
44
+
45
+ // Finalize and serialize
46
+ const trace = recorder.finalize();
47
+ const bytes = writeMoqtrace(trace); // → Uint8Array (.moqtrace binary)
48
+ ```
49
+
50
+ ### Reading a trace file
51
+
52
+ ```typescript
53
+ import { readMoqtrace, readMoqtraceHeader } from '@moqtap/trace';
54
+
55
+ // Quick metadata peek (no event parsing)
56
+ const header = readMoqtraceHeader(bytes);
57
+ console.log(header.protocol); // "moq-transport-14"
58
+ console.log(header.perspective); // "client"
59
+ console.log(header.detail); // "control"
60
+
61
+ // Full parse
62
+ const trace = readMoqtrace(bytes);
63
+ for (const event of trace.events) {
64
+ console.log(event.type, event.timestamp);
65
+ }
66
+ ```
67
+
68
+ ### Streaming writer (for large traces)
69
+
70
+ ```typescript
71
+ import { createMoqtraceWriter } from '@moqtap/trace';
72
+
73
+ const writer = createMoqtraceWriter(header);
74
+ outputStream.write(writer.preamble());
75
+
76
+ for (const event of events) {
77
+ outputStream.write(writer.writeEvent(event));
78
+ }
79
+ ```
80
+
81
+ ## Detail Levels
82
+
83
+ | Level | What's recorded |
84
+ |---|---|
85
+ | `control` | Control messages only (setup, subscribe, publish, goaway) |
86
+ | `headers` | + data stream headers (subgroup/fetch, object metadata) |
87
+ | `headers+sizes` | + payload byte lengths |
88
+ | `headers+data` | + full payload bytes |
89
+ | `full` | + raw wire bytes (pre-decode) |
90
+
91
+ ## JSON Export
92
+
93
+ For human-readable debugging (not lossless — bigint/Uint8Array become strings):
94
+
95
+ ```typescript
96
+ import { traceToJSON } from '@moqtap/trace';
97
+
98
+ const json = traceToJSON(trace);
99
+ console.log(json);
100
+ ```
101
+
102
+ ## File Format
103
+
104
+ See [SPEC.md](./SPEC.md) for the complete `.moqtrace` binary format specification.
105
+
106
+ ## License
107
+
108
+ MIT
package/SPEC.md ADDED
@@ -0,0 +1,228 @@
1
+ # `.moqtrace` File Format Specification
2
+
3
+ > **Version:** 1
4
+ > **Status:** Draft
5
+
6
+ ## Overview
7
+
8
+ `.moqtrace` is a binary file format for recording MoQT (Media over QUIC Transport) sessions. It captures protocol-level events at configurable detail levels, from lightweight control-message-only traces to full wire-level recordings with payload bytes.
9
+
10
+ The format is designed to be:
11
+
12
+ - **Streamable** — events can be appended as they occur; no backpatching required
13
+ - **Compact** — CBOR encoding handles binary data natively (no base64 overhead)
14
+ - **Self-describing** — the header declares the detail level, protocol version, and recording context so readers know what to expect without scanning events
15
+ - **Cross-language** — readable by any language with a CBOR library (JS, Rust, Go, Python)
16
+
17
+ ## File Layout
18
+
19
+ ```
20
+ Offset Length Content
21
+ ------ ------ -------
22
+ 0 8 Magic bytes: "MOQTRACE" (0x4d4f515452414345)
23
+ 8 4 Format version (uint32 LE) — currently 1
24
+ 12 4 Header length (uint32 LE) — byte length of the CBOR header blob
25
+ 16 N Header (CBOR map) — session metadata and recording configuration
26
+ 16+N ... Event stream (CBOR sequence) — concatenated CBOR items, one per event
27
+ ```
28
+
29
+ ### Magic Bytes
30
+
31
+ The 8-byte ASCII string `MOQTRACE` identifies the file format. Any tool can check these bytes before attempting to parse.
32
+
33
+ ### Format Version
34
+
35
+ uint32 little-endian. Currently `1`. Readers MUST reject files with a version they don't support.
36
+
37
+ ### Header Length
38
+
39
+ uint32 little-endian. The byte length of the CBOR-encoded header that immediately follows. This allows readers to extract metadata without scanning the event stream — useful for file browsers, search indexing, and quick filtering ("show me all draft-14 client traces").
40
+
41
+ ## Header
42
+
43
+ A single CBOR map with the following keys:
44
+
45
+ | Key | CBOR Type | Required | Description |
46
+ | --------------- | ----------- | -------- | --------------------------------------------------------------------------------------------- |
47
+ | `"protocol"` | text string | Yes | MoQT version identifier (e.g., `"draft-14"`, `"rfc9999"`) |
48
+ | `"perspective"` | text string | Yes | Recording viewpoint: `"client"`, `"server"`, or `"observer"` |
49
+ | `"detail"` | text string | Yes | Detail level (see below) |
50
+ | `"startTime"` | integer | Yes | Recording start time (Unix epoch milliseconds) |
51
+ | `"transport"` | text string | No | Transport type (e.g., `"webtransport"`, `"raw-quic"`) |
52
+ | `"endTime"` | integer | No | Recording end time (Unix epoch milliseconds). Written when trace is finalized. |
53
+ | `"source"` | text string | No | Software that produced the trace (e.g., `"moqtap-devtools/0.1.0"`, `"my-relay/2.3.1"`) |
54
+ | `"endpoint"` | text string | No | Remote peer URI (e.g., `"https://relay.example.com/moq"`) |
55
+ | `"sessionId"` | text string | No | Opaque identifier for correlating traces from the same session across multiple capture points |
56
+ | `"custom"` | map | No | User-defined metadata (arbitrary key-value pairs) |
57
+
58
+ ### Protocol Identifier
59
+
60
+ The `"protocol"` field identifies the MoQT version without assuming lifecycle stage. Use the IETF document name minus the `draft-ietf-` prefix for drafts, and the RFC number for published standards:
61
+
62
+ - Draft phase: `"moq-transport-07"`, `"moq-transport-14"`
63
+ - RFC phase: `"moq-transport-rfc9999"`
64
+
65
+ ### Perspective
66
+
67
+ The `"perspective"` field describes the vantage point of the recording:
68
+
69
+ - `"client"` — captured at the MoQT client (initiator of the QUIC connection)
70
+ - `"server"` — captured at the MoQT server or relay
71
+ - `"observer"` — passive capture (e.g., DevTools extension, network tap) that intercepts traffic without participating in the session
72
+
73
+ ### Detail Levels
74
+
75
+ The `"detail"` field declares what was recorded. Each level is a strict superset of the one above it. Readers can handle any level — they just find more or fewer fields populated per event.
76
+
77
+ | Detail Level | What's Recorded | Typical Use Case |
78
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
79
+ | `"control"` | Control messages only (setup, subscribe, publish, goaway, etc.). No data stream events, no object payloads. | Lightweight protocol flow analysis. DevTools default. |
80
+ | `"headers"` | Control messages + data stream headers (subgroup/fetch/datagram headers, object metadata: group, object ID, priority, status). No payload bytes. | Delivery pattern analysis, timing. |
81
+ | `"headers+sizes"` | Everything in `"headers"` + payload byte lengths for each object. | Bandwidth analysis without storing media. |
82
+ | `"headers+data"` | Everything in `"headers"` + full payload bytes for each object. | Full session replay, debugging media corruption. |
83
+ | `"full"` | Everything above + raw wire bytes for every message (pre-decode). | Wire-level debugging, compliance testing. |
84
+
85
+ At any detail level, the recorder MAY be configured to **mask payloads** — replacing payload bytes with zeroes before writing. This preserves payload sizes for bandwidth analysis while stripping media content. The header SHOULD include `"payloadMasked": true` in the `"custom"` map when this is active.
86
+
87
+ ## Event Stream
88
+
89
+ After the header, the remainder of the file is a sequence of concatenated CBOR items (one per event). This is a standard CBOR sequence (RFC 8742) — no array wrapper, no separators.
90
+
91
+ ### Common Event Fields
92
+
93
+ Every event is a CBOR map with at minimum:
94
+
95
+ | Key | CBOR Type | Description |
96
+ | ----- | ---------------- | --------------------------------------------------------------------------------------------------- |
97
+ | `"n"` | unsigned integer | Monotonically increasing sequence number (0-based). Disambiguates events with identical timestamps. |
98
+ | `"t"` | integer | Timestamp in microseconds since `startTime` in the header |
99
+ | `"e"` | integer | Event type (see table below) |
100
+
101
+ Short key names are used intentionally — traces can contain hundreds of thousands of events, and CBOR encodes short strings more compactly.
102
+
103
+ ### Event Types
104
+
105
+ | `"e"` Value | Name | Description |
106
+ | ----------- | --------------- | -------------------------------------------------------- |
107
+ | `0` | Control message | A control-stream message was sent or received |
108
+ | `1` | Stream opened | A unidirectional or bidirectional QUIC stream was opened |
109
+ | `2` | Stream closed | A QUIC stream was closed |
110
+ | `3` | Object header | An object header was parsed from a data stream |
111
+ | `4` | Object payload | Object payload bytes were received/sent |
112
+ | `5` | State change | Session FSM phase transition |
113
+ | `6` | Error | Protocol error or transport error |
114
+ | `7` | Annotation | User-defined event (custom label + data) |
115
+
116
+ ### Event-Specific Fields
117
+
118
+ #### Event 0: Control Message
119
+
120
+ | Key | Type | Detail Level | Description |
121
+ | ------- | ----------- | ------------ | ------------------------------------------------- |
122
+ | `"d"` | integer | `control`+ | Direction: `0` = sent (tx), `1` = received (rx) |
123
+ | `"mt"` | integer | `control`+ | Wire message type ID (e.g., `0x03` for SUBSCRIBE) |
124
+ | `"msg"` | map | `control`+ | Decoded message fields (draft-specific structure) |
125
+ | `"raw"` | byte string | `full` only | Raw wire bytes (including type and length prefix) |
126
+
127
+ #### Event 1: Stream Opened
128
+
129
+ | Key | Type | Detail Level | Description |
130
+ | ------- | ------- | ------------ | -------------------------------------------------------- |
131
+ | `"sid"` | integer | `headers`+ | QUIC stream ID |
132
+ | `"d"` | integer | `headers`+ | Direction: `0` = outgoing, `1` = incoming |
133
+ | `"st"` | integer | `headers`+ | Stream type: `0` = subgroup, `1` = datagram, `2` = fetch |
134
+
135
+ #### Event 2: Stream Closed
136
+
137
+ | Key | Type | Detail Level | Description |
138
+ | ------- | ------- | ------------ | ---------------------------- |
139
+ | `"sid"` | integer | `headers`+ | QUIC stream ID |
140
+ | `"ec"` | integer | `headers`+ | Error code (0 = clean close) |
141
+
142
+ #### Event 3: Object Header
143
+
144
+ Object header events and object payload events (event 4) for the same object share `(sid, g, o)` as a composite key. An object header event is always emitted before its corresponding payload event.
145
+
146
+ | Key | Type | Detail Level | Description |
147
+ | ------- | ------- | ------------ | -------------------------------------------------------------------------- |
148
+ | `"sid"` | integer | `headers`+ | Stream ID this object arrived on |
149
+ | `"g"` | integer | `headers`+ | Group ID |
150
+ | `"o"` | integer | `headers`+ | Object ID |
151
+ | `"pp"` | integer | `headers`+ | Publisher priority |
152
+ | `"os"` | integer | `headers`+ | Object status (0=normal, 1=end-of-group, 2=end-of-track, 3=does-not-exist) |
153
+
154
+ #### Event 4: Object Payload
155
+
156
+ | Key | Type | Detail Level | Description |
157
+ | ------- | ----------- | ---------------- | ----------------------------------- |
158
+ | `"sid"` | integer | `headers+sizes`+ | Stream ID |
159
+ | `"g"` | integer | `headers+sizes`+ | Group ID |
160
+ | `"o"` | integer | `headers+sizes`+ | Object ID |
161
+ | `"sz"` | integer | `headers+sizes`+ | Payload size in bytes |
162
+ | `"pl"` | byte string | `headers+data`+ | Payload bytes (or zeroed if masked) |
163
+
164
+ #### Event 5: State Change
165
+
166
+ | Key | Type | Detail Level | Description |
167
+ | -------- | ----------- | ------------ | ---------------------- |
168
+ | `"from"` | text string | `control`+ | Previous session phase |
169
+ | `"to"` | text string | `control`+ | New session phase |
170
+
171
+ #### Event 6: Error
172
+
173
+ | Key | Type | Detail Level | Description |
174
+ | ---------- | ----------- | ------------ | --------------------- |
175
+ | `"ec"` | integer | `control`+ | Error code |
176
+ | `"reason"` | text string | `control`+ | Human-readable reason |
177
+
178
+ #### Event 7: Annotation
179
+
180
+ | Key | Type | Detail Level | Description |
181
+ | --------- | ----------- | ------------ | --------------------------------- |
182
+ | `"label"` | text string | any | User-defined label |
183
+ | `"data"` | any | any | User-defined data (any CBOR type) |
184
+
185
+ ## Reading a `.moqtrace` File
186
+
187
+ ```
188
+ 1. Read 8 bytes, verify magic == "MOQTRACE"
189
+ 2. Read 4 bytes (uint32 LE) → format version. Reject if unsupported.
190
+ 3. Read 4 bytes (uint32 LE) → header length N.
191
+ 4. Read N bytes → CBOR-decode to get header map.
192
+ 5. Read remaining bytes as a CBOR sequence:
193
+ - Repeatedly CBOR-decode one item at a time until EOF.
194
+ - Each item is one event (a CBOR map).
195
+ ```
196
+
197
+ ## Writing a `.moqtrace` File
198
+
199
+ ```
200
+ 1. Write magic bytes "MOQTRACE"
201
+ 2. Write format version (uint32 LE, currently 1)
202
+ 3. CBOR-encode the header map → get header bytes
203
+ 4. Write header length (uint32 LE)
204
+ 5. Write header bytes
205
+ 6. For each event:
206
+ - CBOR-encode the event map
207
+ - Append to file
208
+ 7. When finalizing:
209
+ - Optionally seek back and update header (e.g., set endTime)
210
+ - Or accept that endTime may be absent in crash-truncated files
211
+ ```
212
+
213
+ ## Versioning and Compatibility
214
+
215
+ - **Version 1** is defined by this document.
216
+ - Readers MUST check the format version before parsing.
217
+ - Unknown keys in header maps or event maps MUST be ignored (forward-compatible).
218
+ - New event types (`"e"` values) MAY be added in future versions; readers SHOULD skip unknown event types.
219
+ - The detail level in the header is informational — readers MUST handle missing fields gracefully regardless of declared level.
220
+
221
+ ## Interoperability
222
+
223
+ The `.moqtrace` format is shared between the JavaScript (`@moqtap/trace`) and Rust (`moqtap-trace`) implementations. A file written by either is readable by both. CBOR libraries used:
224
+
225
+ - JavaScript: `cbor-x`
226
+ - Rust: `ciborium`
227
+
228
+ Both produce deterministic CBOR output (canonical CBOR is NOT required, but map keys SHOULD be sorted for consistency).