@sideband/secure-relay 0.0.1

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.
@@ -0,0 +1,169 @@
1
+ // SPDX-FileCopyrightText: 2025-present Sideband
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ /**
5
+ * E2EE handshake protocol for Sideband Relay Protocol (SBRP).
6
+ *
7
+ * Implements the authenticated key exchange between client and daemon,
8
+ * with Ed25519 signatures for MITM protection.
9
+ */
10
+
11
+ import {
12
+ computeSharedSecret,
13
+ createSignaturePayload,
14
+ createTranscriptHash,
15
+ deriveSessionKeys,
16
+ generateEphemeralKeyPair,
17
+ signPayload,
18
+ verifySignature,
19
+ zeroize,
20
+ } from "./crypto.js";
21
+ import type {
22
+ DaemonId,
23
+ EphemeralKeyPair,
24
+ HandshakeAccept,
25
+ HandshakeInit,
26
+ IdentityKeyPair,
27
+ SessionKeys,
28
+ } from "./types.js";
29
+ import { SbrpError, SbrpErrorCode } from "./types.js";
30
+
31
+ /** Result of a successful daemon handshake */
32
+ export interface DaemonHandshakeResult {
33
+ sessionKeys: SessionKeys;
34
+ ephemeralKeyPair: EphemeralKeyPair;
35
+ signature: Uint8Array;
36
+ }
37
+
38
+ /** Result of a successful client handshake */
39
+ export interface ClientHandshakeResult {
40
+ sessionKeys: SessionKeys;
41
+ ephemeralKeyPair: EphemeralKeyPair;
42
+ }
43
+
44
+ /**
45
+ * Create a handshake init message (client side).
46
+ *
47
+ * Generates an ephemeral X25519 keypair for this session.
48
+ */
49
+ export function createHandshakeInit(): {
50
+ message: HandshakeInit;
51
+ ephemeralKeyPair: EphemeralKeyPair;
52
+ } {
53
+ const ephemeralKeyPair = generateEphemeralKeyPair();
54
+ return {
55
+ message: {
56
+ type: "handshake.init",
57
+ initPublicKey: ephemeralKeyPair.publicKey,
58
+ },
59
+ ephemeralKeyPair,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Process handshake init and create accept message (daemon side).
65
+ *
66
+ * 1. Generate ephemeral X25519 keypair
67
+ * 2. Sign ephemeral public key with identity key (context-bound)
68
+ * 3. Derive session keys
69
+ */
70
+ export function processHandshakeInit(
71
+ init: HandshakeInit,
72
+ daemonId: DaemonId,
73
+ identityKeyPair: IdentityKeyPair,
74
+ ): { message: HandshakeAccept; result: DaemonHandshakeResult } {
75
+ const ephemeralKeyPair = generateEphemeralKeyPair();
76
+
77
+ // Sign ephemeral key with context binding
78
+ const signaturePayload = createSignaturePayload(
79
+ daemonId,
80
+ init.initPublicKey,
81
+ ephemeralKeyPair.publicKey,
82
+ );
83
+ const signature = signPayload(signaturePayload, identityKeyPair.privateKey);
84
+
85
+ // Derive session keys
86
+ const sharedSecret = computeSharedSecret(
87
+ ephemeralKeyPair.privateKey,
88
+ init.initPublicKey,
89
+ );
90
+ const transcriptHash = createTranscriptHash(
91
+ daemonId,
92
+ init.initPublicKey,
93
+ ephemeralKeyPair.publicKey,
94
+ signature,
95
+ );
96
+ const sessionKeys = deriveSessionKeys(sharedSecret, transcriptHash);
97
+
98
+ // Best-effort zeroize shared secret
99
+ zeroize(sharedSecret);
100
+
101
+ return {
102
+ message: {
103
+ type: "handshake.accept",
104
+ acceptPublicKey: ephemeralKeyPair.publicKey,
105
+ signature,
106
+ },
107
+ result: {
108
+ sessionKeys,
109
+ ephemeralKeyPair,
110
+ signature,
111
+ },
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Process handshake accept message (client side).
117
+ *
118
+ * 1. Verify signature using PINNED identity key (TOFU)
119
+ * 2. Derive session keys using same transcript hash as daemon
120
+ *
121
+ * @throws {SbrpError} with code HandshakeFailed if signature verification fails
122
+ */
123
+ export function processHandshakeAccept(
124
+ accept: HandshakeAccept,
125
+ daemonId: DaemonId,
126
+ pinnedIdentityPublicKey: Uint8Array,
127
+ ephemeralKeyPair: EphemeralKeyPair,
128
+ ): ClientHandshakeResult {
129
+ // Verify daemon signature using PINNED key (not relay-provided!)
130
+ const signaturePayload = createSignaturePayload(
131
+ daemonId,
132
+ ephemeralKeyPair.publicKey,
133
+ accept.acceptPublicKey,
134
+ );
135
+
136
+ const valid = verifySignature(
137
+ signaturePayload,
138
+ accept.signature,
139
+ pinnedIdentityPublicKey,
140
+ );
141
+
142
+ if (!valid) {
143
+ throw new SbrpError(
144
+ SbrpErrorCode.HandshakeFailed,
145
+ "Signature verification failed",
146
+ );
147
+ }
148
+
149
+ // Derive session keys using same transcript hash as daemon
150
+ const sharedSecret = computeSharedSecret(
151
+ ephemeralKeyPair.privateKey,
152
+ accept.acceptPublicKey,
153
+ );
154
+ const transcriptHash = createTranscriptHash(
155
+ daemonId,
156
+ ephemeralKeyPair.publicKey,
157
+ accept.acceptPublicKey,
158
+ accept.signature,
159
+ );
160
+ const sessionKeys = deriveSessionKeys(sharedSecret, transcriptHash);
161
+
162
+ // Best-effort zeroize shared secret
163
+ zeroize(sharedSecret);
164
+
165
+ return {
166
+ sessionKeys,
167
+ ephemeralKeyPair,
168
+ };
169
+ }
package/src/index.ts ADDED
@@ -0,0 +1,132 @@
1
+ // SPDX-FileCopyrightText: 2025-present Sideband
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ /**
5
+ * @sideband/secure-relay
6
+ *
7
+ * Sideband Relay Protocol (SBRP) implementation for E2EE communication
8
+ * between daemons and clients via a relay server.
9
+ *
10
+ * Features:
11
+ * - Ed25519 identity signatures for MITM protection
12
+ * - X25519 ephemeral key exchange for forward secrecy
13
+ * - ChaCha20-Poly1305 authenticated encryption
14
+ * - TOFU (Trust On First Use) identity pinning
15
+ * - Bitmap-based replay protection
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * // Daemon side: generate identity keypair on first run
20
+ * const identity = generateIdentityKeyPair();
21
+ *
22
+ * // Client side: create handshake init
23
+ * const { message: init, ephemeralKeyPair } = createHandshakeInit();
24
+ *
25
+ * // Daemon side: process init and create accept
26
+ * const { message: accept, result } = processHandshakeInit(init, daemonId, identity);
27
+ * const clientSession = createClientSession(clientId, result.sessionKeys);
28
+ *
29
+ * // Client side: process accept (with TOFU-pinned identity)
30
+ * const { sessionKeys } = processHandshakeAccept(accept, daemonId, pinnedKey, ephemeralKeyPair);
31
+ * const daemonSession = createDaemonSession(sessionKeys);
32
+ *
33
+ * // Encrypt/decrypt messages
34
+ * const encrypted = encryptClientToDaemon(daemonSession, plaintext);
35
+ * const decrypted = decryptClientToDaemon(clientSession, encrypted);
36
+ * ```
37
+ */
38
+
39
+ // Types
40
+ export type {
41
+ ClientId,
42
+ DaemonId,
43
+ EncryptedMessage,
44
+ EphemeralKeyPair,
45
+ HandshakeAccept,
46
+ HandshakeInit,
47
+ IdentityKeyPair,
48
+ PinnedIdentity,
49
+ SessionKeys,
50
+ } from "./types.js";
51
+
52
+ export {
53
+ asClientId,
54
+ asDaemonId,
55
+ Direction,
56
+ SbrpError,
57
+ SbrpErrorCode,
58
+ } from "./types.js";
59
+
60
+ // Constants
61
+ export {
62
+ AUTH_TAG_LENGTH,
63
+ DEFAULT_REPLAY_WINDOW_SIZE,
64
+ DIRECTION_CLIENT_TO_DAEMON,
65
+ DIRECTION_DAEMON_TO_CLIENT,
66
+ ED25519_PRIVATE_KEY_LENGTH,
67
+ ED25519_PUBLIC_KEY_LENGTH,
68
+ ED25519_SIGNATURE_LENGTH,
69
+ NONCE_LENGTH,
70
+ SBRP_HANDSHAKE_CONTEXT,
71
+ SBRP_SESSION_KEYS_INFO,
72
+ SBRP_TRANSCRIPT_CONTEXT,
73
+ SESSION_KEYS_LENGTH,
74
+ SYMMETRIC_KEY_LENGTH,
75
+ X25519_PRIVATE_KEY_LENGTH,
76
+ X25519_PUBLIC_KEY_LENGTH,
77
+ } from "./constants.js";
78
+
79
+ // Crypto primitives
80
+ export {
81
+ computeFingerprint,
82
+ computeSharedSecret,
83
+ constructNonce,
84
+ createSignaturePayload,
85
+ createTranscriptHash,
86
+ decrypt,
87
+ deriveSessionKeys,
88
+ encrypt,
89
+ extractSequence,
90
+ generateEphemeralKeyPair,
91
+ generateIdentityKeyPair,
92
+ randomBytes,
93
+ signPayload,
94
+ verifySignature,
95
+ zeroize,
96
+ } from "./crypto.js";
97
+
98
+ // Handshake
99
+ export type {
100
+ ClientHandshakeResult,
101
+ DaemonHandshakeResult,
102
+ } from "./handshake.js";
103
+
104
+ export {
105
+ createHandshakeInit,
106
+ processHandshakeAccept,
107
+ processHandshakeInit,
108
+ } from "./handshake.js";
109
+
110
+ // Replay protection
111
+ export type { ReplayWindow } from "./replay.js";
112
+
113
+ export {
114
+ checkAndUpdateReplay,
115
+ createReplayWindow,
116
+ isValidSequence,
117
+ resetReplayWindow,
118
+ } from "./replay.js";
119
+
120
+ // Session management
121
+ export type { ClientSession, DaemonSession } from "./session.js";
122
+
123
+ export {
124
+ clearClientSession,
125
+ clearDaemonSession,
126
+ createClientSession,
127
+ createDaemonSession,
128
+ decryptClientToDaemon,
129
+ decryptDaemonToClient,
130
+ encryptClientToDaemon,
131
+ encryptDaemonToClient,
132
+ } from "./session.js";
package/src/replay.ts ADDED
@@ -0,0 +1,115 @@
1
+ // SPDX-FileCopyrightText: 2025-present Sideband
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ /**
5
+ * Bitmap-based replay protection for Sideband Relay Protocol (SBRP).
6
+ *
7
+ * Uses a sliding window to track seen sequence numbers and prevent
8
+ * replay attacks while avoiding memory exhaustion from attacker-controlled
9
+ * sequence numbers.
10
+ */
11
+
12
+ import { DEFAULT_REPLAY_WINDOW_SIZE } from "./constants.js";
13
+
14
+ /** Replay window state */
15
+ export interface ReplayWindow {
16
+ /** Highest accepted sequence number */
17
+ maxSeen: bigint;
18
+ /** Bitmap: bit i set = sequence (maxSeen - i) was seen */
19
+ bitmap: bigint;
20
+ /** Window size in bits */
21
+ windowSize: bigint;
22
+ }
23
+
24
+ /**
25
+ * Create a new replay window.
26
+ *
27
+ * @param windowSize - Window size in bits (default: 64)
28
+ */
29
+ export function createReplayWindow(
30
+ windowSize: bigint = DEFAULT_REPLAY_WINDOW_SIZE,
31
+ ): ReplayWindow {
32
+ return {
33
+ maxSeen: -1n, // No messages seen yet
34
+ bitmap: 0n,
35
+ windowSize,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Check if a sequence number is valid (not a replay) and update the window.
41
+ *
42
+ * @returns true if the sequence is valid and accepted, false if it's a replay
43
+ */
44
+ export function checkAndUpdateReplay(
45
+ seq: bigint,
46
+ window: ReplayWindow,
47
+ ): boolean {
48
+ // First message ever
49
+ if (window.maxSeen === -1n) {
50
+ window.maxSeen = seq;
51
+ window.bitmap = 1n;
52
+ return true;
53
+ }
54
+
55
+ if (seq > window.maxSeen) {
56
+ // New high sequence - shift window
57
+ const shift = seq - window.maxSeen;
58
+ if (shift >= window.windowSize) {
59
+ // Sequence is far ahead, reset bitmap
60
+ window.bitmap = 1n;
61
+ } else {
62
+ window.bitmap = (window.bitmap << shift) | 1n;
63
+ }
64
+ window.maxSeen = seq;
65
+ return true;
66
+ }
67
+
68
+ // Sequence is within or before the window
69
+ const diff = window.maxSeen - seq;
70
+ if (diff >= window.windowSize) {
71
+ // Too old, outside window
72
+ return false;
73
+ }
74
+
75
+ const mask = 1n << diff;
76
+ if (window.bitmap & mask) {
77
+ // Already seen (replay)
78
+ return false;
79
+ }
80
+
81
+ // Mark as seen
82
+ window.bitmap |= mask;
83
+ return true;
84
+ }
85
+
86
+ /**
87
+ * Check if a sequence number would be valid without updating the window.
88
+ *
89
+ * Useful for pre-validation before decryption.
90
+ */
91
+ export function isValidSequence(seq: bigint, window: ReplayWindow): boolean {
92
+ if (window.maxSeen === -1n) {
93
+ return true;
94
+ }
95
+
96
+ if (seq > window.maxSeen) {
97
+ return true;
98
+ }
99
+
100
+ const diff = window.maxSeen - seq;
101
+ if (diff >= window.windowSize) {
102
+ return false;
103
+ }
104
+
105
+ const mask = 1n << diff;
106
+ return (window.bitmap & mask) === 0n;
107
+ }
108
+
109
+ /**
110
+ * Reset the replay window to initial state.
111
+ */
112
+ export function resetReplayWindow(window: ReplayWindow): void {
113
+ window.maxSeen = -1n;
114
+ window.bitmap = 0n;
115
+ }
package/src/session.ts ADDED
@@ -0,0 +1,196 @@
1
+ // SPDX-FileCopyrightText: 2025-present Sideband
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ /**
5
+ * Session management for Sideband Relay Protocol (SBRP).
6
+ *
7
+ * Handles encrypted message sending/receiving with proper key selection,
8
+ * sequence number tracking, and replay protection.
9
+ */
10
+
11
+ import { decrypt, encrypt, extractSequence, zeroize } from "./crypto.js";
12
+ import {
13
+ checkAndUpdateReplay,
14
+ createReplayWindow,
15
+ type ReplayWindow,
16
+ } from "./replay.js";
17
+ import type { ClientId, EncryptedMessage, SessionKeys } from "./types.js";
18
+ import { Direction, SbrpError, SbrpErrorCode } from "./types.js";
19
+
20
+ /** Crypto state for one direction of communication (traffic key, counters, replay) */
21
+ interface ChannelState {
22
+ trafficKey: Uint8Array;
23
+ sendSeq: bigint;
24
+ recvWindow: ReplayWindow;
25
+ }
26
+
27
+ /** Client session (daemon-side state for each connected client) */
28
+ export interface ClientSession {
29
+ clientId: ClientId;
30
+ clientToDaemon: ChannelState;
31
+ daemonToClient: ChannelState;
32
+ }
33
+
34
+ /** Daemon session (client-side state for communicating with daemon) */
35
+ export interface DaemonSession {
36
+ clientToDaemon: ChannelState;
37
+ daemonToClient: ChannelState;
38
+ }
39
+
40
+ /**
41
+ * Create a client session (daemon side).
42
+ *
43
+ * Used by daemon to manage state for each connected client.
44
+ */
45
+ export function createClientSession(
46
+ clientId: ClientId,
47
+ sessionKeys: SessionKeys,
48
+ ): ClientSession {
49
+ return {
50
+ clientId,
51
+ clientToDaemon: {
52
+ trafficKey: sessionKeys.clientToDaemon,
53
+ sendSeq: 0n,
54
+ recvWindow: createReplayWindow(),
55
+ },
56
+ daemonToClient: {
57
+ trafficKey: sessionKeys.daemonToClient,
58
+ sendSeq: 0n,
59
+ recvWindow: createReplayWindow(),
60
+ },
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Create a daemon session (client side).
66
+ *
67
+ * Used by client to communicate with daemon.
68
+ */
69
+ export function createDaemonSession(sessionKeys: SessionKeys): DaemonSession {
70
+ return {
71
+ clientToDaemon: {
72
+ trafficKey: sessionKeys.clientToDaemon,
73
+ sendSeq: 0n,
74
+ recvWindow: createReplayWindow(),
75
+ },
76
+ daemonToClient: {
77
+ trafficKey: sessionKeys.daemonToClient,
78
+ sendSeq: 0n,
79
+ recvWindow: createReplayWindow(),
80
+ },
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Encrypt a message from client to daemon.
86
+ */
87
+ export function encryptClientToDaemon(
88
+ session: DaemonSession,
89
+ plaintext: Uint8Array,
90
+ ): EncryptedMessage {
91
+ const seq = session.clientToDaemon.sendSeq++;
92
+ const data = encrypt(
93
+ session.clientToDaemon.trafficKey,
94
+ Direction.ClientToDaemon,
95
+ seq,
96
+ plaintext,
97
+ );
98
+
99
+ return { type: "encrypted", seq, data };
100
+ }
101
+
102
+ /**
103
+ * Encrypt a message from daemon to client.
104
+ */
105
+ export function encryptDaemonToClient(
106
+ session: ClientSession,
107
+ plaintext: Uint8Array,
108
+ ): EncryptedMessage {
109
+ const seq = session.daemonToClient.sendSeq++;
110
+ const data = encrypt(
111
+ session.daemonToClient.trafficKey,
112
+ Direction.DaemonToClient,
113
+ seq,
114
+ plaintext,
115
+ );
116
+
117
+ return { type: "encrypted", seq, data };
118
+ }
119
+
120
+ /**
121
+ * Decrypt a message received by daemon from client.
122
+ *
123
+ * @throws {SbrpError} with code SequenceError if replay detected
124
+ * @throws {SbrpError} with code DecryptFailed if decryption fails
125
+ */
126
+ export function decryptClientToDaemon(
127
+ session: ClientSession,
128
+ message: EncryptedMessage,
129
+ ): Uint8Array {
130
+ // Check replay protection
131
+ const seq = extractSequence(message.data);
132
+ if (!checkAndUpdateReplay(seq, session.clientToDaemon.recvWindow)) {
133
+ throw new SbrpError(
134
+ SbrpErrorCode.SequenceError,
135
+ "Sequence number outside valid window or replay detected",
136
+ );
137
+ }
138
+
139
+ try {
140
+ return decrypt(session.clientToDaemon.trafficKey, message.data);
141
+ } catch {
142
+ throw new SbrpError(
143
+ SbrpErrorCode.DecryptFailed,
144
+ "Message decryption failed",
145
+ );
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Decrypt a message received by client from daemon.
151
+ *
152
+ * @throws {SbrpError} with code SequenceError if replay detected
153
+ * @throws {SbrpError} with code DecryptFailed if decryption fails
154
+ */
155
+ export function decryptDaemonToClient(
156
+ session: DaemonSession,
157
+ message: EncryptedMessage,
158
+ ): Uint8Array {
159
+ // Check replay protection
160
+ const seq = extractSequence(message.data);
161
+ if (!checkAndUpdateReplay(seq, session.daemonToClient.recvWindow)) {
162
+ throw new SbrpError(
163
+ SbrpErrorCode.SequenceError,
164
+ "Sequence number outside valid window or replay detected",
165
+ );
166
+ }
167
+
168
+ try {
169
+ return decrypt(session.daemonToClient.trafficKey, message.data);
170
+ } catch {
171
+ throw new SbrpError(
172
+ SbrpErrorCode.DecryptFailed,
173
+ "Message decryption failed",
174
+ );
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Clear all key material from a client session.
180
+ *
181
+ * Best-effort zeroization (JS/GC limitations apply).
182
+ */
183
+ export function clearClientSession(session: ClientSession): void {
184
+ zeroize(session.clientToDaemon.trafficKey);
185
+ zeroize(session.daemonToClient.trafficKey);
186
+ }
187
+
188
+ /**
189
+ * Clear all key material from a daemon session.
190
+ *
191
+ * Best-effort zeroization (JS/GC limitations apply).
192
+ */
193
+ export function clearDaemonSession(session: DaemonSession): void {
194
+ zeroize(session.clientToDaemon.trafficKey);
195
+ zeroize(session.daemonToClient.trafficKey);
196
+ }
package/src/types.ts ADDED
@@ -0,0 +1,104 @@
1
+ // SPDX-FileCopyrightText: 2025-present Sideband
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ /**
5
+ * Type definitions for Sideband Relay Protocol (SBRP).
6
+ */
7
+
8
+ /** Branded type for daemon identifiers */
9
+ export type DaemonId = string & { readonly __brand: "DaemonId" };
10
+
11
+ /** Branded type for client session identifiers (relay-assigned) */
12
+ export type ClientId = string & { readonly __brand: "ClientId" };
13
+
14
+ /** Ed25519 identity keypair for daemon authentication */
15
+ export interface IdentityKeyPair {
16
+ publicKey: Uint8Array; // 32 bytes
17
+ privateKey: Uint8Array; // 32 bytes (seed) or 64 bytes (expanded)
18
+ }
19
+
20
+ /** X25519 ephemeral keypair for key exchange */
21
+ export interface EphemeralKeyPair {
22
+ publicKey: Uint8Array; // 32 bytes
23
+ privateKey: Uint8Array; // 32 bytes
24
+ }
25
+
26
+ /**
27
+ * TOFU trust record for daemon identity.
28
+ * Pinned on first connect, verified on reconnect to detect MITM.
29
+ * Per-client; not synced via relay.
30
+ */
31
+ export interface PinnedIdentity {
32
+ daemonId: DaemonId;
33
+ identityPublicKey: Uint8Array; // 32 bytes Ed25519 public key
34
+ firstSeen: Date;
35
+ lastSeen: Date;
36
+ }
37
+
38
+ /** Session keys derived from handshake (directional symmetric keys) */
39
+ export interface SessionKeys {
40
+ /** Key for encrypting client→daemon messages */
41
+ clientToDaemon: Uint8Array; // 32 bytes
42
+ /** Key for encrypting daemon→client messages */
43
+ daemonToClient: Uint8Array; // 32 bytes
44
+ }
45
+
46
+ /** Handshake init message (client → daemon) */
47
+ export interface HandshakeInit {
48
+ type: "handshake.init";
49
+ initPublicKey: Uint8Array; // X25519 ephemeral public key
50
+ }
51
+
52
+ /** Handshake accept message (daemon → client) */
53
+ export interface HandshakeAccept {
54
+ type: "handshake.accept";
55
+ acceptPublicKey: Uint8Array; // X25519 ephemeral public key
56
+ signature: Uint8Array; // Ed25519 signature
57
+ }
58
+
59
+ /** Encrypted message envelope */
60
+ export interface EncryptedMessage {
61
+ type: "encrypted";
62
+ seq: bigint;
63
+ data: Uint8Array; // nonce || ciphertext || authTag
64
+ }
65
+
66
+ /** Direction of message flow (used in nonce construction) */
67
+ export const enum Direction {
68
+ ClientToDaemon = 1,
69
+ DaemonToClient = 2,
70
+ }
71
+
72
+ /** SBRP error codes */
73
+ export const enum SbrpErrorCode {
74
+ Unauthorized = "unauthorized",
75
+ DaemonNotFound = "daemon_not_found",
76
+ DaemonOffline = "daemon_offline",
77
+ DaemonNotOwned = "daemon_not_owned",
78
+ IdentityKeyChanged = "identity_key_changed",
79
+ HandshakeFailed = "handshake_failed",
80
+ DecryptFailed = "decrypt_failed",
81
+ SequenceError = "sequence_error",
82
+ RateLimited = "rate_limited",
83
+ }
84
+
85
+ /** SBRP-specific error */
86
+ export class SbrpError extends Error {
87
+ constructor(
88
+ public readonly code: SbrpErrorCode,
89
+ message: string,
90
+ ) {
91
+ super(message);
92
+ this.name = "SbrpError";
93
+ }
94
+ }
95
+
96
+ /** Brand a string as DaemonId (no validation) */
97
+ export function asDaemonId(value: string): DaemonId {
98
+ return value as DaemonId;
99
+ }
100
+
101
+ /** Brand a string as ClientId (no validation) */
102
+ export function asClientId(value: string): ClientId {
103
+ return value as ClientId;
104
+ }