@blockcast/mmt-container 0.4.1 → 0.5.0-main.34c7b9c2cfa0

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 (87) hide show
  1. package/README.md +2 -2
  2. package/dist/browser-runtime.js +3769 -364
  3. package/dist/browser-runtime.js.map +4 -4
  4. package/dist/cmcd-producer.d.ts +5 -1
  5. package/dist/cmcd-producer.d.ts.map +1 -1
  6. package/dist/cmcd-producer.js.map +2 -2
  7. package/dist/delivery-catalog.js +18 -2
  8. package/dist/delivery-catalog.js.map +2 -2
  9. package/dist/fec-manager.d.ts +21 -16
  10. package/dist/fec-manager.d.ts.map +1 -1
  11. package/dist/fec-manager.js +197 -80
  12. package/dist/fec-manager.js.map +3 -3
  13. package/dist/held-layer-admission.d.ts +58 -0
  14. package/dist/held-layer-admission.d.ts.map +1 -0
  15. package/dist/held-layer-admission.js +105 -0
  16. package/dist/held-layer-admission.js.map +7 -0
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js.map +2 -2
  20. package/dist/mmtp-container-parser.d.ts +10 -0
  21. package/dist/mmtp-container-parser.d.ts.map +1 -1
  22. package/dist/mmtp-container-parser.js +31 -1
  23. package/dist/mmtp-container-parser.js.map +2 -2
  24. package/dist/mmtp-container-parser.no-modules.js +2713 -1024
  25. package/dist/mmtp-container-parser.no-modules.js.map +4 -4
  26. package/dist/mmtp-decode-pipeline.js +31 -1
  27. package/dist/mmtp-decode-pipeline.js.map +2 -2
  28. package/dist/mmtp-fec-session.d.ts +36 -3
  29. package/dist/mmtp-fec-session.d.ts.map +1 -1
  30. package/dist/mmtp-fec-session.js +114 -5
  31. package/dist/mmtp-fec-session.js.map +3 -3
  32. package/dist/mmtp-router.d.ts +96 -29
  33. package/dist/mmtp-router.d.ts.map +1 -1
  34. package/dist/mmtp-router.js +522 -205
  35. package/dist/mmtp-router.js.map +2 -2
  36. package/dist/multicast-auth-catalog.d.ts +10 -2
  37. package/dist/multicast-auth-catalog.d.ts.map +1 -1
  38. package/dist/multicast-auth-catalog.js +422 -22
  39. package/dist/multicast-auth-catalog.js.map +4 -4
  40. package/dist/multicast-manager.d.ts +78 -11
  41. package/dist/multicast-manager.d.ts.map +1 -1
  42. package/dist/multicast-manager.js +3663 -333
  43. package/dist/multicast-manager.js.map +4 -4
  44. package/dist/multicast-transport.d.ts +4 -1
  45. package/dist/multicast-transport.d.ts.map +1 -1
  46. package/dist/multicast-transport.js +20 -3
  47. package/dist/multicast-transport.js.map +2 -2
  48. package/dist/no-modules.d.ts +1 -2
  49. package/dist/no-modules.d.ts.map +1 -1
  50. package/dist/no-modules.js +2699 -1008
  51. package/dist/no-modules.js.map +4 -4
  52. package/dist/repair-fetch.d.ts +50 -10
  53. package/dist/repair-fetch.d.ts.map +1 -1
  54. package/dist/repair-fetch.js +62 -20
  55. package/dist/repair-fetch.js.map +2 -2
  56. package/dist/wasm-mmtp-container-parser.js +31 -1
  57. package/dist/wasm-mmtp-container-parser.js.map +2 -2
  58. package/package.json +7 -15
  59. package/src/cmcd-producer.ts +6 -1
  60. package/src/fec-manager.ts +147 -131
  61. package/src/held-layer-admission.ts +161 -0
  62. package/src/index.ts +6 -9
  63. package/src/mmtp-container-parser.ts +48 -0
  64. package/src/mmtp-fec-session.ts +76 -3
  65. package/src/mmtp-router.ts +637 -274
  66. package/src/multicast-auth-catalog.ts +81 -8
  67. package/src/multicast-manager.ts +274 -49
  68. package/src/multicast-transport.ts +6 -2
  69. package/src/no-modules.ts +1 -6
  70. package/src/repair-fetch.ts +154 -34
  71. package/test-vectors/catalog/negative/multicast-auth-padded-key.json +0 -1
  72. package/test-vectors/catalog/negative/multicast-auth-reused-track-format.json +0 -1
  73. package/test-vectors/catalog/negative/multicast-auth-root-dotted-key.json +0 -1
  74. package/test-vectors/catalog/negative/multicast-auth-trailing-pad-bits.json +0 -1
  75. package/test-vectors/repair-policy-v1.json +113 -0
  76. package/dist/manifest-auth-plan.d.ts +0 -31
  77. package/dist/manifest-auth-plan.d.ts.map +0 -1
  78. package/dist/manifest-auth-plan.js +0 -426
  79. package/dist/manifest-auth-plan.js.map +0 -7
  80. package/dist/source-auth.d.ts +0 -2
  81. package/dist/source-auth.d.ts.map +0 -1
  82. package/dist/source-auth.js +0 -3912
  83. package/dist/source-auth.js.map +0 -7
  84. package/src/manifest-auth-plan.ts +0 -55
  85. package/src/source-auth.ts +0 -1
  86. package/test-vectors/generate-source-auth-fixture.mjs +0 -114
  87. package/test-vectors/source-auth-v1.json +0 -61
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../mmt-base/src/mmtp.ts", "../../mmt-base/src/ntp-short.ts", "../../mmt-base/src/fec-block-profile.ts", "../../mmt-base/src/catalog-fec-source.ts", "../../mmt-base/src/multicast-catalog.ts", "../../mmt-manifest-verify/src/source-auth.ts", "../../mmt-manifest-verify/src/verifier.ts", "../../mmt-manifest-verify/src/bc-provenance-v2.ts", "../src/multicast-auth-catalog.ts", "../src/manifest-auth-plan.ts"],
4
- "sourcesContent": ["/**\n * @mmt/container - MMTP Parser\n *\n * MMTP (MMT Protocol) packet parser based on ISO/IEC 23008-1:2023 Section 9.2\n *\n * MMTP header format (12 bytes):\n * - Byte 0: V(2) | C(1) | FEC_type(2) | r(1) | X(1) | R(1)\n * - Byte 1: reserved(2) | packet_type(6)\n * - Bytes 2-3: packet_id (big-endian)\n * - Bytes 4-7: timestamp (big-endian, 32-bit)\n * - Bytes 8-11: sequence_number (big-endian)\n *\n * FEC_type values:\n * - 0: No FEC / source packet\n * - 1: FEC mode 0 (source with FEC payload ID)\n * - 2: FEC repair packet mode 0\n * - 3: FEC repair packet mode 1\n *\n * @packageDocumentation\n */\n\nexport interface MMTPHeader {\n version: number\n /** Packet type: 0x00=MPU, 0x02=Signaling, 0x03=Repair */\n packetType: number\n packetId: number\n timestamp: bigint\n sequenceNumber: number\n payloadLength: number\n fecType: number\n /** C flag: header carries a 32-bit packet_counter. */\n packetCounterFlag: boolean\n packetCounter?: number\n /** X flag: header carries an extension header and value. */\n extensionFlag: boolean\n extension?: { type: number; data: Uint8Array }\n /** Byte offset at which the MMTP payload begins. */\n headerLength: number\n /** Random Access Point flag - indicates keyframe (ISO 23008-1, byte 0 bit 0) */\n rapFlag: boolean\n}\n\nexport interface MPUHeader {\n mpuSequenceNumber: number\n fragmentationIndicator: number // FI: 0=complete, 1=first, 2=middle, 3=last\n /** 8-bit MPU fragment_counter from byte 3. */\n fragmentCounter: number\n /** T flag: 1=timed media (14-byte MFU DU header), 0=non-timed (4-byte header) */\n timedFlag: number\n /** A flag: aggregated MPU payload. This strict profile requires A=0. */\n aggregationFlag: boolean\n /** MPU payload_length (bytes 0-1): bytes after the length field, including\n * the remaining six MPU-header bytes and all data-unit bytes. */\n payloadLength: number\n}\n\nexport interface MFUDuHeader {\n movieFragmentSequenceNumber: number\n sampleNumber: number\n offset: number\n priority: number\n dependencyCounter: number\n}\n\nexport interface MMTPPacket {\n header: MMTPHeader\n payload: Uint8Array\n}\n\nexport const MMTP_HEADER_SIZE = 12\nexport const MPU_HEADER_SIZE = 8\nexport const MFU_DU_HEADER_SIZE_TIMED = 14 // T=1: timed media (video/audio samples)\nexport const MFU_DU_HEADER_SIZE_NONTIMED = 4 // T=0: non-timed media (item_id only)\n\nconst MPU_LENGTH_FIELD_SIZE = 2\nconst MPU_HEADER_REMAINDER_SIZE = MPU_HEADER_SIZE - MPU_LENGTH_FIELD_SIZE\nexport const SOURCE_FEC_PAYLOAD_ID_SIZE = 4\n\n// MMTP Packet Types (ISO/IEC 23008-1:2023 Section 9.2.2)\nexport const PACKET_TYPE_MPU = 0x00\nexport const PACKET_TYPE_SIGNALING = 0x02\nexport const PACKET_TYPE_REPAIR = 0x03\n\n// AL-FEC Signaling Message ID (ISO/IEC 23008-1:2023 Amendment 1:2025)\nexport const ALFEC_MESSAGE_ID = 0x0203\n\n/**\n * MMTP `packet_id` of the signaling flow, which carries PACKET_TYPE_SIGNALING\n * packets such as the AL-FEC message (ALFEC_MESSAGE_ID). The FFmpeg `moq_mmt`\n * muxer sends that message on this packet_id into the same multicast group as\n * the media, and moqcast-draft (PR #44) makes advertising it for a track a\n * MUST NOT. A track's packet_id is therefore 1..MMTP_PACKET_ID_MAX.\n */\nexport const MMTP_SIGNALING_PACKET_ID = 0\n\n/** Largest value of the 16-bit MMTP `packet_id` header field. */\nexport const MMTP_PACKET_ID_MAX = 0xffff\n\n// fec_code_id_for_repair_flow code points (ISO/IEC 23008-10 Table 1;\n// draft-ramadan-moq-mmt-01 Section 8.3).\nexport const FEC_CODE_REED_SOLOMON = 1\nexport const FEC_CODE_RAPTORQ = 3\n\n/**\n * Parse MMTP header from buffer\n */\nexport function parseMMTPHeader(buffer: Uint8Array): MMTPHeader {\n if (buffer.length < MMTP_HEADER_SIZE) {\n throw new Error(`MMTP packet too small: ${buffer.length} bytes`)\n }\n\n const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n\n const version = (buffer[0]! >> 6) & 0x03\n if (version !== 0) {\n throw new Error(`Unsupported MMTP version: ${version}`)\n }\n\n // Extract FEC type from byte 0, bits 3-4\n const fecType = (buffer[0]! >> 3) & 0x03\n const packetCounterFlag = (buffer[0]! & 0x20) !== 0\n const extensionFlag = (buffer[0]! & 0x02) !== 0\n\n // Extract RAP flag from byte 0, bit 0 (R bit - Random Access Point)\n const rapFlag = (buffer[0]! & 0x01) === 1\n\n // ISO/IEC 23008-1:2023 Figure 8 (V=0) places reserved(2) before\n // packet_type(6), so packet_type occupies the low six bits of byte 1.\n const packetType = buffer[1]! & 0x3F\n\n let offset = 2\n\n // Packet ID (2 bytes, big-endian)\n const packetId = view.getUint16(offset, false)\n offset += 2\n\n // Timestamp (4 bytes, big-endian)\n const timestamp = BigInt(view.getUint32(offset, false))\n offset += 4\n\n // Sequence number (4 bytes, big-endian)\n const sequenceNumber = view.getUint32(offset, false)\n offset += 4\n\n let packetCounter: number | undefined\n if (packetCounterFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in packet_counter')\n packetCounter = view.getUint32(offset, false)\n offset += 4\n }\n\n let extension: { type: number; data: Uint8Array } | undefined\n if (extensionFlag) {\n if (offset + 4 > buffer.length) throw new Error('MMTP packet truncated in header extension')\n const type = view.getUint16(offset, false)\n const length = view.getUint16(offset + 2, false)\n offset += 4\n if (offset + length > buffer.length) throw new Error('MMTP packet truncated in header extension value')\n extension = { type, data: buffer.subarray(offset, offset + length) }\n offset += length\n }\n\n return {\n version,\n packetType,\n packetId,\n timestamp,\n sequenceNumber,\n payloadLength: buffer.length - offset,\n fecType,\n packetCounterFlag,\n packetCounter,\n extensionFlag,\n extension,\n headerLength: offset,\n rapFlag,\n }\n}\n\n/**\n * The MMTP `packet_id` of `buffer`, or null exactly when parseMMTPHeader would\n * throw for it (short buffer, V != 0, truncated packet_counter or header\n * extension).\n *\n * This is the per-packet filter a receiver runs before any other work, so it\n * builds no header object, DataView or BigInt timestamp (BLO-36599). Use\n * parseMMTPHeader when any other header field is needed.\n */\nexport function readMMTPPacketId(buffer: Uint8Array): number | null {\n if (buffer.length < MMTP_HEADER_SIZE) return null\n const flags = buffer[0]!\n if (((flags >> 6) & 0x03) !== 0) return null\n let offset = MMTP_HEADER_SIZE\n if ((flags & 0x20) !== 0) {\n if (offset + 4 > buffer.length) return null\n offset += 4\n }\n if ((flags & 0x02) !== 0) {\n if (offset + 4 > buffer.length) return null\n const length = (buffer[offset + 2]! << 8) | buffer[offset + 3]!\n if (offset + 4 + length > buffer.length) return null\n }\n return (buffer[2]! << 8) | buffer[3]!\n}\n\n/**\n * Unwrap MMTP packet to extract payload\n */\nexport function unwrapMMTP(buffer: Uint8Array): MMTPPacket {\n const header = parseMMTPHeader(buffer)\n const payload = buffer.subarray(header.headerLength)\n\n return { header, payload }\n}\n\n/**\n * Parse MPU header from payload\n * ISO/IEC 23008-1:2023 Section 9.2.3.3\n *\n * MPU Header format (8 bytes) as written by FFmpeg/libmmt:\n * - Bytes 0-1: payload_length (16 bits, big-endian)\n * - Byte 2: FT(4) | T(1) | FI(2) | A(1)\n * - Byte 3: fragment_counter (8 bits)\n * - Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n */\nexport function parseMPUHeader(payload: Uint8Array): MPUHeader {\n if (payload.length < MPU_HEADER_SIZE) {\n throw new Error(`Payload too small for MPU header: ${payload.length} bytes`)\n }\n\n const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength)\n\n // Bytes 0-1: payload_length (16 bits, big-endian)\n const payloadLength = view.getUint16(0, false)\n\n // Byte 2: FT(4) | T(1) | FI(2) | A(1)\n const flagsByte = payload[2]!\n const timedFlag = (flagsByte >> 3) & 0x01\n const fragmentationIndicator = (flagsByte >> 1) & 0x03\n const aggregationFlag = (flagsByte & 0x01) !== 0\n\n // Byte 3: fragment_counter (8 bits). The high bit is part of the counter in\n // the current libmmt/FFmpeg writer ABI.\n const fragmentCounter = payload[3]!\n\n // Bytes 4-7: MPU_sequence_number (32 bits, big-endian)\n const mpuSequenceNumber = view.getUint32(4, false)\n\n return {\n mpuSequenceNumber,\n fragmentationIndicator,\n fragmentCounter,\n timedFlag,\n aggregationFlag,\n payloadLength,\n }\n}\n\nexport function parseTimedMfuDuHeader(payload: Uint8Array, offset: number): MFUDuHeader | null {\n if (offset < 0 || offset + MFU_DU_HEADER_SIZE_TIMED > payload.length) return null\n const view = new DataView(payload.buffer, payload.byteOffset + offset, MFU_DU_HEADER_SIZE_TIMED)\n return {\n movieFragmentSequenceNumber: view.getUint32(0, false),\n sampleNumber: view.getUint32(4, false),\n offset: view.getUint32(8, false),\n priority: payload[offset + 12]!,\n dependencyCounter: payload[offset + 13]!,\n }\n}\n\n/**\n * Why `extractMFUPayload` refused a packet.\n *\n * These identifiers are part of the public contract: callers surface them in\n * diagnostics, so renaming one is a breaking change.\n */\nexport type MFURejectionGate =\n | 'packet-shorter-than-mmtp-plus-mpu-header'\n | 'mmtp-header-unparseable'\n | 'not-an-mpu-packet'\n | 'payload-shorter-than-mpu-header'\n | 'aggregation-flag-set'\n | 'mpu-payload-length-below-header-remainder'\n | 'mpu-declared-end-exceeds-payload'\n | 'mfu-offset-at-or-past-declared-end'\n | 'source-fec-payload-id-width-mismatch'\n | 'declared-trailers-unresolved'\n\nexport interface MFURejection {\n gate: MFURejectionGate\n /** Human-readable detail, including the byte arithmetic that decided it. */\n detail: string\n}\n\ninterface MfuPayloadLayout {\n hasMfuDuHeader: boolean\n mfuOffset: number\n dataEnd: number\n}\n\nfunction resolveMfuPayloadLayout(\n mpu: MPUHeader,\n fragmentType: number,\n payload: Uint8Array,\n): { layout: MfuPayloadLayout } | { rejection: MFURejection } {\n if (mpu.payloadLength < MPU_HEADER_REMAINDER_SIZE) {\n return {\n rejection: {\n gate: 'mpu-payload-length-below-header-remainder',\n detail: `payload_length=${mpu.payloadLength} < ${MPU_HEADER_REMAINDER_SIZE}`,\n },\n }\n }\n\n // ISO/IEC 23008-1 defines payload_length from immediately after its own\n // two-byte field. It therefore includes the remaining six MPU-header bytes.\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + mpu.payloadLength\n if (declaredEnd > payload.length) {\n return {\n rejection: {\n gate: 'mpu-declared-end-exceeds-payload',\n detail:\n `payload_length=${mpu.payloadLength} implies end=${declaredEnd}, ` +\n `payload is ${payload.length} bytes`,\n },\n }\n }\n\n const hasHeader = fragmentType === 2 && mpu.fragmentationIndicator <= 1\n const mfuHeaderSize = mpu.timedFlag ? MFU_DU_HEADER_SIZE_TIMED : MFU_DU_HEADER_SIZE_NONTIMED\n const mfuOffset = MPU_HEADER_SIZE + (hasHeader ? mfuHeaderSize : 0)\n if (mfuOffset >= declaredEnd) {\n return {\n rejection: {\n gate: 'mfu-offset-at-or-past-declared-end',\n detail: `mfuOffset=${mfuOffset} >= declaredEnd=${declaredEnd}`,\n },\n }\n }\n\n return {\n layout: {\n hasMfuDuHeader: hasHeader,\n mfuOffset,\n dataEnd: declaredEnd,\n },\n }\n}\n\n// Legacy detached per-packet authenticator suffix `[0xf0a1][len][auth]`. Its\n// producer was removed (BLO-16483) and nothing verifies it any more; provenance\n// is the signed per-group manifest. Receivers still skip an exact suffix so a\n// stale producer degrades to unauthenticated playback instead of dropped\n// packets. ponytail: hard-cut to reject once no deployed producer can emit it.\nconst LEGACY_TRAILER_EXT_TYPE = 0xf0a1\nconst LEGACY_TRAILER_HEADER_SIZE = 4\n\nfunction isEmptyOrExactLegacyTrailer(payload: Uint8Array, start: number, end: number): boolean {\n if (start === end) return true\n if (start < 0 || end > payload.length || start + LEGACY_TRAILER_HEADER_SIZE > end) {\n return false\n }\n\n const view = new DataView(\n payload.buffer,\n payload.byteOffset + start,\n end - start,\n )\n if (view.getUint16(0, false) !== LEGACY_TRAILER_EXT_TYPE) return false\n\n const authLength = view.getUint16(2, false)\n return start + LEGACY_TRAILER_HEADER_SIZE + authLength === end\n}\n\ninterface DeclaredTrailerLayout {\n hasFecId: boolean\n hasOverlayFecId: boolean\n}\n\n/** Options for extractMFUPayload and explainMFURejection. */\nexport interface MFUExtractOptions {\n /**\n * When true, do NOT subtract the Source FEC Payload ID from the payload\n * boundary, even if fecType >= 1. For FEC-recovered symbols whose FEC PID\n * was already stripped before WASM decoding.\n */\n skipFecId?: boolean\n /**\n * The catalog declares a keyframe overlay (`scope: \"keyframe\"` repair track)\n * for this packet's source. A RAP packet then carries two Source FEC Payload\n * IDs, base first and overlay last (BLO-32719 Clarification 7). This is a\n * catalog fact, never inferred: the header has no overlay bit, and a payload\n * four bytes shorter is byte-identical to a missing overlay. Mirrors\n * mmt-core `split_payload_and_source_fec_trailers(overlay_declared)`.\n */\n overlayDeclared?: boolean\n}\n\nfunction resolveDeclaredTrailers(\n payload: Uint8Array,\n declaredEnd: number,\n fecType: number,\n options?: MFUExtractOptions,\n rapFlag = false,\n): DeclaredTrailerLayout | null {\n if (declaredEnd > payload.length) return null\n\n if (options?.skipFecId) {\n return isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)\n ? { hasFecId: false, hasOverlayFecId: false }\n : null\n }\n\n const overlay = options?.overlayDeclared === true && rapFlag\n if (fecType === 1) {\n const fecStart = payload.length - sourceFecTrailerSize(overlay)\n if (fecStart < declaredEnd || !isEmptyOrExactLegacyTrailer(payload, declaredEnd, fecStart)) {\n return null\n }\n return { hasFecId: true, hasOverlayFecId: overlay }\n }\n // An overlay ID rides behind a base ID the header says is absent.\n if (fecType !== 0 || overlay) return null\n\n if (isEmptyOrExactLegacyTrailer(payload, declaredEnd, payload.length)) {\n return { hasFecId: false, hasOverlayFecId: false }\n }\n\n return null\n}\n\nfunction sourceFecTrailerSize(overlay: boolean): number {\n return overlay ? 2 * SOURCE_FEC_PAYLOAD_ID_SIZE : SOURCE_FEC_PAYLOAD_ID_SIZE\n}\n\n/**\n * Extract NAL unit payload from MMTP packet\n */\nexport function extractNALUnit(mmtpPacket: Uint8Array): Uint8Array {\n const extraction = extractMFUPayload(mmtpPacket)\n if (!extraction) throw new Error('Malformed MMTP MPU payload')\n return extraction.data\n}\n\n/** Result of extracting MFU payload from an MMTP packet */\nexport interface MFUExtraction {\n /** MMTP header fields */\n header: MMTPHeader\n /** MPU header fields */\n mpu: MPUHeader\n /** Fragment type (FT field, 4 bits): 0=metadata, 2=MFU data */\n fragmentType: number\n /** MFU payload data (video/audio bytes, headers stripped, FEC ID excluded) */\n data: Uint8Array\n /** Parsed timed MFU DU header when present, otherwise null */\n mfuDuHeader: MFUDuHeader | null\n /** Non-timed MFU Item_ID when the 4-byte DU header is present. */\n itemId: number | null\n /** Source FEC Payload ID if present (`FEC_type == 1`), or null */\n fecPayloadId: { ssId: number } | null\n /** Keyframe-overlay Source FEC Payload ID: RAP packets with `overlayDeclared` only. */\n overlayFecPayloadId: { ssId: number } | null\n}\n\n/**\n * Extract MFU payload from a raw MMTP packet.\n *\n * Handles all the byte arithmetic for:\n * - MMTP header (12 bytes)\n * - MPU sub-header (8 bytes, payloadLength field)\n * - MFU DU header (14 bytes timed / 4 bytes non-timed, only for FI \u2264 1)\n * - Source FEC Payload ID (4 bytes at end, only when `FEC_type == 1`)\n *\n * `payloadLength` is the ISO length-after-field value: six remaining MPU-header\n * bytes, then the MFU DU header when present, then media. The legacy\n * authenticator suffix and the Source FEC trailer are outside that declared span and must consume the exact suffix.\n *\n * @param mmtpPacket Raw MMTP packet (from multicast or MoQ)\n * @param options.skipFecId When true, do NOT subtract the 4-byte Source FEC Payload ID\n * from the payload boundary, even if fecType >= 1 in the MMTP header. Use this for\n * FEC-recovered symbols where the FEC PID was already stripped before WASM decoding\n * but the fecType bits in byte 0 still reflect the original packet (FEC Type=1 per\n * draft-ramadan-moq-mmt-01 \u00A73.1).\n * @param options.overlayDeclared The catalog declares a keyframe overlay for this\n * source, so a RAP packet carries an 8-byte trailer (see MFUExtractOptions).\n * @returns Extracted MFU data + metadata, or null if packet is too small\n */\ninterface MfuPlan {\n header: MMTPHeader\n payload: Uint8Array\n mpu: MPUHeader\n fragmentType: number\n layout: MfuPayloadLayout\n trailerLayout: DeclaredTrailerLayout\n}\n\n/**\n * Single source of truth for whether a packet yields an MFU payload.\n *\n * `extractMFUPayload` and `explainMFURejection` both delegate here, so the\n * reason reported to operators can never drift from the decision the parser\n * actually made.\n */\nfunction planMfuExtraction(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n parsedHeader?: MMTPHeader,\n): { plan: MfuPlan } | { rejection: MFURejection } {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'packet-shorter-than-mmtp-plus-mpu-header',\n detail: `${mmtpPacket.length} bytes < ${MMTP_HEADER_SIZE + MPU_HEADER_SIZE}`,\n },\n }\n }\n\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch (error) {\n return {\n rejection: {\n gate: 'mmtp-header-unparseable',\n detail: error instanceof Error ? error.message : String(error),\n },\n }\n }\n }\n if (header.packetType !== PACKET_TYPE_MPU) {\n return {\n rejection: {\n gate: 'not-an-mpu-packet',\n detail: `packetType=0x${header.packetType.toString(16).padStart(2, '0')}`,\n },\n }\n }\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) {\n return {\n rejection: {\n gate: 'payload-shorter-than-mpu-header',\n detail: `${payload.length} bytes < ${MPU_HEADER_SIZE}`,\n },\n }\n }\n\n const mpu = parseMPUHeader(payload)\n if (mpu.aggregationFlag) {\n return {\n rejection: { gate: 'aggregation-flag-set', detail: 'strict profile requires A=0' },\n }\n }\n\n // Fragment type from byte 2 upper nibble (not returned by parseMPUHeader)\n const fragmentType = (payload[2]! >> 4) & 0x0f\n\n const layoutResult = resolveMfuPayloadLayout(mpu, fragmentType, payload)\n if ('rejection' in layoutResult) return layoutResult\n const { layout } = layoutResult\n\n const trailerLayout = resolveDeclaredTrailers(\n payload,\n layout.dataEnd,\n header.fecType,\n options,\n header.rapFlag,\n )\n if (!trailerLayout) {\n // The most common cause is a producer whose Source FEC Payload ID width\n // disagrees with ISO/IEC 23008-1 \u00A7C.5.2. Name that explicitly rather than\n // leaving operators to infer it from a generic trailer error.\n if (!options?.skipFecId && header.fecType === 1) {\n const observed = payload.length - layout.dataEnd\n const expected = sourceFecTrailerSize(options?.overlayDeclared === true && header.rapFlag)\n if (observed !== expected) {\n return {\n rejection: {\n gate: 'source-fec-payload-id-width-mismatch',\n detail:\n `producer wrote a ${observed}-byte Source FEC Payload ID, ` +\n `ISO/IEC 23008-1 \u00A7C.5.2 requires ${expected} ` +\n `(payload=${payload.length}, declaredEnd=${layout.dataEnd})`,\n },\n }\n }\n }\n return {\n rejection: {\n gate: 'declared-trailers-unresolved',\n detail:\n `trailer bytes [${layout.dataEnd}, ${payload.length}) are neither empty ` +\n `nor an exact legacy authenticator trailer (fecType=${header.fecType})`,\n },\n }\n }\n\n return { plan: { header, payload, mpu, fragmentType, layout, trailerLayout } }\n}\n\n/**\n * Report why `extractMFUPayload` would refuse this packet, or null if it would\n * accept it.\n *\n * `extractMFUPayload` returns bare `null` on ten distinct conditions, which\n * makes a producer/consumer framing disagreement indistinguishable from a\n * publisher that never started \u2014 both render as silence. Use this wherever\n * that distinction matters: diagnostics, contract tests, and operator-facing\n * logs.\n */\nexport function explainMFURejection(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFURejection | null {\n const result = planMfuExtraction(mmtpPacket, options)\n return 'rejection' in result ? result.rejection : null\n}\n\nexport function extractMFUPayload(\n mmtpPacket: Uint8Array,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options))\n}\n\n/**\n * extractMFUPayload for a caller that already holds this packet's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet; the\n * result is then identical to `extractMFUPayload(mmtpPacket, options)`.\n */\nexport function extractMFUPayloadWithHeader(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n options?: MFUExtractOptions,\n): MFUExtraction | null {\n return extractPlannedMfu(mmtpPacket, planMfuExtraction(mmtpPacket, options, header))\n}\n\nfunction extractPlannedMfu(\n mmtpPacket: Uint8Array,\n result: { plan: MfuPlan } | { rejection: MFURejection },\n): MFUExtraction | null {\n if ('rejection' in result) return null\n const { header, payload, mpu, fragmentType, layout, trailerLayout } = result.plan\n\n const mfuDuHeaderPresent = layout.hasMfuDuHeader\n const mfuOffset = layout.mfuOffset\n const dataEnd = layout.dataEnd\n\n const mfuDuHeader =\n mfuDuHeaderPresent && mpu.timedFlag === 1\n ? parseTimedMfuDuHeader(payload, MPU_HEADER_SIZE)\n : null\n const itemId =\n mfuDuHeaderPresent && mpu.timedFlag === 0\n ? new DataView(payload.buffer, payload.byteOffset + MPU_HEADER_SIZE, 4).getUint32(0, false)\n : null\n\n const data = payload.subarray(mfuOffset, dataEnd)\n\n const trailerStart =\n mmtpPacket.length - sourceFecTrailerSize(trailerLayout.hasOverlayFecId)\n const ssIdAt = (offset: number) => ({\n ssId: new DataView(mmtpPacket.buffer, mmtpPacket.byteOffset + offset, 4).getUint32(0, false),\n })\n const fecPayloadId = trailerLayout.hasFecId ? ssIdAt(trailerStart) : null\n const overlayFecPayloadId = trailerLayout.hasOverlayFecId\n ? ssIdAt(trailerStart + SOURCE_FEC_PAYLOAD_ID_SIZE)\n : null\n\n return { header, mpu, fragmentType, data, mfuDuHeader, itemId, fecPayloadId, overlayFecPayloadId }\n}\n\n/**\n * Extract timestamp from MMTP packet\n */\nexport function extractTimestamp(mmtpPacket: Uint8Array): bigint {\n const { header } = unwrapMMTP(mmtpPacket)\n return header.timestamp\n}\n\n/**\n * Check if MMTP packet is an FEC repair packet\n * FEC_type 2 and 3 are repair packets\n */\nexport function isFecRepairPacket(mmtpPacket: Uint8Array): boolean {\n if (mmtpPacket.length < 1) return false\n const fecType = (mmtpPacket[0]! >> 3) & 0x03\n return fecType >= 2\n}\n\n/**\n * Extract Source FEC Payload ID (SS_ID) from the last 4 bytes of an MMTP\n * source packet per ISO/IEC 23008-1:2023 Section C.5.2 Figure C.10.\n *\n * SS_ID is a flat 32-bit monotonic counter (incremented by 1 per packet\n * for ssbg_mode0 and ssbg_mode1). Block membership and ESI use the\n * configured K; repair SS_Start identifies the first SS_ID of the block and\n * SSB_length records the authored source-symbol count:\n * SBN = floor(SS_ID / K), ESI = SS_ID % K\n * SS_Start = SBN * K (first SS_ID of the block)\n *\n * This matches FFmpeg moqenc_mmt.c, the drafts (draft-ramadan-moq-fec \u00A76.2),\n * and ATSC A/331 \u00A78.1.2.5 which defers to ISO 23008-1 Annex C.\n */\nexport function extractFecPayloadId(mmtpPacket: Uint8Array): { ssId: number } | null {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header ? sourceFecPayloadIdOf(mmtpPacket, header) : null\n}\n\n/**\n * Return true when FEC_type=1 and the exact suffix after the ISO-declared MPU\n * payload contains a four-byte Source FEC Payload ID.\n */\nexport function hasSourceFecPayloadId(mmtpPacket: Uint8Array): boolean {\n const header = parseHeaderForSourceFecId(mmtpPacket)\n return header !== null && sourceFecPayloadIdOf(mmtpPacket, header) !== null\n}\n\nfunction parseHeaderForSourceFecId(mmtpPacket: Uint8Array): MMTPHeader | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n try {\n return parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n}\n\n/**\n * The Source FEC Payload ID of a packet whose MMTP header the caller already\n * parsed, or null when the packet carries none: present only when FEC_type=1\n * and the exact suffix after the ISO-declared MPU payload holds the four-byte\n * ID (ISO/IEC 23008-1 \u00A7C.5.2). Same answer as extractFecPayloadId, without a\n * second header parse (BLO-36599).\n *\n * `header` must be `parseMMTPHeader(mmtpPacket)` of this same packet.\n */\nexport function sourceFecPayloadIdOf(\n mmtpPacket: Uint8Array,\n header: MMTPHeader,\n): { ssId: number } | null {\n if (mmtpPacket.length < MMTP_HEADER_SIZE + MPU_HEADER_SIZE) return null\n if (header.packetType !== PACKET_TYPE_MPU) return null\n\n const payload = mmtpPacket.subarray(header.headerLength)\n if (payload.length < MPU_HEADER_SIZE) return null\n const payloadLength = (payload[0]! << 8) | payload[1]!\n if (payloadLength < MPU_HEADER_REMAINDER_SIZE) return null\n const declaredEnd = MPU_LENGTH_FIELD_SIZE + payloadLength\n if (!resolveDeclaredTrailers(payload, declaredEnd, header.fecType)?.hasFecId) return null\n\n // Flat 4-byte SS_ID: encoder writes block_id * K + symbol_id\n const off = mmtpPacket.length - SOURCE_FEC_PAYLOAD_ID_SIZE\n return {\n ssId:\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0,\n }\n}\n\n/** Size of Repair FEC Payload ID per ISO/IEC 23008-1 Section C.5.3 */\nexport const REPAIR_FEC_PID_SIZE = 13\n\n/**\n * Parse the 13-byte Repair FEC Payload ID after the complete dynamic MMTP header.\n * `parsedHeader`, when given, must be `parseMMTPHeader(mmtpPacket)` of this\n * same packet; it spares the second header parse (BLO-36599).\n */\nfunction readRepairFecPayloadIdFields(mmtpPacket: Uint8Array, parsedHeader?: MMTPHeader): {\n packetId: number; ssStart: number; rsbLength: number; rsId: number; ssbLength: number; dataOffset: number\n} | null {\n let header: MMTPHeader\n if (parsedHeader) {\n header = parsedHeader\n } else {\n try {\n header = parseMMTPHeader(mmtpPacket)\n } catch {\n return null\n }\n }\n if (header.packetType !== PACKET_TYPE_REPAIR || header.fecType !== 2) return null\n\n const off = header.headerLength\n if (mmtpPacket.length < off + REPAIR_FEC_PID_SIZE) return null\n const ssStart =\n ((mmtpPacket[off]! << 24) |\n (mmtpPacket[off + 1]! << 16) |\n (mmtpPacket[off + 2]! << 8) |\n mmtpPacket[off + 3]!) >>>\n 0 // 32 bits\n const rsbLength = (mmtpPacket[off + 4]! << 16) | (mmtpPacket[off + 5]! << 8) | mmtpPacket[off + 6]! // 24 bits\n const rsId = (mmtpPacket[off + 7]! << 16) | (mmtpPacket[off + 8]! << 8) | mmtpPacket[off + 9]! // 24 bits\n const ssbLength = (mmtpPacket[off + 10]! << 16) | (mmtpPacket[off + 11]! << 8) | mmtpPacket[off + 12]! // 24 bits\n return {\n packetId: header.packetId,\n ssStart,\n rsbLength,\n rsId,\n ssbLength,\n dataOffset: off + REPAIR_FEC_PID_SIZE,\n }\n}\n\n/**\n * Extract Repair FEC Payload ID from a raw-multicast MMTP repair packet.\n * Per ISO/IEC 23008-1:2023 Section C.5.3 (ssbg_mode0, one-stage, no FFSRP_TS):\n * [MMTP header][SS_Start:4][RSB_length:3][RS_ID:3][SSB_length:3][repair data]\n * OTI is delivered via AL-FEC signaling message (Table C.3), not per-packet.\n *\n * Byte 1 is reserved(2)|packet_type(6) for V=0, so repair type 0x03 is 0x03.\n * Pre-migration high-bits bytes (0x0c) are rejected \u2014 no compatibility decode.\n */\nexport function extractRepairFecPayloadId(mmtpPacket: Uint8Array): {\n ssStart: number; rsbLength: number; rsId: number; ssbLength: number\n} | null {\n const fields = readRepairFecPayloadIdFields(mmtpPacket)\n if (!fields) return null\n return {\n ssStart: fields.ssStart,\n rsbLength: fields.rsbLength,\n rsId: fields.rsId,\n ssbLength: fields.ssbLength,\n }\n}\n\n/** Parsed MoQ repair frame per ISO/IEC 23008-1 Section C.4.3 + C.5.3 */\nexport interface MoqRepairFrame {\n /** MMTP packetId (1=video, 2=audio) \u2014 for routing to correct FEC decoder */\n packetId: number\n /** SS_Start (32 bits): SS_ID of first source symbol in block */\n ssStart: number\n /** RSB_length (24 bits): number of repair symbols (P) */\n rsbLength: number\n /** RS_ID (24 bits): repair symbol index (0-based) */\n rsId: number\n /** SSB_length (24 bits): authored source-symbol count (1..=configured K) */\n ssbLength: number\n /** Repair symbol data after the dynamic MMTP header and 13-byte FEC payload ID. */\n symbolData: Uint8Array\n}\n\n/**\n * Parse a MoQ repair frame (ISO/IEC 23008-1 Annex C format).\n *\n * Wire format per ISO 23008-1 \u00A7C.4.3 + \u00A7C.5.3:\n * MMTP Header: fixed 12 bytes plus C/X optional fields\n * then SS_Start (4 bytes) \u2192 block identifier\n * then RSB_length (3 bytes) \u2192 P\n * then RS_ID (3 bytes) \u2192 ESI = K + RS_ID\n * then SSB_length (3 bytes) \u2192 authored source-symbol count (1..=K)\n * then Repair Symbol Data (T bytes)\n *\n * @param frame - Raw MoQ repair frame bytes\n * @returns Parsed frame or null if invalid\n */\nexport function parseMoqRepairFrame(frame: Uint8Array): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame))\n}\n\n/**\n * parseMoqRepairFrame for a caller that already holds this frame's parsed MMTP\n * header, so the header is not parsed a second time (BLO-36599). The router's\n * multicast repair branch uses it: one repair packet in every K + P otherwise\n * paid two header parses.\n *\n * `header` must be `parseMMTPHeader(frame)` of this same frame; the result is\n * then identical to `parseMoqRepairFrame(frame)`.\n */\nexport function parseMoqRepairFrameWithHeader(\n frame: Uint8Array,\n header: MMTPHeader,\n): MoqRepairFrame | null {\n return moqRepairFrameFrom(frame, readRepairFecPayloadIdFields(frame, header))\n}\n\nfunction moqRepairFrameFrom(\n frame: Uint8Array,\n repairId: ReturnType<typeof readRepairFecPayloadIdFields>,\n): MoqRepairFrame | null {\n if (!repairId) return null\n\n const symbolData = frame.subarray(repairId.dataOffset)\n if (\n repairId.rsbLength === 0 ||\n repairId.ssbLength === 0 ||\n repairId.rsId >= repairId.rsbLength ||\n symbolData.length === 0\n ) return null\n\n return {\n packetId: repairId.packetId,\n ssStart: repairId.ssStart,\n rsbLength: repairId.rsbLength,\n rsId: repairId.rsId,\n ssbLength: repairId.ssbLength,\n symbolData,\n }\n}\n\n// The signalling-message parsers (9.3.4.2 payload header, AL-FEC message,\n// PA/MPT) live in ./signalling.ts.\n", "/**\n * NTP short format (RFC 5905 \u00A76): an unsigned 32-bit, 16.16 fixed-point count\n * of seconds. The MMTP header `timestamp` field (ISO/IEC 23008-1 \u00A79.2.2)\n * carries each packet's UTC sending instant in this format, so the value wraps\n * to 0 every 2^16 s (~18.2 h).\n *\n * Plain numeric order on these values breaks at every wrap: a retained\n * `Math.max` stays at the pre-wrap value and every later instant compares as\n * older. Order send times only with {@link compareNtpShort}, which applies\n * RFC 1982 serial-number arithmetic over the field's width.\n */\n\n/** Width in bits of the MMTP `timestamp` field (NTP short). */\nexport const NTP_SHORT_BITS = 32\n\n/** Number of distinct NTP-short values; the field wraps to 0 here. */\nexport const NTP_SHORT_MODULUS = 2 ** NTP_SHORT_BITS\n\n/**\n * RFC 1982 \u00A73.2's `2^(SERIAL_BITS - 1)`. Two values whose forward distance is\n * below this are ordered; exactly this far apart they are not ordered at all.\n * Two instants must be less than this far apart (2^15 s, ~9.1 h of send time)\n * for the comparison to give their true order.\n */\nexport const NTP_SHORT_HALF_RANGE = 2 ** (NTP_SHORT_BITS - 1)\n\n/**\n * Serial order of one NTP-short value relative to another.\n *\n * `'unordered'` is RFC 1982's undefined case: the values are exactly\n * {@link NTP_SHORT_HALF_RANGE} apart, so neither precedes the other. It is\n * never folded into `'before'` or `'after'`; each caller decides what an\n * unorderable pair means for it.\n */\nexport type NtpShortOrder = 'before' | 'equal' | 'after' | 'unordered'\n\n/** True when `value` is an NTP-short value: an integer in [0, NTP_SHORT_MODULUS). */\nexport function isNtpShort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value < NTP_SHORT_MODULUS\n}\n\n/**\n * RFC 1982 serial comparison of NTP-short `a` against `b`: `'after'` when `a`\n * is the later instant, `'before'` when it is the earlier one.\n *\n * The answer is the true order only while the two instants are less than\n * {@link NTP_SHORT_HALF_RANGE} apart. Past that a 32-bit field cannot tell\n * later from earlier, and the result names whichever direction is nearer.\n * This is not a total order (it is not transitive across the whole space), so\n * it must never be used to sort.\n *\n * @throws {RangeError} if either operand is not an NTP-short value\n */\nexport function compareNtpShort(a: number, b: number): NtpShortOrder {\n if (!isNtpShort(a) || !isNtpShort(b)) {\n throw new RangeError(\n `NTP-short values must be integers in [0, 2^${NTP_SHORT_BITS}), got ${String(a)} and ${String(b)}`,\n )\n }\n if (a === b) return 'equal'\n // Forward distance from b to a, reduced into [1, NTP_SHORT_MODULUS). Both\n // operands are below 2^32, so every step is exact in a double.\n const forward = (a - b + NTP_SHORT_MODULUS) % NTP_SHORT_MODULUS\n if (forward < NTP_SHORT_HALF_RANGE) return 'after'\n if (forward > NTP_SHORT_HALF_RANGE) return 'before'\n return 'unordered'\n}\n", "/**\n * FEC block profile (BLO-35597, ISO/IEC 23008-1 Annex C).\n *\n * Every catalog `fec` object that declares an FEC instance names its block\n * profile in `fec.blockProfile`. A receiver applies all of a profile's rules\n * or none of them, so the token is the one explicit per-stream discriminator:\n * it is never defaulted and never inferred from the wire.\n *\n * draft-ramadan-moq-fec \u00A75.1 defines exactly one value, and it is the only\n * profile this library implements:\n *\n * - `iso-ssbg1-v1`: ssbg_mode1 (C.3.3): a source symbol is\n * `u16 length || protected packet || zero padding` to T; a block is a\n * K' = SSB_length RaptorQ block (C.5.3) and repair ESI = SSB_length + RS_ID;\n * SS_ID increments by one per FEC source packet and wraps at 2^32 (C.5.2),\n * so a block is located by membership in `[SS_Start, SS_Start + SSB_length)`\n * under RFC 1982 serial arithmetic; the MoQ repair group id is the 62-bit\n * wrap-epoch-extended SS_Start. The AL-FEC coding byte for this profile is\n * 0x14 (one-stage, ssbg_mode1, fec_payload_id_mode 0, Amd 1:2025 Table C.3).\n *\n * A track whose `blockProfile` is absent or names any other token (the\n * retired Blockcast-private `k-padded-v1` included) has no FEC: a receiver\n * consumes its source without FEC and joins none of its repair tracks\n * (draft-ramadan-moq-fec \u00A75.1). `parseFecBlockProfile` throws for such a\n * token; the per-track catalog resolver turns the same rule into a typed\n * per-track \"FEC unavailable\" outcome instead of failing the session.\n */\nimport { serial32Add, serial32Distance, serial32Precedes } from './serial-number.js'\n\n/** The block profile a catalog `fec.blockProfile` may name: the one this library implements. */\nexport type FecBlockProfile = 'iso-ssbg1-v1'\n\n/** Every accepted `fec.blockProfile` token. */\nexport const FEC_BLOCK_PROFILES: readonly FecBlockProfile[] = Object.freeze([\n 'iso-ssbg1-v1',\n])\n\n/** True for a block-profile token this library implements. */\nexport function isFecBlockProfile(value: unknown): value is FecBlockProfile {\n return value === 'iso-ssbg1-v1'\n}\n\n/**\n * Read a required `fec.blockProfile`. Throws on an absent token and on any\n * token other than `iso-ssbg1-v1`: there is no default and nothing is\n * inferred from the wire (draft-ramadan-moq-fec \u00A75.1).\n *\n * @param where the catalog path of the field, for the error message.\n */\nexport function parseFecBlockProfile(value: unknown, where: string): FecBlockProfile {\n if (isFecBlockProfile(value)) return value\n if (value === undefined) {\n throw new Error(`${where} is required: name the FEC block profile (\"iso-ssbg1-v1\")`)\n }\n throw new Error(`${where} must be \"iso-ssbg1-v1\" (got ${JSON.stringify(value)})`)\n}\n\n/** ISO/IEC 23008-1 C.3.3 ssbg_mode1: the two-octet packet length field. */\nexport const SSBG1_LENGTH_FIELD_BYTES = 2\n\n/** The largest protected packet an ssbg_mode1 symbol of size T can carry. */\nexport function ssbg1MaxPacketLength(symbolSize: number): number {\n return symbolSize - SSBG1_LENGTH_FIELD_BYTES\n}\n\n/** Whether a protected packet of `packetLength` bytes fits one T-byte ssbg_mode1 symbol. */\nexport function ssbg1FitsSymbol(packetLength: number, symbolSize: number): boolean {\n return (\n Number.isSafeInteger(packetLength) &&\n packetLength >= 0 &&\n packetLength <= ssbg1MaxPacketLength(symbolSize) &&\n packetLength <= 0xffff\n )\n}\n\n/**\n * Build one ssbg_mode1 source symbol (C.3.3): the protected packet's length\n * in two octets, network byte order, then the packet, then zero octets up to\n * T. The protected packet is the MMTP packet before its Source FEC Payload ID\n * is appended (C.4.2).\n *\n * @throws when `length + 2 > T`: the packet cannot be protected under this\n * profile, and silently truncating it would corrupt every repair symbol.\n */\nexport function buildSsbg1SourceSymbol(packet: Uint8Array, symbolSize: number): Uint8Array {\n if (!Number.isSafeInteger(symbolSize) || symbolSize <= SSBG1_LENGTH_FIELD_BYTES) {\n throw new Error(`ssbg_mode1 symbol size must exceed ${SSBG1_LENGTH_FIELD_BYTES} (got ${symbolSize})`)\n }\n if (!ssbg1FitsSymbol(packet.byteLength, symbolSize)) {\n throw new Error(\n `ssbg_mode1 source symbol needs ${packet.byteLength} + ${SSBG1_LENGTH_FIELD_BYTES} bytes but T is ${symbolSize}`,\n )\n }\n const symbol = new Uint8Array(symbolSize)\n symbol[0] = (packet.byteLength >>> 8) & 0xff\n symbol[1] = packet.byteLength & 0xff\n symbol.set(packet, SSBG1_LENGTH_FIELD_BYTES)\n return symbol\n}\n\n/**\n * Recover the protected packet from one ssbg_mode1 source symbol: the bytes\n * the length field names. Returns null, never a guess, when the length field\n * points past the symbol or any padding octet is non-zero (C.3.3 padding is\n * all 00h), since either means the recovered symbol is not what was sent.\n */\nexport function trimSsbg1SourceSymbol(symbol: Uint8Array): Uint8Array | null {\n if (symbol.byteLength < SSBG1_LENGTH_FIELD_BYTES) return null\n const length = (symbol[0]! << 8) | symbol[1]!\n const end = SSBG1_LENGTH_FIELD_BYTES + length\n if (end > symbol.byteLength) return null\n for (let i = end; i < symbol.byteLength; i++) {\n if (symbol[i] !== 0) return null\n }\n return symbol.subarray(SSBG1_LENGTH_FIELD_BYTES, end)\n}\n\n/** The SS_ID after the last source symbol of `[ssStart, ssStart + ssbLength)`. */\nexport function isoBlockEnd(ssStart: number, ssbLength: number): number {\n return serial32Add(ssStart, ssbLength)\n}\n\n/**\n * Range membership (C.5.2, C.5.3): whether `ssId` is one of the\n * `ssbLength` consecutive SS_IDs starting at `ssStart`, modulo 2^32.\n */\nexport function isoBlockContains(ssStart: number, ssbLength: number, ssId: number): boolean {\n return serial32Distance(ssStart, ssId) < ssbLength\n}\n\n/**\n * A lower bound on how many blocks the block starting at `a` lies behind the\n * one starting at `b` (0 when `a` does not precede `b`). An iso-ssbg1-v1\n * block holds at most `kMax` source symbols (the catalog `sourceSymbols`, the\n * AL-FEC maximum_k_for_repair_flow), so `n * kMax` SS_IDs always span at least\n * `n` blocks. Every interleave horizon in the receiver (decoder retention,\n * worker retirement, ordered-relay settlement) uses this one bound, so the\n * manager's horizon can never fall inside the decoder's.\n */\nexport function isoBlocksBehindLowerBound(a: number, b: number, kMax: number): number {\n if (!Number.isSafeInteger(kMax) || kMax <= 0) {\n throw new Error(`iso-ssbg1-v1 block horizon needs a positive maximum K (got ${kMax})`)\n }\n return serial32Precedes(a, b) ? Math.floor(serial32Distance(a, b) / kMax) : 0\n}\n", "/**\n * Catalog FEC source rules shared by every catalog reader (BLO-40851).\n *\n * These are the dialect-level reads the container resolvers\n * (`resolveRepairLayers`, `catalogFecUnavailable`), the transport endpoint\n * selection (`selectEndpointPacketIds`) and a player's repair inventory all\n * need to agree on. They live here, in the leaf package every one of those\n * depends on, so there is one copy of each rule instead of copies pinned\n * against each other by tests.\n *\n * - `locateCatalogFecSource`: which catalog entry declares a source's `fec`\n * block. hang `video`/`audio` rendition blocks are authoritative over an MSF\n * `tracks[]` entry of the same name.\n * - `catalogFecAvailable`: draft-ramadan-moq-fec \u00A75.1, whether a block's FEC\n * is usable at all (implemented `blockProfile`, usable span).\n * - `catalogFecDeclaresUnconfigurableInstance`: a declared FEC instance whose\n * geometry a decoder cannot configure, which is a malformed catalog rather\n * than \"no FEC\".\n *\n * Every function is total: it never throws, whatever the input.\n */\nimport { isFecBlockProfile } from './fec-block-profile.js'\n\n/** The catalog media roles that carry hang renditions. */\nexport type CatalogFecRole = 'video' | 'audio'\n\n/** One media source's `fec` block, as `locateCatalogFecSource` found it. */\nexport type CatalogFecSource =\n | {\n readonly dialect: 'hang'\n readonly role: CatalogFecRole\n /** The rendition object. */\n readonly entry: Record<string, unknown>\n /** The rendition's `fec` value, unvalidated (may be undefined). */\n readonly fec: unknown\n /** JSON path of that `fec` block. */\n readonly fecPath: string\n }\n | {\n readonly dialect: 'msf'\n /** Index of the entry in `tracks[]`. */\n readonly index: number\n /** The `tracks[]` entry (an empty object when the entry is not an object). */\n readonly entry: Record<string, unknown>\n /** The entry's `fec` value, unvalidated (may be undefined). */\n readonly fec: unknown\n /** JSON path of that `fec` block. */\n readonly fecPath: string\n }\n\nconst ALL_ROLES: readonly CatalogFecRole[] = Object.freeze(['video', 'audio'])\n\nfunction recordOf(value: unknown): Record<string, unknown> | undefined {\n return value !== null && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n}\n\n/**\n * Locate the catalog entry that declares one media source's `fec` block, in\n * either catalog dialect.\n *\n * A hang `video`/`audio` rendition named `source` wins, in `roles` order, over\n * a `tracks[]` entry of the same name: when a catalog carries rendition blocks\n * beside a `tracks[]` switching-set index (the unified ABR ladder catalog),\n * the rendition blocks are authoritative, exactly as they are for hang's own\n * catalog decode. The precedence is keyed on the rendition EXISTING as an\n * object, not on it carrying `fec`: a rendition without `fec` locates with\n * `fec: undefined` and never falls through to `tracks[]`.\n *\n * Otherwise the first `tracks[]` entry named `source` locates. Returns\n * undefined when neither dialect names it, or when `catalog` is not an object.\n *\n * @param roles the hang roles to search, in order (both by default). A typed\n * track reference passes its own role only.\n */\nexport function locateCatalogFecSource(\n catalog: unknown,\n source: string,\n roles: readonly CatalogFecRole[] = ALL_ROLES,\n): CatalogFecSource | undefined {\n const root = recordOf(catalog)\n if (root === undefined) return undefined\n for (const role of roles) {\n const renditions = recordOf(recordOf(root[role])?.['renditions'])\n if (renditions === undefined || !Object.hasOwn(renditions, source)) continue\n const entry = recordOf(renditions[source])\n if (entry === undefined) continue\n return Object.freeze({\n dialect: 'hang' as const,\n role,\n entry,\n fec: entry['fec'],\n fecPath: `$.${role}.renditions[${JSON.stringify(source)}].fec`,\n })\n }\n const tracks = root['tracks']\n if (!Array.isArray(tracks)) return undefined\n const index = tracks.findIndex((track) => recordOf(track)?.['name'] === source)\n if (index < 0) return undefined\n const entry = recordOf(tracks[index]) ?? {}\n return Object.freeze({\n dialect: 'msf' as const,\n index,\n entry,\n fec: entry['fec'],\n fecPath: `$.tracks[${index}].fec`,\n })\n}\n\n/**\n * Whether an `iso-ssbg1-v1` block's optional `interleaveDepthMs` span is\n * usable: absent, or a non-negative safe integer. Zero or absence supplies no\n * elapsed deadline and still permits in-band FEC. True for every block that\n * names another profile (the span is not read) and for a non-object.\n */\nexport function isoBlockSpanUsable(fec: unknown): boolean {\n const block = recordOf(fec)\n if (block === undefined || block['blockProfile'] !== 'iso-ssbg1-v1') return true\n const ms = block['interleaveDepthMs']\n return ms === undefined || (typeof ms === 'number' && Number.isSafeInteger(ms) && ms >= 0)\n}\n\n/**\n * Whether one catalog `fec` block leaves its FEC available\n * (draft-ramadan-moq-fec \u00A75.1): the decision the container's\n * `catalogFecUnavailable` explains with a typed reason.\n *\n * False when the block's `algorithm` is a token this receiver does not\n * implement (`fecAlgorithmUnimplemented`); when it declares an FEC instance\n * (any `algorithm` but `\"none\"`) and names no implemented `blockProfile` (the retired `k-padded-v1`\n * included), or carries an `iso-ssbg1-v1` span it cannot use; and when a block\n * declaring no FEC instance carries a profile token that is not implemented.\n * True for a non-object and for every other block. It reads only the profile\n * and the span: malformed geometry (`sourceSymbols`, `symbolSize`) is\n * `catalogFecDeclaresUnconfigurableInstance`'s.\n *\n * A receiver applies no FEC to a source whose block is unavailable and joins\n * none of its repair tracks, without failing the session.\n */\nexport function catalogFecAvailable(fec: unknown): boolean {\n const block = recordOf(fec)\n if (block === undefined) return true\n if (fecAlgorithmUnimplemented(block['algorithm'])) return false\n const declaresFecInstance = block['algorithm'] !== 'none'\n const profile = block['blockProfile']\n const profileOk = isFecBlockProfile(profile) || (profile === undefined && !declaresFecInstance)\n return profileOk && (!declaresFecInstance || isoBlockSpanUsable(block))\n}\n\n/**\n * Whether `algorithm` is a token this receiver does not implement\n * (draft-ramadan-moq-fec-01 4): any string but `none`, `raptorq` and\n * `reed-solomon`. Such a track is consumed without FEC and its repair tracks\n * are not joined; the catalog is not in error. An absent algorithm is not a\n * token (the hang dialect omits it) and stays implemented.\n */\nexport function fecAlgorithmUnimplemented(algorithm: unknown): boolean {\n return typeof algorithm === 'string' && algorithm !== 'none' && algorithm !== 'raptorq' && algorithm !== 'reed-solomon'\n}\n\nfunction isPositiveSafeInteger(value: unknown): boolean {\n return typeof value === 'number' && Number.isSafeInteger(value) && value > 0\n}\n\n/** Smallest `symbolSize` under `iso-ssbg1-v1` (draft-ramadan-moq-fec-01 5.1, 6.3.4 rule 13). */\nexport const ISO_SSBG1_MIN_SYMBOL_SIZE = 14\n\n/**\n * Whether a source's `fec` block declares an FEC instance (any `algorithm`\n * but `\"none\"`) whose `sourceSymbols` or `symbolSize` is not a positive\n * integer, or whose `iso-ssbg1-v1` `symbolSize` is below\n * `ISO_SSBG1_MIN_SYMBOL_SIZE` (rule 13: such a symbol carries no protected\n * MMTP packet). Geometry a decoder cannot configure is a malformed catalog, not\n * \"no FEC\": the container's per-track resolver throws `track_fec` for exactly\n * this block, whatever its profile or span.\n */\nexport function catalogFecDeclaresUnconfigurableInstance(fec: unknown): boolean {\n const block = recordOf(fec)\n if (block === undefined || block['algorithm'] === 'none' || fecAlgorithmUnimplemented(block['algorithm'])) return false\n if (!(isPositiveSafeInteger(block['sourceSymbols']) && isPositiveSafeInteger(block['symbolSize']))) return true\n return block['blockProfile'] === 'iso-ssbg1-v1' && (block['symbolSize'] as number) < ISO_SSBG1_MIN_SYMBOL_SIZE\n}\n", "/**\n * The draft-ramadan-moq-multicast-01 endpoint and `auth` rules, written once\n * for both sides of the catalog: `validateCmsfMmtpCatalog` in\n * `@blockcast/mmt-container` throws the first issue, and the receivers in\n * `@blockcast/mmt-transport` ignore every endpoint an issue names (all of\n * them when the issue is catalog-level). It lives here because mmt-base is\n * the only package both depend on.\n *\n * Draft: Blockcast/moqcast-draft@fd962fd73d21ed281b60219c11ea61cc9e4e91c4\n * (PR #2 head; normative text equal to 3162ef93), sections 4.1 and 7.2.6.\n */\n\nexport type MulticastJoinMode = \"ssm\" | \"asm\";\n\n/** An endpoint's join mode and `S` (4.1): `sourceAddress` on SSM,\n * `auth.senderAddress` on protected ASM, absent on unprotected ASM. */\nexport interface MulticastEndpointSource {\n\tmode: MulticastJoinMode;\n\tsource?: string;\n}\n\nexport type MulticastCatalogIssueCode =\n\t| \"multicast_auth\"\n\t| \"multicast_endpoint\"\n\t| \"multicast_endpoint_auth\"\n\t| \"multicast_track\";\n\nexport interface MulticastCatalogIssue {\n\tcode: MulticastCatalogIssueCode;\n\t/** JSONPath of the offending field. */\n\tpath: string;\n\tmessage: string;\n\t/** Endpoint the issue makes a receiver ignore; absent for a catalog-level\n\t * issue, which leaves the receiver no endpoint at all (7.2.6). */\n\tendpointIndex?: number;\n}\n\n/** Canonical unpadded base64url of exactly 32 octets. */\nexport const ED25519_PUBLIC_KEY_BASE64URL = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;\n\nconst UINT32_MAX = 0xffff_ffff;\nconst AUTH_MEMBERS = new Set([\"scheme\", \"version\", \"sig\", \"keys\", \"minEpoch\"]);\nconst KEY_MEMBERS = new Set([\"kid\", \"publicKey\", \"firstEpoch\"]);\nconst ENDPOINT_AUTH_MEMBERS = new Set([\n\t\"manifestPort\",\n\t\"streamId\",\n\t\"senderAddress\",\n\t\"maxAuthDelayMs\",\n\t\"manifestTimerMs\",\n\t\"manifestTrack\",\n]);\n\ntype JsonObject = Record<string, unknown>;\n\nfunction isObject(value: unknown): value is JsonObject {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n\treturn typeof value === \"string\" && value.trim().length > 0;\n}\n\nfunction isIntegerIn(value: unknown, min: number, max: number): value is number {\n\treturn Number.isSafeInteger(value) && (value as number) >= min && (value as number) <= max;\n}\n\n/**\n * Join mode and `S` of one endpoint, or `undefined` when its fields disagree\n * and a receiver must ignore it (4.1). `sourceAddress` alone selects the mode;\n * `protocol` may only restate it, so \"amt\" and \"atsc3\" (draft -00) are\n * inconsistent here: a bearer is a `networkSource`, never a protocol.\n */\nexport function multicastEndpointSource(endpoint: unknown): MulticastEndpointSource | undefined {\n\tif (!isObject(endpoint)) return undefined;\n\tconst sourceAddress = endpoint[\"sourceAddress\"];\n\tif (sourceAddress !== undefined && !isNonEmptyString(sourceAddress)) return undefined;\n\tconst mode: MulticastJoinMode = sourceAddress === undefined ? \"asm\" : \"ssm\";\n\tconst protocol = endpoint[\"protocol\"];\n\tif (protocol !== undefined && protocol !== mode) return undefined;\n\tconst auth = endpoint[\"auth\"];\n\tconst senderAddress = isObject(auth) ? auth[\"senderAddress\"] : undefined;\n\tif (mode === \"ssm\") return senderAddress === undefined ? { mode, source: sourceAddress as string } : undefined;\n\tif (auth === undefined) return { mode };\n\treturn isNonEmptyString(senderAddress) ? { mode, source: senderAddress } : undefined;\n}\n\nfunction authIssues(auth: unknown): MulticastCatalogIssue[] {\n\tconst base = \"$.multicast.auth\";\n\tconst issue = (path: string, message: string): MulticastCatalogIssue[] => [\n\t\t{ code: \"multicast_auth\", path: `${base}${path}`, message },\n\t];\n\tif (!isObject(auth)) return issue(\"\", \"auth must be an object\");\n\tconst unknown = Object.keys(auth).filter(key => !AUTH_MEMBERS.has(key));\n\tif (unknown.length > 0) {\n\t\treturn issue(\"\", `auth has unknown member(s) ${unknown.join(\", \")}; draft -01 7.2.6 defines scheme, version, sig, keys and minEpoch`);\n\t}\n\tif (auth[\"scheme\"] !== \"bc-provenance\") return issue(\".scheme\", 'scheme must be \"bc-provenance\"');\n\tif (auth[\"version\"] !== 2) return issue(\".version\", \"version must be 2 (bc-provenance version 2, draft -01 7.2)\");\n\tif (auth[\"sig\"] !== \"ed25519\") return issue(\".sig\", 'sig must be \"ed25519\"');\n\tconst keys = auth[\"keys\"];\n\tif (!Array.isArray(keys) || keys.length < 1 || keys.length > 4) {\n\t\treturn issue(\".keys\", \"keys must be an array of one to four {kid, publicKey, firstEpoch} objects\");\n\t}\n\tconst kids = new Set<number>();\n\tconst firstEpochs = new Set<number>();\n\tfor (let index = 0; index < keys.length; index++) {\n\t\tconst key = keys[index];\n\t\tconst at = `.keys[${index}]`;\n\t\tif (!isObject(key) || Object.keys(key).some(member => !KEY_MEMBERS.has(member))) {\n\t\t\treturn issue(at, \"a key is exactly {kid, publicKey, firstEpoch}\");\n\t\t}\n\t\tif (!isIntegerIn(key[\"kid\"], 0, UINT32_MAX)) return issue(`${at}.kid`, \"kid must be an unsigned 32-bit integer\");\n\t\tif (typeof key[\"publicKey\"] !== \"string\" || !ED25519_PUBLIC_KEY_BASE64URL.test(key[\"publicKey\"])) {\n\t\t\treturn issue(`${at}.publicKey`, \"publicKey must be a 32-octet Ed25519 key in canonical unpadded base64url\");\n\t\t}\n\t\tif (!isIntegerIn(key[\"firstEpoch\"], 0, UINT32_MAX)) {\n\t\t\treturn issue(`${at}.firstEpoch`, \"firstEpoch must be an unsigned 32-bit integer\");\n\t\t}\n\t\t// A repeated kid or firstEpoch leaves no key set (7.2.6): fail the whole\n\t\t// block, never fall back to unauthenticated reception.\n\t\tif (kids.has(key[\"kid\"])) return issue(`${at}.kid`, `kid ${key[\"kid\"]} is repeated; the catalog holds no key set`);\n\t\tif (firstEpochs.has(key[\"firstEpoch\"])) {\n\t\t\treturn issue(`${at}.firstEpoch`, `firstEpoch ${key[\"firstEpoch\"]} is repeated; the catalog holds no key set`);\n\t\t}\n\t\tkids.add(key[\"kid\"]);\n\t\tfirstEpochs.add(key[\"firstEpoch\"]);\n\t}\n\tif (!isIntegerIn(auth[\"minEpoch\"], 0, UINT32_MAX)) {\n\t\treturn issue(\".minEpoch\", \"minEpoch must be an unsigned 32-bit integer\");\n\t}\n\treturn [];\n}\n\nfunction endpointAuthIssue(\n\tendpoint: JsonObject,\n\tmode: MulticastJoinMode,\n\tat: string,\n): { path: string; message: string } | undefined {\n\tconst auth = endpoint[\"auth\"];\n\tif (!isObject(auth)) return { path: `${at}.auth`, message: \"auth must be an object\" };\n\tconst unknown = Object.keys(auth).filter(key => !ENDPOINT_AUTH_MEMBERS.has(key));\n\tif (unknown.length > 0) return { path: `${at}.auth`, message: `auth has unknown member(s) ${unknown.join(\", \")}` };\n\tif (!isIntegerIn(auth[\"manifestPort\"], 1, 65535)) {\n\t\treturn { path: `${at}.auth.manifestPort`, message: \"manifestPort must be an integer from 1 to 65535\" };\n\t}\n\tif (!isIntegerIn(auth[\"streamId\"], 0, UINT32_MAX)) {\n\t\treturn { path: `${at}.auth.streamId`, message: \"streamId must be an unsigned 32-bit integer\" };\n\t}\n\tif (mode === \"ssm\" && auth[\"senderAddress\"] !== undefined) {\n\t\treturn { path: `${at}.auth.senderAddress`, message: \"senderAddress must not appear on an SSM endpoint; S is sourceAddress\" };\n\t}\n\tif (mode === \"asm\" && !isNonEmptyString(auth[\"senderAddress\"])) {\n\t\treturn { path: `${at}.auth.senderAddress`, message: \"a protected ASM endpoint requires senderAddress\" };\n\t}\n\tif (auth[\"maxAuthDelayMs\"] !== undefined && !isIntegerIn(auth[\"maxAuthDelayMs\"], 50, 1000)) {\n\t\treturn { path: `${at}.auth.maxAuthDelayMs`, message: \"maxAuthDelayMs must be an integer from 50 to 1000\" };\n\t}\n\tif (auth[\"manifestTimerMs\"] !== undefined && !isIntegerIn(auth[\"manifestTimerMs\"], 1, Number.MAX_SAFE_INTEGER)) {\n\t\treturn { path: `${at}.auth.manifestTimerMs`, message: \"manifestTimerMs must be an integer of at least 1\" };\n\t}\n\tif (auth[\"manifestTrack\"] !== undefined && !isNonEmptyString(auth[\"manifestTrack\"])) {\n\t\treturn { path: `${at}.auth.manifestTrack`, message: \"manifestTrack must be a non-empty string\" };\n\t}\n\tif (!isIntegerIn(endpoint[\"bandwidth\"], 1, Number.MAX_SAFE_INTEGER)) {\n\t\treturn { path: `${at}.bandwidth`, message: \"a protected endpoint requires bandwidth, which bounds the receiver's hold queues\" };\n\t}\n\treturn undefined;\n}\n\n/**\n * Where a catalog object carries the namespace of the catalog track it was\n * read from. A symbol, so it is never wire data and survives object spread;\n * `Symbol.for`, so two bundled copies of this module agree on it.\n */\nexport const CATALOG_NAMESPACE: unique symbol = Symbol.for(\"@blockcast/mmt-base/catalogNamespace\");\n\n/**\n * `catalog` tagged with the namespace of its catalog track. Every receiver\n * rule that takes the catalog (`multicastCatalogIssues`,\n * `admittedMulticastEndpoints` and the transport selectors built on them)\n * then judges the foreign-namespace rule against it, independent of track\n * order. Tag once, where the catalog track is read.\n */\nexport function withCatalogNamespace<T extends object>(catalog: T, namespace: string): T {\n\treturn { ...catalog, [CATALOG_NAMESPACE]: namespace };\n}\n\n/**\n * Every draft -01 4.1 / 7.2.6 violation in `catalog.multicast`, in document\n * order. Packet-id, group-address and track-existence rules are not here; the\n * container validator and the receivers already check those.\n *\n * `catalogNamespace` is the namespace of the catalog track, from `options` or else the\n * catalog's `withCatalogNamespace` tag. Without it the\n * foreign-namespace rule can only compare listed tracks with each other: the\n * first listed track's namespace becomes the reference, so which side of a\n * disagreement is dropped depends on order. It still fails closed; pass\n * `catalogNamespace` wherever the catalog track is known.\n */\nexport function multicastCatalogIssues(\n\tcatalog: unknown,\n\toptions: { catalogNamespace?: string } = {},\n): MulticastCatalogIssue[] {\n\tif (!isObject(catalog)) return [];\n\tconst issues: MulticastCatalogIssue[] = [];\n\tif (\"multicast.auth\" in catalog) {\n\t\t// Fail closed: a draft -00 publisher meant the flow to be protected, and\n\t\t// the key it names verifies nothing in bc-provenance version 2.\n\t\tissues.push({\n\t\t\tcode: \"multicast_auth\",\n\t\t\tpath: '$[\"multicast.auth\"]',\n\t\t\tmessage: 'the root \"multicast.auth\" key is retired; declare auth as multicast.auth (draft -01 7.2.6)',\n\t\t});\n\t}\n\tconst multicast = catalog[\"multicast\"];\n\tif (!isObject(multicast)) return issues;\n\tconst protectedCatalog = multicast[\"auth\"] !== undefined;\n\tif (protectedCatalog) issues.push(...authIssues(multicast[\"auth\"]));\n\tconst endpoints = multicast[\"endpoints\"];\n\tif (!Array.isArray(endpoints)) return issues;\n\n\tconst tracks = Array.isArray(catalog[\"tracks\"]) ? catalog[\"tracks\"] : [];\n\tconst namespaceOf = new Map<string, unknown>();\n\tfor (const track of tracks) {\n\t\tif (isObject(track) && typeof track[\"name\"] === \"string\") namespaceOf.set(track[\"name\"], track[\"namespace\"]);\n\t}\n\tconst tagged = (catalog as { [CATALOG_NAMESPACE]?: unknown })[CATALOG_NAMESPACE];\n\tlet referenceNamespace = options.catalogNamespace ?? (typeof tagged === \"string\" ? tagged : undefined);\n\n\tconst sources: Array<MulticastEndpointSource | undefined> = [];\n\tfor (let index = 0; index < endpoints.length; index++) {\n\t\tconst endpoint = endpoints[index];\n\t\tconst at = `$.multicast.endpoints[${index}]`;\n\t\tconst fail = (code: MulticastCatalogIssueCode, path: string, message: string) => {\n\t\t\tissues.push({ code, path, message, endpointIndex: index });\n\t\t};\n\t\tsources.push(undefined);\n\t\tif (!isObject(endpoint)) {\n\t\t\tfail(\"multicast_endpoint\", at, \"endpoint must be an object\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst protocol = endpoint[\"protocol\"];\n\t\tif (protocol !== undefined && protocol !== \"ssm\" && protocol !== \"asm\") {\n\t\t\tfail(\"multicast_endpoint\", `${at}.protocol`, `protocol must be \"ssm\" or \"asm\"; a bearer such as ${JSON.stringify(protocol)} is a networkSource entry (draft -01 4.2)`);\n\t\t\tcontinue;\n\t\t}\n\t\tconst hasAuth = endpoint[\"auth\"] !== undefined;\n\t\tif (protectedCatalog !== hasAuth) {\n\t\t\tfail(\n\t\t\t\t\"multicast_endpoint_auth\",\n\t\t\t\t`${at}.auth`,\n\t\t\t\tprotectedCatalog\n\t\t\t\t\t? \"every endpoint requires auth when multicast carries auth; a receiver ignores this endpoint\"\n\t\t\t\t\t: \"auth names no key without multicast.auth; a receiver ignores this endpoint\",\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\t\tconst sourceAddress = endpoint[\"sourceAddress\"];\n\t\tif (sourceAddress !== undefined && !isNonEmptyString(sourceAddress)) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.sourceAddress`, \"sourceAddress must be a non-empty string\");\n\t\t\tcontinue;\n\t\t}\n\t\tconst mode: MulticastJoinMode = sourceAddress === undefined ? \"asm\" : \"ssm\";\n\t\tif (protocol !== undefined && protocol !== mode) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.protocol`, `protocol ${JSON.stringify(protocol)} disagrees with sourceAddress, which selects ${mode}`);\n\t\t\tcontinue;\n\t\t}\n\t\t// The (S, G, port) rule below compares ports with ===, so only an\n\t\t// integer may reach it: \"5001\" and 5001 would otherwise both pass.\n\t\tif (!isIntegerIn(endpoint[\"port\"], 1, 65535)) {\n\t\t\tfail(\"multicast_endpoint\", `${at}.port`, \"port must be an integer from 1 to 65535\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (hasAuth) {\n\t\t\tconst authIssue = endpointAuthIssue(endpoint, mode, at);\n\t\t\tif (authIssue) {\n\t\t\t\tfail(\"multicast_endpoint_auth\", authIssue.path, authIssue.message);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\t\tsources[index] = multicastEndpointSource(endpoint);\n\n\t\tconst refs = Array.isArray(endpoint[\"tracks\"]) ? endpoint[\"tracks\"] : [];\n\t\tfor (let refIndex = 0; refIndex < refs.length; refIndex++) {\n\t\t\tconst ref = refs[refIndex];\n\t\t\tconst name = isObject(ref) ? ref[\"name\"] : ref;\n\t\t\tif (typeof name !== \"string\") continue;\n\t\t\tconst namespace = namespaceOf.get(name);\n\t\t\tif (typeof namespace !== \"string\") continue;\n\t\t\treferenceNamespace ??= namespace;\n\t\t\tif (namespace !== referenceNamespace) {\n\t\t\t\tfail(\"multicast_track\", `${at}.tracks[${refIndex}].name`, `track ${JSON.stringify(name)} declares namespace ${JSON.stringify(namespace)}, not the catalog track's ${JSON.stringify(referenceNamespace)}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// One endpoint object per (S, G, port), and a manifestPort that is no port\n\t// and no other manifestPort of its (S, G) (4.1, 7.2.6). The rule binds the\n\t// endpoint whose manifestPort clashes; two equal tuples or two equal\n\t// manifestPorts break it on both sides, so both are named.\n\tconst tupleOf = (index: number) => {\n\t\tconst endpoint = endpoints[index] as JsonObject;\n\t\tconst auth = endpoint[\"auth\"];\n\t\treturn {\n\t\t\tsg: JSON.stringify([sources[index]!.source ?? null, endpoint[\"groupAddress\"]]),\n\t\t\tport: endpoint[\"port\"],\n\t\t\tmanifestPort: isObject(auth) ? auth[\"manifestPort\"] : undefined,\n\t\t};\n\t};\n\tfor (let index = 0; index < endpoints.length; index++) {\n\t\tif (!sources[index]) continue;\n\t\tconst own = tupleOf(index);\n\t\tconst at = `$.multicast.endpoints[${index}]`;\n\t\tif (own.manifestPort !== undefined && own.manifestPort === own.port) {\n\t\t\tissues.push({ code: \"multicast_endpoint_auth\", path: `${at}.auth.manifestPort`, message: \"manifestPort must differ from the endpoint's own port\", endpointIndex: index });\n\t\t\tcontinue;\n\t\t}\n\t\tfor (let other = 0; other < endpoints.length; other++) {\n\t\t\tif (other === index || !sources[other]) continue;\n\t\t\tconst peer = tupleOf(other);\n\t\t\tif (peer.sg !== own.sg) continue;\n\t\t\tif (peer.port === own.port) {\n\t\t\t\tissues.push({ code: \"multicast_endpoint\", path: `${at}.port`, message: `(S, G, port) is shared with endpoint ${other}; one endpoint object per multicast tuple`, endpointIndex: index });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (own.manifestPort !== undefined && (own.manifestPort === peer.port || own.manifestPort === peer.manifestPort)) {\n\t\t\t\tissues.push({ code: \"multicast_endpoint_auth\", path: `${at}.auth.manifestPort`, message: `manifestPort is a port or manifestPort of endpoint ${other} on the same (S, G)`, endpointIndex: index });\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\treturn issues;\n}\n\n/**\n * Indices of the endpoints a receiver may use: none when a catalog-level\n * issue leaves it no key set (7.2.6), otherwise every endpoint no issue\n * names. Other receiver checks (group address, packet ids) still apply.\n */\nexport function admittedMulticastEndpoints(\n\tcatalog: unknown,\n\toptions: { catalogNamespace?: string } = {},\n): ReadonlySet<number> {\n\tconst multicast = isObject(catalog) ? catalog[\"multicast\"] : undefined;\n\tconst endpoints = isObject(multicast) && Array.isArray(multicast[\"endpoints\"]) ? multicast[\"endpoints\"] : [];\n\tconst issues = multicastCatalogIssues(catalog, options);\n\tif (issues.some(issue => issue.endpointIndex === undefined)) return new Set();\n\tconst ignored = new Set(issues.map(issue => issue.endpointIndex));\n\treturn new Set(endpoints.map((_, index) => index).filter(index => !ignored.has(index)));\n}\n", "import { blake3 } from '@noble/hashes/blake3'\n\nexport const SOURCE_AUTH_MANIFEST_VERSION = 1\nexport const SOURCE_AUTH_OBJECT_ENTRY_FRAME = 1\nexport const SOURCE_AUTH_GROUP_CLOSE_FRAME = 2\nexport const SOURCE_AUTH_OBJECT_ENTRY_LENGTH = 52\nexport const SOURCE_AUTH_GROUP_CLOSE_LENGTH = 112\nexport const SOURCE_AUTH_MAX_OBJECTS_PER_GROUP = 4096\nexport const SOURCE_AUTH_MAX_TRUE_LENGTH = 16 * 1024 * 1024\nexport const SOURCE_AUTH_SCOPE_LENGTH = 32\n\nconst SIGNATURE_DOMAIN = new TextEncoder().encode('blockcast-mmt-root-v1\\0\\0\\0')\n\nexport interface SourceAuthObjectEntry {\n kind: 'object'\n groupId: bigint\n objectId: number\n trueLength: number\n digest: Uint8Array\n}\n\nexport interface SourceAuthGroupClose {\n kind: 'close'\n groupId: bigint\n objectCount: number\n root: Uint8Array\n signature: Uint8Array\n}\n\nexport type SourceAuthManifestFrame = SourceAuthObjectEntry | SourceAuthGroupClose\n\nfunction requireBytes(name: string, value: Uint8Array, length: number): void {\n if (value.byteLength !== length) {\n throw new Error(`${name} must be ${length} bytes, got ${value.byteLength}`)\n }\n}\n\nfunction requireU32(name: string, value: number): void {\n if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff) {\n throw new Error(`${name} must be an unsigned 32-bit integer`)\n }\n}\n\nfunction requireU64(name: string, value: bigint): void {\n if (value < 0n || value > 0xffff_ffff_ffff_ffffn) {\n throw new Error(`${name} must be an unsigned 64-bit integer`)\n }\n}\n\nfunction requireTrueLength(value: number): void {\n requireU32('trueLength', value)\n if (value > SOURCE_AUTH_MAX_TRUE_LENGTH) {\n throw new Error(`trueLength exceeds the ${SOURCE_AUTH_MAX_TRUE_LENGTH}-byte profile maximum`)\n }\n}\n\nexport function sourceAuthObjectDigest(object: Uint8Array): Uint8Array {\n return blake3(object)\n}\n\nexport function sourceAuthLeafHash(\n groupId: bigint,\n objectId: number,\n digest: Uint8Array,\n trueLength: number,\n): Uint8Array {\n requireU64('groupId', groupId)\n requireU32('objectId', objectId)\n requireBytes('digest', digest, 32)\n requireTrueLength(trueLength)\n const preimage = new Uint8Array(49)\n const view = new DataView(preimage.buffer)\n view.setBigUint64(1, groupId)\n view.setUint32(9, objectId)\n preimage.set(digest, 13)\n view.setUint32(45, trueLength)\n return blake3(preimage)\n}\n\nexport function sourceAuthMerkleRoot(\n entries: readonly {\n groupId: bigint\n objectId: number\n digest: Uint8Array\n trueLength: number\n }[],\n): Uint8Array {\n if (entries.length === 0) throw new Error('Merkle tree requires at least one object')\n if (entries.length > SOURCE_AUTH_MAX_OBJECTS_PER_GROUP) {\n throw new Error(`group exceeds ${SOURCE_AUTH_MAX_OBJECTS_PER_GROUP} objects`)\n }\n const groupId = entries[0]!.groupId\n const objectIds = new Set<number>()\n const ordered = [...entries].sort((left, right) => left.objectId - right.objectId)\n let level = ordered.map((entry) => {\n if (entry.groupId !== groupId) throw new Error('Merkle entries must belong to one group')\n if (objectIds.has(entry.objectId)) throw new Error(`duplicate objectId ${entry.objectId}`)\n objectIds.add(entry.objectId)\n return sourceAuthLeafHash(entry.groupId, entry.objectId, entry.digest, entry.trueLength)\n })\n while (level.length > 1) {\n const parents: Uint8Array[] = []\n for (let index = 0; index < level.length; index += 2) {\n const left = level[index]!\n const right = level[index + 1] ?? left\n const preimage = new Uint8Array(65)\n preimage[0] = 1\n preimage.set(left, 1)\n preimage.set(right, 33)\n parents.push(blake3(preimage))\n }\n level = parents\n }\n return level[0]!\n}\n\nexport function trimRecoveredSourceAuthObject(\n recoveredSymbol: Uint8Array,\n trueLength: number,\n): Uint8Array {\n requireTrueLength(trueLength)\n if (trueLength > recoveredSymbol.byteLength) {\n throw new Error(`true length ${trueLength} exceeds recovered object size ${recoveredSymbol.byteLength}`)\n }\n return recoveredSymbol.subarray(0, trueLength)\n}\n\nexport function encodeSourceAuthObjectEntry(\n entry: Omit<SourceAuthObjectEntry, 'kind'>,\n): Uint8Array {\n requireU32('objectId', entry.objectId)\n requireTrueLength(entry.trueLength)\n requireU64('groupId', entry.groupId)\n requireBytes('digest', entry.digest, 32)\n const frame = new Uint8Array(SOURCE_AUTH_OBJECT_ENTRY_LENGTH)\n const view = new DataView(frame.buffer)\n frame[0] = SOURCE_AUTH_OBJECT_ENTRY_FRAME\n frame[1] = SOURCE_AUTH_MANIFEST_VERSION\n view.setBigUint64(4, entry.groupId)\n view.setUint32(12, entry.objectId)\n view.setUint32(16, entry.trueLength)\n frame.set(entry.digest, 20)\n return frame\n}\n\nexport function encodeSourceAuthGroupClose(\n close: Omit<SourceAuthGroupClose, 'kind'>,\n): Uint8Array {\n requireU32('objectCount', close.objectCount)\n if (close.objectCount === 0 || close.objectCount > SOURCE_AUTH_MAX_OBJECTS_PER_GROUP) {\n throw new Error(`objectCount must be between 1 and ${SOURCE_AUTH_MAX_OBJECTS_PER_GROUP}`)\n }\n requireU64('groupId', close.groupId)\n requireBytes('root', close.root, 32)\n requireBytes('signature', close.signature, 64)\n const frame = new Uint8Array(SOURCE_AUTH_GROUP_CLOSE_LENGTH)\n const view = new DataView(frame.buffer)\n frame[0] = SOURCE_AUTH_GROUP_CLOSE_FRAME\n frame[1] = SOURCE_AUTH_MANIFEST_VERSION\n view.setBigUint64(4, close.groupId)\n view.setUint32(12, close.objectCount)\n frame.set(close.root, 16)\n frame.set(close.signature, 48)\n return frame\n}\n\nexport function sourceAuthSignaturePreimage(\n authScope: Uint8Array,\n groupId: bigint,\n objectCount: number,\n root: Uint8Array,\n): Uint8Array {\n requireU32('objectCount', objectCount)\n if (objectCount === 0 || objectCount > SOURCE_AUTH_MAX_OBJECTS_PER_GROUP) {\n throw new Error(`objectCount must be between 1 and ${SOURCE_AUTH_MAX_OBJECTS_PER_GROUP}`)\n }\n requireU64('groupId', groupId)\n requireBytes('authScope', authScope, SOURCE_AUTH_SCOPE_LENGTH)\n requireBytes('root', root, 32)\n const preimage = new Uint8Array(100)\n const view = new DataView(preimage.buffer)\n preimage.set(SIGNATURE_DOMAIN)\n preimage.set(authScope, 24)\n view.setBigUint64(56, groupId)\n view.setUint32(64, objectCount)\n preimage.set(root, 68)\n return preimage\n}\n\nexport function decodeSourceAuthManifestFrame(frame: Uint8Array): SourceAuthManifestFrame {\n const frameType = frame[0]\n const expectedLength = frameType === SOURCE_AUTH_OBJECT_ENTRY_FRAME\n ? SOURCE_AUTH_OBJECT_ENTRY_LENGTH\n : frameType === SOURCE_AUTH_GROUP_CLOSE_FRAME\n ? SOURCE_AUTH_GROUP_CLOSE_LENGTH\n : 0\n if (expectedLength === 0) throw new Error(`unsupported manifest frame type ${frameType}`)\n if (frame.byteLength !== expectedLength) {\n throw new Error(`manifest frame has invalid length ${frame.byteLength} for type ${frameType}`)\n }\n if (frame[1] !== SOURCE_AUTH_MANIFEST_VERSION) {\n throw new Error(`unsupported manifest version ${frame[1]}`)\n }\n if (frame[2] !== 0 || frame[3] !== 0) throw new Error('manifest reserved bits must be zero')\n\n const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength)\n const groupId = view.getBigUint64(4)\n if (frameType === SOURCE_AUTH_OBJECT_ENTRY_FRAME) {\n const entry: SourceAuthObjectEntry = {\n kind: 'object',\n groupId,\n objectId: view.getUint32(12),\n trueLength: view.getUint32(16),\n digest: frame.slice(20, 52),\n }\n requireTrueLength(entry.trueLength)\n return entry\n }\n const close: SourceAuthGroupClose = {\n kind: 'close',\n groupId,\n objectCount: view.getUint32(12),\n root: frame.slice(16, 48),\n signature: frame.slice(48, 112),\n }\n if (close.objectCount === 0 || close.objectCount > SOURCE_AUTH_MAX_OBJECTS_PER_GROUP) {\n throw new Error(`objectCount must be between 1 and ${SOURCE_AUTH_MAX_OBJECTS_PER_GROUP}`)\n }\n return close\n}\n", "import {\n decodeSourceAuthManifestFrame,\n sourceAuthMerkleRoot,\n sourceAuthObjectDigest,\n sourceAuthSignaturePreimage,\n type SourceAuthGroupClose,\n type SourceAuthObjectEntry,\n} from './source-auth.js'\n\nexport type ManifestAuthMode = 'off' | 'shadow' | 'enforce'\nexport type MissingManifestPolicy = 'stall' | 'fallback-shadow'\nexport type ManifestFailureReason =\n | 'sigInvalid'\n | 'digestMismatch'\n | 'manifestLate'\n | 'manifestAbsent'\n | 'verifierError'\n\nexport interface ManifestAuthConfig {\n mode: ManifestAuthMode\n onMissingManifest: MissingManifestPolicy\n}\n\nexport interface ManifestObjectIdentity {\n trackName: string\n /** Stable v2 auth binding. Defaults to trackName for legacy single-format callers. */\n bindingId?: string\n /** MoQ group ids are u62; values above 2^53 must be passed as bigint. */\n groupId: number | bigint\n objectId: number\n packetId?: number\n ssId?: number\n}\n\nexport interface ManifestFecBlockMapper {\n readonly d: number\n derive(groupId: number, objectId: number): { ssId: number }\n}\n\nexport interface ManifestTrackContext {\n broadcastNamespaceId: string\n trackName: string\n /** Stable v2 auth binding. Defaults to trackName for legacy single-format callers. */\n bindingId?: string\n sourceSymbolFormat?: string\n keyEpoch: number\n codec: string\n timing: string\n fecGeometry: string\n manifestFormatVersion: number\n /** Authenticated-catalog binding for the namespace, track, epoch, codec,\n * timing, FEC geometry, and manifest version fields above. */\n authScope: Uint8Array\n /** The catalog key set (draft-ramadan-moq-multicast-01 7.2.6). The verifier\n * signs off `keyEpoch` against the one key whose range covers it. */\n keys: readonly ManifestKey[]\n bitrate: number\n groupDurationMs: number\n mapper: ManifestFecBlockMapper\n manifestChannelAuthenticated: boolean\n}\n\nexport interface ManifestVerificationStats {\n verified: number\n /** Terminal outcomes only: an object counted here will never be verified. */\n failures: Record<ManifestFailureReason, number>\n /**\n * Objects currently held in quarantine awaiting a manifest \u2014 pending, NOT\n * failed, and deliberately disjoint from `failures`. An enforce-mode stall is\n * accounted here first and moves into `failures` only once the verifier gives\n * up on it, so a reason breakdown that ignores this gauge under-reports live\n * stalls by whatever is still in flight (BLO-30007).\n */\n quarantined: number\n quarantinedBytes: number\n}\n\nexport interface ManifestFailure {\n trackName: string\n bindingId?: string\n groupId: number | bigint\n objectId: number\n reason: ManifestFailureReason\n}\n\nexport interface ManifestAdmission {\n action: 'play' | 'stall' | 'drop'\n verified: boolean\n reason?: ManifestFailureReason\n}\n\nexport type SignatureVerifier = (\n publicKey: Uint8Array,\n message: Uint8Array,\n signature: Uint8Array,\n) => boolean | Promise<boolean>\n\n/** One `multicast.auth.keys[]` entry, its `publicKey` decoded to 32 octets. */\nexport interface ManifestKey {\n kid: number\n publicKey: Uint8Array\n firstEpoch: number\n}\n\n/**\n * The key that signs `epoch` (draft-ramadan-moq-multicast-01 7.2.6): a key\n * signs from its `firstEpoch` up to, not including, the next larger\n * `firstEpoch` in the set, and the largest signs every later epoch. With\n * `kid`, a key whose range does not cover `epoch` yields nothing, so a\n * retired key cannot carry a record past its successor's `firstEpoch`.\n * A set repeating a `kid` or a `firstEpoch` is no key set and yields nothing.\n */\nexport function manifestKeyFor(\n keys: readonly ManifestKey[],\n epoch: number,\n kid?: number,\n): Uint8Array | undefined {\n if (new Set(keys.map((key) => key.kid)).size !== keys.length) return undefined\n if (new Set(keys.map((key) => key.firstEpoch)).size !== keys.length) return undefined\n let signer: ManifestKey | undefined\n for (const key of keys) {\n if (key.firstEpoch <= epoch && (signer === undefined || key.firstEpoch > signer.firstEpoch)) signer = key\n }\n if (signer === undefined || (kid !== undefined && signer.kid !== kid)) return undefined\n return signer.publicKey\n}\n\nexport function manifestBindingId(trackName: string, sourceSymbolFormat: string): string {\n return JSON.stringify([trackName, sourceSymbolFormat])\n}\n\nexport function createWebCryptoEd25519Verifier(\n subtle: SubtleCrypto = globalThis.crypto.subtle,\n): SignatureVerifier {\n return async (publicKey, message, signature) => {\n const keyBytes = Uint8Array.from(publicKey)\n const messageBytes = Uint8Array.from(message)\n const signatureBytes = Uint8Array.from(signature)\n const key = await subtle.importKey('raw', keyBytes.buffer, 'Ed25519', false, ['verify'])\n return subtle.verify('Ed25519', key, signatureBytes.buffer, messageBytes.buffer)\n }\n}\n\ninterface PendingObject {\n identity: ManifestObjectIdentity\n data: Uint8Array\n release: () => void\n discard?: () => void\n bytes: number\n}\n\ninterface GroupState {\n entries: Map<number, SourceAuthObjectEntry>\n pending: Map<number, PendingObject>\n close?: SourceAuthGroupClose\n /** Close frame that arrived before all object entries (manifest-channel\n * reordering); retried once the entry count matches. */\n pendingClose?: SourceAuthGroupClose\n rootVerified: boolean\n signaturePending?: boolean\n failed?: ManifestFailureReason\n}\n\ninterface TrackState {\n context: ManifestTrackContext\n groups: Map<bigint, GroupState>\n quarantine: PendingObject[]\n quarantinedBytes: number\n capBytes: number\n manifestEntries: number\n maxManifestEntries: number\n maxGroups: number\n}\n\nconst FAILURE_REASONS: readonly ManifestFailureReason[] = [\n 'sigInvalid',\n 'digestMismatch',\n 'manifestLate',\n 'manifestAbsent',\n 'verifierError',\n]\n\nconst equalBytes = (left: Uint8Array, right: Uint8Array): boolean => {\n if (left.byteLength !== right.byteLength) return false\n let diff = 0\n for (let index = 0; index < left.byteLength; index++) diff |= left[index]! ^ right[index]!\n return diff === 0\n}\n\nconst isPromiseLike = (value: boolean | Promise<boolean>): value is Promise<boolean> =>\n typeof value === 'object' && value !== null && typeof value.then === 'function'\n\nexport class ManifestVerifier {\n #config: ManifestAuthConfig\n readonly #verifySignature: SignatureVerifier\n readonly #onFailure?: (failure: ManifestFailure) => void\n readonly #tracks = new Map<string, TrackState>()\n readonly #stats: Omit<ManifestVerificationStats, 'quarantined'> = {\n verified: 0,\n failures: Object.fromEntries(FAILURE_REASONS.map((reason) => [reason, 0])) as Record<ManifestFailureReason, number>,\n quarantinedBytes: 0,\n }\n #disposed = false\n\n constructor(options: {\n manifestAuth: ManifestAuthConfig\n verifySignature: SignatureVerifier\n onFailure?: (failure: ManifestFailure) => void\n }) {\n this.#config = { ...options.manifestAuth }\n this.#verifySignature = options.verifySignature\n this.#onFailure = options.onFailure\n }\n\n get config(): Readonly<ManifestAuthConfig> {\n return this.#config\n }\n\n get stats(): ManifestVerificationStats {\n let quarantined = 0\n for (const track of this.#tracks.values()) quarantined += track.quarantine.length\n return {\n verified: this.#stats.verified,\n failures: { ...this.#stats.failures },\n quarantined,\n quarantinedBytes: this.#stats.quarantinedBytes,\n }\n }\n\n setMode(mode: ManifestAuthMode): void {\n this.#config = { ...this.#config, mode }\n if (mode !== 'enforce') this.#releaseAllQuarantine(false)\n }\n\n registerTrack(context: ManifestTrackContext): void {\n const bindingId = context.bindingId ?? context.trackName\n if (this.#tracks.has(bindingId)) {\n throw new Error(`manifest binding ${bindingId} is already registered`)\n }\n if (!Number.isFinite(context.bitrate) || context.bitrate <= 0) {\n throw new Error('manifest track bitrate must be > 0')\n }\n if (!Number.isFinite(context.groupDurationMs) || context.groupDurationMs <= 0) {\n throw new Error('manifest track groupDurationMs must be > 0')\n }\n if (context.authScope.byteLength !== 32) throw new Error('manifest authScope must be 32 bytes')\n for (const key of context.keys) {\n if (key.publicKey.byteLength !== 32) throw new Error('manifest keys must carry 32-byte public keys')\n }\n if (manifestKeyFor(context.keys, context.keyEpoch) === undefined) {\n throw new Error(`manifest keys hold no key set signing keyEpoch ${context.keyEpoch}`)\n }\n for (const [name, value] of Object.entries({\n broadcastNamespaceId: context.broadcastNamespaceId,\n trackName: context.trackName,\n codec: context.codec,\n timing: context.timing,\n fecGeometry: context.fecGeometry,\n })) {\n if (value.length === 0) throw new Error(`manifest ${name} must not be empty`)\n }\n this.#tracks.set(bindingId, {\n context: { ...context, bindingId },\n groups: new Map(),\n quarantine: [],\n quarantinedBytes: 0,\n capBytes: Math.ceil((context.bitrate * context.groupDurationMs) / 8000),\n manifestEntries: 0,\n maxManifestEntries: Math.ceil((context.bitrate * context.groupDurationMs) / (8000 * 52)),\n maxGroups: context.mapper.d + 1,\n })\n }\n\n ingestManifestFrame(bindingId: string, frameBytes: Uint8Array): void {\n const track = this.#requireTrack(bindingId)\n try {\n const frame = decodeSourceAuthManifestFrame(frameBytes)\n const groupId = frame.groupId\n const group = this.#group(track, groupId)\n if (frame.kind === 'object') {\n if (group.close || group.signaturePending || group.failed) {\n this.#failure({ trackName: track.context.trackName, bindingId, groupId, objectId: frame.objectId }, 'verifierError')\n return\n }\n if (group.entries.has(frame.objectId)) {\n this.#failure({ trackName: track.context.trackName, bindingId, groupId, objectId: frame.objectId }, 'verifierError')\n return\n }\n if (group.entries.size >= 4096 || track.manifestEntries >= track.maxManifestEntries) {\n this.#failure({ trackName: track.context.trackName, bindingId, groupId, objectId: frame.objectId }, 'verifierError')\n return\n }\n group.entries.set(frame.objectId, frame)\n track.manifestEntries++\n const pending = group.pending.get(frame.objectId)\n if (pending) {\n this.#failure(pending.identity, 'manifestLate')\n this.#resolvePending(track, group, pending, frame)\n }\n if (group.pendingClose && group.entries.size === group.pendingClose.objectCount) {\n const close = group.pendingClose\n group.pendingClose = undefined\n this.#closeGroup(track, groupId, group, close)\n }\n return\n }\n this.#closeGroup(track, groupId, group, frame)\n } catch {\n this.#failure({ trackName: track.context.trackName, bindingId, groupId: 0, objectId: 0 }, 'verifierError')\n }\n }\n\n admitObject(\n identity: ManifestObjectIdentity,\n data: Uint8Array,\n release: () => void,\n /**\n * Called instead of `release` when a stalled object is terminally rejected\n * rather than played. A caller that drives a sequence off `release` (the FEC\n * symbol loop in `mmtp-router` does) must pass this, or an enforce-mode\n * discard leaves the sequence waiting on a callback that will never fire.\n * A `drop` returned from this call is the synchronous form of the same\n * outcome and does NOT also invoke `discard`.\n */\n discard?: () => void,\n ): ManifestAdmission {\n if (this.#config.mode === 'off') {\n release()\n return { action: 'play', verified: false }\n }\n const track = this.#tracks.get(identity.bindingId ?? identity.trackName)\n if (!track) {\n release()\n this.#failure(identity, 'verifierError')\n return { action: 'play', verified: false, reason: 'verifierError' }\n }\n try {\n if (!this.#validateIdentity(track, identity)) {\n return this.#rejectOrShadow(identity, release, 'digestMismatch')\n }\n const group = this.#group(track, BigInt(identity.groupId))\n if (group.failed === 'sigInvalid' || group.failed === 'digestMismatch') {\n return this.#rejectOrShadow(identity, release, group.failed)\n }\n const entry = group.entries.get(identity.objectId)\n if (entry) return this.#admitAgainstEntry(track, group, identity, data, release, entry, discard)\n if (this.#config.mode === 'shadow' || this.#config.onMissingManifest === 'fallback-shadow') {\n release()\n this.#failure(identity, 'manifestAbsent')\n return { action: 'play', verified: false, reason: 'manifestAbsent' }\n }\n if (!this.#quarantine(track, group, { identity, data, release, discard, bytes: data.byteLength })) {\n return { action: 'drop', verified: false }\n }\n return { action: 'stall', verified: false }\n } catch {\n release()\n this.#failure(identity, 'verifierError')\n return { action: 'play', verified: false, reason: 'verifierError' }\n }\n }\n\n dispose(): void {\n if (this.#disposed) return\n this.#disposed = true\n this.#releaseAllQuarantine(false)\n this.#tracks.clear()\n }\n\n #requireTrack(bindingId: string): TrackState {\n const track = this.#tracks.get(bindingId)\n if (!track) throw new Error(`unknown manifest binding ${bindingId}`)\n return track\n }\n\n #group(track: TrackState, groupId: bigint): GroupState {\n let group = track.groups.get(groupId)\n if (!group) {\n group = { entries: new Map(), pending: new Map(), rootVerified: false }\n track.groups.set(groupId, group)\n while (track.groups.size > track.maxGroups) {\n const oldest = track.groups.entries().next().value as [bigint, GroupState] | undefined\n if (!oldest) break\n const [oldestId, oldestGroup] = oldest\n for (const pending of [...oldestGroup.pending.values()]) {\n this.#removePending(track, oldestGroup, pending)\n this.#giveUpOnPending(pending)\n // The group aged out with objects still waiting on a manifest that\n // never arrived. That is an absent manifest, not a verifier fault:\n // `verifierError` is reserved for an exception/crash in the verifier\n // itself, so mislabelling here reads on the graduation dashboard as a\n // software bug and hides a benign absent-manifest stall (BLO-30007).\n this.#failure(pending.identity, 'manifestAbsent')\n }\n track.manifestEntries -= oldestGroup.entries.size\n track.groups.delete(oldestId)\n }\n }\n return group\n }\n\n #validateIdentity(track: TrackState, identity: ManifestObjectIdentity): boolean {\n const coordinates = track.context.mapper.derive(Number(identity.groupId), identity.objectId)\n return identity.ssId === undefined || coordinates.ssId === identity.ssId\n }\n\n #admitAgainstEntry(\n track: TrackState,\n group: GroupState,\n identity: ManifestObjectIdentity,\n data: Uint8Array,\n release: () => void,\n entry: SourceAuthObjectEntry,\n discard?: () => void,\n ): ManifestAdmission {\n if (entry.trueLength !== data.byteLength) {\n return this.#rejectOrShadow(identity, release, 'digestMismatch')\n }\n const digestMatches = equalBytes(sourceAuthObjectDigest(data.subarray(0, entry.trueLength)), entry.digest)\n if (!digestMatches) return this.#rejectOrShadow(identity, release, 'digestMismatch')\n if (!track.context.manifestChannelAuthenticated && !group.rootVerified) {\n if (this.#config.mode === 'shadow') {\n release()\n return { action: 'play', verified: false }\n }\n if (!this.#quarantine(track, group, { identity, data, release, discard, bytes: data.byteLength })) {\n return { action: 'drop', verified: false }\n }\n return { action: 'stall', verified: false }\n }\n release()\n this.#stats.verified++\n return { action: 'play', verified: true }\n }\n\n #rejectOrShadow(\n identity: ManifestObjectIdentity,\n release: () => void,\n reason: 'digestMismatch' | 'sigInvalid',\n ): ManifestAdmission {\n this.#failure(identity, reason)\n if (this.#config.mode === 'enforce') {\n console.warn(JSON.stringify({ event: 'manifest-enforce-drop', track: identity.trackName, group: String(identity.groupId), object: identity.objectId, reason }))\n return { action: 'drop', verified: false, reason }\n }\n release()\n return { action: 'play', verified: false, reason }\n }\n\n #resolvePending(\n track: TrackState,\n group: GroupState,\n pending: PendingObject,\n entry: SourceAuthObjectEntry,\n ): void {\n this.#removePending(track, group, pending)\n this.#admitAgainstEntry(track, group, pending.identity, pending.data, pending.release, entry, pending.discard)\n }\n\n #closeGroup(\n track: TrackState,\n groupId: bigint,\n group: GroupState,\n close: SourceAuthGroupClose,\n ): void {\n if (group.close || group.signaturePending) return\n const entries = [...group.entries.values()]\n if (entries.length !== close.objectCount) {\n // Manifest-channel reordering: the close frame beat some object entries.\n // Stash it (latest-wins on duplicates) so the object-entry path can\n // re-attempt the close once the count matches; quarantined objects stay\n // quarantined, and manifestAbsent below is a signal, not an eviction.\n group.pendingClose = close\n const hadPending = group.pending.size > 0\n if (this.#config.onMissingManifest === 'fallback-shadow' || this.#config.mode === 'shadow') {\n for (const pending of [...group.pending.values()]) {\n this.#removePending(track, group, pending)\n pending.release()\n this.#failure(pending.identity, 'manifestAbsent')\n }\n } else {\n for (const pending of group.pending.values()) {\n this.#failure(pending.identity, 'manifestAbsent')\n }\n }\n if (!hadPending) {\n this.#failure({ trackName: track.context.trackName, groupId, objectId: 0 }, 'manifestAbsent')\n }\n return\n }\n if (!equalBytes(sourceAuthMerkleRoot(entries), close.root)) {\n group.failed = 'digestMismatch'\n this.#failGroup(track, groupId, group, 'digestMismatch')\n return\n }\n const preimage = sourceAuthSignaturePreimage(track.context.authScope, close.groupId, close.objectCount, close.root)\n let signatureValid: boolean | Promise<boolean>\n try {\n signatureValid = this.#verifyAcceptedSignature(track, preimage, close.signature)\n } catch {\n this.#failOpenGroup(track, groupId, group)\n return\n }\n if (isPromiseLike(signatureValid)) {\n group.signaturePending = true\n void signatureValid.then(\n (valid) => {\n if (this.#disposed) return\n group.signaturePending = false\n this.#finishGroupSignature(track, groupId, group, close, valid)\n },\n () => {\n if (this.#disposed) return\n group.signaturePending = false\n this.#failOpenGroup(track, groupId, group)\n },\n )\n return\n }\n this.#finishGroupSignature(track, groupId, group, close, signatureValid)\n }\n\n #finishGroupSignature(\n track: TrackState,\n groupId: bigint,\n group: GroupState,\n close: SourceAuthGroupClose,\n signatureValid: boolean,\n ): void {\n if (!signatureValid) {\n group.failed = 'sigInvalid'\n this.#failGroup(track, groupId, group, 'sigInvalid')\n return\n }\n group.failed = undefined\n group.close = close\n group.pendingClose = undefined\n group.rootVerified = true\n for (const pending of [...group.pending.values()]) {\n const entry = group.entries.get(pending.identity.objectId)\n if (entry) this.#resolvePending(track, group, pending, entry)\n else if (this.#config.onMissingManifest === 'fallback-shadow') {\n this.#removePending(track, group, pending)\n pending.release()\n this.#failure(pending.identity, 'manifestAbsent')\n }\n }\n }\n\n #verifyAcceptedSignature(\n track: TrackState,\n message: Uint8Array,\n signature: Uint8Array,\n ): boolean | Promise<boolean> {\n const keys = [manifestKeyFor(track.context.keys, track.context.keyEpoch)!]\n const pending: Promise<boolean | 'error'>[] = []\n let hadSynchronousError = false\n for (const key of keys) {\n try {\n const result = this.#verifySignature(key, message, signature)\n if (isPromiseLike(result)) {\n pending.push(Promise.resolve(result).then((valid) => valid, () => 'error' as const))\n } else if (result) {\n return true\n }\n } catch {\n hadSynchronousError = true\n }\n }\n if (pending.length === 0) {\n if (hadSynchronousError) throw new Error('manifest signature verifier unavailable')\n return false\n }\n return Promise.all(pending).then((results) => {\n if (results.some((valid) => valid === true)) return true\n if (hadSynchronousError || results.some((valid) => valid === 'error')) {\n throw new Error('manifest signature verifier unavailable')\n }\n return false\n })\n }\n\n #failGroup(\n track: TrackState,\n groupId: bigint,\n group: GroupState,\n reason: 'digestMismatch' | 'sigInvalid',\n ): void {\n if (group.pending.size === 0) {\n this.#failure({ trackName: track.context.trackName, groupId, objectId: 0 }, reason)\n return\n }\n for (const pending of [...group.pending.values()]) {\n this.#removePending(track, group, pending)\n // Same contract as an eviction: a quarantined object that is rejected\n // rather than played must still tell the caller it is finished, or a\n // caller sequencing off `release` waits forever (BLO-30095).\n if (this.#rejectOrShadow(pending.identity, pending.release, reason).action === 'drop') {\n pending.discard?.()\n }\n }\n }\n\n #removePending(track: TrackState, group: GroupState, pending: PendingObject): void {\n if (group.pending.get(pending.identity.objectId) === pending) {\n group.pending.delete(pending.identity.objectId)\n }\n const index = track.quarantine.indexOf(pending)\n if (index >= 0) track.quarantine.splice(index, 1)\n track.quarantinedBytes -= pending.bytes\n this.#stats.quarantinedBytes -= pending.bytes\n }\n\n #quarantine(track: TrackState, group: GroupState, pending: PendingObject): boolean {\n if (group.pending.has(pending.identity.objectId)) return false\n group.pending.set(pending.identity.objectId, pending)\n track.quarantine.push(pending)\n track.quarantinedBytes += pending.bytes\n this.#stats.quarantinedBytes += pending.bytes\n this.#enforceCap(track)\n return true\n }\n\n #enforceCap(track: TrackState): void {\n while (track.quarantinedBytes > track.capBytes && track.quarantine.length > 0) {\n const pending = track.quarantine.shift()!\n const group = track.groups.get(BigInt(pending.identity.groupId))\n if (group) group.pending.delete(pending.identity.objectId)\n track.quarantinedBytes -= pending.bytes\n this.#stats.quarantinedBytes -= pending.bytes\n this.#giveUpOnPending(pending)\n // Quarantine budget ran out while the object was still waiting on a\n // manifest. The manifest was absent when we gave up; the verifier did not\n // fault. Same reason in shadow and enforce so the dashboard reads one\n // taxonomy across modes (BLO-30007).\n this.#failure(pending.identity, 'manifestAbsent')\n console.warn(`[ManifestVerifier] quarantine cap eviction track=${pending.identity.trackName} group=${pending.identity.groupId} object=${pending.identity.objectId} disposition=${this.#config.mode === 'enforce' ? 'drop' : 'fail-open'}`)\n }\n }\n\n /**\n * Stop waiting for a manifest that never arrived. In `enforce` the object is\n * discarded: an eviction is a rejection, not a deferred play, so unverified\n * media never reaches the player two groups after the stall was recorded\n * (BLO-30095, CTO disposition on the issue thread).\n *\n * Deliberately NOT used by `#failOpenGroup`/`#failOpenTrack` \u2014 failing open on\n * a genuine verifier fault is correct and stays untouched.\n */\n #giveUpOnPending(pending: PendingObject): void {\n if (this.#config.mode !== 'enforce') {\n pending.release()\n return\n }\n pending.discard?.()\n }\n\n #failOpenTrack(track: TrackState, reason: 'verifierError'): void {\n for (const pending of [...track.quarantine]) {\n const group = track.groups.get(BigInt(pending.identity.groupId))\n if (group) this.#removePending(track, group, pending)\n pending.release()\n this.#failure(pending.identity, reason)\n }\n }\n\n #failOpenGroup(track: TrackState, groupId: bigint, group: GroupState): void {\n const pending = [...group.pending.values()]\n if (pending.length === 0) {\n this.#failure({ trackName: track.context.trackName, groupId, objectId: 0 }, 'verifierError')\n return\n }\n for (const object of pending) {\n this.#removePending(track, group, object)\n object.release()\n this.#failure(object.identity, 'verifierError')\n }\n }\n\n #releaseAllQuarantine(countFailure: boolean): void {\n for (const track of this.#tracks.values()) {\n for (const pending of [...track.quarantine]) {\n const group = track.groups.get(BigInt(pending.identity.groupId))\n if (group) this.#removePending(track, group, pending)\n pending.release()\n if (countFailure) this.#failure(pending.identity, 'verifierError')\n }\n }\n }\n\n #failure(identity: ManifestObjectIdentity, reason: ManifestFailureReason): void {\n this.#stats.failures[reason]++\n this.#onFailure?.({\n trackName: identity.trackName,\n ...(identity.bindingId === undefined ? {} : { bindingId: identity.bindingId }),\n groupId: identity.groupId,\n objectId: identity.objectId,\n reason,\n })\n }\n}\n", "import { sha256 } from '@noble/hashes/sha256'\n\n/** Fixed-width bytes before a record's digest list. */\nexport const BCPV2_HEADER_LENGTH = 68\nexport const BCPV2_SIGNATURE_LENGTH = 64\nexport const BCPV2_MIN_RECORD_LENGTH = BCPV2_HEADER_LENGTH + 32 + BCPV2_SIGNATURE_LENGTH\nexport const BCPV2_MAX_RECORD_LENGTH = 1316\nexport const BCPV2_MAX_DIGESTS = 37\nexport const BCPV2_HASH_ALG_SHA256 = 1\n\nexport interface BcProvenanceV2Endpoint {\n /** S in the AMBI pseudoheader and signature context. */\n sourceAddress: string\n /** G in the AMBI pseudoheader and signature context. */\n groupAddress: string\n /** UDP source and destination port of protected datagrams. */\n port: number\n /** UDP destination port carrying manifest records. */\n manifestPort: number\n /** Catalog-declared channel generation. */\n streamId: number\n}\n\nexport interface BcProvenanceV2Record {\n kid: number\n epoch: number\n mseq: bigint\n firstSeq: bigint\n count: number\n hashAlg: number\n flags: number\n sent: number\n ctx: Uint8Array\n digests: Uint8Array[]\n signature: Uint8Array\n /** Record bytes covered by the signature, excluding the signature itself. */\n body: Uint8Array\n /** A defensive copy of the complete wire record. */\n raw: Uint8Array\n}\n\nconst text = new TextEncoder()\nconst MAGIC = Uint8Array.from([0x4d, 0x48, 0x4d, 0x32]) // MHM2\nconst SIGNING_DOMAIN = text.encode('mcast-manifest-v2\\0')\n\nfunction equalBytes(left: Uint8Array, right: Uint8Array): boolean {\n if (left.byteLength !== right.byteLength) return false\n let different = 0\n for (let index = 0; index < left.byteLength; index++) different |= left[index]! ^ right[index]!\n return different === 0\n}\n\nfunction u16(value: number): Uint8Array {\n const result = new Uint8Array(2)\n new DataView(result.buffer).setUint16(0, value)\n return result\n}\n\nfunction u32(value: number): Uint8Array {\n const result = new Uint8Array(4)\n new DataView(result.buffer).setUint32(0, value)\n return result\n}\n\nfunction validU32(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value <= 0xffff_ffff\n}\n\nfunction validPort(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value <= 0xffff\n}\n\nfunction parseIpv4(address: string): Uint8Array {\n const fields = address.split('.')\n if (fields.length !== 4 || fields.some((field) => !/^\\d+$/.test(field))) {\n throw new Error(`invalid IPv4 address ${address}`)\n }\n const values = fields.map(Number)\n if (values.some((value, index) => value > 255 || String(value) !== fields[index])) {\n throw new Error(`invalid IPv4 address ${address}`)\n }\n return Uint8Array.from(values)\n}\n\nfunction parseIpv6(address: string): Uint8Array {\n const compressed = address.includes('::')\n if ((address.match(/::/g) ?? []).length > 1) throw new Error(`invalid IPv6 address ${address}`)\n const [leftText, rightText = ''] = address.split('::')\n const left = leftText ? leftText.split(':') : []\n const right = rightText ? rightText.split(':') : []\n // Native-width IPv6 is required by the draft; embedded IPv4 text is not.\n if (left.some((group) => group.includes('.')) || right.some((group) => group.includes('.'))) {\n throw new Error(`invalid IPv6 address ${address}`)\n }\n const missing = 8 - left.length - right.length\n if (missing < 0 || (!compressed && missing !== 0) || (compressed && missing < 1)) {\n throw new Error(`invalid IPv6 address ${address}`)\n }\n const groups = [...left, ...Array.from({ length: missing }, () => '0'), ...right]\n if (groups.length !== 8 || groups.some((group) => !/^[0-9a-f]{1,4}$/i.test(group))) {\n throw new Error(`invalid IPv6 address ${address}`)\n }\n const result = new Uint8Array(16)\n const view = new DataView(result.buffer)\n groups.forEach((group, index) => view.setUint16(index * 2, Number.parseInt(group, 16)))\n return result\n}\n\n/** Parse an IPv4 or IPv6 address in the native-width form used on the wire. */\nexport function parseBcProvenanceV2Address(address: string): Uint8Array {\n if (address.includes(':')) return parseIpv6(address)\n return parseIpv4(address)\n}\n\nfunction endpointBytes(endpoint: BcProvenanceV2Endpoint): {\n source: Uint8Array\n group: Uint8Array\n} {\n if (!validPort(endpoint.port) || !validPort(endpoint.manifestPort) || !validU32(endpoint.streamId)) {\n throw new Error('invalid BCPV2 endpoint')\n }\n const source = parseBcProvenanceV2Address(endpoint.sourceAddress)\n const group = parseBcProvenanceV2Address(endpoint.groupAddress)\n if (source.byteLength !== group.byteLength) throw new Error('endpoint address families differ')\n return { source, group }\n}\n\nfunction readU32(view: DataView, offset: number): number {\n return view.getUint32(offset)\n}\n\nfunction readU64(view: DataView, offset: number): bigint {\n return view.getBigUint64(offset)\n}\n\n/**\n * Decode the fixed-layout BCPV2 record from multicast-01 Section 7.2.3.\n * Structural validation happens before callers inspect the variable digest list.\n */\nexport function decodeBcProvenanceV2Record(input: Uint8Array): BcProvenanceV2Record {\n const bytes = Uint8Array.from(input)\n if (\n bytes.byteLength < BCPV2_MIN_RECORD_LENGTH ||\n bytes.byteLength > BCPV2_MAX_RECORD_LENGTH ||\n (bytes.byteLength - 132) % 32 !== 0\n ) {\n throw new Error('invalid BCPV2 record length')\n }\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n if (!equalBytes(bytes.subarray(0, 4), MAGIC)) throw new Error('invalid BCPV2 record magic')\n const count = view.getUint16(28)\n if (count < 1 || count > BCPV2_MAX_DIGESTS || bytes.byteLength !== 132 + count * 32) {\n throw new Error('invalid BCPV2 record count')\n }\n const body = bytes.slice(0, bytes.byteLength - BCPV2_SIGNATURE_LENGTH)\n const digests: Uint8Array[] = []\n for (let index = 0; index < count; index++) {\n digests.push(bytes.slice(BCPV2_HEADER_LENGTH + index * 32, BCPV2_HEADER_LENGTH + (index + 1) * 32))\n }\n return {\n kid: readU32(view, 4),\n epoch: readU32(view, 8),\n mseq: readU64(view, 12),\n firstSeq: readU64(view, 20),\n count,\n hashAlg: bytes[30]!,\n flags: bytes[31]!,\n sent: readU32(view, 32),\n ctx: bytes.slice(36, 68),\n digests,\n signature: bytes.slice(bytes.byteLength - BCPV2_SIGNATURE_LENGTH),\n body,\n raw: bytes,\n }\n}\n\n/** Return the exact Ed25519 preimage from multicast-01 Section 7.2.4. */\nexport function bcProvenanceV2SignedMessage(\n record: BcProvenanceV2Record,\n endpoint: BcProvenanceV2Endpoint,\n): Uint8Array {\n const { source, group } = endpointBytes(endpoint)\n const prefix = new Uint8Array(SIGNING_DOMAIN.byteLength + 4 + source.byteLength + group.byteLength + 2)\n let offset = 0\n prefix.set(SIGNING_DOMAIN, offset)\n offset += SIGNING_DOMAIN.byteLength\n prefix.set(u32(endpoint.streamId), offset)\n offset += 4\n prefix.set(source, offset)\n offset += source.byteLength\n prefix.set(group, offset)\n offset += group.byteLength\n prefix.set(u16(endpoint.manifestPort), offset)\n const message = new Uint8Array(prefix.byteLength + record.body.byteLength)\n message.set(prefix)\n message.set(record.body, prefix.byteLength)\n return message\n}\n\n/**\n * Compute the AMBI Section 3.4.2 digest for one transmitted datagram.\n * The payload includes the Source FEC Payload ID when one was transmitted.\n */\nexport function bcProvenanceV2DatagramDigest(\n endpoint: BcProvenanceV2Endpoint,\n payload: Uint8Array,\n): Uint8Array {\n if (payload.byteLength > 0xffff) throw new Error('datagram payload exceeds u16 length')\n const { source, group } = endpointBytes(endpoint)\n const pseudoheader = new Uint8Array(source.byteLength + group.byteLength + 2 + 2 + 2 + 2 + 4)\n let offset = 0\n pseudoheader.set(source, offset)\n offset += source.byteLength\n pseudoheader.set(group, offset)\n offset += group.byteLength\n pseudoheader.set([0, 0x11], offset)\n offset += 2\n pseudoheader.set(u16(payload.byteLength), offset)\n offset += 2\n pseudoheader.set(u16(endpoint.port), offset)\n offset += 2\n pseudoheader.set(u16(endpoint.port), offset)\n offset += 2\n pseudoheader.set(u32(endpoint.streamId), offset)\n const input = new Uint8Array(pseudoheader.byteLength + payload.byteLength)\n input.set(pseudoheader)\n input.set(payload, pseudoheader.byteLength)\n return sha256(input)\n}\n\n/** Verify the ordered digest list against the transmitted datagrams. */\nexport function verifyBcProvenanceV2Datagrams(\n record: BcProvenanceV2Record,\n endpoint: BcProvenanceV2Endpoint,\n datagrams: readonly Uint8Array[],\n): boolean {\n return record.hashAlg === BCPV2_HASH_ALG_SHA256 &&\n datagrams.length === record.count &&\n datagrams.every((datagram, index) => equalBytes(\n bcProvenanceV2DatagramDigest(endpoint, datagram),\n record.digests[index]!,\n ))\n}\n\nexport interface BcProvenanceV2Params {\n codec: string\n /** 0 where MOQ-MMT Section 4.4.2 resolves none. */\n timescale: number\n groupDurationTicks: number\n /** Absent for an entry with no `fec` object. */\n fec?: {\n algorithm: string\n sourceSymbols: number\n repairSymbols: number\n symbolSize: number\n interleaveDepthMs?: number\n }\n}\n\nfunction concat(parts: Uint8Array[]): Uint8Array {\n const out = new Uint8Array(parts.reduce((n, p) => n + p.byteLength, 0))\n let offset = 0\n for (const part of parts) {\n out.set(part, offset)\n offset += part.byteLength\n }\n return out\n}\n\nfunction lp16(value: Uint8Array | string): Uint8Array {\n const bytes = typeof value === 'string' ? text.encode(value) : value\n if (bytes.byteLength > 0xffff) throw new Error('lp16 field exceeds u16 length')\n return concat([u16(bytes.byteLength), bytes])\n}\n\n/** `params_hash` of multicast-01 Section 7.2.4 (md :1068-1110), hash_alg 1. */\nexport function bcProvenanceV2ParamsHash(params: BcProvenanceV2Params): Uint8Array {\n const fec = params.fec\n return sha256(concat([\n text.encode('BCPV2-params'),\n lp16(params.codec),\n u32(params.timescale),\n u32(params.groupDurationTicks),\n lp16(fec ? fec.algorithm : 'none'),\n u32(fec?.sourceSymbols ?? 0),\n u32(fec?.repairSymbols ?? 0),\n u32(fec?.symbolSize ?? 0),\n u32(fec?.interleaveDepthMs ?? 0),\n ]))\n}\n\n/**\n * `ctx` of multicast-01 Section 7.2.4: the namespace fields in order, the endpoint's\n * packetIds ascending, and `params_hash`. A flow without a MoQ catalog uses 32 zero octets.\n */\nexport function bcProvenanceV2Ctx(\n namespace: readonly (Uint8Array | string)[],\n packetIds: readonly number[],\n params: BcProvenanceV2Params,\n): Uint8Array {\n const ids = [...packetIds].sort((a, b) => a - b)\n if (namespace.length > 0xffff || ids.length > 0xffff) throw new Error('ctx field count exceeds u16')\n return sha256(concat([\n text.encode('BCPV2-ctx'),\n u16(namespace.length),\n ...namespace.map(lp16),\n u16(ids.length),\n ...ids.map(u16),\n bcProvenanceV2ParamsHash(params),\n ]))\n}\n", "/**\n * `multicast.auth` and an endpoint's `auth`: bc-provenance version 2, per\n * draft-ramadan-moq-multicast-01 7.2.6 at\n * Blockcast/moqcast-draft@fd962fd73d21ed281b60219c11ea61cc9e4e91c4.\n *\n * The block lives in the `multicast` object, never the root: the root dotted\n * key `\"multicast.auth\"` was draft -00's (libmmt called its `tracks[]` shape\n * \"multicast.auth v2 (BLO-16700)\", which is the draft's bc-provenance\n * version 1) and is refused. `multicast.auth` declares the profile and its\n * keys; an endpoint's `auth` marks that endpoint's flow protected and carries\n * the flow's parameters. There is no verify-and-report mode (7.2.7): a\n * receiver that implements the profile enforces it.\n *\n * The rules are `multicastCatalogIssues` in `@blockcast/mmt-base`, shared with\n * the transport receivers; this module decodes what they admit.\n */\n\nimport { multicastCatalogIssues } from \"@blockcast/mmt-base\";\nimport { manifestKeyFor, type ManifestKey } from \"@blockcast/mmt-manifest-verify\";\n\nexport type MulticastAuthScheme = \"bc-provenance\";\nexport type MulticastAuthSig = \"ed25519\";\n\nconst RAW_ED25519_KEY_LENGTH = 32;\n\n/** Default `maxAuthDelayMs` and `manifestTimerMs` of an endpoint's `auth`. */\nexport const DEFAULT_MAX_AUTH_DELAY_MS = 200;\nexport const DEFAULT_MANIFEST_TIMER_MS = 20;\n\nconst BASE64URL_ALPHABET =\n\t\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\n/** Encode raw bytes as unpadded base64url \u2014 the one accepted encoding for\n * `multicast.auth.keys[].publicKey`. */\nexport function encodeBase64Url(bytes: Uint8Array): string {\n\tlet output = \"\";\n\tfor (let i = 0; i < bytes.length; i += 3) {\n\t\tconst b0 = bytes[i]!;\n\t\tconst b1 = bytes[i + 1];\n\t\tconst b2 = bytes[i + 2];\n\t\toutput += BASE64URL_ALPHABET[b0 >> 2];\n\t\toutput += BASE64URL_ALPHABET[((b0 & 0x03) << 4) | (b1 === undefined ? 0 : b1 >> 4)];\n\t\tif (b1 === undefined) break;\n\t\toutput += BASE64URL_ALPHABET[((b1 & 0x0f) << 2) | (b2 === undefined ? 0 : b2 >> 6)];\n\t\tif (b2 === undefined) break;\n\t\toutput += BASE64URL_ALPHABET[b2 & 0x3f];\n\t}\n\treturn output;\n}\n\nconst BASE64URL_DECODE_TABLE = (() => {\n\tconst table = new Int8Array(128).fill(-1);\n\tfor (let i = 0; i < BASE64URL_ALPHABET.length; i++) {\n\t\ttable[BASE64URL_ALPHABET.charCodeAt(i)] = i;\n\t}\n\treturn table;\n})();\n\n/** Decode unpadded base64url text to raw bytes. Throws on any padding\n * (`=`), standard-alphabet (`+`/`/`), or otherwise invalid character. */\nexport function decodeBase64Url(text: string): Uint8Array {\n\tif (text.length % 4 === 1) {\n\t\tthrow new Error(`[multicast-auth-catalog] invalid unpadded base64url length for \"${text}\"`);\n\t}\n\tconst bytes: number[] = [];\n\tlet buffer = 0;\n\tlet bits = 0;\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst code = text.charCodeAt(i);\n\t\tconst value = code < 128 ? BASE64URL_DECODE_TABLE[code] : -1;\n\t\tif (value === undefined || value < 0) {\n\t\t\tthrow new Error(`[multicast-auth-catalog] invalid base64url character in \"${text}\"`);\n\t\t}\n\t\tbuffer = (buffer << 6) | value;\n\t\tbits += 6;\n\t\tif (bits >= 8) {\n\t\t\tbits -= 8;\n\t\t\tbytes.push((buffer >> bits) & 0xff);\n\t\t}\n\t}\n\tconst decoded = Uint8Array.from(bytes);\n\tif (encodeBase64Url(decoded) !== text) {\n\t\tthrow new Error(`[multicast-auth-catalog] non-canonical base64url encoding \"${text}\"`);\n\t}\n\treturn decoded;\n}\n\n/** Decode a catalog key in the only accepted representation: exactly 32 raw\n * ed25519 bytes encoded as canonical, unpadded base64url. */\nexport function decodeEd25519PublicKey(publicKey: string): Uint8Array {\n\tconst bytes = decodeBase64Url(publicKey);\n\tif (bytes.byteLength !== RAW_ED25519_KEY_LENGTH) {\n\t\tthrow new Error(\n\t\t\t`[multicast-auth-catalog] public key must decode to ${RAW_ED25519_KEY_LENGTH} raw bytes, got ${bytes.byteLength}`,\n\t\t);\n\t}\n\treturn bytes;\n}\n\n/** One `multicast.auth.keys[]` entry: `kid` and `firstEpoch` are u32,\n * `publicKey` the 32-octet Ed25519 key in canonical unpadded base64url. */\nexport interface MulticastAuthKey {\n\tkid: number;\n\tpublicKey: string;\n\tfirstEpoch: number;\n}\n\n/** `multicast.auth` (7.2.6). `hash_alg` is a record octet, not a member. */\nexport interface MulticastAuthConfig {\n\tscheme: MulticastAuthScheme;\n\tversion: 2;\n\tsig: MulticastAuthSig;\n\t/** One to four keys; `kid` and `firstEpoch` each distinct. */\n\tkeys: MulticastAuthKey[];\n\t/** Lowest `epoch` a receiver accepts at join. */\n\tminEpoch: number;\n}\n\n/** An endpoint's `auth` (7.2.6). A protected endpoint also declares `bandwidth`. */\nexport interface EndpointAuthConfig {\n\t/** Destination port of the record datagrams on the endpoint's `(S, G)`. */\n\tmanifestPort: number;\n\t/** u32 chosen per channel generation. */\n\tstreamId: number;\n\t/** `S` of a protected ASM endpoint; never on SSM. */\n\tsenderAddress?: string;\n\t/** 50..1000, default 200. */\n\tmaxAuthDelayMs?: number;\n\t/** At least 1, default 20. */\n\tmanifestTimerMs?: number;\n\t/** MoQ track mirroring the records. */\n\tmanifestTrack?: string;\n}\n\n/** Structural check of a `multicast.auth` block, duplicate `kid` and\n * `firstEpoch` included. */\nexport function isMulticastAuthConfig(obj: unknown): obj is MulticastAuthConfig {\n\treturn multicastCatalogIssues({ multicast: { auth: obj } }).length === 0;\n}\n\n/** A protected flow as a receiver binds it: one per protected endpoint. */\nexport interface ResolvedMulticastAuthFlow {\n\t/** Index of the endpoint in `multicast.endpoints`. */\n\tendpointIndex: number;\n\t/** `S`: `sourceAddress` on SSM, `auth.senderAddress` on ASM. */\n\tsource: string;\n\tgroupAddress: string;\n\tport: number;\n\tbandwidth: number;\n\tmanifestPort: number;\n\tstreamId: number;\n\tmaxAuthDelayMs: number;\n\tmanifestTimerMs: number;\n\tmanifestTrack?: string;\n\ttracks: Array<{ name: string; packetId: number }>;\n}\n\nexport interface ResolvedMulticastAuthConfig {\n\t/** Decoded keys, ascending by `firstEpoch`. */\n\tkeys: ManifestKey[];\n\tminEpoch: number;\n\tflows: ResolvedMulticastAuthFlow[];\n}\n\n/**\n * Resolve `multicast.auth` and every protected endpoint into decoded keys and\n * per-endpoint flows. Returns `undefined` for an unprotected catalog unless\n * `requireAuth` pins authentication (7.2.7), which then throws.\n *\n * A catalog-level fault (no key set: a malformed block, a repeated `kid` or\n * `firstEpoch`, the retired root key) throws, so nothing is admitted and\n * nothing falls back to unauthenticated reception. An endpoint the rules make\n * a receiver ignore yields no flow; the others still bind.\n */\nexport function resolveMulticastAuthCatalog(\n\tcatalog: unknown,\n\toptions: { requireAuth?: boolean; catalogNamespace?: string } = {},\n): ResolvedMulticastAuthConfig | undefined {\n\tif (typeof catalog !== \"object\" || catalog === null || Array.isArray(catalog)) {\n\t\tthrow new Error(\"[multicast-auth-catalog] catalog must be an object\");\n\t}\n\tconst issues = multicastCatalogIssues(catalog, options);\n\tconst fatal = issues.find((issue) => issue.endpointIndex === undefined);\n\tif (fatal) throw new Error(`[multicast-auth-catalog] ${fatal.path}: ${fatal.message}`);\n\tconst multicast = (catalog as Record<string, unknown>)[\"multicast\"] as Record<string, unknown> | undefined;\n\tconst auth = multicast?.[\"auth\"] as MulticastAuthConfig | undefined;\n\tif (auth === undefined) {\n\t\tif (options.requireAuth) {\n\t\t\tthrow new Error(\"[multicast-auth-catalog] authentication is pinned but the catalog declares no multicast.auth\");\n\t\t}\n\t\treturn undefined;\n\t}\n\tconst ignored = new Set(issues.map((issue) => issue.endpointIndex));\n\tconst endpoints = (multicast![\"endpoints\"] ?? []) as Array<Record<string, unknown>>;\n\tconst flows: ResolvedMulticastAuthFlow[] = [];\n\tendpoints.forEach((endpoint, endpointIndex) => {\n\t\tif (ignored.has(endpointIndex)) return;\n\t\tconst flowAuth = endpoint[\"auth\"] as EndpointAuthConfig;\n\t\tflows.push({\n\t\t\tendpointIndex,\n\t\t\tsource: (endpoint[\"sourceAddress\"] ?? flowAuth.senderAddress) as string,\n\t\t\tgroupAddress: endpoint[\"groupAddress\"] as string,\n\t\t\tport: endpoint[\"port\"] as number,\n\t\t\tbandwidth: endpoint[\"bandwidth\"] as number,\n\t\t\tmanifestPort: flowAuth.manifestPort,\n\t\t\tstreamId: flowAuth.streamId,\n\t\t\tmaxAuthDelayMs: flowAuth.maxAuthDelayMs ?? DEFAULT_MAX_AUTH_DELAY_MS,\n\t\t\tmanifestTimerMs: flowAuth.manifestTimerMs ?? DEFAULT_MANIFEST_TIMER_MS,\n\t\t\t...(flowAuth.manifestTrack === undefined ? {} : { manifestTrack: flowAuth.manifestTrack }),\n\t\t\ttracks: ((endpoint[\"tracks\"] ?? []) as Array<{ name: string; packetId: number }>).map(({ name, packetId }) => ({\n\t\t\t\tname,\n\t\t\t\tpacketId,\n\t\t\t})),\n\t\t});\n\t});\n\treturn {\n\t\tkeys: auth.keys\n\t\t\t.map((key) => ({ kid: key.kid, publicKey: decodeEd25519PublicKey(key.publicKey), firstEpoch: key.firstEpoch }))\n\t\t\t.sort((a, b) => a.firstEpoch - b.firstEpoch),\n\t\tminEpoch: auth.minEpoch,\n\t\tflows,\n\t};\n}\n\n/** The key that signs a record of `kid` at `epoch`, or `undefined` when that\n * `kid` does not sign it (7.2.7: such a record is dropped before verification). */\nexport function multicastAuthKeyFor(\n\tresolved: ResolvedMulticastAuthConfig,\n\tkid: number,\n\tepoch: number,\n): Uint8Array | undefined {\n\treturn manifestKeyFor(resolved.keys, epoch, kid);\n}\n", "import type { ManifestKey } from '@blockcast/mmt-manifest-verify'\nimport {\n multicastAuthKeyFor,\n resolveMulticastAuthCatalog,\n type ResolvedMulticastAuthConfig,\n type ResolvedMulticastAuthFlow,\n} from './multicast-auth-catalog.js'\n\n/**\n * The bc-provenance version 2 plan a receiver binds from one catalog\n * (draft-ramadan-moq-multicast-01 7.2.6): the key set, `minEpoch`, and one\n * flow per protected endpoint. The record verifier and the 7.2.7 state machine\n * consume it; this plan holds no verification state.\n */\nexport interface ManifestAuthPlan {\n keys: readonly ManifestKey[]\n minEpoch: number\n flows: readonly ResolvedMulticastAuthFlow[]\n /** Every `manifestTrack` a flow names, for a unicast receiver to subscribe. */\n manifestTracks: readonly string[]\n /** The key that signs a record of `kid` at `epoch`; `undefined` drops it.\n * Key selection only: it does not apply `minEpoch`, which the record\n * verifier checks (BLO-49771). */\n keyFor(kid: number, epoch: number): Uint8Array | undefined\n /** The flow whose records arrive on `(S, G, manifestPort)`. */\n flowForManifestPort(source: string, groupAddress: string, manifestPort: number): ResolvedMulticastAuthFlow | undefined\n /** The flow a manifest track mirrors. */\n flowForManifestTrack(manifestTrack: string): ResolvedMulticastAuthFlow | undefined\n}\n\n/** Build the plan, or `undefined` for an unprotected catalog. Throws where\n * `resolveMulticastAuthCatalog` does: no key set, or authentication pinned\n * by `requireAuth` on a catalog without `multicast.auth`. */\nexport function createManifestAuthPlan(\n catalog: unknown,\n options: { requireAuth?: boolean; catalogNamespace?: string } = {},\n): ManifestAuthPlan | undefined {\n const resolved: ResolvedMulticastAuthConfig | undefined = resolveMulticastAuthCatalog(catalog, options)\n if (!resolved) return undefined\n const byManifestTrack = new Map<string, ResolvedMulticastAuthFlow>()\n for (const flow of resolved.flows) {\n if (flow.manifestTrack !== undefined) byManifestTrack.set(flow.manifestTrack, flow)\n }\n return {\n keys: resolved.keys,\n minEpoch: resolved.minEpoch,\n flows: resolved.flows,\n manifestTracks: [...byManifestTrack.keys()],\n keyFor: (kid, epoch) => multicastAuthKeyFor(resolved, kid, epoch),\n flowForManifestPort: (source, groupAddress, manifestPort) =>\n resolved.flows.find(flow =>\n flow.source === source && flow.groupAddress === groupAddress && flow.manifestPort === manifestPort),\n flowForManifestTrack: manifestTrack => byManifestTrack.get(manifestTrack),\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["text"]
7
- }
@@ -1,2 +0,0 @@
1
- export * from '@blockcast/mmt-manifest-verify';
2
- //# sourceMappingURL=source-auth.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"source-auth.d.ts","sourceRoot":"","sources":["../src/source-auth.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAA"}