@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.
- package/README.md +6 -4
- package/dist/.tsbuildinfo +1 -0
- package/dist/constants.d.ts +49 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +51 -0
- package/dist/constants.js.map +1 -0
- package/dist/crypto.d.ts +70 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +144 -0
- package/dist/crypto.js.map +1 -0
- package/dist/frame.d.ts +219 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +554 -0
- package/dist/frame.js.map +1 -0
- package/dist/handshake.d.ts +39 -0
- package/dist/handshake.d.ts.map +1 -0
- package/dist/handshake.js +93 -0
- package/dist/handshake.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/replay.d.ts +32 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +88 -0
- package/dist/replay.js.map +1 -0
- package/dist/session.d.ts +67 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +122 -0
- package/dist/session.js.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +81 -0
- package/dist/types.js.map +1 -0
- package/package.json +1 -1
- package/src/constants.ts +3 -3
- package/src/crypto.test.ts +5 -5
- package/src/frame.test.ts +113 -47
- package/src/frame.ts +119 -86
- package/src/handshake.test.ts +29 -41
- package/src/handshake.ts +25 -27
- package/src/index.ts +4 -10
- package/src/integration.test.ts +97 -138
- package/src/session.test.ts +12 -10
- package/src/types.ts +4 -14
- /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.
|
|
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 (
|
|
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 =
|
|
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;
|
package/src/crypto.test.ts
CHANGED
|
@@ -153,7 +153,7 @@ describe("crypto", () => {
|
|
|
153
153
|
|
|
154
154
|
const signature = signPayload(payload, privateKey);
|
|
155
155
|
// Tamper with signature
|
|
156
|
-
signature[0]
|
|
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]
|
|
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]
|
|
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]
|
|
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(
|
|
347
|
-
const frame = new Uint8Array(FRAME_HEADER_SIZE +
|
|
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,
|
|
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(
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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]
|
|
748
|
-
expect(frames[0]
|
|
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]
|
|
762
|
-
expect(frames[1]
|
|
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]
|
|
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]
|
|
860
|
+
expect(allFrames[0]!.type).toBe(FrameType.Ping);
|
|
795
861
|
});
|
|
796
862
|
|
|
797
863
|
it("resets state correctly", () => {
|