@blockcast/fec-worker 0.3.0 → 0.4.0-main.404c51bd317c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/fec-worker.ts CHANGED
@@ -22,8 +22,6 @@ import type {
22
22
  } from "./fec-worker-types.js";
23
23
  import { toOwnedArrayBuffer } from "./transfer.js";
24
24
  import {
25
- blockPrecedes,
26
- blocksBehind,
27
25
  isoBlockContains,
28
26
  isoBlocksBehind,
29
27
  isoBlocksOverlap,
@@ -36,10 +34,9 @@ import {
36
34
  // The decoder derives SBN/ESI internally — do not pre-compute.
37
35
  interface WasmFecDecoder {
38
36
  /**
39
- * ISO 23008-1 §C.5.2: ss_id is the flat 32-bit Source Symbol ID.
40
- * `k-padded-v1`: data is the source symbol. `iso-ssbg1-v1`: data is the
41
- * protected packet, unpadded; the decoder builds the ssbg_mode1 symbol and
42
- * holds it until a repair names the block containing it.
37
+ * ISO 23008-1 §C.5.2: ss_id is the flat 32-bit Source Symbol ID; data is
38
+ * the protected packet, unpadded. The decoder builds the ssbg_mode1 symbol
39
+ * and holds it until a repair names the block containing it.
43
40
  */
44
41
  add_source(
45
42
  ss_id: number,
@@ -47,11 +44,9 @@ interface WasmFecDecoder {
47
44
  timestamp: number,
48
45
  ): Uint8Array | undefined;
49
46
  /**
50
- * ISO 23008-1 §C.5.3. `k-padded-v1`: repair ESI = configured K + rs_id,
51
- * SBN = ss_start / configured K. `iso-ssbg1-v1`: the block is
52
- * `[ss_start, ss_start + ssb_length)` with K' = ssb_length and repair
53
- * ESI = ssb_length + rs_id; a recovered block is ssb_length x T bytes of
54
- * ssbg_mode1 symbols.
47
+ * ISO 23008-1 §C.5.3: the block is `[ss_start, ss_start + ssb_length)`
48
+ * with K' = ssb_length and repair ESI = ssb_length + rs_id; a recovered
49
+ * block is ssb_length x T bytes of ssbg_mode1 symbols.
55
50
  */
56
51
  add_repair(
57
52
  ss_start: number,
@@ -68,14 +63,12 @@ interface WasmFecDecoder {
68
63
  */
69
64
  cleanup_blocks_detailed(sbns: Uint32Array): void;
70
65
  /**
71
- * `k-padded-v1` only (throws under `iso-ssbg1-v1`): remove decoder blocks
72
- * below its configured live-SBN interleave horizon; returns their SBNs.
73
- */
74
- cleanup_by_sbn(current_sbn: number): Uint32Array;
75
- /**
76
- * `iso-ssbg1-v1` only: remove decoder blocks, and held unplaced sources,
77
- * more than interleaveDepth `isoBlocksBehind` the live SS_ID; returns the
78
- * SS_Starts of the blocks it removed.
66
+ * Remove decoder blocks, and held unplaced sources, more than
67
+ * interleaveDepth `isoBlocksBehind` the live SS_ID; returns the SS_Starts
68
+ * of the blocks it removed. It runs the decoder's `retire_unplaced` sweep
69
+ * itself, so the Worker never calls that separately: synchronizeDecoderHorizon
70
+ * calls this after every source and repair it accepts, which is what keeps
71
+ * the decoder's unplaced pool bounded when a block's repairs never arrive.
79
72
  */
80
73
  cleanup_by_ss_start(current: number): Uint32Array;
81
74
  /** The block profile the decoder was constructed with. */
@@ -104,9 +97,6 @@ interface WasmBindings {
104
97
 
105
98
  let wasmDecoder: WasmFecDecoder | null = null;
106
99
  let config: FecTrackConfig | null = null;
107
- let altaEnabled = false;
108
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
- let altaVerifier: any = null; // JsAltaVerifier instance (loaded from ALTA WASM)
110
100
  let snapshotTimer: ReturnType<typeof setInterval> | null = null;
111
101
  // blockUpdate events are posted only after a client subscribes (BLO-36599).
112
102
  let blockUpdatesSubscribed = false;
@@ -137,20 +127,6 @@ let repairSymbols = 0;
137
127
  let blocksComplete = 0;
138
128
  let blocksRecovered = 0;
139
129
  let blocksFailed = 0;
140
- let altaVerified = 0;
141
- let altaFailed = 0; // Crypto-confirmed tamper (MAC / sig / hash chain mismatch)
142
- const altaFailedDetails: Array<{
143
- seq: number;
144
- error: string;
145
- authLen: number;
146
- payloadLen: number;
147
- path: string; // "direct" | "recovered" | "pending-retry"
148
- }> = [];
149
- let altaPending = 0; // Unverifiable but not tamper (no trailer, verifier not ready, anchor missing)
150
- let altaPendingEvictedCap = 0; // Incoming "pending" packets rejected because the queue was at cap after stale-sweep (flood indicator)
151
- let altaPendingEvictedTtl = 0; // Deferred-queue entries dropped by TTL (retry window expired)
152
- let altaError = 0; // verifyDetached threw — infrastructure flake, not tamper
153
- let altaInitFailed = false; // ALTA configured but WASM init failed; altaEnabled was forced off
154
130
  let relayBlocksVerified = 0;
155
131
  let relayBlocksFailed = 0;
156
132
  let liveSbn = 0;
@@ -188,35 +164,6 @@ let sourceUnplacedRetired = 0;
188
164
  // Recovery timing
189
165
  const recoveryTimes: number[] = [];
190
166
 
191
- // ── Deferred ALTA verification queue (08-07 Path 3) ───────────────────────
192
- //
193
- // When an ALTA packet arrives whose MAC references point at sequences not yet
194
- // in the verifier's ring buffer, we can't anchor trust — verify returns "No
195
- // trust anchor". Typical cause: a predecessor packet was lost and hasn't been
196
- // FEC-recovered yet. Rather than drop these, hold them in a bounded FIFO and
197
- // retry verification after every successful verify (direct or FEC-recovered)
198
- // that seeds the ring buffer with new raw bytes.
199
- //
200
- // Zero-copy (1B-B): the queue entry stores a Uint8Array view into the
201
- // original `data` ArrayBuffer. JS GC keeps the buffer alive via the view
202
- // reference; no bytes are copied on queue.
203
- //
204
- // Timeout entries (TTL exceeded) and cap-rejections count as `altaPending`
205
- // (2C-B): they're not tamper indicators, they're just packets that couldn't
206
- // be authenticated within the receive window — either loss or, in the cap
207
- // case, a flood of unanchorable arrivals (real or attacker-driven) that
208
- // exceeded the queue budget. See enqueuePendingVerify for the rate-limit
209
- // protocol.
210
- interface PendingVerify {
211
- seq: number;
212
- auth: Uint8Array;
213
- payload: Uint8Array;
214
- arrivalTs: number;
215
- }
216
- const pendingVerify = new Map<number, PendingVerify>();
217
- let pendingVerifyCap = 0;
218
- let pendingVerifyTtlMs = 0;
219
-
220
167
  // --- Helpers ---
221
168
 
222
169
  function emit(event: FecWorkerEvent, transfer?: Transferable[]): void {
@@ -241,7 +188,6 @@ function clearBlockTracking(): void {
241
188
  acceptedSourceEsis.clear();
242
189
  acceptedRepairEsis.clear();
243
190
  unplacedSources.clear();
244
- pendingVerify.clear();
245
191
  recoveryTimes.length = 0;
246
192
  liveSbn = 0;
247
193
  liveSbnKnown = false;
@@ -255,15 +201,9 @@ function clearBlockTracking(): void {
255
201
  */
256
202
  function isRetiredSbn(sbn: number): boolean {
257
203
  if (config === null || !liveSbnKnown) return false;
258
- // iso-ssbg1-v1: `sbn` is an SS_ID (a block's SS_Start or a source's own
259
- // SS_ID) and liveSbn the live SS_ID; K is only the maximum block length.
260
- return config.blockProfile === "iso-ssbg1-v1"
261
- ? isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth
262
- : blocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth;
263
- }
264
-
265
- function isIsoProfile(): boolean {
266
- return config?.blockProfile === "iso-ssbg1-v1";
204
+ // `sbn` is an SS_ID (a block's SS_Start or a source's own SS_ID) and
205
+ // liveSbn the live SS_ID; K is only the maximum block length.
206
+ return isoBlocksBehind(sbn, liveSbn, config.k) > config.interleaveDepth;
267
207
  }
268
208
 
269
209
  /**
@@ -291,9 +231,7 @@ function findIsoBlock(ssId: number): FecBlockSnapshot | undefined {
291
231
  * retired, after which the SS_ID retirement check refuses every member.
292
232
  */
293
233
  function isRetiredBlock(key: number, block: FecBlockSnapshot): boolean {
294
- return isIsoProfile()
295
- ? isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN)
296
- : isRetiredSbn(key);
234
+ return isRetiredSbn((key + isoKnownLength(block) - 1) % SS_ID_SPAN);
297
235
  }
298
236
 
299
237
  /**
@@ -310,15 +248,10 @@ function trimBlockRetention(): void {
310
248
  }
311
249
  }
312
250
 
313
- /** Advance the live position: an SBN (k-padded-v1) or an SS_ID / SS_Start (iso-ssbg1-v1). */
251
+ /** Advance the live position: an SS_ID / SS_Start, in serial order. */
314
252
  function advanceMediaHorizon(sbn: number): void {
315
253
  if (!config) return;
316
- if (liveSbnKnown) {
317
- const ahead = config.blockProfile === "iso-ssbg1-v1"
318
- ? serialPrecedes(liveSbn, sbn)
319
- : blockPrecedes(liveSbn, sbn, config.k);
320
- if (!ahead) return;
321
- }
254
+ if (liveSbnKnown && !serialPrecedes(liveSbn, sbn)) return;
322
255
  liveSbn = sbn;
323
256
  liveSbnKnown = true;
324
257
  trimBlockRetention();
@@ -336,10 +269,7 @@ function markBlockTerminal(
336
269
 
337
270
  function synchronizeDecoderHorizon(): void {
338
271
  if (!wasmDecoder || !liveSbnKnown) return;
339
- const iso = isIsoProfile();
340
- const expired = iso
341
- ? wasmDecoder.cleanup_by_ss_start(liveSbn)
342
- : wasmDecoder.cleanup_by_sbn(liveSbn);
272
+ const expired = wasmDecoder.cleanup_by_ss_start(liveSbn);
343
273
  for (const sbn of expired) {
344
274
  const block = blocks.get(sbn);
345
275
  if (!block || isTerminalBlock(block)) {
@@ -350,7 +280,7 @@ function synchronizeDecoderHorizon(): void {
350
280
  blocksFailed++;
351
281
  emitBlockUpdate(block);
352
282
  }
353
- if (iso) retireUnplacedSources();
283
+ retireUnplacedSources();
354
284
  trimBlockRetention();
355
285
  }
356
286
 
@@ -373,163 +303,6 @@ function retireUnplacedSources(): void {
373
303
  }
374
304
  }
375
305
 
376
- /**
377
- * Run detached ALTA verify against the current verifier. Four-state result:
378
- * - "valid": crypto passed, packet authentic. Caller increments altaVerified.
379
- * - "failed": crypto confirmed tamper (MAC/sig mismatch, hash chain mismatch).
380
- * Caller increments altaFailed. Not retryable.
381
- * - "pending": anchor missing (MAC refs not in buffer, no signature, not
382
- * early-warmup). Caller may enqueue for retry after the next
383
- * successful verify seeds the ring buffer.
384
- * - "error": verifier threw — WASM trap, unmarshal panic, OOM. Caller
385
- * increments altaError (infrastructure flake, not tamper).
386
- * The distinction matters for ops alerting: a spike in
387
- * altaError indicates a runtime problem, not an attack.
388
- */
389
- let _lastVerifyError = "";
390
- function runDetachedVerify(
391
- auth: Uint8Array,
392
- payload: Uint8Array,
393
- ): "valid" | "failed" | "pending" | "error" {
394
- if (!altaVerifier) return "pending";
395
- try {
396
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
397
- const r: any = altaVerifier.verifyDetached(auth, payload);
398
- if (r.valid) {
399
- _lastVerifyError = "";
400
- return "valid";
401
- }
402
- const err = (r.error as string | undefined) ?? "";
403
- _lastVerifyError = err;
404
- // The bootstrap-friendly verifier returns this specific error string when
405
- // the only obstacle to verification is a missing anchor (see alta-rs
406
- // verifier.rs verify()). Distinguishing this from tamper cases keeps us
407
- // from queuing packets that are definitively invalid.
408
- if (err.startsWith("No trust anchor")) return "pending";
409
- return "failed";
410
- } catch (e) {
411
- // Ratelimit: first ~N occurrences get logged. A sustained exception
412
- // stream indicates a real problem — the altaError counter is the
413
- // durable signal.
414
- if (altaError < 5) {
415
- console.warn("[FecWorker] verifyDetached threw:", e);
416
- }
417
- return "error";
418
- }
419
- }
420
-
421
- /** Extract u32 BE sequence number from the ALTA authenticator header (offset 0..3). */
422
- function seqFromAuth(auth: Uint8Array): number {
423
- return (
424
- ((auth[0]! << 24) | (auth[1]! << 16) | (auth[2]! << 8) | auth[3]!) >>> 0
425
- );
426
- }
427
-
428
- /**
429
- * Enqueue a packet whose ALTA verification returned "pending" (anchor
430
- * missing). Dedupes by sequence number.
431
- *
432
- * Flood resistance (08-08 review): when the queue is at cap we do NOT
433
- * evict the oldest entry. The oldest entry is typically the most
434
- * load-bearing one — it's the packet whose anchor everything else in the
435
- * queue is waiting on. Evict-oldest-on-insert let an attacker emitting a
436
- * stream of "No trust anchor" packets flood out legitimate entries and
437
- * prevent them from ever anchoring. Instead:
438
- * 1. First try to free stale TTL'd entries (they'd be evicted on next
439
- * drain anyway — pulling that forward is free).
440
- * 2. If still full, reject the NEW entry. altaPendingEvictedCap counts
441
- * these rejections (rate-limit trigger).
442
- *
443
- * Stores Uint8Array views into the original data + auth buffers — no copy.
444
- * The views keep their underlying ArrayBuffers alive via GC references.
445
- */
446
- function enqueuePendingVerify(
447
- auth: Uint8Array,
448
- payload: Uint8Array,
449
- ): void {
450
- const seq = seqFromAuth(auth);
451
- if (pendingVerify.has(seq)) return;
452
-
453
- if (pendingVerify.size >= pendingVerifyCap) {
454
- // Step 1: sweep stale entries so legitimate traffic isn't penalized
455
- // by TTL'd junk that happens to still occupy slots.
456
- const now = performance.now();
457
- for (const [staleSeq, entry] of pendingVerify) {
458
- if (now - entry.arrivalTs > pendingVerifyTtlMs) {
459
- pendingVerify.delete(staleSeq);
460
- altaPendingEvictedTtl++;
461
- }
462
- }
463
- // Step 2: if the queue is still at cap it's a genuine flood —
464
- // reject the new entry rather than evict an existing one. The
465
- // rejected packet counts as altaPendingEvictedCap (which is
466
- // surfaced up through stats for rate-limit alerting).
467
- if (pendingVerify.size >= pendingVerifyCap) {
468
- altaPendingEvictedCap++;
469
- return;
470
- }
471
- }
472
-
473
- pendingVerify.set(seq, {
474
- seq,
475
- auth,
476
- payload,
477
- arrivalTs: performance.now(),
478
- });
479
- }
480
-
481
- /**
482
- * Retry every queued entry. Called after every successful verify (direct
483
- * source or FEC-recovered) — new raw bytes in the verifier's ring buffer may
484
- * unblock previously-pending MAC references.
485
- *
486
- * Three outcomes per entry:
487
- * - "valid" → remove from queue, altaVerified++
488
- * - "failed" → remove from queue, altaFailed++ (tamper on retry)
489
- * - "pending" → keep in queue, unless TTL expired (then evict as altaPending)
490
- *
491
- * Iteration order: Map preserves insertion order, so we drain oldest-first.
492
- * No-op when the queue is empty or the verifier isn't ready.
493
- */
494
- function drainPendingVerify(): void {
495
- if (pendingVerify.size === 0 || !altaVerifier) return;
496
- const now = performance.now();
497
- // Collect keys to avoid mutating Map during iteration
498
- const keys = Array.from(pendingVerify.keys());
499
- for (const seq of keys) {
500
- const entry = pendingVerify.get(seq);
501
- if (!entry) continue;
502
- if (now - entry.arrivalTs > pendingVerifyTtlMs) {
503
- pendingVerify.delete(seq);
504
- altaPendingEvictedTtl++;
505
- continue;
506
- }
507
- const result = runDetachedVerify(entry.auth, entry.payload);
508
- if (result === "valid") {
509
- pendingVerify.delete(seq);
510
- altaVerified++;
511
- } else if (result === "failed") {
512
- pendingVerify.delete(seq);
513
- altaFailed++;
514
- if (altaFailedDetails.length < 16) {
515
- altaFailedDetails.push({
516
- seq: entry.seq,
517
- error: _lastVerifyError,
518
- authLen: entry.auth.length,
519
- payloadLen: entry.payload.length,
520
- path: "pending-retry",
521
- });
522
- }
523
- } else if (result === "error") {
524
- // Infrastructure failure — remove from queue, count as altaError.
525
- // Keeping it around would just re-throw on every subsequent drain.
526
- pendingVerify.delete(seq);
527
- altaError++;
528
- }
529
- // "pending" → leave in queue for next drain
530
- }
531
- }
532
-
533
306
  /**
534
307
  * Look up or create a block. `isoSsbLength` creates an iso-ssbg1-v1 block,
535
308
  * whose K' is its SSB_length (C.5.3); without it the block has the catalog K.
@@ -548,8 +321,9 @@ function getOrCreateBlock(sbn: number, isoSsbLength?: number): FecBlockSnapshot
548
321
  repairCount: config.repairCount,
549
322
  firstSymbolTs: now,
550
323
  lastSymbolTs: now,
324
+ // A positive interleaveMs makes this the catalog completion deadline;
325
+ // zero makes deliveryWindowMs a receiver-local cleanup bound.
551
326
  deadlineTs: now + config.deliveryWindowMs,
552
- altaVerified: 0,
553
327
  relayBlockSigValid: null,
554
328
  };
555
329
  if (isoSsbLength !== undefined) block.ssbLength = isoSsbLength;
@@ -583,8 +357,10 @@ function buildStatsSnapshot(): FecTrackStats {
583
357
 
584
358
  // Cap block array to interleave window
585
359
  const maxBlocks = Math.max(config.interleaveDepth * 3, 24);
360
+ // Newest first in SS_ID serial order, so the cap keeps the newest blocks
361
+ // across the 2^32 wrap too: every block start precedes the live SS_ID.
586
362
  const blockArray = Array.from(blocks.values())
587
- .sort((a, b) => b.sbn - a.sbn)
363
+ .sort((a, b) => serialDistance(a.sbn, liveSbn) - serialDistance(b.sbn, liveSbn))
588
364
  .slice(0, maxBlocks);
589
365
 
590
366
  return {
@@ -600,15 +376,6 @@ function buildStatsSnapshot(): FecTrackStats {
600
376
  (blocksRecovered / (blocksRecovered + blocksFailed)) * 100,
601
377
  ) || 0
602
378
  : 0,
603
- altaVerified,
604
- altaFailed,
605
- altaPending,
606
- altaPendingEvictedCap,
607
- altaPendingEvictedTtl,
608
- altaError,
609
- altaInitFailed,
610
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
611
- altaFailedDetails: altaFailedDetails.slice() as any,
612
379
  relayBlocksVerified,
613
380
  relayBlocksFailed,
614
381
  liveSbn,
@@ -620,7 +387,7 @@ function buildStatsSnapshot(): FecTrackStats {
620
387
  sourceDroppedRetired,
621
388
  sourceDroppedTerminal,
622
389
  sourceDroppedDuplicate,
623
- ...(config.blockProfile === "iso-ssbg1-v1" ? { sourceUnplacedRetired } : {}),
390
+ sourceUnplacedRetired,
624
391
  decoder,
625
392
  };
626
393
  }
@@ -662,33 +429,23 @@ function emitRecoveredSymbols(
662
429
  return;
663
430
  }
664
431
 
665
- const iso = config.blockProfile === "iso-ssbg1-v1";
666
432
  for (let i = 0; i < ssbLength; i++) {
667
433
  const symbol = recoveredBlock.subarray(i * symbolSize, (i + 1) * symbolSize);
668
434
  // ssbg_mode1 symbols are emitted whole: the router trims them by their
669
435
  // length field. A field past T-2 or non-zero padding means the
670
436
  // recovered symbol is not what was sent, so it is refused, not trimmed.
671
- if (iso) {
672
- const invalid = invalidSsbg1Symbol(symbol, symbolSize);
673
- if (invalid !== null) {
674
- emit({
675
- type: "error",
676
- code: "recovered-ssbg1-symbol-invalid",
677
- message: `[FecWorker] Dropping recovered ssbg_mode1 symbol (ssStart=${sbn}, esi=${i}, SSB_length=${ssbLength}): ${invalid}`,
678
- });
679
- continue;
680
- }
437
+ const invalid = invalidSsbg1Symbol(symbol, symbolSize);
438
+ if (invalid !== null) {
439
+ emit({
440
+ type: "error",
441
+ code: "recovered-ssbg1-symbol-invalid",
442
+ message: `[FecWorker] Dropping recovered ssbg_mode1 symbol (ssStart=${sbn}, esi=${i}, SSB_length=${ssbLength}): ${invalid}`,
443
+ });
444
+ continue;
681
445
  }
682
446
 
683
- // Source authentication now runs once at router admission in
684
- // @blockcast/mmt-manifest-verify. The worker only records that this
685
- // legacy ALTA-enabled symbol bypassed hot-path verification.
686
- const recoveredAltaVerified: boolean | null = null;
687
- if (altaEnabled) altaPending++;
688
-
689
447
  const buf = toOwnedArrayBuffer(symbol);
690
448
  const meta: FrameMeta = {
691
- altaVerified: recoveredAltaVerified,
692
449
  recovered: true,
693
450
  sbn,
694
451
  esi: i,
@@ -857,7 +614,6 @@ function feedIsoSource(
857
614
  advanceMediaHorizon(cmd.ssId);
858
615
  unplacedSources.set(cmd.ssId, performance.now());
859
616
  sourceSymbols++;
860
- if (altaEnabled) altaPending++;
861
617
  // No repair has named this source's block, so the decoder cannot have
862
618
  // one to recover; data here is a Worker/decoder disagreement that
863
619
  // cannot be attributed to a block, and is refused rather than guessed.
@@ -893,7 +649,6 @@ function feedIsoSource(
893
649
  block.sourceReceived++;
894
650
  block.lastSymbolTs = performance.now();
895
651
  sourceSymbols++;
896
- if (altaEnabled) altaPending++;
897
652
  settleSourceArrival(key, block, result);
898
653
  }
899
654
 
@@ -990,12 +745,12 @@ async function handleConfigure(
990
745
  const blockProfile: unknown = cmd.config.blockProfile;
991
746
  if (blockProfile === undefined) {
992
747
  throw new Error(
993
- '[FecWorker] configure: blockProfile is required: name the FEC block profile ("k-padded-v1" or "iso-ssbg1-v1")',
748
+ '[FecWorker] configure: blockProfile is required: name the FEC block profile ("iso-ssbg1-v1")',
994
749
  );
995
750
  }
996
- if (blockProfile !== "k-padded-v1" && blockProfile !== "iso-ssbg1-v1") {
751
+ if (blockProfile !== "iso-ssbg1-v1") {
997
752
  throw new Error(
998
- `[FecWorker] configure: blockProfile must be "k-padded-v1" or "iso-ssbg1-v1" (got ${JSON.stringify(blockProfile)})`,
753
+ `[FecWorker] configure: blockProfile must be "iso-ssbg1-v1" (got ${JSON.stringify(blockProfile)})`,
999
754
  );
1000
755
  }
1001
756
  if (
@@ -1010,10 +765,11 @@ async function handleConfigure(
1010
765
  }
1011
766
  if (
1012
767
  !Number.isFinite(cmd.config.interleaveMs) ||
1013
- cmd.config.interleaveMs <= 0
768
+ !Number.isSafeInteger(cmd.config.interleaveMs) ||
769
+ cmd.config.interleaveMs < 0
1014
770
  ) {
1015
771
  throw new Error(
1016
- `[FecWorker] configure: interleaveMs must be finite and > 0 (got ${cmd.config.interleaveMs})`,
772
+ `[FecWorker] configure: interleaveMs must be a finite non-negative integer (got ${cmd.config.interleaveMs})`,
1017
773
  );
1018
774
  }
1019
775
  if (
@@ -1061,50 +817,6 @@ async function handleConfigure(
1061
817
  verifyDecoderProfile(decoder, blockProfile);
1062
818
  wasmDecoder = decoder;
1063
819
  config = cmd.config;
1064
- altaEnabled = !!cmd.altaPublicKey;
1065
-
1066
- // Size the deferred-verify queue strictly from catalog-derived FEC
1067
- // parameters. CLAUDE.md bans Math.max(x, floor) magic — if catalog
1068
- // values are zero/missing, throw at init rather than silently applying
1069
- // a hardcoded floor. Consumers of an undersized queue cap would see
1070
- // altaPendingEvictedCap spike with no actionable signal.
1071
- // Cap: 1.5× interleave depth — wide enough to hold one full FEC recovery
1072
- // window plus jitter, narrow enough to bound memory.
1073
- pendingVerifyCap = Math.ceil(config.interleaveDepth * 1.5);
1074
- // TTL: 1.5× block duration (K symbols at one per interleave tick) —
1075
- // after this many ms, any unresolved entry has missed its FEC window.
1076
- pendingVerifyTtlMs = Math.ceil(config.interleaveMs * config.k * 1.5);
1077
-
1078
- // Initialize ALTA WASM verifier if public key and WASM module provided (D-05, D-09)
1079
- altaVerifier = null;
1080
- altaInitFailed = false;
1081
- if (cmd.altaPublicKey && cmd.altaWasmModule) {
1082
- try {
1083
- // Load ALTA JS glue (wasm-bindgen output from wasm-pack build).
1084
- // Mirrors the RaptorQ WASM pattern: initSync({ module }) then construct.
1085
- // Dynamic import path matches the IWA static asset layout.
1086
- // Cast via string to prevent TypeScript from attempting static resolution
1087
- // of a runtime-only URL (same pattern as fec-worker-entry.js loading).
1088
- const altaGlueUrl = '/wasm/alta/alta_rs.js' as string;
1089
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1090
- const altaGlue: any = await import(/* webpackIgnore: true */ altaGlueUrl);
1091
- altaGlue.initSync({ module: cmd.altaWasmModule });
1092
- const keyBytes = new Uint8Array(cmd.altaPublicKey);
1093
- altaVerifier = new altaGlue.JsAltaVerifier(keyBytes);
1094
- } catch (e) {
1095
- // T-08-15: WASM init failure must NOT silently look like "not-ready-yet".
1096
- // Force altaEnabled = false so handleFeedSource takes the disabled
1097
- // branch instead of continuously bumping altaPending under the
1098
- // misleading "verifier not yet ready" comment. altaInitFailed
1099
- // flag in the snapshot tells consumers this is an error state, not
1100
- // an opt-out.
1101
- console.warn('[FecWorker] ALTA WASM init failed, verification disabled:', e);
1102
- altaVerifier = null;
1103
- altaEnabled = false;
1104
- altaInitFailed = true;
1105
- }
1106
- }
1107
-
1108
820
  wasmDecoder.configure_params(config.k, config.symbolSize);
1109
821
 
1110
822
  // Tell the client which optional commands this Worker accepts, so it can
@@ -1134,51 +846,7 @@ function handleFeedSource(
1134
846
  throw new Error("[FecWorker] feedSource: invalid source symbol");
1135
847
  }
1136
848
 
1137
- const view = new Uint8Array(cmd.data);
1138
- if (config.blockProfile === "iso-ssbg1-v1") {
1139
- feedIsoSource(cmd, view, wasmDecoder, config);
1140
- return;
1141
- }
1142
-
1143
- // Derive SBN locally for block-level accounting. WASM derives the same
1144
- // value internally (ISO 23008-1 §C.5.2: SBN = floor(SS_ID / K)).
1145
- const sbn = config.k > 0 ? Math.floor(cmd.ssId / config.k) : cmd.ssId;
1146
- const esi = cmd.ssId % config.k;
1147
- if (isRetiredSbn(sbn)) {
1148
- sourceDroppedRetired++;
1149
- return;
1150
- }
1151
- const existingBlock = blocks.get(sbn);
1152
- if (existingBlock && isTerminalBlock(existingBlock)) {
1153
- sourceDroppedTerminal++;
1154
- return;
1155
- }
1156
- if (acceptedSourceEsis.get(sbn)?.has(esi)) {
1157
- sourceDroppedDuplicate++;
1158
- return;
1159
- }
1160
-
1161
- const result = wasmDecoder.add_source(cmd.ssId, view, cmd.ts);
1162
- advanceMediaHorizon(sbn);
1163
- let sourceEsis = acceptedSourceEsis.get(sbn);
1164
- if (!sourceEsis) {
1165
- sourceEsis = new Set<number>();
1166
- acceptedSourceEsis.set(sbn, sourceEsis);
1167
- }
1168
- sourceEsis.add(esi);
1169
-
1170
- // Update block state
1171
- const block = getOrCreateBlock(sbn);
1172
- block.sourceReceived++;
1173
- block.lastSymbolTs = performance.now();
1174
- sourceSymbols++;
1175
-
1176
- // The FEC worker must remain decode-only on the per-symbol hot path. ALTA
1177
- // WASM is retained temporarily for low-rate signaling users, but media is
1178
- // authenticated by the canonical manifest verifier at router admission.
1179
- if (altaEnabled) altaPending++;
1180
-
1181
- settleSourceArrival(sbn, block, result);
849
+ feedIsoSource(cmd, new Uint8Array(cmd.data), wasmDecoder, config);
1182
850
  }
1183
851
 
1184
852
  function handleFeedRepair(
@@ -1190,17 +858,14 @@ function handleFeedRepair(
1190
858
  if (!config) {
1191
859
  throw new Error("[FecWorker] feedRepair: config missing");
1192
860
  }
1193
- // k-padded-v1 blocks start at multiples of K and repair ESI = K + RS_ID;
1194
861
  // iso-ssbg1-v1 blocks start at any SS_ID and repair ESI = SSB_length + RS_ID.
1195
- const iso = config.blockProfile === "iso-ssbg1-v1";
1196
862
  if (
1197
863
  !isUint32(cmd.ssStart) ||
1198
864
  !isUint32(cmd.ssbLength) ||
1199
865
  cmd.ssbLength <= 0 ||
1200
866
  cmd.ssbLength > config.k ||
1201
- (!iso && cmd.ssStart % config.k !== 0) ||
1202
867
  !isUint32(cmd.rsId) ||
1203
- (iso ? cmd.ssbLength : config.k) + cmd.rsId > UINT24_MAX ||
868
+ cmd.ssbLength + cmd.rsId > UINT24_MAX ||
1204
869
  !(cmd.data instanceof ArrayBuffer)
1205
870
  ) {
1206
871
  throw new Error("[FecWorker] feedRepair: invalid repair symbol");
@@ -1218,51 +883,7 @@ function handleFeedRepair(
1218
883
  }
1219
884
  return;
1220
885
  }
1221
- if (iso) {
1222
- feedIsoRepair(cmd, view, wasmDecoder);
1223
- return;
1224
- }
1225
- const sbn = Math.floor(cmd.ssStart / config.k);
1226
- const repairEsi = config.k + cmd.rsId;
1227
- // The repair path's mirror drops stay silent and uncounted: BLO-35597
1228
- // defect 2 is a source-symbol shortfall, so only the source side needed
1229
- // evidence. The asymmetry is deliberate, not an oversight — extend these
1230
- // the same way if a repair-side shortfall ever needs attributing.
1231
- if (isRetiredSbn(sbn)) return;
1232
- const existingBlock = blocks.get(sbn);
1233
- if (existingBlock && isTerminalBlock(existingBlock)) return;
1234
- if (acceptedRepairEsis.get(sbn)?.has(repairEsi)) return;
1235
- // One block has one authored SSB_length; a repair symbol naming another
1236
- // is dropped before it reaches the decoder (which rejects it too).
1237
- if (existingBlock?.ssbLength !== undefined && existingBlock.ssbLength !== cmd.ssbLength) {
1238
- emit({
1239
- type: "error",
1240
- code: "repair-ssb-length-conflict",
1241
- message: `[FecWorker] Dropping repair symbol whose SSB_length ${cmd.ssbLength} conflicts with ${existingBlock.ssbLength} (sbn=${sbn})`,
1242
- });
1243
- return;
1244
- }
1245
-
1246
- const result = wasmDecoder.add_repair(
1247
- cmd.ssStart,
1248
- cmd.ssbLength,
1249
- cmd.rsId,
1250
- view,
1251
- cmd.ts,
1252
- );
1253
- advanceMediaHorizon(sbn);
1254
- let repairEsis = acceptedRepairEsis.get(sbn);
1255
- if (!repairEsis) {
1256
- repairEsis = new Set<number>();
1257
- acceptedRepairEsis.set(sbn, repairEsis);
1258
- }
1259
- repairEsis.add(repairEsi);
1260
- const block = getOrCreateBlock(sbn);
1261
- block.ssbLength = cmd.ssbLength;
1262
- block.repairReceived++;
1263
- block.lastSymbolTs = performance.now();
1264
- repairSymbols++;
1265
- settleRepairArrival(sbn, block, cmd.ssbLength, result);
886
+ feedIsoRepair(cmd, view, wasmDecoder);
1266
887
  }
1267
888
 
1268
889
  function handleRelayBlockSig(
@@ -1271,8 +892,8 @@ function handleRelayBlockSig(
1271
892
  // No-op AUTH stub: store sig on block, mark as not-yet-verified
1272
893
  const block = blocks.get(cmd.sbn);
1273
894
  if (block) {
1274
- // Real verification deferred to ALTA milestone
1275
- // For now, mark as null (not verified) — will be wired when ALTA is implemented
895
+ // Relay block signatures are not verified here; source provenance is the
896
+ // signed per-group manifest checked at router admission.
1276
897
  block.relayBlockSigValid = null;
1277
898
  emitBlockUpdate(block);
1278
899
  }
@@ -1315,10 +936,9 @@ function handleCleanup(
1315
936
  continue;
1316
937
  }
1317
938
  currentBlock ??= getOrCreateBlock(sbn);
1318
- // k-padded-v1 compares with K, unchanged. iso-ssbg1-v1 compares with
1319
- // the block's SSB_length; a key no repair declared has none, so a
1320
- // block the Worker never knew can only have failed.
1321
- const authoredLength = isIsoProfile() ? currentBlock.ssbLength : currentBlock.k;
939
+ // Compare with the block's SSB_length; a key no repair declared has
940
+ // none, so a block the Worker never knew can only have failed.
941
+ const authoredLength = currentBlock.ssbLength;
1322
942
  if (authoredLength !== undefined && currentBlock.sourceReceived >= authoredLength) {
1323
943
  markBlockTerminal(currentBlock, "complete");
1324
944
  blocksComplete++;
@@ -1343,29 +963,12 @@ function handleDispose(): void {
1343
963
  snapshotTimer = null;
1344
964
  }
1345
965
 
1346
- // Free ALTA verifier WASM object (MUST call to release WASM memory —
1347
- // Worker.terminate() fires immediately after dispose, so the FinalizationRegistry
1348
- // callback has no chance to run and this memory would leak without explicit free)
1349
- if (altaVerifier) {
1350
- try {
1351
- altaVerifier.free();
1352
- } catch (_e) {
1353
- // Best-effort — verifier may already be freed
1354
- }
1355
- altaVerifier = null;
1356
- }
1357
-
1358
966
  // Free WASM decoder (MUST call to release WASM memory)
1359
967
  if (wasmDecoder) {
1360
968
  wasmDecoder.free();
1361
969
  wasmDecoder = null;
1362
970
  }
1363
971
 
1364
- // Clear deferred-verify queue so pinned ArrayBuffers drop.
1365
- pendingVerify.clear();
1366
- pendingVerifyCap = 0;
1367
- pendingVerifyTtlMs = 0;
1368
-
1369
972
  config = null;
1370
973
  clearBlockTracking();
1371
974
  }
@@ -1392,13 +995,6 @@ function handleReset(): void {
1392
995
  blocksComplete = 0;
1393
996
  blocksRecovered = 0;
1394
997
  blocksFailed = 0;
1395
- altaVerified = 0;
1396
- altaFailed = 0;
1397
- altaFailedDetails.length = 0;
1398
- altaPending = 0;
1399
- altaPendingEvictedCap = 0;
1400
- altaPendingEvictedTtl = 0;
1401
- altaError = 0;
1402
998
  relayBlocksVerified = 0;
1403
999
  relayBlocksFailed = 0;
1404
1000
  repairSymbolSizeMismatch = 0;
@@ -1448,8 +1044,7 @@ async function dispatchCommand(cmd: FecWorkerCommand): Promise<void> {
1448
1044
  try {
1449
1045
  switch (cmd.type) {
1450
1046
  case "configure":
1451
- // handleConfigure is async (ALTA WASM init uses dynamic import).
1452
- // Await it so feed/cleanup messages cannot run against a partially
1047
+ // Await handleConfigure so feed/cleanup messages cannot run against a partially
1453
1048
  // configured decoder.
1454
1049
  await handleConfigure(cmd);
1455
1050
  break;