@sideband/secure-relay 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +6 -4
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/constants.d.ts +49 -0
  4. package/dist/constants.d.ts.map +1 -0
  5. package/dist/constants.js +51 -0
  6. package/dist/constants.js.map +1 -0
  7. package/dist/crypto.d.ts +70 -0
  8. package/dist/crypto.d.ts.map +1 -0
  9. package/dist/crypto.js +144 -0
  10. package/dist/crypto.js.map +1 -0
  11. package/dist/frame.d.ts +219 -0
  12. package/dist/frame.d.ts.map +1 -0
  13. package/dist/frame.js +554 -0
  14. package/dist/frame.js.map +1 -0
  15. package/dist/handshake.d.ts +39 -0
  16. package/dist/handshake.d.ts.map +1 -0
  17. package/dist/handshake.js +93 -0
  18. package/dist/handshake.js.map +1 -0
  19. package/dist/index.d.ts +46 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +12 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/replay.d.ts +32 -0
  24. package/dist/replay.d.ts.map +1 -0
  25. package/dist/replay.js +88 -0
  26. package/dist/replay.js.map +1 -0
  27. package/dist/session.d.ts +67 -0
  28. package/dist/session.d.ts.map +1 -0
  29. package/dist/session.js +122 -0
  30. package/dist/session.js.map +1 -0
  31. package/dist/types.d.ts +120 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +81 -0
  34. package/dist/types.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/constants.ts +3 -3
  37. package/src/crypto.test.ts +5 -5
  38. package/src/frame.test.ts +113 -47
  39. package/src/frame.ts +119 -86
  40. package/src/handshake.test.ts +29 -41
  41. package/src/handshake.ts +25 -27
  42. package/src/index.ts +4 -10
  43. package/src/integration.test.ts +97 -138
  44. package/src/session.test.ts +12 -10
  45. package/src/types.ts +4 -14
  46. /package/{dist/LICENSE → LICENSE} +0 -0
