@moqtap/codec 0.10.0 → 0.12.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.
Files changed (262) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/README.md +3 -2
  3. package/dist/auth-redaction-B9JJOlIY.d.cts +100 -0
  4. package/dist/auth-redaction-B9JJOlIY.d.ts +100 -0
  5. package/dist/{chunk-RYKQXBG6.cjs → chunk-27K5BUAG.cjs} +481 -517
  6. package/dist/{chunk-WCPENYNE.js → chunk-3RNQMGKA.js} +359 -390
  7. package/dist/{chunk-TSUZ6GJV.js → chunk-5AWIXOLB.js} +438 -469
  8. package/dist/{chunk-4XLYCT5Q.cjs → chunk-5IMHJIAZ.cjs} +52 -2
  9. package/dist/{chunk-KTNERCTZ.cjs → chunk-5JGZCKJO.cjs} +488 -518
  10. package/dist/{chunk-WCTMPDUX.cjs → chunk-73K4OSAN.cjs} +401 -432
  11. package/dist/{chunk-N2V7Z37J.js → chunk-7VQKRGRD.js} +19 -3
  12. package/dist/{chunk-BIBMDTU5.cjs → chunk-A3UCQOIB.cjs} +486 -517
  13. package/dist/{chunk-K2B3OVFK.js → chunk-A7PDMVXM.js} +24 -3
  14. package/dist/{chunk-VZHL2U2N.cjs → chunk-A7VQO6YC.cjs} +54 -38
  15. package/dist/{chunk-AQ445VV7.cjs → chunk-ACQHZLRZ.cjs} +66 -46
  16. package/dist/{chunk-L5DRACXS.js → chunk-BBICOMW6.js} +25 -3
  17. package/dist/{chunk-T4VWPKDG.cjs → chunk-BNIUVOCJ.cjs} +529 -599
  18. package/dist/{chunk-UKKDOOUO.cjs → chunk-CNC6ANS4.cjs} +58 -37
  19. package/dist/{chunk-OZRK73NQ.js → chunk-FDLQSWHK.js} +433 -469
  20. package/dist/{chunk-IJAF6PAY.js → chunk-FFNOE4UR.js} +19 -3
  21. package/dist/{chunk-CZNW3R5E.cjs → chunk-GMENT63T.cjs} +54 -38
  22. package/dist/{chunk-XCRCAUGI.cjs → chunk-HM7BLRVP.cjs} +291 -297
  23. package/dist/{chunk-XHAP2YST.js → chunk-HZONHFNO.js} +19 -3
  24. package/dist/{chunk-SDJTSMOW.cjs → chunk-ILYABWDK.cjs} +60 -38
  25. package/dist/{chunk-IDV2JYJ6.cjs → chunk-KRSFTZVO.cjs} +56 -40
  26. package/dist/{chunk-RIIA4ALQ.cjs → chunk-KSPH6KMH.cjs} +54 -38
  27. package/dist/{chunk-OUNN4IQB.cjs → chunk-KTY4LAUV.cjs} +482 -497
  28. package/dist/chunk-OBATJBNL.cjs +868 -0
  29. package/dist/{chunk-RZTLQAKF.js → chunk-OJNLJT5D.js} +0 -1
  30. package/dist/{chunk-FT3HDNIB.js → chunk-PJK2ZS3I.js} +467 -537
  31. package/dist/{chunk-G53WYUXL.js → chunk-PSMZKHOC.js} +441 -456
  32. package/dist/{chunk-7RE6KSIY.cjs → chunk-Q5JQGHWY.cjs} +0 -1
  33. package/dist/{chunk-BX2N7TZM.js → chunk-SQTUQY5E.js} +31 -11
  34. package/dist/chunk-T2TCLZZV.js +2304 -0
  35. package/dist/chunk-WB65UG7G.cjs +2304 -0
  36. package/dist/{chunk-CF5DDD3Q.js → chunk-WCRLAKDW.js} +239 -245
  37. package/dist/{chunk-KYVTM5VI.js → chunk-WL2YUEXH.js} +19 -3
  38. package/dist/chunk-XPCBPACE.js +868 -0
  39. package/dist/{chunk-KGDVC4O6.js → chunk-YCNIVZS7.js} +439 -469
  40. package/dist/{chunk-KLPWJU6W.js → chunk-YRZQVNZO.js} +50 -0
  41. package/dist/{codec-DOmAmFkI.d.ts → codec-B-oUU-cp.d.ts} +21 -2
  42. package/dist/{codec-DNmiG2e0.d.cts → codec-B3RpCI9k.d.cts} +21 -2
  43. package/dist/codec-B5pcOS3m.d.cts +38 -0
  44. package/dist/{codec-BNy5syCU.d.ts → codec-BLjxwK2f.d.ts} +21 -2
  45. package/dist/{codec-alVA27Uc.d.ts → codec-BV3CL_3m.d.ts} +21 -2
  46. package/dist/{codec-Czejcmxr.d.cts → codec-BYCDWgED.d.ts} +22 -3
  47. package/dist/{codec-DaXjRrOV.d.ts → codec-Bwqq7ZQT.d.ts} +22 -3
  48. package/dist/{codec-CFWu5KVT.d.cts → codec-BzWxOFby.d.cts} +22 -3
  49. package/dist/codec-C-nJhNNl.d.ts +38 -0
  50. package/dist/{codec-EvybrBuY.d.cts → codec-C11VEPbQ.d.cts} +21 -2
  51. package/dist/codec-C9U0Vzy9.d.cts +68 -0
  52. package/dist/{codec-Cm3D261p.d.ts → codec-CCx22jnd.d.ts} +21 -2
  53. package/dist/{codec-CwZ6OCPP.d.ts → codec-CP3zkfTv.d.ts} +22 -3
  54. package/dist/{codec-BxQTybUX.d.cts → codec-CRmOLDpt.d.ts} +22 -3
  55. package/dist/{codec-DzkdW4kO.d.cts → codec-CUu56DDU.d.cts} +21 -2
  56. package/dist/codec-Cb8mUre3.d.ts +68 -0
  57. package/dist/{codec-f7vxeVIS.d.cts → codec-CemqAQdR.d.cts} +22 -3
  58. package/dist/{codec-gl4_QEva.d.ts → codec-CxMvsVMS.d.ts} +21 -2
  59. package/dist/{codec-Bd60SGKH.d.cts → codec-DIgMVTi4.d.cts} +21 -2
  60. package/dist/{codec--CVeqGQv.d.ts → codec-DYzozF68.d.ts} +21 -2
  61. package/dist/{codec-CWnf8mn_.d.cts → codec-D_O9zxax.d.ts} +22 -3
  62. package/dist/{codec-08ZOr5wI.d.ts → codec-Ddc-QxV6.d.cts} +22 -3
  63. package/dist/{codec-CBdOIfMY.d.ts → codec-DoojMl2z.d.cts} +22 -3
  64. package/dist/{codec-BrTi4K7r.d.cts → codec-DqdjiOQF.d.ts} +22 -3
  65. package/dist/{codec-CXCjya_G.d.cts → codec-Dstemwrq.d.cts} +21 -2
  66. package/dist/{codec-Dl_QIXMz.d.ts → codec-Dt6nuejp.d.ts} +21 -2
  67. package/dist/{codec-Bux_Q40X.d.ts → codec-DuYhd8fP.d.cts} +22 -3
  68. package/dist/{codec-Cfo_z5ET.d.cts → codec-DwNOFgPU.d.cts} +21 -2
  69. package/dist/{codec-guzqtfMk.d.cts → codec-l01BpNhF.d.cts} +21 -2
  70. package/dist/{codec-C3mdsGx3.d.ts → codec-psN2vB6I.d.cts} +22 -3
  71. package/dist/draft07-session.d.cts +3 -3
  72. package/dist/draft07-session.d.ts +3 -3
  73. package/dist/draft07.cjs +48 -4
  74. package/dist/draft07.d.cts +44 -4
  75. package/dist/draft07.d.ts +44 -4
  76. package/dist/draft07.js +49 -5
  77. package/dist/draft08-session.d.cts +3 -3
  78. package/dist/draft08-session.d.ts +3 -3
  79. package/dist/draft08.cjs +76 -3
  80. package/dist/draft08.d.cts +73 -5
  81. package/dist/draft08.d.ts +73 -5
  82. package/dist/draft08.js +77 -4
  83. package/dist/draft09-session.d.cts +3 -3
  84. package/dist/draft09-session.d.ts +3 -3
  85. package/dist/draft09.cjs +76 -3
  86. package/dist/draft09.d.cts +73 -5
  87. package/dist/draft09.d.ts +73 -5
  88. package/dist/draft09.js +77 -4
  89. package/dist/draft10-session.d.cts +3 -3
  90. package/dist/draft10-session.d.ts +3 -3
  91. package/dist/draft10.cjs +76 -3
  92. package/dist/draft10.d.cts +73 -5
  93. package/dist/draft10.d.ts +73 -5
  94. package/dist/draft10.js +77 -4
  95. package/dist/draft11-session.d.cts +3 -3
  96. package/dist/draft11-session.d.ts +3 -3
  97. package/dist/draft11.cjs +104 -3
  98. package/dist/draft11.d.cts +101 -5
  99. package/dist/draft11.d.ts +101 -5
  100. package/dist/draft11.js +105 -4
  101. package/dist/draft12-session.d.cts +3 -3
  102. package/dist/draft12-session.d.ts +3 -3
  103. package/dist/draft12.cjs +113 -3
  104. package/dist/draft12.d.cts +110 -5
  105. package/dist/draft12.d.ts +110 -5
  106. package/dist/draft12.js +114 -4
  107. package/dist/draft13-session.d.cts +3 -3
  108. package/dist/draft13-session.d.ts +3 -3
  109. package/dist/draft13.cjs +105 -3
  110. package/dist/draft13.d.cts +102 -5
  111. package/dist/draft13.d.ts +102 -5
  112. package/dist/draft13.js +106 -4
  113. package/dist/draft14-session.d.cts +3 -3
  114. package/dist/draft14-session.d.ts +3 -3
  115. package/dist/draft14.cjs +107 -3
  116. package/dist/draft14.d.cts +104 -31
  117. package/dist/draft14.d.ts +104 -31
  118. package/dist/draft14.js +108 -4
  119. package/dist/draft15-session.d.cts +4 -4
  120. package/dist/draft15-session.d.ts +4 -4
  121. package/dist/draft15.cjs +69 -3
  122. package/dist/draft15.d.cts +68 -25
  123. package/dist/draft15.d.ts +68 -25
  124. package/dist/draft15.js +70 -4
  125. package/dist/draft16-session.cjs +2 -2
  126. package/dist/draft16-session.d.cts +4 -4
  127. package/dist/draft16-session.d.ts +4 -4
  128. package/dist/draft16-session.js +1 -1
  129. package/dist/draft16.cjs +76 -8
  130. package/dist/draft16.d.cts +70 -16
  131. package/dist/draft16.d.ts +70 -16
  132. package/dist/draft16.js +73 -5
  133. package/dist/draft17-session.d.cts +4 -4
  134. package/dist/draft17-session.d.ts +4 -4
  135. package/dist/draft17.cjs +77 -3
  136. package/dist/draft17.d.cts +75 -6
  137. package/dist/draft17.d.ts +75 -6
  138. package/dist/draft17.js +78 -4
  139. package/dist/draft18-session.d.cts +4 -4
  140. package/dist/draft18-session.d.ts +4 -4
  141. package/dist/draft18.cjs +8 -3
  142. package/dist/draft18.d.cts +6 -5
  143. package/dist/draft18.d.ts +6 -5
  144. package/dist/draft18.js +9 -4
  145. package/dist/draft19-session.d.cts +4 -4
  146. package/dist/draft19-session.d.ts +4 -4
  147. package/dist/draft19.cjs +11 -4
  148. package/dist/draft19.d.cts +6 -5
  149. package/dist/draft19.d.ts +6 -5
  150. package/dist/draft19.js +12 -5
  151. package/dist/draft20-session.d.cts +4 -4
  152. package/dist/draft20-session.d.ts +4 -4
  153. package/dist/draft20.cjs +8 -3
  154. package/dist/draft20.d.cts +6 -5
  155. package/dist/draft20.d.ts +6 -5
  156. package/dist/draft20.js +9 -4
  157. package/dist/draft21-session.cjs +8 -0
  158. package/dist/draft21-session.d.cts +195 -0
  159. package/dist/draft21-session.d.ts +195 -0
  160. package/dist/draft21-session.js +8 -0
  161. package/dist/draft21.cjs +222 -0
  162. package/dist/draft21.d.cts +176 -0
  163. package/dist/draft21.d.ts +176 -0
  164. package/dist/draft21.js +222 -0
  165. package/dist/index.cjs +37 -31
  166. package/dist/index.d.cts +29 -23
  167. package/dist/index.d.ts +29 -23
  168. package/dist/index.js +22 -16
  169. package/dist/{session-types-Dn7b3RZM.d.ts → session-types-Cxx7W8zZ.d.ts} +1 -1
  170. package/dist/{session-types-B9idsih-.d.cts → session-types-DZkI_qFG.d.cts} +1 -1
  171. package/dist/session.cjs +7 -3
  172. package/dist/session.d.cts +11 -9
  173. package/dist/session.d.ts +11 -9
  174. package/dist/session.js +6 -2
  175. package/dist/{types-DG5_iKuv.d.cts → types-2NisMA8_.d.cts} +1 -1
  176. package/dist/{types-DG5_iKuv.d.ts → types-2NisMA8_.d.ts} +1 -1
  177. package/dist/{types-C1rxh-id.d.cts → types-Btp416jt.d.cts} +8 -0
  178. package/dist/{types-C1rxh-id.d.ts → types-Btp416jt.d.ts} +8 -0
  179. package/dist/{types-CZTzbIzQ.d.cts → types-BxTL1NXw.d.cts} +7 -0
  180. package/dist/{types-CZTzbIzQ.d.ts → types-BxTL1NXw.d.ts} +7 -0
  181. package/dist/{types-pShrs3fN.d.cts → types-CuibHYcf.d.cts} +7 -0
  182. package/dist/{types-pShrs3fN.d.ts → types-CuibHYcf.d.ts} +7 -0
  183. package/dist/{types-CY-QWxd-.d.cts → types-CwN7AEoL.d.cts} +13 -1
  184. package/dist/{types-CY-QWxd-.d.ts → types-CwN7AEoL.d.ts} +13 -1
  185. package/dist/{types-D4bmW8rY.d.cts → types-DAnalt5K.d.cts} +7 -0
  186. package/dist/{types-D4bmW8rY.d.ts → types-DAnalt5K.d.ts} +7 -0
  187. package/dist/types-DDwidj2d.d.cts +371 -0
  188. package/dist/types-DDwidj2d.d.ts +371 -0
  189. package/dist/{types-Bg86GGVZ.d.cts → types-DxXNP32l.d.cts} +8 -0
  190. package/dist/{types-Bg86GGVZ.d.ts → types-DxXNP32l.d.ts} +8 -0
  191. package/package.json +15 -3
  192. package/src/core/auth-redaction.ts +190 -0
  193. package/src/core/types.ts +1 -0
  194. package/src/drafts/draft07/codec.ts +40 -0
  195. package/src/drafts/draft07/error-codes.ts +44 -0
  196. package/src/drafts/draft07/index.ts +15 -0
  197. package/src/drafts/draft08/codec.ts +40 -0
  198. package/src/drafts/draft08/error-codes.ts +76 -0
  199. package/src/drafts/draft08/index.ts +21 -0
  200. package/src/drafts/draft09/codec.ts +40 -0
  201. package/src/drafts/draft09/error-codes.ts +76 -0
  202. package/src/drafts/draft09/index.ts +21 -0
  203. package/src/drafts/draft10/codec.ts +40 -0
  204. package/src/drafts/draft10/error-codes.ts +76 -0
  205. package/src/drafts/draft10/index.ts +21 -0
  206. package/src/drafts/draft11/codec.ts +60 -0
  207. package/src/drafts/draft11/error-codes.ts +106 -0
  208. package/src/drafts/draft11/index.ts +23 -0
  209. package/src/drafts/draft12/codec.ts +61 -0
  210. package/src/drafts/draft12/error-codes.ts +116 -0
  211. package/src/drafts/draft12/index.ts +25 -0
  212. package/src/drafts/draft13/codec.ts +69 -11
  213. package/src/drafts/draft13/error-codes.ts +107 -0
  214. package/src/drafts/draft13/index.ts +23 -0
  215. package/src/drafts/draft13/types.ts +1 -1
  216. package/src/drafts/draft14/codec.ts +65 -4
  217. package/src/drafts/draft14/data-streams.ts +331 -259
  218. package/src/drafts/draft14/error-codes.ts +109 -0
  219. package/src/drafts/draft14/index.ts +23 -0
  220. package/src/drafts/draft15/codec.ts +65 -8
  221. package/src/drafts/draft15/data-streams.ts +489 -444
  222. package/src/drafts/draft15/error-codes.ts +66 -0
  223. package/src/drafts/draft15/index.ts +15 -0
  224. package/src/drafts/draft15/types.ts +7 -0
  225. package/src/drafts/draft16/codec.ts +146 -52
  226. package/src/drafts/draft16/data-streams.ts +513 -442
  227. package/src/drafts/draft16/error-codes.ts +68 -0
  228. package/src/drafts/draft16/index.ts +15 -0
  229. package/src/drafts/draft16/messages.ts +4 -4
  230. package/src/drafts/draft16/session-fsm.ts +0 -1
  231. package/src/drafts/draft16/types.ts +22 -8
  232. package/src/drafts/draft17/codec.ts +73 -12
  233. package/src/drafts/draft17/data-streams.ts +567 -488
  234. package/src/drafts/draft17/index.ts +15 -0
  235. package/src/drafts/draft17/types.ts +7 -0
  236. package/src/drafts/draft18/codec.ts +66 -9
  237. package/src/drafts/draft18/data-streams.ts +570 -482
  238. package/src/drafts/draft18/index.ts +3 -0
  239. package/src/drafts/draft18/types.ts +16 -8
  240. package/src/drafts/draft19/codec.ts +70 -10
  241. package/src/drafts/draft19/data-streams.ts +570 -482
  242. package/src/drafts/draft19/error-codes.ts +3 -1
  243. package/src/drafts/draft19/index.ts +3 -0
  244. package/src/drafts/draft19/messages.ts +3 -1
  245. package/src/drafts/draft19/types.ts +22 -11
  246. package/src/drafts/draft20/codec.ts +90 -23
  247. package/src/drafts/draft20/data-streams.ts +610 -563
  248. package/src/drafts/draft20/index.ts +3 -0
  249. package/src/drafts/draft20/types.ts +14 -2
  250. package/src/drafts/draft21/codec.ts +1816 -0
  251. package/src/drafts/draft21/data-streams.ts +1338 -0
  252. package/src/drafts/draft21/error-codes.ts +135 -0
  253. package/src/drafts/draft21/index.ts +168 -0
  254. package/src/drafts/draft21/messages.ts +68 -0
  255. package/src/drafts/draft21/rules.ts +107 -0
  256. package/src/drafts/draft21/session-fsm.ts +1063 -0
  257. package/src/drafts/draft21/session.ts +21 -0
  258. package/src/drafts/draft21/types.ts +473 -0
  259. package/src/index.ts +9 -1
  260. package/src/session.ts +4 -0
  261. package/dist/codec-BJ7BHuiZ.d.ts +0 -18
  262. package/dist/codec-C6Mx4bwz.d.cts +0 -18