package/dist/types.js ADDED
@@ -0,0 +1,81 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /** Direction of message flow (used in nonce construction) */
3
+ export const Direction = {
4
+ ClientToDaemon: 1,
5
+ DaemonToClient: 2,
6
+ };
7
+ /**
8
+ * SBRP error codes (string constants for SDK use).
9
+ *
10
+ * Wire codes use numeric values in Control frames (see frame.ts).
11
+ * These string constants provide type-safe SDK-level error handling.
12
+ */
13
+ export const SbrpErrorCode = {
14
+ // Authentication (0x01xx) - Terminal
15
+ Unauthorized: "unauthorized",
16
+ Forbidden: "forbidden",
17
+ // Routing (0x02xx) - Terminal
18
+ DaemonNotFound: "daemon_not_found",
19
+ DaemonOffline: "daemon_offline",
20
+ // Session (0x03xx) - Terminal
21
+ SessionNotFound: "session_not_found",
22
+ SessionExpired: "session_expired",
23
+ // Wire Format (0x04xx) - Terminal
24
+ MalformedFrame: "malformed_frame",
25
+ PayloadTooLarge: "payload_too_large",
26
+ InvalidFrameType: "invalid_frame_type",
27
+ InvalidSessionId: "invalid_session_id",
28
+ DisallowedSender: "disallowed_sender",
29
+ // Internal (0x06xx) - Terminal
30
+ InternalError: "internal_error",
31
+ // Throttling (0x09xx) - Varies (rate_limited=N, backpressure=T)
32
+ RateLimited: "rate_limited",
33
+ Backpressure: "backpressure",
34
+ // Session State (0x10xx) - Non-terminal
35
+ SessionPaused: "session_paused",
36
+ SessionResumed: "session_resumed",
37
+ SessionEnded: "session_ended",
38
+ SessionPending: "session_pending",
39
+ // Endpoint-only (0xExxx) - Never on wire
40
+ IdentityKeyChanged: "identity_key_changed",
41
+ HandshakeFailed: "handshake_failed",
42
+ HandshakeTimeout: "handshake_timeout",
43
+ DecryptFailed: "decrypt_failed",
44
+ SequenceError: "sequence_error",
45
+ };
46
+ /** Signal codes for Signal frame (0x04) */
47
+ export const SignalCode = {
48
+ Ready: 0x00,
49
+ Close: 0x01,
50
+ };
51
+ /** Reason codes for Signal frames (§13.4) */
52
+ export const SignalReason = {
53
+ /** No specific reason (default for ready signal) */
54
+ None: 0x00,
55
+ /** Process restart, memory cleared */
56
+ StateLost: 0x01,
57
+ /** Graceful daemon shutdown */
58
+ Shutdown: 0x02,
59
+ /** Internal policy denial */
60
+ Policy: 0x03,
61
+ /** Internal daemon error */
62
+ Error: 0x04,
63
+ };
64
+ /** SBRP-specific error */
65
+ export class SbrpError extends Error {
66
+ code;
67
+ constructor(code, message) {
68
+ super(message);
69
+ this.code = code;
70
+ this.name = "SbrpError";
71
+ }
72
+ }
73
+ /** Brand a string as DaemonId (no validation) */
74
+ export function asDaemonId(value) {
75
+ return value;
76
+ }
77
+ /** Brand a string as ClientId (no validation) */
78
+ export function asClientId(value) {
79
+ return value;
80
+ }
81
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sCAAsC;AA4DtC,6DAA6D;AAC7D,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,cAAc,EAAE,CAAC;IACjB,cAAc,EAAE,CAAC;CACT,CAAC;AAIX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,qCAAqC;IACrC,YAAY,EAAE,cAAc;IAC5B,SAAS,EAAE,WAAW;IAEtB,8BAA8B;IAC9B,cAAc,EAAE,kBAAkB;IAClC,aAAa,EAAE,gBAAgB;IAE/B,8BAA8B;IAC9B,eAAe,EAAE,mBAAmB;IACpC,cAAc,EAAE,iBAAiB;IAEjC,kCAAkC;IAClC,cAAc,EAAE,iBAAiB;IACjC,eAAe,EAAE,mBAAmB;IACpC,gBAAgB,EAAE,oBAAoB;IACtC,gBAAgB,EAAE,oBAAoB;IACtC,gBAAgB,EAAE,mBAAmB;IAErC,+BAA+B;IAC/B,aAAa,EAAE,gBAAgB;IAE/B,gEAAgE;IAChE,WAAW,EAAE,cAAc;IAC3B,YAAY,EAAE,cAAc;IAE5B,wCAAwC;IACxC,aAAa,EAAE,gBAAgB;IAC/B,cAAc,EAAE,iBAAiB;IACjC,YAAY,EAAE,eAAe;IAC7B,cAAc,EAAE,iBAAiB;IAEjC,yCAAyC;IACzC,kBAAkB,EAAE,sBAAsB;IAC1C,eAAe,EAAE,kBAAkB;IACnC,gBAAgB,EAAE,mBAAmB;IACrC,aAAa,EAAE,gBAAgB;IAC/B,aAAa,EAAE,gBAAgB;CACvB,CAAC;AAIX,2CAA2C;AAC3C,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,KAAK,EAAE,IAAI;IACX,KAAK,EAAE,IAAI;CACH,CAAC;AAIX,6CAA6C;AAC7C,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,oDAAoD;IACpD,IAAI,EAAE,IAAI;IACV,sCAAsC;IACtC,SAAS,EAAE,IAAI;IACf,+BAA+B;IAC/B,QAAQ,EAAE,IAAI;IACd,6BAA6B;IAC7B,MAAM,EAAE,IAAI;IACZ,4BAA4B;IAC5B,KAAK,EAAE,IAAI;CACH,CAAC;AAIX,0BAA0B;AAC1B,MAAM,OAAO,SAAU,SAAQ,KAAK;IAEhB;IADlB,YACkB,IAAmB,EACnC,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,SAAI,GAAJ,IAAI,CAAe;QAInC,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IAC1B,CAAC;CACF;AAED,iDAAiD;AACjD,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAiB,CAAC;AAC3B,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAiB,CAAC;AAC3B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sideband/secure-relay",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Secure Relay Protocol (SBRP): E2EE handshake, session encryption, and TOFU identity pinning for relay-mediated communication.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/constants.ts CHANGED
@@ -13,7 +13,7 @@ export const SBRP_TRANSCRIPT_CONTEXT = "sbrp-v1-transcript";
13
13
  /** HKDF info string for session key derivation */
14
14
  export const SBRP_SESSION_KEYS_INFO = "sbrp-session-keys";
15
15
 
16
- /** Length of session keys in bytes (browserToDaemon + daemonToBrowser) */
16
+ /** Length of session keys in bytes (clientToDaemon + daemonToClient) */
17
17
  export const SESSION_KEYS_LENGTH = 64;
18
18
 
19
19
  /** Length of a single symmetric key in bytes */
@@ -58,8 +58,8 @@ export const MAX_PAYLOAD_SIZE = 65536;
58
58
  /** HandshakeInit payload: X25519 ephemeral public key */
59
59
  export const HANDSHAKE_INIT_PAYLOAD_SIZE = 32;
60
60
 
61
- /** HandshakeAccept payload: X25519 ephemeral (32) + Ed25519 signature (64) */
62
- export const HANDSHAKE_ACCEPT_PAYLOAD_SIZE = 96;
61
+ /** HandshakeAccept payload: Ed25519 identity key (32) + X25519 ephemeral (32) + Ed25519 signature (64) */
62
+ export const HANDSHAKE_ACCEPT_PAYLOAD_SIZE = 128;
63
63
 
64
64
  /** Minimum encrypted payload: nonce (12) + authTag (16), no plaintext */
65
65
  export const MIN_ENCRYPTED_PAYLOAD_SIZE = NONCE_LENGTH + AUTH_TAG_LENGTH;