@@ -0,0 +1,1816 @@
1
+ import {
2
+ type AuthRedaction,
3
+ recordAuthValueSpan,
4
+ redactWith,
5
+ withoutAuthValueSpans,
6
+ } from '../../core/auth-redaction.js'
7
+ import { MoqtBufferReader as BufferReader } from '../../core/buffer-reader.js'
8
+ import { MoqtBufferWriter as BufferWriter } from '../../core/buffer-writer.js'
9
+ import { bytesToHex, hexToBytes } from '../../core/hex.js'
10
+ import type { BaseCodec, DecodeResult } from '../../core/types.js'
11
+ import { DecodeError } from '../../core/types.js'
12
+ import {
13
+ MESSAGE_ID_MAP,
14
+ MSG_FETCH,
15
+ MSG_FETCH_OK,
16
+ MSG_GOAWAY,
17
+ MSG_NAMESPACE,
18
+ MSG_NAMESPACE_DONE,
19
+ MSG_PUBLISH,
20
+ MSG_PUBLISH_DONE,
21
+ MSG_PUBLISH_NAMESPACE,
22
+ MSG_PUBLISH_SKIPPED,
23
+ MSG_PUBLISH_STATE_NOTIFY,
24
+ MSG_REQUEST_ERROR,
25
+ MSG_REQUEST_OK,
26
+ MSG_REQUEST_UPDATE,
27
+ MSG_SETUP,
28
+ MSG_SUBSCRIBE,
29
+ MSG_SUBSCRIBE_NAMESPACE,
30
+ MSG_SUBSCRIBE_OK,
31
+ MSG_SUBSCRIBE_TRACKS,
32
+ MSG_TRACK_STATUS,
33
+ SETUP_OPT_AUTHORITY,
34
+ SETUP_OPT_AUTHORIZATION_TOKEN,
35
+ SETUP_OPT_MAX_AUTH_TOKEN_CACHE_SIZE,
36
+ SETUP_OPT_MAX_FILTER_RANGES,
37
+ SETUP_OPT_MAX_REQUEST_UPDATES,
38
+ SETUP_OPT_MOQT_IMPLEMENTATION,
39
+ SETUP_OPT_PATH,
40
+ } from './messages.js'
41
+ import type {
42
+ AuthorizationToken,
43
+ DatagramObject,
44
+ DataStreamEvent,
45
+ Draft21DataStream,
46
+ Draft21FillParameters,
47
+ Draft21Message,
48
+ Draft21Params,
49
+ Draft21SetupOptions,
50
+ Draft21TrackProperties,
51
+ FetchStream,
52
+ FetchStreamHeader,
53
+ LocationFilter,
54
+ ObjectPayload,
55
+ RangeFilter,
56
+ RangeFilterRange,
57
+ Redirect,
58
+ SubgroupStream,
59
+ SubgroupStreamHeader,
60
+ UnknownParam,
61
+ } from './types.js'
62
+
63
+ const textEncoder = /* @__PURE__ */ new TextEncoder()
64
+ const textDecoder = /* @__PURE__ */ new TextDecoder()
65
+
66
+ const REQUEST_ERROR_REDIRECT = 0x34n
67
+
68
+ /** The largest value MOQT's vi64 can carry (draft-21 Section 8.1, nine-byte form). */
69
+ const MAX_U64 = 0xffffffffffffffffn
70
+
71
+ /**
72
+ * `PUBLISH_DONE.Stream Count` "unknown" sentinel — draft-21 Section 9.9.
73
+ *
74
+ * draft-19 used `2^62 - 1`; draft-20 raised it to `2^64 - 1`. Both are
75
+ * encodable because MOQT's vi64 (Section 8.1) is a leading-ones-length
76
+ * prefix reaching a full 64 bits in nine bytes — it is NOT the QUIC varint and
77
+ * has no `2^62 - 1` ceiling. On the wire this is `ff` followed by eight `ff`
78
+ * bytes.
79
+ *
80
+ * JS `bigint` is required: `2^64 - 1` does not fit a `number`, and comparing a
81
+ * `Number(stream_count)` against this sentinel would be wrong for every value
82
+ * above `2^53`.
83
+ *
84
+ * Note (SPEC-DELTA Section 11 Q14): unlike the old `2^62 - 1` marker, this
85
+ * value is also a well-formed exact count. There is no longer any headroom
86
+ * that separates "unknown" from "absurdly many streams"; the draft does not
87
+ * acknowledge the collision.
88
+ */
89
+ export const UNKNOWN_STREAM_COUNT = 0xffffffffffffffffn
90
+
91
+ // ─── Setup Options Encoding/Decoding (KVP, no count prefix) ─────────────────
92
+
93
+ function encodeSetupOptions(opts: Draft21SetupOptions, writer: BufferWriter): void {
94
+ // Collect all options sorted by type ID
95
+ const entries: Array<{ type: bigint; encode: (w: BufferWriter) => void }> = []
96
+
97
+ if (opts.path !== undefined) {
98
+ entries.push({
99
+ type: SETUP_OPT_PATH,
100
+ encode: (w) => {
101
+ const encoded = textEncoder.encode(opts.path!)
102
+ w.writeVarInt(BigInt(encoded.byteLength))
103
+ w.writeBytes(encoded)
104
+ },
105
+ })
106
+ }
107
+ for (const token of opts.authorization_token ?? []) {
108
+ entries.push({
109
+ type: SETUP_OPT_AUTHORIZATION_TOKEN,
110
+ encode: (w) => {
111
+ const tmpW = new BufferWriter(64)
112
+ encodeAuthorizationToken(token, tmpW)
113
+ const raw = tmpW.finish()
114
+ w.writeVarInt(BigInt(raw.byteLength))
115
+ w.writeBytes(raw)
116
+ },
117
+ })
118
+ }
119
+ if (opts.max_auth_token_cache_size !== undefined) {
120
+ entries.push({
121
+ type: SETUP_OPT_MAX_AUTH_TOKEN_CACHE_SIZE,
122
+ encode: (w) => w.writeVarInt(opts.max_auth_token_cache_size!),
123
+ })
124
+ }
125
+ if (opts.authority !== undefined) {
126
+ entries.push({
127
+ type: SETUP_OPT_AUTHORITY,
128
+ encode: (w) => {
129
+ const encoded = textEncoder.encode(opts.authority!)
130
+ w.writeVarInt(BigInt(encoded.byteLength))
131
+ w.writeBytes(encoded)
132
+ },
133
+ })
134
+ }
135
+ if (opts.max_filter_ranges !== undefined) {
136
+ entries.push({
137
+ type: SETUP_OPT_MAX_FILTER_RANGES,
138
+ encode: (w) => w.writeVarInt(opts.max_filter_ranges!),
139
+ })
140
+ }
141
+ if (opts.moqt_implementation !== undefined) {
142
+ entries.push({
143
+ type: SETUP_OPT_MOQT_IMPLEMENTATION,
144
+ encode: (w) => {
145
+ const encoded = textEncoder.encode(opts.moqt_implementation!)
146
+ w.writeVarInt(BigInt(encoded.byteLength))
147
+ w.writeBytes(encoded)
148
+ },
149
+ })
150
+ }
151
+ if (opts.max_request_updates !== undefined) {
152
+ entries.push({
153
+ type: SETUP_OPT_MAX_REQUEST_UPDATES,
154
+ encode: (w) => w.writeVarInt(opts.max_request_updates!),
155
+ })
156
+ }
157
+ if (opts.unknown) {
158
+ for (const u of opts.unknown) {
159
+ const id = BigInt(u.id)
160
+ entries.push({
161
+ type: id,
162
+ encode: (w) => {
163
+ if (id % 2n === 0n) {
164
+ const raw = hexToBytes(u.raw_hex)
165
+ const tmpReader = new BufferReader(raw)
166
+ w.writeVarInt(tmpReader.readVarInt())
167
+ } else {
168
+ const raw = hexToBytes(u.raw_hex)
169
+ w.writeVarInt(BigInt(raw.byteLength))
170
+ w.writeBytes(raw)
171
+ }
172
+ },
173
+ })
174
+ }
175
+ }
176
+
177
+ // Sort by type (ascending) and write with delta encoding
178
+ entries.sort((a, b) => (a.type < b.type ? -1 : a.type > b.type ? 1 : 0))
179
+ let prevType = 0n
180
+ for (const entry of entries) {
181
+ writer.writeVarInt(entry.type - prevType)
182
+ entry.encode(writer)
183
+ prevType = entry.type
184
+ }
185
+ }
186
+
187
+ function decodeSetupOptions(reader: BufferReader, payloadEnd: number): Draft21SetupOptions {
188
+ const result: Draft21SetupOptions = {}
189
+ const unknown: UnknownParam[] = []
190
+ let prevType = 0n
191
+
192
+ while (reader.offset < payloadEnd) {
193
+ const delta = reader.readVarInt()
194
+ const optType = prevType + delta
195
+ prevType = optType
196
+
197
+ if (optType % 2n === 0n) {
198
+ // Even: single varint value
199
+ const value = reader.readVarInt()
200
+ if (optType === SETUP_OPT_MAX_AUTH_TOKEN_CACHE_SIZE) {
201
+ result.max_auth_token_cache_size = value
202
+ } else if (optType === SETUP_OPT_MAX_FILTER_RANGES) {
203
+ result.max_filter_ranges = value
204
+ } else if (optType === SETUP_OPT_MAX_REQUEST_UPDATES) {
205
+ result.max_request_updates = value
206
+ } else {
207
+ // Section 9.1: "endpoints MUST ignore unknown Setup Options" — kept
208
+ // for passthrough rather than discarded, but never an error.
209
+ const tmpWriter = new BufferWriter(16)
210
+ tmpWriter.writeVarInt(value)
211
+ const raw = tmpWriter.finish()
212
+ unknown.push({
213
+ id: `0x${optType.toString(16)}`,
214
+ length: raw.byteLength,
215
+ raw_hex: bytesToHex(raw),
216
+ })
217
+ }
218
+ } else {
219
+ // Odd: length-prefixed bytes
220
+ const length = Number(reader.readVarInt())
221
+ const bytes = reader.readBytes(length)
222
+ if (optType === SETUP_OPT_PATH) {
223
+ result.path = textDecoder.decode(bytes)
224
+ } else if (optType === SETUP_OPT_AUTHORIZATION_TOKEN) {
225
+ const token = decodeAuthorizationToken(new BufferReader(bytes))
226
+ noteAuthTokenValue(reader.offset - length, length, token)
227
+ result.authorization_token = [...(result.authorization_token ?? []), token]
228
+ } else if (optType === SETUP_OPT_AUTHORITY) {
229
+ result.authority = textDecoder.decode(bytes)
230
+ } else if (optType === SETUP_OPT_MOQT_IMPLEMENTATION) {
231
+ result.moqt_implementation = textDecoder.decode(bytes)
232
+ } else {
233
+ unknown.push({
234
+ id: `0x${optType.toString(16)}`,
235
+ length,
236
+ raw_hex: bytesToHex(bytes),
237
+ })
238
+ }
239
+ }
240
+ }
241
+
242
+ if (unknown.length > 0) result.unknown = unknown
243
+ return result
244
+ }
245
+
246
+ // ─── Authorization Token Encoding/Decoding ────────────────────────────────────
247
+
248
+ function encodeAuthorizationToken(token: AuthorizationToken, w: BufferWriter): void {
249
+ const aliasType = Number(token.alias_type)
250
+ w.writeVarInt(token.alias_type)
251
+ if (aliasType === 0 || aliasType === 1 || aliasType === 2) {
252
+ // DELETE, REGISTER, USE_ALIAS: token_alias present
253
+ w.writeVarInt(token.token_alias ?? 0n)
254
+ }
255
+ if (aliasType === 1 || aliasType === 3) {
256
+ // REGISTER, USE_VALUE: token_type + token_value present. The Token Value is
257
+ // NOT length-prefixed inside the Token structure; it consumes the remainder
258
+ // of the outer length-prefixed buffer (draft-21 Section 9.20.3).
259
+ w.writeVarInt(token.token_type ?? 0n)
260
+ const tv = token.token_value ?? new Uint8Array(0)
261
+ w.writeBytes(tv)
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Record where a Token Value sits, so it can be overwritten before the frame
267
+ * reaches anything that keeps it.
268
+ *
269
+ * The span falls out of the decode with no second walk. The Token Value is the
270
+ * tail of the Token structure — draft-21 Section 9.20.3, Figure 3: it is NOT
271
+ * length-prefixed inside the Token and consumes the remainder of the outer
272
+ * length-prefixed parameter value. So its start is the parameter value's end
273
+ * minus the value's own length, and the fields before it (Alias Type, Token
274
+ * Alias, Token Type) are left alone deliberately: they are structure, not
275
+ * secret, and they are what distinguishes a session that failed to
276
+ * authenticate from one that never tried.
277
+ *
278
+ * DELETE (0x0) and USE_ALIAS (0x2) carry no value at all, so they record
279
+ * nothing and their frames pass through untouched.
280
+ */
281
+ function noteAuthTokenValue(
282
+ valueStart: number,
283
+ valueLength: number,
284
+ token: AuthorizationToken,
285
+ ): void {
286
+ const value = token.token_value
287
+ if (value === undefined || value.byteLength === 0) return
288
+ recordAuthValueSpan(valueStart + valueLength - value.byteLength, value.byteLength)
289
+ }
290
+
291
+ function decodeAuthorizationToken(r: BufferReader): AuthorizationToken {
292
+ const alias_type = r.readVarInt()
293
+ const aliasType = Number(alias_type)
294
+ const result: AuthorizationToken = { alias_type }
295
+
296
+ if (aliasType === 0 || aliasType === 1 || aliasType === 2) {
297
+ ;(result as { token_alias: bigint }).token_alias = r.readVarInt()
298
+ }
299
+ if (aliasType === 1 || aliasType === 3) {
300
+ ;(result as { token_type: bigint }).token_type = r.readVarInt()
301
+ // Token Value extends to the end of the outer length-prefixed buffer
302
+ const bytes = r.readBytes(r.remaining)
303
+ ;(result as { token_value: Uint8Array }).token_value = bytes
304
+ }
305
+
306
+ return result
307
+ }
308
+
309
+ // ─── Tuple (Track Namespace) encode/decode for params ────────────────────────
310
+
311
+ function encodeNamespaceTuple(ns: string[], w: BufferWriter): void {
312
+ w.writeVarInt(BigInt(ns.length))
313
+ for (const part of ns) {
314
+ const encoded = textEncoder.encode(part)
315
+ w.writeVarInt(BigInt(encoded.byteLength))
316
+ w.writeBytes(encoded)
317
+ }
318
+ }
319
+
320
+ function decodeNamespaceTuple(r: BufferReader): string[] {
321
+ const count = Number(r.readVarInt())
322
+ const parts: string[] = []
323
+ for (let i = 0; i < count; i++) {
324
+ const len = Number(r.readVarInt())
325
+ parts.push(textDecoder.decode(r.readBytes(len)))
326
+ }
327
+ return parts
328
+ }
329
+
330
+ // ─── Message Parameter type codes (draft-21 Section 16.7, Table 13) ─────────
331
+
332
+ const PARAM_OBJECT_DELIVERY_TIMEOUT = 0x02n
333
+ const PARAM_AUTHORIZATION_TOKEN = 0x03n
334
+ const PARAM_RENDEZVOUS_TIMEOUT = 0x04n
335
+ const PARAM_SUBGROUP_DELIVERY_TIMEOUT = 0x06n
336
+ const PARAM_EXPIRES = 0x08n
337
+ const PARAM_LARGEST_OBJECT = 0x09n
338
+ const PARAM_FILL_TIMEOUT = 0x0an
339
+ const PARAM_FORWARD = 0x10n
340
+ const PARAM_SUBSCRIBER_PRIORITY = 0x20n
341
+ const PARAM_LOCATION_FILTER = 0x21n
342
+ const PARAM_GROUP_ORDER = 0x22n
343
+ const PARAM_FILL_PARAMETERS = 0x23n // NEW in draft-20
344
+ const PARAM_SUBGROUP_FILTER = 0x25n
345
+ const PARAM_OBJECTID_FILTER = 0x26n
346
+ const PARAM_PRIORITY_FILTER = 0x27n
347
+ const PARAM_OBJECT_PROPERTY_FILTER = 0x28n
348
+ const PARAM_TRACK_PROPERTY_FILTER = 0x29n
349
+ const PARAM_NEW_GROUP_REQUEST = 0x32n
350
+ const PARAM_TRACK_NAMESPACE_PREFIX = 0x34n
351
+ const PARAM_INCLUDE_PROPERTIES = 0x35n // NEW in draft-20
352
+
353
+ /**
354
+ * The pseudo message-type used for the parameter scope inside FILL_PARAMETERS.
355
+ *
356
+ * DECISION (DECISIONS.md D2, SPEC-DELTA Section 11 Q2): the nested value is a
357
+ * *separate parameter scope*. draft-21 Section 9.20.16 says "Parameters inside
358
+ * it are not considered to appear in the enclosing message for the purposes of
359
+ * Section 9.20, so a Parameter Type MAY appear both in the message and inside
360
+ * FILL_PARAMETERS." Modelling it as its own scope key falls straight out of
361
+ * that sentence — but the draft never states the consequence explicitly.
362
+ */
363
+ const FILL_SCOPE = 'fill_parameters'
364
+
365
+ // ─── Range Filter Encoding/Decoding (draft-21 Section 3.3.2) ────────────────
366
+ // A Range Filter's length-prefixed value is: SetID (uint8), an optional
367
+ // Property Type (varint, only for the Object/Track Property filters), then a
368
+ // sequence of delta-encoded inclusive Start/End Range pairs. The final End may
369
+ // be omitted (bounded by the length prefix) to indicate an open upper bound.
370
+ // In REQUEST_UPDATE a length-0 value removes the filter.
371
+ //
372
+ // Unchanged from draft-19: same codepoints, same field order, same delta
373
+ // arithmetic. What changed is applicability — PUBLISH_OK was dropped from
374
+ // every one of them, and 0x25-0x28 may now nest inside FILL_PARAMETERS.
375
+
376
+ function encodeRangeFilter(filter: RangeFilter, hasProperty: boolean, w: BufferWriter): void {
377
+ if (filter.removed) {
378
+ // REQUEST_UPDATE removal form: zero-length value
379
+ w.writeVarInt(0n)
380
+ return
381
+ }
382
+ const tmpW = new BufferWriter(32)
383
+ tmpW.writeUint8(Number(filter.set_id ?? 0n))
384
+ if (hasProperty) {
385
+ tmpW.writeVarInt(filter.property_type ?? 0n)
386
+ }
387
+ let prevEnd = 0n
388
+ for (const range of filter.ranges ?? []) {
389
+ tmpW.writeVarInt(range.start - prevEnd)
390
+ if (range.end !== undefined) {
391
+ // Ranges are inclusive Start/End pairs; End is delta-encoded from its
392
+ // own Start with no adjustment. Nothing here is offset by one.
393
+ tmpW.writeVarInt(range.end - range.start)
394
+ prevEnd = range.end
395
+ }
396
+ }
397
+ const raw = tmpW.finish()
398
+ w.writeVarInt(BigInt(raw.byteLength))
399
+ w.writeBytes(raw)
400
+ }
401
+
402
+ function decodeRangeFilter(reader: BufferReader, hasProperty: boolean): RangeFilter {
403
+ const length = Number(reader.readVarInt())
404
+ if (length === 0) {
405
+ // REQUEST_UPDATE removal form
406
+ return { removed: true }
407
+ }
408
+ const end = reader.offset + length
409
+ const set_id = BigInt(reader.readUint8())
410
+ let property_type: bigint | undefined
411
+ if (hasProperty) {
412
+ property_type = reader.readVarInt()
413
+ }
414
+ const ranges: RangeFilterRange[] = []
415
+ let prevEnd = 0n
416
+ while (reader.offset < end) {
417
+ const start = reader.readVarInt() + prevEnd
418
+ if (reader.offset < end) {
419
+ const rangeEnd = reader.readVarInt() + start
420
+ ranges.push({ start, end: rangeEnd })
421
+ prevEnd = rangeEnd
422
+ } else {
423
+ ranges.push({ start })
424
+ }
425
+ }
426
+ return property_type !== undefined ? { set_id, property_type, ranges } : { set_id, ranges }
427
+ }
428
+
429
+ // ─── LOCATION_FILTER (0x21) — draft-21 Section 3.3.1 ───────────────────────
430
+
431
+ /**
432
+ * Encode a LOCATION_FILTER value.
433
+ *
434
+ * The fields are written in order and the parameter's Length is whatever they
435
+ * come to; the *count* of vi64 fields is the discriminator on the way back in
436
+ * (see {@link decodeLocationFilter}).
437
+ *
438
+ * Note what is absent: no `+ 1` on `end_object`, and no "Object of 0 means the
439
+ * whole group" special case. draft-21 Section 3.3.1 says "A Location filter
440
+ * specifies an inclusive range of Locations", and Section 9.11 repeats it for
441
+ * FETCH. draft-19 Section 10.12.1 encoded a fetch end as "the end Location,
442
+ * plus 1", so an encoder ported forward with its arithmetic intact fetches one
443
+ * object too many. This is DECISIONS.md D4; the change is absent from the
444
+ * draft's own change log, which is why it is called out here rather than
445
+ * assumed obvious.
446
+ */
447
+ function encodeLocationFilter(f: LocationFilter, w: BufferWriter): void {
448
+ if (f.removed) {
449
+ // Length == 0: no filter. In REQUEST_UPDATE this removes an existing one.
450
+ w.writeVarInt(0n)
451
+ return
452
+ }
453
+ const tmpW = new BufferWriter(48)
454
+ if (f.start_group !== undefined) tmpW.writeVarInt(f.start_group)
455
+ if (f.start_object !== undefined) tmpW.writeVarInt(f.start_object)
456
+ if (f.end_group_delta !== undefined) tmpW.writeVarInt(f.end_group_delta)
457
+ if (f.end_object !== undefined) tmpW.writeVarInt(f.end_object)
458
+ const raw = tmpW.finish()
459
+ w.writeVarInt(BigInt(raw.byteLength))
460
+ w.writeBytes(raw)
461
+ }
462
+
463
+ /**
464
+ * Decode a LOCATION_FILTER value.
465
+ *
466
+ * DECISION (DECISIONS.md D3, SPEC-DELTA Section 11 Q3): the field count comes
467
+ * from parsing, never from the byte length. draft-21 Section 3.3.1 says
468
+ * "Length (in bytes) determines how many optional vi64 fields are present",
469
+ * which is not implementable as written: MOQT varints are 1-9 bytes and need
470
+ * not be minimally encoded (Section 8.1), so a Length of 2 is equally
471
+ * consistent with two 1-byte fields and one 2-byte field. The only rule that
472
+ * round-trips is to decode vi64 values until exactly Length bytes are consumed
473
+ * and then switch on the count.
474
+ *
475
+ * Two corollaries the draft does not give, chosen here:
476
+ * - a field that would overrun Length is a PROTOCOL_VIOLATION;
477
+ * - more than four decoded values is a PROTOCOL_VIOLATION.
478
+ */
479
+ function decodeLocationFilter(reader: BufferReader): LocationFilter {
480
+ const length = Number(reader.readVarInt())
481
+ // Slice exactly Length bytes so a varint cannot silently run into whatever
482
+ // parameter follows: inside this sub-reader an overrun is an end-of-input.
483
+ const valueBytes = reader.readBytes(length)
484
+ if (length === 0) {
485
+ // The draft does state this one: "A length of 0 indicates no filter, for
486
+ // example to remove the filter in REQUEST_UPDATE."
487
+ return { removed: true }
488
+ }
489
+
490
+ const r = new BufferReader(valueBytes)
491
+ const fields: bigint[] = []
492
+ while (r.remaining > 0) {
493
+ let value: bigint
494
+ try {
495
+ value = r.readVarInt()
496
+ } catch {
497
+ throw new DecodeError(
498
+ 'CONSTRAINT_VIOLATION',
499
+ "a LOCATION_FILTER field must not extend past the parameter's Length",
500
+ reader.offset,
501
+ )
502
+ }
503
+ fields.push(value)
504
+ if (fields.length > 4) {
505
+ throw new DecodeError(
506
+ 'CONSTRAINT_VIOLATION',
507
+ 'a LOCATION_FILTER value may hold at most four vi64 fields',
508
+ reader.offset,
509
+ )
510
+ }
511
+ }
512
+
513
+ const filter: {
514
+ start_group?: bigint
515
+ start_object?: bigint
516
+ end_group_delta?: bigint
517
+ end_object?: bigint
518
+ } = {}
519
+ if (fields.length >= 1) filter.start_group = fields[0] as bigint
520
+ if (fields.length >= 2) filter.start_object = fields[1] as bigint
521
+ if (fields.length >= 3) filter.end_group_delta = fields[2] as bigint
522
+ if (fields.length >= 4) filter.end_object = fields[3] as bigint
523
+
524
+ if (filter.start_group !== undefined && filter.end_group_delta !== undefined) {
525
+ // Section 3.3.1: "If StartGroup + EndGroupDelta exceeds 2^64 - 1, the
526
+ // endpoint MUST close the session with a PROTOCOL_VIOLATION." The sum is
527
+ // the absolute End Group; EndGroupDelta is delta-encoded from StartGroup
528
+ // but both groups are absolute, not relative to Largest Object.
529
+ if (filter.start_group + filter.end_group_delta > MAX_U64) {
530
+ throw new DecodeError(
531
+ 'CONSTRAINT_VIOLATION',
532
+ 'StartGroup + EndGroupDelta exceeds 2^64 - 1',
533
+ reader.offset,
534
+ )
535
+ }
536
+ }
537
+
538
+ return filter
539
+ }
540
+
541
+ /**
542
+ * Which messages each Message Parameter's own definition names.
543
+ *
544
+ * Section 9.20.1: "Each Message Parameter definition indicates the message
545
+ * types in which it can appear. If it appears in some other type of message,
546
+ * the receiving endpoint MUST close the connection with a PROTOCOL_VIOLATION."
547
+ *
548
+ * Read out of draft-21's per-parameter sections, not carried over from
549
+ * draft-19. The biggest delta is that `request_ok` — which is also PUBLISH_OK,
550
+ * REQUEST_UPDATE_OK, TRACK_STATUS_OK, SUBSCRIBE_NAMESPACE_OK,
551
+ * SUBSCRIBE_TRACKS_OK and PUBLISH_NAMESPACE_OK — carries almost no subscription
552
+ * parameter. Only EXPIRES (0x08) names it, where draft-19 also lists it on
553
+ * OBJECT_DELIVERY_TIMEOUT (0x02), SUBGROUP_DELIVERY_TIMEOUT (0x06), FORWARD
554
+ * (0x10), SUBSCRIBER_PRIORITY (0x20), LOCATION_FILTER (0x21) and
555
+ * NEW_GROUP_REQUEST (0x32). Subscription parameters travel on PUBLISH (initial values) and
556
+ * REQUEST_UPDATE (changes) instead.
557
+ *
558
+ * `fill_parameters` in a set means the type is one of the eight Table 6 permits
559
+ * inside FILL_PARAMETERS (Section 9.20.16). TRACK_PROPERTY_FILTER (0x29) is
560
+ * deliberately not among them.
561
+ */
562
+ const PARAMETER_SCOPE = new Map<bigint, Set<string>>([
563
+ // 0x02 OBJECT_DELIVERY_TIMEOUT (10.2.4) — d19 also listed PUBLISH_OK; d20 added PUBLISH.
564
+ [PARAM_OBJECT_DELIVERY_TIMEOUT, new Set(['subscribe', 'publish', 'request_update'])],
565
+ // 0x03 AUTHORIZATION_TOKEN (10.2.2) — any request or response that needs authorization.
566
+ [
567
+ PARAM_AUTHORIZATION_TOKEN,
568
+ new Set([
569
+ 'publish',
570
+ 'subscribe',
571
+ 'request_update',
572
+ 'subscribe_namespace',
573
+ 'subscribe_tracks',
574
+ 'publish_namespace',
575
+ 'track_status',
576
+ 'fetch',
577
+ ]),
578
+ ],
579
+ // 0x04 RENDEZVOUS_TIMEOUT (10.2.6) — "MAY appear in a SUBSCRIBE message", nothing else.
580
+ [PARAM_RENDEZVOUS_TIMEOUT, new Set(['subscribe'])],
581
+ // 0x06 SUBGROUP_DELIVERY_TIMEOUT (10.2.3) — d19 also listed PUBLISH_OK; d20 added PUBLISH.
582
+ [PARAM_SUBGROUP_DELIVERY_TIMEOUT, new Set(['subscribe', 'publish', 'request_update'])],
583
+ // 0x08 EXPIRES (10.2.16) — the one parameter that still names PUBLISH_OK.
584
+ [PARAM_EXPIRES, new Set(['subscribe_ok', 'publish', 'request_ok'])],
585
+ // 0x09 LARGEST_OBJECT (10.2.17) — PUBLISH_STATE_NOTIFY here; draft-19 (10.2.16) omits it.
586
+ [
587
+ PARAM_LARGEST_OBJECT,
588
+ new Set(['subscribe_ok', 'publish', 'request_ok', 'publish_state_notify']),
589
+ ],
590
+ // 0x0A FILL_TIMEOUT (10.2.5) — FETCH, or nested inside FILL_PARAMETERS.
591
+ [PARAM_FILL_TIMEOUT, new Set(['fetch', FILL_SCOPE])],
592
+ // 0x10 FORWARD (10.2.18) — PUBLISH_STATE_NOTIFY here where draft-19 (10.2.17) lists PUBLISH_OK.
593
+ [
594
+ PARAM_FORWARD,
595
+ new Set(['subscribe', 'request_update', 'publish', 'subscribe_tracks', 'publish_state_notify']),
596
+ ],
597
+ // 0x20 SUBSCRIBER_PRIORITY (10.2.7) — PUBLISH here where draft-19 lists PUBLISH_OK.
598
+ [
599
+ PARAM_SUBSCRIBER_PRIORITY,
600
+ new Set(['subscribe', 'publish', 'fetch', 'request_update', FILL_SCOPE]),
601
+ ],
602
+ // 0x21 LOCATION_FILTER (10.2.9) — PUBLISH and PUBLISH_STATE_NOTIFY here where draft-19 lists PUBLISH_OK.
603
+ [
604
+ PARAM_LOCATION_FILTER,
605
+ new Set([
606
+ 'fetch',
607
+ 'subscribe',
608
+ 'publish',
609
+ 'request_update',
610
+ 'publish_state_notify',
611
+ FILL_SCOPE,
612
+ ]),
613
+ ],
614
+ // 0x22 GROUP_ORDER (10.2.8) — PUBLISH and FILL_PARAMETERS nesting here; draft-19 has neither.
615
+ [PARAM_GROUP_ORDER, new Set(['subscribe', 'publish', 'subscribe_tracks', 'fetch', FILL_SCOPE])],
616
+ // 0x23 FILL_PARAMETERS (10.2.15) — NEW. Subscriptions only, and never nested
617
+ // in itself: a FETCH already is a fetch, so it has nothing to fill.
618
+ [PARAM_FILL_PARAMETERS, new Set(['subscribe', 'request_update'])],
619
+ // 0x25-0x28 range filters (5.1.4) — permitted inside FILL_PARAMETERS (Table 6).
620
+ [
621
+ PARAM_SUBGROUP_FILTER,
622
+ new Set(['fetch', 'subscribe', 'subscribe_tracks', 'request_update', FILL_SCOPE]),
623
+ ],
624
+ [
625
+ PARAM_OBJECTID_FILTER,
626
+ new Set(['fetch', 'subscribe', 'subscribe_tracks', 'request_update', FILL_SCOPE]),
627
+ ],
628
+ [
629
+ PARAM_PRIORITY_FILTER,
630
+ new Set(['fetch', 'subscribe', 'subscribe_tracks', 'request_update', FILL_SCOPE]),
631
+ ],
632
+ [
633
+ PARAM_OBJECT_PROPERTY_FILTER,
634
+ new Set(['fetch', 'subscribe', 'subscribe_tracks', 'request_update', FILL_SCOPE]),
635
+ ],
636
+ // 0x29 TRACK_PROPERTY_FILTER (10.2.14) — NOT permitted inside
637
+ // FILL_PARAMETERS: Table 6 omits it. A relay forwarding a downstream
638
+ // FILL_PARAMETERS upstream has to strip it rather than trip this rule.
639
+ [PARAM_TRACK_PROPERTY_FILTER, new Set(['subscribe_tracks', 'request_update'])],
640
+ // 0x32 NEW_GROUP_REQUEST (10.2.19) — draft-19 (10.2.18) also lists PUBLISH_OK.
641
+ [PARAM_NEW_GROUP_REQUEST, new Set(['subscribe', 'request_update'])],
642
+ // 0x34 TRACK_NAMESPACE_PREFIX (10.2.20) — REQUEST_UPDATE for a namespace subscription.
643
+ [PARAM_TRACK_NAMESPACE_PREFIX, new Set(['request_update'])],
644
+ // 0x35 INCLUDE_PROPERTIES (10.2.21) — NEW in draft-20.
645
+ [PARAM_INCLUDE_PROPERTIES, new Set(['subscribe', 'track_status', 'fetch', 'subscribe_tracks'])],
646
+ ])
647
+
648
+ function encodeParams(params: Draft21Params, writer: BufferWriter): void {
649
+ // Collect and sort params by type
650
+ const entries: Array<{ type: bigint; encode: (w: BufferWriter) => void }> = []
651
+
652
+ if (params.object_delivery_timeout !== undefined) {
653
+ entries.push({
654
+ type: PARAM_OBJECT_DELIVERY_TIMEOUT,
655
+ encode: (w) => w.writeVarInt(params.object_delivery_timeout!),
656
+ })
657
+ }
658
+ for (const token of params.authorization_token ?? []) {
659
+ entries.push({
660
+ type: PARAM_AUTHORIZATION_TOKEN,
661
+ encode: (w) => {
662
+ const tmpW = new BufferWriter(64)
663
+ encodeAuthorizationToken(token, tmpW)
664
+ const raw = tmpW.finish()
665
+ w.writeVarInt(BigInt(raw.byteLength))
666
+ w.writeBytes(raw)
667
+ },
668
+ })
669
+ }
670
+ if (params.rendezvous_timeout !== undefined) {
671
+ entries.push({
672
+ type: PARAM_RENDEZVOUS_TIMEOUT,
673
+ encode: (w) => w.writeVarInt(params.rendezvous_timeout!),
674
+ })
675
+ }
676
+ if (params.subgroup_delivery_timeout !== undefined) {
677
+ entries.push({
678
+ type: PARAM_SUBGROUP_DELIVERY_TIMEOUT,
679
+ encode: (w) => w.writeVarInt(params.subgroup_delivery_timeout!),
680
+ })
681
+ }
682
+ if (params.expires !== undefined) {
683
+ entries.push({
684
+ type: PARAM_EXPIRES,
685
+ encode: (w) => w.writeVarInt(params.expires!),
686
+ })
687
+ }
688
+ if (params.largest_object !== undefined) {
689
+ entries.push({
690
+ type: PARAM_LARGEST_OBJECT,
691
+ // A Location is "Two consecutive varints (Group, Object)", which is a
692
+ // different value encoding from "Length-prefixed". Nothing states the
693
+ // length, so nothing writes one — even though 0x09 is odd, because the
694
+ // Key-Value-Pair odd/even rule does not apply to Message Parameters.
695
+ encode: (w) => {
696
+ w.writeVarInt(params.largest_object!.group)
697
+ w.writeVarInt(params.largest_object!.object)
698
+ },
699
+ })
700
+ }
701
+ if (params.fill_timeout !== undefined) {
702
+ entries.push({
703
+ type: PARAM_FILL_TIMEOUT,
704
+ encode: (w) => w.writeVarInt(params.fill_timeout!),
705
+ })
706
+ }
707
+ if (params.forward !== undefined) {
708
+ entries.push({
709
+ type: PARAM_FORWARD,
710
+ encode: (w) => w.writeUint8(Number(params.forward!)),
711
+ })
712
+ }
713
+ if (params.subscriber_priority !== undefined) {
714
+ entries.push({
715
+ type: PARAM_SUBSCRIBER_PRIORITY,
716
+ encode: (w) => w.writeUint8(Number(params.subscriber_priority!)),
717
+ })
718
+ }
719
+ if (params.location_filter !== undefined) {
720
+ entries.push({
721
+ type: PARAM_LOCATION_FILTER,
722
+ encode: (w) => encodeLocationFilter(params.location_filter!, w),
723
+ })
724
+ }
725
+ if (params.group_order !== undefined) {
726
+ entries.push({
727
+ type: PARAM_GROUP_ORDER,
728
+ encode: (w) => w.writeUint8(Number(params.group_order!)),
729
+ })
730
+ }
731
+ if (params.fill_parameters !== undefined) {
732
+ entries.push({
733
+ type: PARAM_FILL_PARAMETERS,
734
+ encode: (w) => {
735
+ // DECISION (DECISIONS.md D1, SPEC-DELTA Section 11 Q1): the value is a
736
+ // full parameter block and therefore BEGINS with Number of Parameters.
737
+ // Section 9.20.16 calls it "a sequence of Parameters ... encoded as if
738
+ // they were Parameters for a separate message"; Section 9.20 defines a
739
+ // parameter block as count-bounded rather than length-bounded, because
740
+ // unknown parameters cannot be skipped. The draft does not state the
741
+ // consequence, and the outer length prefix makes the count look
742
+ // redundant — but a block "encoded as if for a message" is a block
743
+ // with the count. An empty FILL_PARAMETERS is therefore Length 1
744
+ // carrying the single byte 0x00, NOT Length 0.
745
+ //
746
+ // DECISION (D2): encodeParams starts its own Type Delta chain from 0,
747
+ // and the caller's chain is untouched — the outer parameter after
748
+ // FILL_PARAMETERS deltas from 0x23, not from the last inner type.
749
+ const tmpW = new BufferWriter(64)
750
+ encodeParams(params.fill_parameters as Draft21Params, tmpW)
751
+ const raw = tmpW.finish()
752
+ w.writeVarInt(BigInt(raw.byteLength))
753
+ w.writeBytes(raw)
754
+ },
755
+ })
756
+ }
757
+ for (const filter of params.subgroup_filter ?? []) {
758
+ entries.push({
759
+ type: PARAM_SUBGROUP_FILTER,
760
+ encode: (w) => encodeRangeFilter(filter, false, w),
761
+ })
762
+ }
763
+ for (const filter of params.objectid_filter ?? []) {
764
+ entries.push({
765
+ type: PARAM_OBJECTID_FILTER,
766
+ encode: (w) => encodeRangeFilter(filter, false, w),
767
+ })
768
+ }
769
+ for (const filter of params.priority_filter ?? []) {
770
+ entries.push({
771
+ type: PARAM_PRIORITY_FILTER,
772
+ encode: (w) => encodeRangeFilter(filter, false, w),
773
+ })
774
+ }
775
+ for (const filter of params.object_property_filter ?? []) {
776
+ entries.push({
777
+ type: PARAM_OBJECT_PROPERTY_FILTER,
778
+ encode: (w) => encodeRangeFilter(filter, true, w),
779
+ })
780
+ }
781
+ for (const filter of params.track_property_filter ?? []) {
782
+ entries.push({
783
+ type: PARAM_TRACK_PROPERTY_FILTER,
784
+ encode: (w) => encodeRangeFilter(filter, true, w),
785
+ })
786
+ }
787
+ if (params.new_group_request !== undefined) {
788
+ entries.push({
789
+ type: PARAM_NEW_GROUP_REQUEST,
790
+ encode: (w) => w.writeVarInt(params.new_group_request!),
791
+ })
792
+ }
793
+ if (params.track_namespace_prefix !== undefined) {
794
+ entries.push({
795
+ type: PARAM_TRACK_NAMESPACE_PREFIX,
796
+ // The parameter "uses the Track Namespace encoding", which is a tuple
797
+ // that states its own field count. That is not the Length-prefixed
798
+ // encoding, so there is no byte length ahead of it.
799
+ encode: (w) => {
800
+ encodeNamespaceTuple(params.track_namespace_prefix!, w)
801
+ },
802
+ })
803
+ }
804
+ if (params.include_properties !== undefined) {
805
+ entries.push({
806
+ type: PARAM_INCLUDE_PROPERTIES,
807
+ // uint8: one raw byte, 0 or 1 (draft-21 Section 9.20.22).
808
+ encode: (w) => w.writeUint8(Number(params.include_properties!)),
809
+ })
810
+ }
811
+
812
+ // Unknown params
813
+ if (params.unknown) {
814
+ for (const u of params.unknown) {
815
+ const id = BigInt(u.id)
816
+ entries.push({
817
+ type: id,
818
+ encode: (w) => {
819
+ const raw = hexToBytes(u.raw_hex)
820
+ // For unknown params, we store raw bytes and re-emit them
821
+ w.writeBytes(raw)
822
+ },
823
+ })
824
+ }
825
+ }
826
+
827
+ entries.sort((a, b) => (a.type < b.type ? -1 : a.type > b.type ? 1 : 0))
828
+
829
+ writer.writeVarInt(BigInt(entries.length))
830
+ let prevType = 0n
831
+ for (const entry of entries) {
832
+ writer.writeVarInt(entry.type - prevType)
833
+ entry.encode(writer)
834
+ prevType = entry.type
835
+ }
836
+ }
837
+
838
+ function decodeParams(reader: BufferReader, messageType: string): Draft21Params {
839
+ const count = Number(reader.readVarInt())
840
+ const result: Draft21Params = {}
841
+ let prevType = 0n
842
+
843
+ for (let i = 0; i < count; i++) {
844
+ const delta = reader.readVarInt()
845
+ const paramType = prevType + delta
846
+ prevType = paramType
847
+
848
+ const scope = PARAMETER_SCOPE.get(paramType)
849
+ if (scope !== undefined && !scope.has(messageType)) {
850
+ throw new DecodeError(
851
+ 'CONSTRAINT_VIOLATION',
852
+ `Message Parameter 0x${paramType.toString(16)} may not appear in ${messageType}`,
853
+ reader.offset,
854
+ )
855
+ }
856
+
857
+ if (paramType === PARAM_OBJECT_DELIVERY_TIMEOUT) {
858
+ result.object_delivery_timeout = reader.readVarInt()
859
+ } else if (paramType === PARAM_AUTHORIZATION_TOKEN) {
860
+ const length = Number(reader.readVarInt())
861
+ const bytes = reader.readBytes(length)
862
+ const token = decodeAuthorizationToken(new BufferReader(bytes))
863
+ noteAuthTokenValue(reader.offset - length, length, token)
864
+ result.authorization_token = [...(result.authorization_token ?? []), token]
865
+ } else if (paramType === PARAM_RENDEZVOUS_TIMEOUT) {
866
+ result.rendezvous_timeout = reader.readVarInt()
867
+ } else if (paramType === PARAM_SUBGROUP_DELIVERY_TIMEOUT) {
868
+ result.subgroup_delivery_timeout = reader.readVarInt()
869
+ } else if (paramType === PARAM_EXPIRES) {
870
+ result.expires = reader.readVarInt()
871
+ } else if (paramType === PARAM_LARGEST_OBJECT) {
872
+ // Location: 2 bare varints (not length-prefixed)
873
+ const group = reader.readVarInt()
874
+ const object = reader.readVarInt()
875
+ result.largest_object = { group, object }
876
+ } else if (paramType === PARAM_FILL_TIMEOUT) {
877
+ result.fill_timeout = reader.readVarInt()
878
+ } else if (paramType === PARAM_FORWARD) {
879
+ // uint8: single raw byte
880
+ result.forward = BigInt(reader.readUint8())
881
+ } else if (paramType === PARAM_SUBSCRIBER_PRIORITY) {
882
+ // uint8: single raw byte
883
+ result.subscriber_priority = BigInt(reader.readUint8())
884
+ } else if (paramType === PARAM_LOCATION_FILTER) {
885
+ result.location_filter = decodeLocationFilter(reader)
886
+ } else if (paramType === PARAM_GROUP_ORDER) {
887
+ // uint8: single raw byte
888
+ result.group_order = BigInt(reader.readUint8())
889
+ } else if (paramType === PARAM_FILL_PARAMETERS) {
890
+ result.fill_parameters = decodeFillParameters(reader)
891
+ } else if (paramType === PARAM_SUBGROUP_FILTER) {
892
+ result.subgroup_filter = [...(result.subgroup_filter ?? []), decodeRangeFilter(reader, false)]
893
+ } else if (paramType === PARAM_OBJECTID_FILTER) {
894
+ result.objectid_filter = [...(result.objectid_filter ?? []), decodeRangeFilter(reader, false)]
895
+ } else if (paramType === PARAM_PRIORITY_FILTER) {
896
+ result.priority_filter = [...(result.priority_filter ?? []), decodeRangeFilter(reader, false)]
897
+ } else if (paramType === PARAM_OBJECT_PROPERTY_FILTER) {
898
+ result.object_property_filter = [
899
+ ...(result.object_property_filter ?? []),
900
+ decodeRangeFilter(reader, true),
901
+ ]
902
+ } else if (paramType === PARAM_TRACK_PROPERTY_FILTER) {
903
+ result.track_property_filter = [
904
+ ...(result.track_property_filter ?? []),
905
+ decodeRangeFilter(reader, true),
906
+ ]
907
+ } else if (paramType === PARAM_NEW_GROUP_REQUEST) {
908
+ result.new_group_request = reader.readVarInt()
909
+ } else if (paramType === PARAM_TRACK_NAMESPACE_PREFIX) {
910
+ // Track Namespace encoding, read in place: the tuple's own field count
911
+ // bounds it.
912
+ result.track_namespace_prefix = decodeNamespaceTuple(reader)
913
+ } else if (paramType === PARAM_INCLUDE_PROPERTIES) {
914
+ // Section 9.20.22: "The allowed values are 0 (do not send Properties) or
915
+ // 1 (send Properties), and the default is 1. If an endpoint receives a
916
+ // value outside this range, it MUST close the session with
917
+ // PROTOCOL_VIOLATION."
918
+ const value = BigInt(reader.readUint8())
919
+ if (value !== 0n && value !== 1n) {
920
+ throw new DecodeError(
921
+ 'CONSTRAINT_VIOLATION',
922
+ `INCLUDE_PROPERTIES accepts only 0 or 1, got ${value}`,
923
+ reader.offset,
924
+ )
925
+ }
926
+ result.include_properties = value
927
+ } else {
928
+ // Section 9.20: "All Message Parameters MUST be defined in the negotiated
929
+ // version of MOQT or negotiated via Setup Options. An endpoint that
930
+ // receives an unknown Message Parameter MUST close the session with
931
+ // PROTOCOL_VIOLATION."
932
+ //
933
+ // There is no skipping an unknown one and carrying on: the value's
934
+ // encoding comes from its definition, so a receiver that does not know
935
+ // the Type does not know how many bytes it spans either.
936
+ throw new DecodeError(
937
+ 'INVALID_PARAMETER',
938
+ `Unknown Message Parameter type 0x${paramType.toString(16)}`,
939
+ reader.offset,
940
+ )
941
+ }
942
+ }
943
+
944
+ return result
945
+ }
946
+
947
+ /**
948
+ * Decode a FILL_PARAMETERS (0x23) value — draft-21 Section 9.20.16.
949
+ *
950
+ * See the encoder for D1 (the value begins with Number of Parameters) and D2
951
+ * (the Type Delta chain restarts here and the enclosing chain is unaffected).
952
+ * The eight permitted inner types are expressed as the {@link FILL_SCOPE} entry
953
+ * in {@link PARAMETER_SCOPE}, so "an endpoint that receives a parameter inside
954
+ * FILL_PARAMETERS that is not listed above MUST close the session with
955
+ * PROTOCOL_VIOLATION" falls out of the ordinary scope check.
956
+ */
957
+ function decodeFillParameters(reader: BufferReader): Draft21FillParameters {
958
+ const length = Number(reader.readVarInt())
959
+ const valueBytes = reader.readBytes(length)
960
+ const nested = new BufferReader(valueBytes)
961
+ const params = withoutAuthValueSpans(() => decodeParams(nested, FILL_SCOPE))
962
+ if (nested.remaining > 0) {
963
+ // The parameter count and the byte length disagree. The draft does not say
964
+ // what to do; leaving the slack unread would let a sender smuggle bytes no
965
+ // receiver interprets, so refuse it.
966
+ throw new DecodeError(
967
+ 'CONSTRAINT_VIOLATION',
968
+ `FILL_PARAMETERS has ${nested.remaining} trailing byte(s) after its parameter block`,
969
+ reader.offset,
970
+ )
971
+ }
972
+ return params as Draft21FillParameters
973
+ }
974
+
975
+ // ─── Track Properties Encoding/Decoding (KVP, no count prefix) ──────────────
976
+
977
+ const TPROP_OBJECT_DELIVERY_TIMEOUT = 0x02n
978
+ const TPROP_MAX_CACHE_DURATION = 0x04n
979
+ const TPROP_SUBGROUP_DELIVERY_TIMEOUT = 0x06n
980
+ const TPROP_IMMUTABLE_PROPERTIES = 0x0bn
981
+ const TPROP_DEFAULT_PUBLISHER_PRIORITY = 0x0en
982
+ const TPROP_DEFAULT_PUBLISHER_GROUP_ORDER = 0x22n
983
+ const TPROP_DYNAMIC_GROUPS = 0x30n
984
+
985
+ function encodeTrackProperties(props: Draft21TrackProperties, writer: BufferWriter): void {
986
+ const entries: Array<{ type: bigint; encode: (w: BufferWriter) => void }> = []
987
+
988
+ if (props.object_delivery_timeout !== undefined) {
989
+ entries.push({
990
+ type: TPROP_OBJECT_DELIVERY_TIMEOUT,
991
+ encode: (w) => w.writeVarInt(props.object_delivery_timeout!),
992
+ })
993
+ }
994
+ if (props.max_cache_duration !== undefined) {
995
+ entries.push({
996
+ type: TPROP_MAX_CACHE_DURATION,
997
+ encode: (w) => w.writeVarInt(props.max_cache_duration!),
998
+ })
999
+ }
1000
+ if (props.subgroup_delivery_timeout !== undefined) {
1001
+ entries.push({
1002
+ type: TPROP_SUBGROUP_DELIVERY_TIMEOUT,
1003
+ encode: (w) => w.writeVarInt(props.subgroup_delivery_timeout!),
1004
+ })
1005
+ }
1006
+ if (props.immutable_properties !== undefined) {
1007
+ entries.push({
1008
+ type: TPROP_IMMUTABLE_PROPERTIES,
1009
+ encode: (w) => {
1010
+ const raw = props.immutable_properties!
1011
+ w.writeVarInt(BigInt(raw.byteLength))
1012
+ w.writeBytes(raw)
1013
+ },
1014
+ })
1015
+ }
1016
+ if (props.default_publisher_priority !== undefined) {
1017
+ entries.push({
1018
+ type: TPROP_DEFAULT_PUBLISHER_PRIORITY,
1019
+ encode: (w) => w.writeVarInt(props.default_publisher_priority!),
1020
+ })
1021
+ }
1022
+ if (props.default_publisher_group_order !== undefined) {
1023
+ entries.push({
1024
+ type: TPROP_DEFAULT_PUBLISHER_GROUP_ORDER,
1025
+ encode: (w) => w.writeVarInt(props.default_publisher_group_order!),
1026
+ })
1027
+ }
1028
+ if (props.dynamic_groups !== undefined) {
1029
+ entries.push({
1030
+ type: TPROP_DYNAMIC_GROUPS,
1031
+ encode: (w) => w.writeVarInt(props.dynamic_groups!),
1032
+ })
1033
+ }
1034
+
1035
+ if (props.unknown) {
1036
+ for (const u of props.unknown) {
1037
+ const id = BigInt(u.id)
1038
+ entries.push({
1039
+ type: id,
1040
+ encode: (w) => {
1041
+ if (id % 2n === 0n) {
1042
+ const raw = hexToBytes(u.raw_hex)
1043
+ const tmpReader = new BufferReader(raw)
1044
+ w.writeVarInt(tmpReader.readVarInt())
1045
+ } else {
1046
+ const raw = hexToBytes(u.raw_hex)
1047
+ w.writeVarInt(BigInt(raw.byteLength))
1048
+ w.writeBytes(raw)
1049
+ }
1050
+ },
1051
+ })
1052
+ }
1053
+ }
1054
+
1055
+ entries.sort((a, b) => (a.type < b.type ? -1 : a.type > b.type ? 1 : 0))
1056
+
1057
+ let prevType = 0n
1058
+ for (const entry of entries) {
1059
+ writer.writeVarInt(entry.type - prevType)
1060
+ entry.encode(writer)
1061
+ prevType = entry.type
1062
+ }
1063
+ }
1064
+
1065
+ function decodeTrackProperties(reader: BufferReader, payloadEnd: number): Draft21TrackProperties {
1066
+ const result: Draft21TrackProperties = {}
1067
+ const unknown: UnknownParam[] = []
1068
+ let prevType = 0n
1069
+
1070
+ while (reader.offset < payloadEnd) {
1071
+ const delta = reader.readVarInt()
1072
+ const propType = prevType + delta
1073
+ prevType = propType
1074
+
1075
+ if (propType % 2n === 0n) {
1076
+ // Even: single varint value
1077
+ const value = reader.readVarInt()
1078
+ if (propType === TPROP_OBJECT_DELIVERY_TIMEOUT) {
1079
+ result.object_delivery_timeout = value
1080
+ } else if (propType === TPROP_MAX_CACHE_DURATION) {
1081
+ result.max_cache_duration = value
1082
+ } else if (propType === TPROP_SUBGROUP_DELIVERY_TIMEOUT) {
1083
+ result.subgroup_delivery_timeout = value
1084
+ } else if (propType === TPROP_DEFAULT_PUBLISHER_PRIORITY) {
1085
+ result.default_publisher_priority = value
1086
+ } else if (propType === TPROP_DEFAULT_PUBLISHER_GROUP_ORDER) {
1087
+ result.default_publisher_group_order = value
1088
+ } else if (propType === TPROP_DYNAMIC_GROUPS) {
1089
+ result.dynamic_groups = value
1090
+ } else {
1091
+ // Section 16.8: endpoints MUST ignore unknown Property types, skipping
1092
+ // them by the Key-Value-Pair odd/even rule. Kept for passthrough.
1093
+ const tmpWriter = new BufferWriter(16)
1094
+ tmpWriter.writeVarInt(value)
1095
+ const raw = tmpWriter.finish()
1096
+ unknown.push({
1097
+ id: `0x${propType.toString(16)}`,
1098
+ length: raw.byteLength,
1099
+ raw_hex: bytesToHex(raw),
1100
+ })
1101
+ }
1102
+ } else {
1103
+ // Odd: length-prefixed bytes
1104
+ const length = Number(reader.readVarInt())
1105
+ const bytes = reader.readBytes(length)
1106
+ if (propType === TPROP_IMMUTABLE_PROPERTIES) {
1107
+ result.immutable_properties = bytes
1108
+ } else {
1109
+ unknown.push({
1110
+ id: `0x${propType.toString(16)}`,
1111
+ length,
1112
+ raw_hex: bytesToHex(bytes),
1113
+ })
1114
+ }
1115
+ }
1116
+ }
1117
+
1118
+ if (unknown.length > 0) result.unknown = unknown
1119
+ return result
1120
+ }
1121
+
1122
+ // ─── Redirect Structure Encoding/Decoding ───────────────────────────────────
1123
+
1124
+ function encodeRedirect(redirect: Redirect, w: BufferWriter): void {
1125
+ const uri = textEncoder.encode(redirect.connect_uri)
1126
+ w.writeVarInt(BigInt(uri.byteLength))
1127
+ w.writeBytes(uri)
1128
+ w.writeTuple(redirect.track_namespace)
1129
+ w.writeString(redirect.track_name)
1130
+ }
1131
+
1132
+ function decodeRedirect(r: BufferReader): Redirect {
1133
+ const uriLen = Number(r.readVarInt())
1134
+ const uriBytes = r.readBytes(uriLen)
1135
+ const connect_uri = textDecoder.decode(uriBytes)
1136
+ // draft-21 Section 9.4.1 names this pair "the Redirect target". draft-19's
1137
+ // rule that a zero-length namespace and name meant "reuse the values from the
1138
+ // original request" was deleted, so an empty pair is a literal empty pair.
1139
+ const track_namespace = r.readTuple()
1140
+ const track_name = r.readString()
1141
+ return { connect_uri, track_namespace, track_name }
1142
+ }
1143
+
1144
+ // ─── Payload Encoders ──────────────────────────────────────────────────────────
1145
+
1146
+ function encodeSetupPayload(msg: Draft21Message & { type: 'setup' }, w: BufferWriter): void {
1147
+ encodeSetupOptions(msg.options, w)
1148
+ }
1149
+
1150
+ function encodeSubscribePayload(
1151
+ msg: Draft21Message & { type: 'subscribe' },
1152
+ w: BufferWriter,
1153
+ ): void {
1154
+ w.writeVarInt(msg.request_id)
1155
+ w.writeTuple(msg.track_namespace)
1156
+ w.writeString(msg.track_name)
1157
+ encodeParams(msg.parameters, w)
1158
+ }
1159
+
1160
+ function encodeSubscribeOkPayload(
1161
+ msg: Draft21Message & { type: 'subscribe_ok' },
1162
+ w: BufferWriter,
1163
+ ): void {
1164
+ w.writeVarInt(msg.track_alias)
1165
+ encodeParams(msg.parameters, w)
1166
+ encodeTrackProperties(msg.track_properties, w)
1167
+ }
1168
+
1169
+ function encodeRequestUpdatePayload(
1170
+ msg: Draft21Message & { type: 'request_update' },
1171
+ w: BufferWriter,
1172
+ ): void {
1173
+ w.writeVarInt(msg.request_id)
1174
+ encodeParams(msg.parameters, w)
1175
+ }
1176
+
1177
+ function encodePublishStateNotifyPayload(
1178
+ msg: Draft21Message & { type: 'publish_state_notify' },
1179
+ w: BufferWriter,
1180
+ ): void {
1181
+ // No Request ID: the subscription's bidirectional stream identifies it.
1182
+ encodeParams(msg.parameters, w)
1183
+ }
1184
+
1185
+ function encodePublishPayload(msg: Draft21Message & { type: 'publish' }, w: BufferWriter): void {
1186
+ w.writeVarInt(msg.request_id)
1187
+ w.writeTuple(msg.track_namespace)
1188
+ w.writeString(msg.track_name)
1189
+ w.writeVarInt(msg.track_alias)
1190
+ encodeParams(msg.parameters, w)
1191
+ encodeTrackProperties(msg.track_properties, w)
1192
+ }
1193
+
1194
+ function encodePublishDonePayload(
1195
+ msg: Draft21Message & { type: 'publish_done' },
1196
+ w: BufferWriter,
1197
+ ): void {
1198
+ w.writeVarInt(msg.status_code)
1199
+ // bigint all the way: UNKNOWN_STREAM_COUNT is 2^64 - 1 and MOQT's vi64
1200
+ // reaches it in nine bytes.
1201
+ w.writeVarInt(msg.stream_count)
1202
+ w.writeString(msg.reason_phrase)
1203
+ }
1204
+
1205
+ function encodePublishNamespacePayload(
1206
+ msg: Draft21Message & { type: 'publish_namespace' },
1207
+ w: BufferWriter,
1208
+ ): void {
1209
+ w.writeVarInt(msg.request_id)
1210
+ w.writeTuple(msg.track_namespace)
1211
+ encodeParams(msg.parameters, w)
1212
+ }
1213
+
1214
+ function encodeNamespacePayload(
1215
+ msg: Draft21Message & { type: 'namespace' },
1216
+ w: BufferWriter,
1217
+ ): void {
1218
+ w.writeTuple(msg.namespace_suffix)
1219
+ }
1220
+
1221
+ function encodeNamespaceDonePayload(
1222
+ msg: Draft21Message & { type: 'namespace_done' },
1223
+ w: BufferWriter,
1224
+ ): void {
1225
+ w.writeTuple(msg.namespace_suffix)
1226
+ }
1227
+
1228
+ function encodeSubscribeNamespacePayload(
1229
+ msg: Draft21Message & { type: 'subscribe_namespace' },
1230
+ w: BufferWriter,
1231
+ ): void {
1232
+ w.writeVarInt(msg.request_id)
1233
+ w.writeTuple(msg.namespace_prefix)
1234
+ encodeParams(msg.parameters, w)
1235
+ }
1236
+
1237
+ function encodeSubscribeTracksPayload(
1238
+ msg: Draft21Message & { type: 'subscribe_tracks' },
1239
+ w: BufferWriter,
1240
+ ): void {
1241
+ w.writeVarInt(msg.request_id)
1242
+ w.writeTuple(msg.namespace_prefix)
1243
+ encodeParams(msg.parameters, w)
1244
+ }
1245
+
1246
+ function encodePublishSkippedPayload(
1247
+ msg: Draft21Message & { type: 'publish_skipped' },
1248
+ w: BufferWriter,
1249
+ ): void {
1250
+ w.writeTuple(msg.namespace_suffix)
1251
+ w.writeString(msg.track_name)
1252
+ }
1253
+
1254
+ /**
1255
+ * FETCH (0x16) — draft-21 Section 9.11, Figure 15.
1256
+ *
1257
+ * Track Namespace and Track Name are inline, in exactly the positions they
1258
+ * occupied inside draft-19's Standalone Fetch struct. There is no Fetch Type
1259
+ * ahead of them and no Start/End Location behind them: the range is a
1260
+ * LOCATION_FILTER parameter now, and it is inclusive (see
1261
+ * {@link encodeLocationFilter}).
1262
+ */
1263
+ function encodeFetchPayload(msg: Draft21Message & { type: 'fetch' }, w: BufferWriter): void {
1264
+ w.writeVarInt(msg.request_id)
1265
+ w.writeTuple(msg.track_namespace)
1266
+ w.writeString(msg.track_name)
1267
+ encodeParams(msg.parameters, w)
1268
+ }
1269
+
1270
+ function encodeFetchOkPayload(msg: Draft21Message & { type: 'fetch_ok' }, w: BufferWriter): void {
1271
+ w.writeUint8(msg.end_of_track)
1272
+ // End Location, written as given. draft-19 wrote "the last Object, plus 1"
1273
+ // here and used an Object of 0 to mean the whole group; draft-21 Section
1274
+ // 10.14 dropped both, so this is the last Object the response covers and no
1275
+ // arithmetic is applied on the way out (DECISIONS.md D4).
1276
+ w.writeVarInt(msg.end_group)
1277
+ w.writeVarInt(msg.end_object)
1278
+ encodeParams(msg.parameters, w)
1279
+ encodeTrackProperties(msg.track_properties, w)
1280
+ }
1281
+
1282
+ function encodeTrackStatusPayload(
1283
+ msg: Draft21Message & { type: 'track_status' },
1284
+ w: BufferWriter,
1285
+ ): void {
1286
+ w.writeVarInt(msg.request_id)
1287
+ w.writeTuple(msg.track_namespace)
1288
+ w.writeString(msg.track_name)
1289
+ encodeParams(msg.parameters, w)
1290
+ }
1291
+
1292
+ function encodeRequestOkPayload(
1293
+ msg: Draft21Message & { type: 'request_ok' },
1294
+ w: BufferWriter,
1295
+ ): void {
1296
+ encodeParams(msg.parameters, w)
1297
+ encodeTrackProperties(msg.track_properties, w)
1298
+ }
1299
+
1300
+ function encodeRequestErrorPayload(
1301
+ msg: Draft21Message & { type: 'request_error' },
1302
+ w: BufferWriter,
1303
+ ): void {
1304
+ w.writeVarInt(msg.error_code)
1305
+ w.writeVarInt(msg.retry_interval)
1306
+ w.writeString(msg.reason_phrase)
1307
+ if (msg.error_code === REQUEST_ERROR_REDIRECT && msg.redirect) {
1308
+ encodeRedirect(msg.redirect, w)
1309
+ }
1310
+ }
1311
+
1312
+ function encodeGoAwayPayload(msg: Draft21Message & { type: 'goaway' }, w: BufferWriter): void {
1313
+ w.writeString(msg.new_session_uri)
1314
+ w.writeVarInt(msg.timeout)
1315
+ }
1316
+
1317
+ // ─── Payload Decoders ──────────────────────────────────────────────────────────
1318
+
1319
+ function decodeSetupPayload(r: BufferReader, payloadEnd: number): Draft21Message {
1320
+ const options = decodeSetupOptions(r, payloadEnd)
1321
+ return { type: 'setup', options }
1322
+ }
1323
+
1324
+ function decodeSubscribePayload(r: BufferReader): Draft21Message {
1325
+ const request_id = r.readVarInt()
1326
+ const track_namespace = r.readTuple()
1327
+ const track_name = r.readString()
1328
+ const parameters = decodeParams(r, 'subscribe')
1329
+ return {
1330
+ type: 'subscribe',
1331
+ request_id,
1332
+ track_namespace,
1333
+ track_name,
1334
+ parameters,
1335
+ }
1336
+ }
1337
+
1338
+ function decodeSubscribeOkPayload(r: BufferReader, payloadEnd: number): Draft21Message {
1339
+ const track_alias = r.readVarInt()
1340
+ const parameters = decodeParams(r, 'subscribe_ok')
1341
+ const track_properties = decodeTrackProperties(r, payloadEnd)
1342
+ return { type: 'subscribe_ok', track_alias, parameters, track_properties }
1343
+ }
1344
+
1345
+ function decodeRequestUpdatePayload(r: BufferReader): Draft21Message {
1346
+ const request_id = r.readVarInt()
1347
+ const parameters = decodeParams(r, 'request_update')
1348
+ return {
1349
+ type: 'request_update',
1350
+ request_id,
1351
+ parameters,
1352
+ }
1353
+ }
1354
+
1355
+ function decodePublishStateNotifyPayload(r: BufferReader): Draft21Message {
1356
+ const parameters = decodeParams(r, 'publish_state_notify')
1357
+ return { type: 'publish_state_notify', parameters }
1358
+ }
1359
+
1360
+ function decodePublishPayload(r: BufferReader, payloadEnd: number): Draft21Message {
1361
+ const request_id = r.readVarInt()
1362
+ const track_namespace = r.readTuple()
1363
+ const track_name = r.readString()
1364
+ const track_alias = r.readVarInt()
1365
+ const parameters = decodeParams(r, 'publish')
1366
+ const track_properties = decodeTrackProperties(r, payloadEnd)
1367
+ return {
1368
+ type: 'publish',
1369
+ request_id,
1370
+ track_namespace,
1371
+ track_name,
1372
+ track_alias,
1373
+ parameters,
1374
+ track_properties,
1375
+ }
1376
+ }
1377
+
1378
+ function decodePublishDonePayload(r: BufferReader): Draft21Message {
1379
+ // An unassigned status code decodes. Section 13: "Receipt of an unknown error
1380
+ // code in any error context (Session Termination, REQUEST_ERROR,
1381
+ // PUBLISH_DONE, or Data Stream Reset) MUST be treated as equivalent to
1382
+ // INTERNAL_ERROR for that context. An endpoint MUST NOT close the session
1383
+ // because it received an unknown error code in a REQUEST_ERROR or
1384
+ // PUBLISH_DONE." A retired code is an unknown code, so refusing the frame
1385
+ // here would break that MUST NOT — and a decoder that refuses cannot hand the
1386
+ // message to anything that could apply the INTERNAL_ERROR reading.
1387
+ // RETIRED_PUBLISH_DONE_CODES stays exported for callers that want to flag the
1388
+ // peer as speaking an older draft; it is advisory, not a decode gate.
1389
+ const status_code = r.readVarInt()
1390
+ const stream_count = r.readVarInt()
1391
+ const reason_phrase = r.readString()
1392
+ return { type: 'publish_done', status_code, stream_count, reason_phrase }
1393
+ }
1394
+
1395
+ function decodePublishNamespacePayload(r: BufferReader): Draft21Message {
1396
+ const request_id = r.readVarInt()
1397
+ const track_namespace = r.readTuple()
1398
+ const parameters = decodeParams(r, 'publish_namespace')
1399
+ return {
1400
+ type: 'publish_namespace',
1401
+ request_id,
1402
+ track_namespace,
1403
+ parameters,
1404
+ }
1405
+ }
1406
+
1407
+ function decodeNamespacePayload(r: BufferReader): Draft21Message {
1408
+ const namespace_suffix = r.readTuple()
1409
+ return { type: 'namespace', namespace_suffix }
1410
+ }
1411
+
1412
+ function decodeNamespaceDonePayload(r: BufferReader): Draft21Message {
1413
+ const namespace_suffix = r.readTuple()
1414
+ return { type: 'namespace_done', namespace_suffix }
1415
+ }
1416
+
1417
+ function decodeSubscribeNamespacePayload(r: BufferReader): Draft21Message {
1418
+ const request_id = r.readVarInt()
1419
+ const namespace_prefix = r.readTuple()
1420
+ const parameters = decodeParams(r, 'subscribe_namespace')
1421
+ return {
1422
+ type: 'subscribe_namespace',
1423
+ request_id,
1424
+ namespace_prefix,
1425
+ parameters,
1426
+ }
1427
+ }
1428
+
1429
+ function decodeSubscribeTracksPayload(r: BufferReader): Draft21Message {
1430
+ const request_id = r.readVarInt()
1431
+ const namespace_prefix = r.readTuple()
1432
+ const parameters = decodeParams(r, 'subscribe_tracks')
1433
+ return {
1434
+ type: 'subscribe_tracks',
1435
+ request_id,
1436
+ namespace_prefix,
1437
+ parameters,
1438
+ }
1439
+ }
1440
+
1441
+ function decodePublishSkippedPayload(r: BufferReader): Draft21Message {
1442
+ const namespace_suffix = r.readTuple()
1443
+ const track_name = r.readString()
1444
+ return { type: 'publish_skipped', namespace_suffix, track_name }
1445
+ }
1446
+
1447
+ function decodeFetchPayload(r: BufferReader): Draft21Message {
1448
+ // No Fetch Type varint here. The first field after Request ID is the Track
1449
+ // Namespace tuple's own field count — which is exactly the byte a draft-19
1450
+ // decoder would read as a Fetch Type. See the Draft21Fetch doc comment.
1451
+ const request_id = r.readVarInt()
1452
+ const track_namespace = r.readTuple()
1453
+ const track_name = r.readString()
1454
+ const parameters = decodeParams(r, 'fetch')
1455
+ return {
1456
+ type: 'fetch',
1457
+ request_id,
1458
+ track_namespace,
1459
+ track_name,
1460
+ parameters,
1461
+ }
1462
+ }
1463
+
1464
+ function decodeFetchOkPayload(r: BufferReader, payloadEnd: number): Draft21Message {
1465
+ const end_of_track = r.readUint8()
1466
+ // Inclusive end. No decrement here, as no encoder incremented.
1467
+ const end_group = r.readVarInt()
1468
+ const end_object = r.readVarInt()
1469
+ const parameters = decodeParams(r, 'fetch_ok')
1470
+ const track_properties = decodeTrackProperties(r, payloadEnd)
1471
+ return {
1472
+ type: 'fetch_ok',
1473
+ end_of_track,
1474
+ end_group,
1475
+ end_object,
1476
+ parameters,
1477
+ track_properties,
1478
+ }
1479
+ }
1480
+
1481
+ function decodeTrackStatusPayload(r: BufferReader): Draft21Message {
1482
+ const request_id = r.readVarInt()
1483
+ const track_namespace = r.readTuple()
1484
+ const track_name = r.readString()
1485
+ const parameters = decodeParams(r, 'track_status')
1486
+ return {
1487
+ type: 'track_status',
1488
+ request_id,
1489
+ track_namespace,
1490
+ track_name,
1491
+ parameters,
1492
+ }
1493
+ }
1494
+
1495
+ function decodeRequestOkPayload(r: BufferReader, payloadEnd: number): Draft21Message {
1496
+ const parameters = decodeParams(r, 'request_ok')
1497
+ const track_properties = decodeTrackProperties(r, payloadEnd)
1498
+ return { type: 'request_ok', parameters, track_properties }
1499
+ }
1500
+
1501
+ function decodeRequestErrorPayload(r: BufferReader, payloadEnd: number): Draft21Message {
1502
+ // Unassigned error codes decode — see decodePublishDonePayload for the
1503
+ // Section 13 rule this obeys. RETIRED_REQUEST_ERROR_CODES is advisory.
1504
+ const error_code = r.readVarInt()
1505
+ const retry_interval = r.readVarInt()
1506
+ const reason_phrase = r.readString()
1507
+ let redirect: Redirect | undefined
1508
+ if (error_code === REQUEST_ERROR_REDIRECT && r.offset < payloadEnd) {
1509
+ redirect = decodeRedirect(r)
1510
+ }
1511
+ if (redirect !== undefined) {
1512
+ return { type: 'request_error', error_code, retry_interval, reason_phrase, redirect }
1513
+ }
1514
+ return { type: 'request_error', error_code, retry_interval, reason_phrase }
1515
+ }
1516
+
1517
+ function decodeGoAwayPayload(r: BufferReader): Draft21Message {
1518
+ const new_session_uri = r.readString()
1519
+ const timeout = r.readVarInt()
1520
+ return { type: 'goaway', new_session_uri, timeout }
1521
+ }
1522
+
1523
+ // ─── Public API ────────────────────────────────────────────────────────────────
1524
+
1525
+ /**
1526
+ * Encode a draft-21 control message: type (vi64) + length (uint16 BE) + payload.
1527
+ */
1528
+ export function encodeMessage(message: Draft21Message): Uint8Array {
1529
+ const typeId = MESSAGE_ID_MAP.get(message.type)
1530
+ if (typeId === undefined) {
1531
+ throw new Error(`Unknown message type: ${message.type}`)
1532
+ }
1533
+
1534
+ const payloadWriter = new BufferWriter()
1535
+ encodePayload(message, payloadWriter)
1536
+ const payload = payloadWriter.finishView()
1537
+
1538
+ if (payload.byteLength > 0xffff) {
1539
+ throw new Error(`Payload too large for 16-bit length: ${payload.byteLength}`)
1540
+ }
1541
+
1542
+ const writer = new BufferWriter(payload.byteLength + 16)
1543
+ writer.writeVarInt(typeId)
1544
+ writer.writeUint8((payload.byteLength >> 8) & 0xff)
1545
+ writer.writeUint8(payload.byteLength & 0xff)
1546
+ writer.writeBytes(payload)
1547
+
1548
+ return writer.finish()
1549
+ }
1550
+
1551
+ function encodePayload(msg: Draft21Message, w: BufferWriter): void {
1552
+ switch (msg.type) {
1553
+ case 'setup':
1554
+ return encodeSetupPayload(msg, w)
1555
+ case 'subscribe':
1556
+ return encodeSubscribePayload(msg, w)
1557
+ case 'subscribe_ok':
1558
+ return encodeSubscribeOkPayload(msg, w)
1559
+ case 'request_update':
1560
+ return encodeRequestUpdatePayload(msg, w)
1561
+ case 'publish_state_notify':
1562
+ return encodePublishStateNotifyPayload(msg, w)
1563
+ case 'publish':
1564
+ return encodePublishPayload(msg, w)
1565
+ case 'publish_done':
1566
+ return encodePublishDonePayload(msg, w)
1567
+ case 'publish_namespace':
1568
+ return encodePublishNamespacePayload(msg, w)
1569
+ case 'namespace':
1570
+ return encodeNamespacePayload(msg, w)
1571
+ case 'namespace_done':
1572
+ return encodeNamespaceDonePayload(msg, w)
1573
+ case 'subscribe_namespace':
1574
+ return encodeSubscribeNamespacePayload(msg, w)
1575
+ case 'subscribe_tracks':
1576
+ return encodeSubscribeTracksPayload(msg, w)
1577
+ case 'publish_skipped':
1578
+ return encodePublishSkippedPayload(msg, w)
1579
+ case 'fetch':
1580
+ return encodeFetchPayload(msg, w)
1581
+ case 'fetch_ok':
1582
+ return encodeFetchOkPayload(msg, w)
1583
+ case 'track_status':
1584
+ return encodeTrackStatusPayload(msg, w)
1585
+ case 'request_ok':
1586
+ return encodeRequestOkPayload(msg, w)
1587
+ case 'request_error':
1588
+ return encodeRequestErrorPayload(msg, w)
1589
+ case 'goaway':
1590
+ return encodeGoAwayPayload(msg, w)
1591
+ default: {
1592
+ const _exhaustive: never = msg
1593
+ throw new Error(`Unhandled message type: ${(_exhaustive as Draft21Message).type}`)
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ /**
1599
+ * Decode a draft-21 control message from bytes (type + uint16 length + payload).
1600
+ */
1601
+ export function decodeMessage(bytes: Uint8Array): DecodeResult<Draft21Message> {
1602
+ try {
1603
+ const reader = new BufferReader(bytes)
1604
+ const typeId = reader.readVarInt()
1605
+
1606
+ const lenHi = reader.readUint8()
1607
+ const lenLo = reader.readUint8()
1608
+ const payloadLength = (lenHi << 8) | lenLo
1609
+
1610
+ const payloadBytes = reader.readBytes(payloadLength)
1611
+ const payloadReader = new BufferReader(payloadBytes)
1612
+
1613
+ let message: Draft21Message
1614
+
1615
+ if (typeId === MSG_SETUP) {
1616
+ message = decodeSetupPayload(payloadReader, payloadLength)
1617
+ } else if (typeId === MSG_SUBSCRIBE) {
1618
+ message = decodeSubscribePayload(payloadReader)
1619
+ } else if (typeId === MSG_SUBSCRIBE_OK) {
1620
+ message = decodeSubscribeOkPayload(payloadReader, payloadLength)
1621
+ } else if (typeId === MSG_REQUEST_UPDATE) {
1622
+ message = decodeRequestUpdatePayload(payloadReader)
1623
+ } else if (typeId === MSG_PUBLISH_STATE_NOTIFY) {
1624
+ message = decodePublishStateNotifyPayload(payloadReader)
1625
+ } else if (typeId === MSG_PUBLISH) {
1626
+ message = decodePublishPayload(payloadReader, payloadLength)
1627
+ } else if (typeId === MSG_PUBLISH_DONE) {
1628
+ message = decodePublishDonePayload(payloadReader)
1629
+ } else if (typeId === MSG_PUBLISH_NAMESPACE) {
1630
+ message = decodePublishNamespacePayload(payloadReader)
1631
+ } else if (typeId === MSG_NAMESPACE) {
1632
+ message = decodeNamespacePayload(payloadReader)
1633
+ } else if (typeId === MSG_NAMESPACE_DONE) {
1634
+ message = decodeNamespaceDonePayload(payloadReader)
1635
+ } else if (typeId === MSG_SUBSCRIBE_NAMESPACE) {
1636
+ message = decodeSubscribeNamespacePayload(payloadReader)
1637
+ } else if (typeId === MSG_SUBSCRIBE_TRACKS) {
1638
+ message = decodeSubscribeTracksPayload(payloadReader)
1639
+ } else if (typeId === MSG_PUBLISH_SKIPPED) {
1640
+ message = decodePublishSkippedPayload(payloadReader)
1641
+ } else if (typeId === MSG_FETCH) {
1642
+ message = decodeFetchPayload(payloadReader)
1643
+ } else if (typeId === MSG_FETCH_OK) {
1644
+ message = decodeFetchOkPayload(payloadReader, payloadLength)
1645
+ } else if (typeId === MSG_TRACK_STATUS) {
1646
+ message = decodeTrackStatusPayload(payloadReader)
1647
+ } else if (typeId === MSG_REQUEST_OK) {
1648
+ message = decodeRequestOkPayload(payloadReader, payloadLength)
1649
+ } else if (typeId === MSG_REQUEST_ERROR) {
1650
+ message = decodeRequestErrorPayload(payloadReader, payloadLength)
1651
+ } else if (typeId === MSG_GOAWAY) {
1652
+ message = decodeGoAwayPayload(payloadReader)
1653
+ } else {
1654
+ // Section 9: "An endpoint that receives an unknown message type MUST
1655
+ // close the session."
1656
+ return {
1657
+ ok: false,
1658
+ error: new DecodeError(
1659
+ 'UNKNOWN_MESSAGE_TYPE',
1660
+ `Unknown message type ID: 0x${typeId.toString(16)}`,
1661
+ 0,
1662
+ ),
1663
+ }
1664
+ }
1665
+
1666
+ return { ok: true, value: message, bytesRead: reader.offset }
1667
+ } catch (e) {
1668
+ if (e instanceof DecodeError) {
1669
+ return { ok: false, error: e }
1670
+ }
1671
+ throw e
1672
+ }
1673
+ }
1674
+
1675
+ // ─── Data Stream Encoding/Decoding (re-exported from data-streams.ts) ───────
1676
+
1677
+ import {
1678
+ createDataStreamDecoder,
1679
+ createFetchStreamDecoder,
1680
+ createSubgroupStreamDecoder,
1681
+ decodeDatagram,
1682
+ decodeDataStream,
1683
+ decodeFetchStream,
1684
+ decodeSubgroupStream,
1685
+ encodeDatagram,
1686
+ encodeFetchStream,
1687
+ encodeSubgroupStream,
1688
+ } from './data-streams.js'
1689
+
1690
+ export {
1691
+ createDataStreamDecoder,
1692
+ createFetchStreamDecoder,
1693
+ createSubgroupStreamDecoder,
1694
+ decodeDatagram,
1695
+ decodeDataStream,
1696
+ decodeFetchStream,
1697
+ decodeSubgroupStream,
1698
+ encodeDatagram,
1699
+ encodeFetchStream,
1700
+ encodeSubgroupStream,
1701
+ }
1702
+
1703
+ // ─── Stream Decoders ───────────────────────────────────────────────────────────
1704
+
1705
+ export function createStreamDecoder(): TransformStream<Uint8Array, Draft21Message> {
1706
+ let buffer = new Uint8Array(0)
1707
+ let offset = 0
1708
+
1709
+ return new TransformStream<Uint8Array, Draft21Message>({
1710
+ transform(chunk, controller) {
1711
+ if (offset > 0) {
1712
+ buffer = buffer.subarray(offset)
1713
+ offset = 0
1714
+ }
1715
+ const newBuffer = new Uint8Array(buffer.length + chunk.length)
1716
+ newBuffer.set(buffer, 0)
1717
+ newBuffer.set(chunk, buffer.length)
1718
+ buffer = newBuffer
1719
+
1720
+ while (offset < buffer.length) {
1721
+ const result = decodeMessage(buffer.subarray(offset))
1722
+ if (!result.ok) {
1723
+ if (result.error.code === 'UNEXPECTED_END') {
1724
+ break
1725
+ }
1726
+ controller.error(result.error)
1727
+ return
1728
+ }
1729
+ controller.enqueue(result.value)
1730
+ offset += result.bytesRead
1731
+ }
1732
+ },
1733
+
1734
+ flush(controller) {
1735
+ if (offset < buffer.length) {
1736
+ controller.error(
1737
+ new DecodeError('UNEXPECTED_END', 'Stream ended with incomplete message data', 0),
1738
+ )
1739
+ }
1740
+ },
1741
+ })
1742
+ }
1743
+
1744
+ // ─── Codec Factory ─────────────────────────────────────────────────────────────
1745
+
1746
+ export interface Draft21Codec extends BaseCodec<Draft21Message> {
1747
+ readonly draft: '21'
1748
+ encodeSubgroupStream(stream: SubgroupStream): Uint8Array
1749
+ encodeDatagram(dg: DatagramObject): Uint8Array
1750
+ encodeFetchStream(stream: FetchStream): Uint8Array
1751
+ decodeSubgroupStream(bytes: Uint8Array): DecodeResult<SubgroupStream>
1752
+ decodeDatagram(bytes: Uint8Array): DecodeResult<DatagramObject>
1753
+ decodeFetchStream(bytes: Uint8Array): DecodeResult<FetchStream>
1754
+ decodeDataStream(
1755
+ streamType: 'subgroup' | 'datagram' | 'fetch',
1756
+ bytes: Uint8Array,
1757
+ ): DecodeResult<Draft21DataStream>
1758
+ createStreamDecoder(): TransformStream<Uint8Array, Draft21Message>
1759
+ createSubgroupStreamDecoder(): TransformStream<Uint8Array, SubgroupStreamHeader | ObjectPayload>
1760
+ createFetchStreamDecoder(): TransformStream<Uint8Array, FetchStreamHeader | ObjectPayload>
1761
+ createDataStreamDecoder(): TransformStream<Uint8Array, DataStreamEvent>
1762
+ }
1763
+
1764
+ export function createDraft21Codec(): Draft21Codec {
1765
+ return {
1766
+ draft: '21',
1767
+ encodeMessage,
1768
+ decodeMessage,
1769
+ encodeSubgroupStream,
1770
+ encodeDatagram,
1771
+ encodeFetchStream,
1772
+ decodeSubgroupStream,
1773
+ decodeDatagram,
1774
+ decodeFetchStream,
1775
+ decodeDataStream,
1776
+ createStreamDecoder,
1777
+ createSubgroupStreamDecoder,
1778
+ createFetchStreamDecoder,
1779
+ createDataStreamDecoder,
1780
+ }
1781
+ }
1782
+
1783
+ /**
1784
+ * Overwrite every Authorization Token value in a draft-21 control frame.
1785
+ *
1786
+ * One decode, whose *output is discarded*: the point is not to read the
1787
+ * message, it is to learn where the tokens were so the caller never has to hold
1788
+ * a frame that contains one. A caller that also wants the message should decode
1789
+ * the **redacted** frame with {@link decodeMessage}, which then cannot produce a
1790
+ * token value because there is no longer one in the bytes.
1791
+ *
1792
+ * A frame that fails to decode is still redacted as far as the decode got. That
1793
+ * is the useful direction to fail in: a value that was read is a value that was
1794
+ * exposed, whatever went wrong after it.
1795
+ *
1796
+ * See {@link AuthRedaction.incomplete} for the one result a caller must not
1797
+ * treat as clean, and {@link AuthRedaction.decoded} for the residual it cannot
1798
+ * close.
1799
+ */
1800
+ export function redactAuthTokens(bytes: Uint8Array): AuthRedaction {
1801
+ let payloadStart: number
1802
+ try {
1803
+ const probe = new BufferReader(bytes)
1804
+ probe.readVarInt()
1805
+ probe.readUint8()
1806
+ probe.readUint8()
1807
+ payloadStart = probe.offset
1808
+ } catch {
1809
+ // Too short to carry a message header, and so too short for the parameters
1810
+ // that carry a token. Reported as not decoded rather than as clean: the
1811
+ // framer only ever emits complete frames, so a caller that gets here is
1812
+ // holding something this function did not check.
1813
+ return { bytes, redacted: 0, incomplete: false, decoded: false }
1814
+ }
1815
+ return redactWith(bytes, payloadStart, () => decodeMessage(bytes).ok)
1816
+ }