@@ -153,7 +153,7 @@ describe("crypto", () => {
153
153
 
154
154
  const signature = signPayload(payload, privateKey);
155
155
  // Tamper with signature
156
- signature[0] ^= 0xff;
156
+ signature[0] = signature[0]! ^ 0xff;
157
157
 
158
158
  const valid = verifySignature(payload, signature, publicKey);
159
159
  expect(valid).toBe(false);
@@ -398,7 +398,7 @@ describe("crypto", () => {
398
398
  expect(encrypted.length).toBeGreaterThanOrEqual(NONCE_LENGTH + 16); // nonce + authTag
399
399
  const nonce = encrypted.slice(0, NONCE_LENGTH);
400
400
  const expected = constructNonce(Direction.ClientToDaemon, 42n);
401
- expect(nonce).toEqual(expected);
401
+ expect(Array.from(nonce)).toEqual(Array.from(expected));
402
402
  });
403
403
 
404
404
  it("decryption fails with wrong key", () => {
@@ -416,7 +416,7 @@ describe("crypto", () => {
416
416
 
417
417
  const encrypted = encrypt(key, Direction.ClientToDaemon, 1n, plaintext);
418
418
  // Tamper with ciphertext (after nonce)
419
- encrypted[NONCE_LENGTH + 1] ^= 0xff;
419
+ encrypted[NONCE_LENGTH + 1] = encrypted[NONCE_LENGTH + 1]! ^ 0xff;
420
420
 
421
421
  expect(() => decrypt(key, encrypted)).toThrow();
422
422
  });
@@ -427,7 +427,7 @@ describe("crypto", () => {
427
427
 
428
428
  const encrypted = encrypt(key, Direction.ClientToDaemon, 1n, plaintext);
429
429
  // Tamper with last byte (auth tag)
430
- encrypted[encrypted.length - 1] ^= 0xff;
430
+ encrypted[encrypted.length - 1] = encrypted[encrypted.length - 1]! ^ 0xff;
431
431
 
432
432
  expect(() => decrypt(key, encrypted)).toThrow();
433
433
  });
@@ -438,7 +438,7 @@ describe("crypto", () => {
438
438
 
439
439
  const encrypted = encrypt(key, Direction.ClientToDaemon, 1n, plaintext);
440
440
  // Tamper with nonce
441
- encrypted[0] ^= 0xff;
441
+ encrypted[0] = encrypted[0]! ^ 0xff;
442
442
 
443
443
  expect(() => decrypt(key, encrypted)).toThrow();
444
444
  });
package/src/frame.test.ts CHANGED
@@ -100,6 +100,13 @@ describe("frame codec", () => {
100
100
  expect(() => readFrameHeader(short)).toThrow(SbrpError);
101
101
  });
102
102
 
103
+ it("rejects unknown frame type", () => {
104
+ const frame = new Uint8Array(FRAME_HEADER_SIZE);
105
+ frame[0] = 0x99; // Unknown type
106
+ expect(() => readFrameHeader(frame)).toThrow(SbrpError);
107
+ expect(() => readFrameHeader(frame)).toThrow(/Unknown frame type/);
108
+ });
109
+
103
110
  it("rejects invalid payload length in header", () => {
104
111
  const frame = new Uint8Array(FRAME_HEADER_SIZE);
105
112
  frame[0] = FrameType.Data;
@@ -222,9 +229,23 @@ describe("frame codec", () => {
222
229
  );
223
230
  });
224
231
 
232
+ it("rejects wrong identityPublicKey size", () => {
233
+ const wrongSize: HandshakeAccept = {
234
+ type: "handshake.accept",
235
+ identityPublicKey: new Uint8Array(16), // should be 32
236
+ acceptPublicKey: new Uint8Array(32),
237
+ signature: new Uint8Array(64),
238
+ };
239
+ expect(() => encodeHandshakeAccept(1n, wrongSize)).toThrow(SbrpError);
240
+ expect(() => encodeHandshakeAccept(1n, wrongSize)).toThrow(
241
+ /identityPublicKey must be 32 bytes/,
242
+ );
243
+ });
244
+
225
245
  it("rejects wrong acceptPublicKey size", () => {
226
246
  const wrongSize: HandshakeAccept = {
227
247
  type: "handshake.accept",
248
+ identityPublicKey: new Uint8Array(32),
228
249
  acceptPublicKey: new Uint8Array(16), // should be 32
229
250
  signature: new Uint8Array(64),
230
251
  };
@@ -234,6 +255,7 @@ describe("frame codec", () => {
234
255
  it("rejects wrong signature size", () => {
235
256
  const wrongSize: HandshakeAccept = {
236
257
  type: "handshake.accept",
258
+ identityPublicKey: new Uint8Array(32),
237
259
  acceptPublicKey: new Uint8Array(32),
238
260
  signature: new Uint8Array(32), // should be 64
239
261
  };
@@ -313,10 +335,12 @@ describe("frame codec", () => {
313
335
 
314
336
  describe("HandshakeAccept", () => {
315
337
  it("encodes and decodes correctly", () => {
338
+ const identityPublicKey = new Uint8Array(32).fill(0xab);
316
339
  const acceptPublicKey = new Uint8Array(32).fill(0xcd);
317
340
  const signature = new Uint8Array(64).fill(0xef);
318
341
  const accept: HandshakeAccept = {
319
342
  type: "handshake.accept",
343
+ identityPublicKey,
320
344
  acceptPublicKey,
321
345
  signature,
322
346
  };
@@ -331,6 +355,7 @@ describe("frame codec", () => {
331
355
 
332
356
  const decoded = decodeHandshakeAccept(frame);
333
357
  expect(decoded.type).toBe("handshake.accept");
358
+ expect(decoded.identityPublicKey).toEqual(identityPublicKey);
334
359
  expect(decoded.acceptPublicKey).toEqual(acceptPublicKey);
335
360
  expect(decoded.signature).toEqual(signature);
336
361
  });
@@ -343,10 +368,10 @@ describe("frame codec", () => {
343
368
  });
344
369
 
345
370
  it("rejects zero sessionId on decode", () => {
346
- const payload = new Uint8Array(96);
347
- const frame = new Uint8Array(FRAME_HEADER_SIZE + 96);
371
+ const payload = new Uint8Array(128);
372
+ const frame = new Uint8Array(FRAME_HEADER_SIZE + 128);
348
373
  frame[0] = FrameType.HandshakeAccept;
349
- new DataView(frame.buffer).setUint32(1, 96, false);
374
+ new DataView(frame.buffer).setUint32(1, 128, false);
350
375
  frame.set(payload, FRAME_HEADER_SIZE);
351
376
 
352
377
  // Validation happens at decodeFrame level (via readFrameHeader)
@@ -434,7 +459,7 @@ describe("frame codec", () => {
434
459
 
435
460
  const signal = decodeSignal(frame);
436
461
  expect(signal.signal).toBe(SignalCode.Ready);
437
- expect(signal.reason).toBe(0);
462
+ expect(signal.reason).toBe(SignalReason.None);
438
463
  });
439
464
 
440
465
  it("encodes and decodes close signal with reason", () => {
@@ -472,6 +497,22 @@ describe("frame codec", () => {
472
497
  expect(signal.reason).toBe(SignalReason.None);
473
498
  });
474
499
 
500
+ it("rejects unknown signal code", () => {
501
+ const payload = new Uint8Array([0xff, 0x00]); // Unknown signal, valid reason
502
+ const encoded = encodeFrame(FrameType.Signal, 1n, payload);
503
+ const frame = decodeFrame(encoded);
504
+ expect(() => decodeSignal(frame)).toThrow(SbrpError);
505
+ expect(() => decodeSignal(frame)).toThrow(/Unknown signal code/);
506
+ });
507
+
508
+ it("rejects unknown signal reason", () => {
509
+ const payload = new Uint8Array([0x01, 0xff]); // Valid signal (Close), unknown reason
510
+ const encoded = encodeFrame(FrameType.Signal, 1n, payload);
511
+ const frame = decodeFrame(encoded);
512
+ expect(() => decodeSignal(frame)).toThrow(SbrpError);
513
+ expect(() => decodeSignal(frame)).toThrow(/Unknown signal reason/);
514
+ });
515
+
475
516
  it("rejects wrong payload size", () => {
476
517
  const wrongPayload = new Uint8Array(3);
477
518
  const encoded = encodeFrame(FrameType.Signal, 1n, wrongPayload);
@@ -515,6 +556,17 @@ describe("frame codec", () => {
515
556
  expect(control.code).toBe(WireControlCode.SessionPaused);
516
557
  });
517
558
 
559
+ it("encodes and decodes backpressure (0x0902, terminal, SID=0)", () => {
560
+ const encoded = encodeControl(0n, WireControlCode.Backpressure);
561
+ const frame = decodeFrame(encoded);
562
+ expect(frame.sessionId).toBe(0n);
563
+
564
+ const control = decodeControl(frame);
565
+ expect(control.code).toBe(WireControlCode.Backpressure);
566
+ expect(control.code).toBe(0x0902);
567
+ expect(isTerminalCode(control.code)).toBe(true);
568
+ });
569
+
518
570
  it("handles invalid UTF-8 by replacing", () => {
519
571
  // Create control frame with invalid UTF-8 in message
520
572
  const payload = new Uint8Array([0x01, 0x01, 0xff, 0xfe]); // code 0x0101 + invalid UTF-8
@@ -531,30 +583,49 @@ describe("frame codec", () => {
531
583
  const frame = decodeFrame(encoded);
532
584
  expect(() => decodeControl(frame)).toThrow(SbrpError);
533
585
  });
586
+
587
+ it("rejects unknown control code", () => {
588
+ const payload = new Uint8Array([0x99, 0x99]); // Unknown code 0x9999
589
+ const encoded = encodeFrame(FrameType.Control, 0n, payload);
590
+ const frame = decodeFrame(encoded);
591
+ expect(() => decodeControl(frame)).toThrow(SbrpError);
592
+ expect(() => decodeControl(frame)).toThrow(/Unknown control code/);
593
+ });
534
594
  });
535
595
 
536
596
  describe("isTerminalCode", () => {
537
597
  it("returns true for terminal codes", () => {
538
- expect(isTerminalCode(WireControlCode.Unauthorized)).toBe(true);
539
- expect(isTerminalCode(WireControlCode.Forbidden)).toBe(true);
540
- expect(isTerminalCode(WireControlCode.DaemonNotFound)).toBe(true);
541
- expect(isTerminalCode(WireControlCode.SessionNotFound)).toBe(true);
542
- expect(isTerminalCode(WireControlCode.SessionExpired)).toBe(true);
543
- expect(isTerminalCode(WireControlCode.MalformedFrame)).toBe(true);
544
- expect(isTerminalCode(WireControlCode.PayloadTooLarge)).toBe(true);
545
- expect(isTerminalCode(WireControlCode.InvalidFrameType)).toBe(true);
546
- expect(isTerminalCode(WireControlCode.InvalidSessionId)).toBe(true);
547
- expect(isTerminalCode(WireControlCode.DisallowedSender)).toBe(true);
548
- expect(isTerminalCode(WireControlCode.InternalError)).toBe(true);
598
+ const terminalCodes: WireControlCode[] = [
599
+ WireControlCode.Unauthorized,
600
+ WireControlCode.Forbidden,
601
+ WireControlCode.DaemonNotFound,
602
+ WireControlCode.DaemonOffline,
603
+ WireControlCode.SessionNotFound,
604
+ WireControlCode.SessionExpired,
605
+ WireControlCode.MalformedFrame,
606
+ WireControlCode.PayloadTooLarge,
607
+ WireControlCode.InvalidFrameType,
608
+ WireControlCode.InvalidSessionId,
609
+ WireControlCode.DisallowedSender,
610
+ WireControlCode.InternalError,
611
+ WireControlCode.Backpressure,
612
+ ];
613
+ for (const code of terminalCodes) {
614
+ expect(isTerminalCode(code)).toBe(true);
615
+ }
549
616
  });
550
617
 
551
618
  it("returns false for non-terminal codes", () => {
552
- expect(isTerminalCode(WireControlCode.DaemonOffline)).toBe(false);
553
- expect(isTerminalCode(WireControlCode.RateLimited)).toBe(false);
554
- expect(isTerminalCode(WireControlCode.SessionPaused)).toBe(false);
555
- expect(isTerminalCode(WireControlCode.SessionResumed)).toBe(false);
556
- expect(isTerminalCode(WireControlCode.SessionEnded)).toBe(false);
557
- expect(isTerminalCode(WireControlCode.SessionPending)).toBe(false);
619
+ const nonTerminalCodes: WireControlCode[] = [
620
+ WireControlCode.RateLimited,
621
+ WireControlCode.SessionPaused,
622
+ WireControlCode.SessionResumed,
623
+ WireControlCode.SessionEnded,
624
+ WireControlCode.SessionPending,
625
+ ];
626
+ for (const code of nonTerminalCodes) {
627
+ expect(isTerminalCode(code)).toBe(false);
628
+ }
558
629
  });
559
630
  });
560
631
 
@@ -582,8 +653,9 @@ describe("frame codec", () => {
582
653
  // Internal (0x06xx)
583
654
  expect(WireControlCode.InternalError).toBe(0x0601);
584
655
 
585
- // Rate Limiting (0x09xx)
656
+ // Rate Limiting / Backpressure (0x09xx)
586
657
  expect(WireControlCode.RateLimited).toBe(0x0901);
658
+ expect(WireControlCode.Backpressure).toBe(0x0902);
587
659
 
588
660
  // Session State (0x10xx)
589
661
  expect(WireControlCode.SessionPaused).toBe(0x1001);
@@ -705,24 +777,6 @@ describe("frame codec", () => {
705
777
  );
706
778
  });
707
779
 
708
- it("converts all WireControlCode to SbrpErrorCode", () => {
709
- expect(fromWireControlCode(WireControlCode.Unauthorized)).toBe(
710
- SbrpErrorCode.Unauthorized,
711
- );
712
- expect(fromWireControlCode(WireControlCode.MalformedFrame)).toBe(
713
- SbrpErrorCode.MalformedFrame,
714
- );
715
- expect(fromWireControlCode(WireControlCode.InvalidSessionId)).toBe(
716
- SbrpErrorCode.InvalidSessionId,
717
- );
718
- expect(fromWireControlCode(WireControlCode.InternalError)).toBe(
719
- SbrpErrorCode.InternalError,
720
- );
721
- expect(fromWireControlCode(WireControlCode.SessionPaused)).toBe(
722
- SbrpErrorCode.SessionPaused,
723
- );
724
- });
725
-
726
780
  it("throws on endpoint-only codes (never transmitted on wire)", () => {
727
781
  expect(() =>
728
782
  toWireControlCode(SbrpErrorCode.IdentityKeyChanged),
@@ -733,8 +787,20 @@ describe("frame codec", () => {
733
787
  expect(() => toWireControlCode(SbrpErrorCode.SequenceError)).toThrow();
734
788
  });
735
789
 
790
+ it("covers all WireControlCode entries bidirectionally", () => {
791
+ // Exhaustive check: every wire code must round-trip through both mappings.
792
+ // This catches missing entries (e.g., 0x0902 backpressure) that a spot-check would miss.
793
+ const allCodes = Object.values(WireControlCode) as WireControlCode[];
794
+ for (const wireCode of allCodes) {
795
+ const sbrpCode = fromWireControlCode(wireCode);
796
+ expect(toWireControlCode(sbrpCode)).toBe(wireCode);
797
+ }
798
+ });
799
+
736
800
  it("throws on unknown wire codes", () => {
737
- expect(() => fromWireControlCode(0x9999 as WireControlCode)).toThrow();
801
+ expect(() => fromWireControlCode(0x9999 as WireControlCode)).toThrow(
802
+ /0x9999/,
803
+ );
738
804
  });
739
805
  });
740
806
 
@@ -744,8 +810,8 @@ describe("frame codec", () => {
744
810
  const frame = encodePing();
745
811
  const frames = [...decoder.push(frame)];
746
812
  expect(frames.length).toBe(1);
747
- expect(frames[0].type).toBe(FrameType.Ping);
748
- expect(frames[0].sessionId).toBe(0n);
813
+ expect(frames[0]!.type).toBe(FrameType.Ping);
814
+ expect(frames[0]!.sessionId).toBe(0n);
749
815
  });
750
816
 
751
817
  it("decodes multiple frames in one push", () => {
@@ -758,8 +824,8 @@ describe("frame codec", () => {
758
824
 
759
825
  const frames = [...decoder.push(combined)];
760
826
  expect(frames.length).toBe(2);
761
- expect(frames[0].type).toBe(FrameType.Ping);
762
- expect(frames[1].type).toBe(FrameType.Pong);
827
+ expect(frames[0]!.type).toBe(FrameType.Ping);
828
+ expect(frames[1]!.type).toBe(FrameType.Pong);
763
829
  });
764
830
 
765
831
  it("buffers incomplete frames", () => {
@@ -774,7 +840,7 @@ describe("frame codec", () => {
774
840
  // Push rest
775
841
  frames = [...decoder.push(frame.subarray(FRAME_HEADER_SIZE))];
776
842
  expect(frames.length).toBe(1);
777
- expect(frames[0].type).toBe(FrameType.Control);
843
+ expect(frames[0]!.type).toBe(FrameType.Control);
778
844
  expect(decoder.bufferedBytes).toBe(0);
779
845
  });
780
846
 
@@ -791,7 +857,7 @@ describe("frame codec", () => {
791
857
  }
792
858
 
793
859
  expect(allFrames.length).toBe(1);
794
- expect(allFrames[0].type).toBe(FrameType.Ping);
860
+ expect(allFrames[0]!.type).toBe(FrameType.Ping);
795
861
  });
796
862
 
797
863
  it("resets state correctly", () => {