@floegence/flowersec-core 0.1.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.
- package/LICENSE +22 -0
- package/README.md +42 -0
- package/YAMUX_ALIGNMENT.md +127 -0
- package/dist/_examples/flowersec/demo/v1.facade.gen.d.ts +12 -0
- package/dist/_examples/flowersec/demo/v1.facade.gen.js +15 -0
- package/dist/_examples/flowersec/demo/v1.gen.d.ts +16 -0
- package/dist/_examples/flowersec/demo/v1.gen.js +86 -0
- package/dist/_examples/flowersec/demo/v1.rpc.gen.d.ts +11 -0
- package/dist/_examples/flowersec/demo/v1.rpc.gen.js +22 -0
- package/dist/browser/connect.d.ts +12 -0
- package/dist/browser/connect.js +31 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.js +1 -0
- package/dist/client-connect/common.d.ts +26 -0
- package/dist/client-connect/common.js +167 -0
- package/dist/client-connect/connectCore.d.ts +42 -0
- package/dist/client-connect/connectCore.js +302 -0
- package/dist/client-connect/tunnelAttachCloseReason.d.ts +3 -0
- package/dist/client-connect/tunnelAttachCloseReason.js +16 -0
- package/dist/client.d.ts +17 -0
- package/dist/client.js +1 -0
- package/dist/direct-client/connect.d.ts +4 -0
- package/dist/direct-client/connect.js +67 -0
- package/dist/direct-client/index.d.ts +1 -0
- package/dist/direct-client/index.js +1 -0
- package/dist/e2ee/constants.d.ts +9 -0
- package/dist/e2ee/constants.js +18 -0
- package/dist/e2ee/errors.d.ts +5 -0
- package/dist/e2ee/errors.js +8 -0
- package/dist/e2ee/framing.d.ts +12 -0
- package/dist/e2ee/framing.js +57 -0
- package/dist/e2ee/handshake.d.ts +80 -0
- package/dist/e2ee/handshake.js +322 -0
- package/dist/e2ee/index.d.ts +7 -0
- package/dist/e2ee/index.js +7 -0
- package/dist/e2ee/kdf.d.ts +15 -0
- package/dist/e2ee/kdf.js +39 -0
- package/dist/e2ee/record.d.ts +11 -0
- package/dist/e2ee/record.js +69 -0
- package/dist/e2ee/secureChannel.d.ts +82 -0
- package/dist/e2ee/secureChannel.js +265 -0
- package/dist/e2ee/transcript.d.ts +23 -0
- package/dist/e2ee/transcript.js +31 -0
- package/dist/facade.d.ts +21 -0
- package/dist/facade.js +61 -0
- package/dist/gen/flowersec/controlplane/v1.gen.d.ts +36 -0
- package/dist/gen/flowersec/controlplane/v1.gen.js +135 -0
- package/dist/gen/flowersec/direct/v1.gen.d.ts +21 -0
- package/dist/gen/flowersec/direct/v1.gen.js +101 -0
- package/dist/gen/flowersec/e2ee/v1.gen.d.ts +68 -0
- package/dist/gen/flowersec/e2ee/v1.gen.js +194 -0
- package/dist/gen/flowersec/rpc/v1.gen.d.ts +30 -0
- package/dist/gen/flowersec/rpc/v1.gen.js +107 -0
- package/dist/gen/flowersec/tunnel/v1.gen.d.ts +23 -0
- package/dist/gen/flowersec/tunnel/v1.gen.js +104 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/node/connect.d.ts +9 -0
- package/dist/node/connect.js +13 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +2 -0
- package/dist/node/wsFactory.d.ts +2 -0
- package/dist/node/wsFactory.js +69 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/observer.d.ts +23 -0
- package/dist/observability/observer.js +28 -0
- package/dist/rpc/callError.d.ts +5 -0
- package/dist/rpc/callError.js +11 -0
- package/dist/rpc/caller.d.ts +8 -0
- package/dist/rpc/caller.js +1 -0
- package/dist/rpc/client.d.ts +22 -0
- package/dist/rpc/client.js +170 -0
- package/dist/rpc/framing.d.ts +4 -0
- package/dist/rpc/framing.js +24 -0
- package/dist/rpc/index.d.ts +6 -0
- package/dist/rpc/index.js +6 -0
- package/dist/rpc/server.d.ts +15 -0
- package/dist/rpc/server.js +67 -0
- package/dist/rpc/typed.d.ts +5 -0
- package/dist/rpc/typed.js +9 -0
- package/dist/rpc/validate.d.ts +2 -0
- package/dist/rpc/validate.js +27 -0
- package/dist/rpc-proxy/index.d.ts +1 -0
- package/dist/rpc-proxy/index.js +1 -0
- package/dist/rpc-proxy/rpcProxy.d.ts +13 -0
- package/dist/rpc-proxy/rpcProxy.js +59 -0
- package/dist/streamhello/index.d.ts +1 -0
- package/dist/streamhello/index.js +1 -0
- package/dist/streamhello/streamHello.d.ts +3 -0
- package/dist/streamhello/streamHello.js +13 -0
- package/dist/tunnel-client/connect.d.ts +7 -0
- package/dist/tunnel-client/connect.js +125 -0
- package/dist/tunnel-client/index.d.ts +1 -0
- package/dist/tunnel-client/index.js +1 -0
- package/dist/utils/base64url.d.ts +2 -0
- package/dist/utils/base64url.js +40 -0
- package/dist/utils/bin.d.ts +6 -0
- package/dist/utils/bin.js +55 -0
- package/dist/utils/errors.d.ts +26 -0
- package/dist/utils/errors.js +42 -0
- package/dist/utils/number.d.ts +2 -0
- package/dist/utils/number.js +9 -0
- package/dist/ws/index.d.ts +1 -0
- package/dist/ws/index.js +1 -0
- package/dist/ws-client/binaryTransport.d.ts +49 -0
- package/dist/ws-client/binaryTransport.js +301 -0
- package/dist/yamux/byteReader.d.ts +10 -0
- package/dist/yamux/byteReader.js +50 -0
- package/dist/yamux/constants.d.ts +10 -0
- package/dist/yamux/constants.js +14 -0
- package/dist/yamux/header.d.ts +17 -0
- package/dist/yamux/header.js +26 -0
- package/dist/yamux/index.d.ts +5 -0
- package/dist/yamux/index.js +5 -0
- package/dist/yamux/session.d.ts +44 -0
- package/dist/yamux/session.js +228 -0
- package/dist/yamux/stream.d.ts +30 -0
- package/dist/yamux/stream.js +222 -0
- package/package.json +112 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { RECORD_FLAG_APP, RECORD_FLAG_PING, RECORD_FLAG_REKEY } from "./constants.js";
|
|
2
|
+
import { decryptRecord, encryptRecord, maxPlaintextBytes } from "./record.js";
|
|
3
|
+
import { deriveRekeyKey } from "./kdf.js";
|
|
4
|
+
// SecureChannel encrypts/decrypts records and buffers application payloads.
|
|
5
|
+
export class SecureChannel {
|
|
6
|
+
// Underlying transport for encrypted record frames.
|
|
7
|
+
transport;
|
|
8
|
+
// Maximum allowed bytes per record frame.
|
|
9
|
+
maxRecordBytes;
|
|
10
|
+
// Upper bound for buffered plaintext in memory.
|
|
11
|
+
maxBufferedBytes;
|
|
12
|
+
// Active encryption keys and nonce prefixes for the current epoch.
|
|
13
|
+
sendKey;
|
|
14
|
+
recvKey;
|
|
15
|
+
sendNoncePrefix;
|
|
16
|
+
recvNoncePrefix;
|
|
17
|
+
// Rekey base secret derived from the handshake.
|
|
18
|
+
rekeyBase;
|
|
19
|
+
// Transcript hash binding rekeys to the handshake.
|
|
20
|
+
transcriptHash;
|
|
21
|
+
// Rekey direction identifiers for send/recv.
|
|
22
|
+
sendDir;
|
|
23
|
+
recvDir;
|
|
24
|
+
// Monotonic record sequence numbers per direction.
|
|
25
|
+
sendSeq;
|
|
26
|
+
recvSeq;
|
|
27
|
+
// Send queue and waiters for backpressure.
|
|
28
|
+
sendQueue = [];
|
|
29
|
+
sendQueueHead = 0;
|
|
30
|
+
sendWaiters = [];
|
|
31
|
+
sendWaitersHead = 0;
|
|
32
|
+
sendClosed = false;
|
|
33
|
+
sendErr = null;
|
|
34
|
+
// Receive queue and waiters for plaintext delivery.
|
|
35
|
+
recvQueue = [];
|
|
36
|
+
recvQueueHead = 0;
|
|
37
|
+
recvQueueBytes = 0;
|
|
38
|
+
recvWaiters = [];
|
|
39
|
+
readErr = null;
|
|
40
|
+
closed = false;
|
|
41
|
+
constructor(args) {
|
|
42
|
+
this.transport = args.transport;
|
|
43
|
+
this.maxRecordBytes = args.maxRecordBytes;
|
|
44
|
+
this.maxBufferedBytes = Math.max(0, args.maxBufferedBytes ?? 4 * (1 << 20));
|
|
45
|
+
this.sendKey = args.sendKey;
|
|
46
|
+
this.recvKey = args.recvKey;
|
|
47
|
+
this.sendNoncePrefix = args.sendNoncePrefix;
|
|
48
|
+
this.recvNoncePrefix = args.recvNoncePrefix;
|
|
49
|
+
this.rekeyBase = args.rekeyBase;
|
|
50
|
+
this.transcriptHash = args.transcriptHash;
|
|
51
|
+
this.sendDir = args.sendDir;
|
|
52
|
+
this.recvDir = args.recvDir;
|
|
53
|
+
this.sendSeq = args.sendSeq ?? 1n;
|
|
54
|
+
this.recvSeq = args.recvSeq ?? 1n;
|
|
55
|
+
void this.readLoop();
|
|
56
|
+
void this.sendLoop();
|
|
57
|
+
}
|
|
58
|
+
// write splits payloads into record-sized chunks and queues them for send.
|
|
59
|
+
async write(plaintext) {
|
|
60
|
+
const maxPlain = Math.max(1, maxPlaintextBytes(this.maxRecordBytes) || plaintext.length);
|
|
61
|
+
let off = 0;
|
|
62
|
+
while (off < plaintext.length) {
|
|
63
|
+
const chunk = plaintext.slice(off, Math.min(plaintext.length, off + maxPlain));
|
|
64
|
+
await this.enqueueSend("app", chunk);
|
|
65
|
+
off += chunk.length;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// read resolves with the next plaintext chunk or throws on errors/close.
|
|
69
|
+
async read() {
|
|
70
|
+
while (true) {
|
|
71
|
+
if (this.readErr != null)
|
|
72
|
+
throw this.readErr;
|
|
73
|
+
if (this.recvQueueHead < this.recvQueue.length) {
|
|
74
|
+
const b = this.recvQueue[this.recvQueueHead];
|
|
75
|
+
this.recvQueueHead++;
|
|
76
|
+
if (this.recvQueueHead > 1024 && this.recvQueueHead * 2 > this.recvQueue.length) {
|
|
77
|
+
this.recvQueue.splice(0, this.recvQueueHead);
|
|
78
|
+
this.recvQueueHead = 0;
|
|
79
|
+
}
|
|
80
|
+
this.recvQueueBytes -= b.length;
|
|
81
|
+
return b;
|
|
82
|
+
}
|
|
83
|
+
if (this.closed)
|
|
84
|
+
throw new Error("closed");
|
|
85
|
+
await new Promise((resolve) => this.recvWaiters.push(resolve));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// close shuts down the transport and rejects any pending senders.
|
|
89
|
+
close() {
|
|
90
|
+
if (this.closed)
|
|
91
|
+
return;
|
|
92
|
+
this.closed = true;
|
|
93
|
+
this.sendClosed = true;
|
|
94
|
+
this.rejectQueuedSenders(this.sendErr ?? new Error("closed"));
|
|
95
|
+
this.wakeSendWaiters();
|
|
96
|
+
this.transport.close();
|
|
97
|
+
this.recvQueue.length = 0;
|
|
98
|
+
this.recvQueueHead = 0;
|
|
99
|
+
this.recvQueueBytes = 0;
|
|
100
|
+
const ws = this.recvWaiters;
|
|
101
|
+
this.recvWaiters = [];
|
|
102
|
+
for (const w of ws)
|
|
103
|
+
w();
|
|
104
|
+
}
|
|
105
|
+
// sendPing emits a keepalive record.
|
|
106
|
+
async sendPing() {
|
|
107
|
+
await this.enqueueSend("ping");
|
|
108
|
+
}
|
|
109
|
+
// rekeyNow emits a rekey record and advances the send key.
|
|
110
|
+
async rekeyNow() {
|
|
111
|
+
await this.enqueueSend("rekey");
|
|
112
|
+
}
|
|
113
|
+
enqueueSend(kind, payload) {
|
|
114
|
+
if (this.sendErr != null)
|
|
115
|
+
return Promise.reject(this.sendErr);
|
|
116
|
+
if (this.closed || this.sendClosed)
|
|
117
|
+
return Promise.reject(new Error("closed"));
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
if (this.sendErr != null) {
|
|
120
|
+
reject(this.sendErr);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (this.closed || this.sendClosed) {
|
|
124
|
+
reject(new Error("closed"));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const req = payload === undefined ? { kind, resolve, reject } : { kind, payload, resolve, reject };
|
|
128
|
+
this.sendQueue.push(req);
|
|
129
|
+
const w = this.shiftSendWaiter();
|
|
130
|
+
if (w != null)
|
|
131
|
+
w();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async nextSend() {
|
|
135
|
+
const immediate = this.dequeueSend();
|
|
136
|
+
if (immediate != null)
|
|
137
|
+
return immediate;
|
|
138
|
+
if (this.closed || this.sendClosed)
|
|
139
|
+
return null;
|
|
140
|
+
return await new Promise((resolve) => {
|
|
141
|
+
this.sendWaiters.push(() => resolve(this.dequeueSend()));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
dequeueSend() {
|
|
145
|
+
if (this.sendQueueHead >= this.sendQueue.length)
|
|
146
|
+
return null;
|
|
147
|
+
const req = this.sendQueue[this.sendQueueHead];
|
|
148
|
+
this.sendQueueHead++;
|
|
149
|
+
if (this.sendQueueHead > 1024 && this.sendQueueHead * 2 > this.sendQueue.length) {
|
|
150
|
+
this.sendQueue.splice(0, this.sendQueueHead);
|
|
151
|
+
this.sendQueueHead = 0;
|
|
152
|
+
}
|
|
153
|
+
return req;
|
|
154
|
+
}
|
|
155
|
+
shiftSendWaiter() {
|
|
156
|
+
if (this.sendWaitersHead >= this.sendWaiters.length)
|
|
157
|
+
return undefined;
|
|
158
|
+
const w = this.sendWaiters[this.sendWaitersHead];
|
|
159
|
+
this.sendWaitersHead++;
|
|
160
|
+
if (this.sendWaitersHead > 1024 && this.sendWaitersHead * 2 > this.sendWaiters.length) {
|
|
161
|
+
this.sendWaiters.splice(0, this.sendWaitersHead);
|
|
162
|
+
this.sendWaitersHead = 0;
|
|
163
|
+
}
|
|
164
|
+
return w;
|
|
165
|
+
}
|
|
166
|
+
wakeSendWaiters() {
|
|
167
|
+
const ws = this.sendWaiters;
|
|
168
|
+
const start = this.sendWaitersHead;
|
|
169
|
+
this.sendWaiters = [];
|
|
170
|
+
this.sendWaitersHead = 0;
|
|
171
|
+
for (let i = start; i < ws.length; i++)
|
|
172
|
+
ws[i]();
|
|
173
|
+
}
|
|
174
|
+
rejectQueuedSenders(err) {
|
|
175
|
+
const queued = this.sendQueue;
|
|
176
|
+
const start = this.sendQueueHead;
|
|
177
|
+
this.sendQueue = [];
|
|
178
|
+
this.sendQueueHead = 0;
|
|
179
|
+
for (let i = start; i < queued.length; i++)
|
|
180
|
+
queued[i].reject(err);
|
|
181
|
+
}
|
|
182
|
+
failSend(err) {
|
|
183
|
+
if (this.sendErr != null)
|
|
184
|
+
return;
|
|
185
|
+
this.sendErr = err;
|
|
186
|
+
this.rejectQueuedSenders(err);
|
|
187
|
+
this.wakeSendWaiters();
|
|
188
|
+
}
|
|
189
|
+
async sendLoop() {
|
|
190
|
+
while (true) {
|
|
191
|
+
const req = await this.nextSend();
|
|
192
|
+
if (req == null)
|
|
193
|
+
return;
|
|
194
|
+
if (this.sendErr != null) {
|
|
195
|
+
req.reject(this.sendErr);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (this.closed || this.sendClosed) {
|
|
199
|
+
req.reject(new Error("closed"));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
let frame;
|
|
204
|
+
if (req.kind === "app") {
|
|
205
|
+
const seq = this.sendSeq++;
|
|
206
|
+
frame = encryptRecord(this.sendKey, this.sendNoncePrefix, RECORD_FLAG_APP, seq, req.payload ?? new Uint8Array(), this.maxRecordBytes);
|
|
207
|
+
}
|
|
208
|
+
else if (req.kind === "ping") {
|
|
209
|
+
const seq = this.sendSeq++;
|
|
210
|
+
frame = encryptRecord(this.sendKey, this.sendNoncePrefix, RECORD_FLAG_PING, seq, new Uint8Array(), this.maxRecordBytes);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const seq = this.sendSeq++;
|
|
214
|
+
frame = encryptRecord(this.sendKey, this.sendNoncePrefix, RECORD_FLAG_REKEY, seq, new Uint8Array(), this.maxRecordBytes);
|
|
215
|
+
// Update the send key after enqueuing the rekey frame.
|
|
216
|
+
this.sendKey = deriveRekeyKey(this.rekeyBase, this.transcriptHash, seq, this.sendDir);
|
|
217
|
+
}
|
|
218
|
+
await this.transport.writeBinary(frame);
|
|
219
|
+
req.resolve();
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
req.reject(e);
|
|
223
|
+
this.failSend(e);
|
|
224
|
+
this.close();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async readLoop() {
|
|
230
|
+
try {
|
|
231
|
+
while (!this.closed) {
|
|
232
|
+
const frame = await this.transport.readBinary();
|
|
233
|
+
const { flags, seq, plaintext } = decryptRecord(this.recvKey, this.recvNoncePrefix, frame, this.recvSeq, this.maxRecordBytes);
|
|
234
|
+
this.recvSeq = seq + 1n;
|
|
235
|
+
if (flags === RECORD_FLAG_APP) {
|
|
236
|
+
if (this.maxBufferedBytes > 0 && this.recvQueueBytes + plaintext.length > this.maxBufferedBytes) {
|
|
237
|
+
throw new Error("recv buffer exceeded");
|
|
238
|
+
}
|
|
239
|
+
this.recvQueue.push(plaintext);
|
|
240
|
+
this.recvQueueBytes += plaintext.length;
|
|
241
|
+
const ws = this.recvWaiters;
|
|
242
|
+
this.recvWaiters = [];
|
|
243
|
+
for (const w of ws)
|
|
244
|
+
w();
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (flags === RECORD_FLAG_PING)
|
|
248
|
+
continue;
|
|
249
|
+
if (flags === RECORD_FLAG_REKEY) {
|
|
250
|
+
this.recvKey = deriveRekeyKey(this.rekeyBase, this.transcriptHash, seq, this.recvDir);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`unknown record flag ${flags}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
this.readErr = e;
|
|
258
|
+
const ws = this.recvWaiters;
|
|
259
|
+
this.recvWaiters = [];
|
|
260
|
+
for (const w of ws)
|
|
261
|
+
w();
|
|
262
|
+
this.close();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type TranscriptInputs = Readonly<{
|
|
2
|
+
/** Protocol version byte used in the transcript. */
|
|
3
|
+
version: number;
|
|
4
|
+
/** Numeric suite identifier (see e2ee suite). */
|
|
5
|
+
suite: number;
|
|
6
|
+
/** Role byte (client=1, server=2). */
|
|
7
|
+
role: number;
|
|
8
|
+
/** Client feature bitset. */
|
|
9
|
+
clientFeatures: number;
|
|
10
|
+
/** Server feature bitset. */
|
|
11
|
+
serverFeatures: number;
|
|
12
|
+
/** Channel identifier shared by both endpoints. */
|
|
13
|
+
channelId: string;
|
|
14
|
+
/** Client nonce (32 bytes). */
|
|
15
|
+
nonceC: Uint8Array;
|
|
16
|
+
/** Server nonce (32 bytes). */
|
|
17
|
+
nonceS: Uint8Array;
|
|
18
|
+
/** Client ephemeral public key bytes. */
|
|
19
|
+
clientEphPub: Uint8Array;
|
|
20
|
+
/** Server ephemeral public key bytes. */
|
|
21
|
+
serverEphPub: Uint8Array;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function transcriptHash(inputs: TranscriptInputs): Uint8Array;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
2
|
+
import { concatBytes, u16be, u32be } from "../utils/bin.js";
|
|
3
|
+
const te = new TextEncoder();
|
|
4
|
+
// transcriptHash computes the SHA-256 hash of the handshake transcript.
|
|
5
|
+
export function transcriptHash(inputs) {
|
|
6
|
+
if (inputs.nonceC.length !== 32 || inputs.nonceS.length !== 32)
|
|
7
|
+
throw new Error("nonce must be 32 bytes");
|
|
8
|
+
const channelIdBytes = te.encode(inputs.channelId);
|
|
9
|
+
if (channelIdBytes.length > 0xffff)
|
|
10
|
+
throw new Error("channel_id too long");
|
|
11
|
+
if (inputs.clientEphPub.length > 0xffff || inputs.serverEphPub.length > 0xffff)
|
|
12
|
+
throw new Error("pub too long");
|
|
13
|
+
const prefix = te.encode("flowersec-e2ee-v1");
|
|
14
|
+
const body = concatBytes([
|
|
15
|
+
prefix,
|
|
16
|
+
new Uint8Array([inputs.version & 0xff]),
|
|
17
|
+
u16be(inputs.suite),
|
|
18
|
+
new Uint8Array([inputs.role & 0xff]),
|
|
19
|
+
u32be(inputs.clientFeatures >>> 0),
|
|
20
|
+
u32be(inputs.serverFeatures >>> 0),
|
|
21
|
+
u16be(channelIdBytes.length),
|
|
22
|
+
channelIdBytes,
|
|
23
|
+
inputs.nonceC,
|
|
24
|
+
inputs.nonceS,
|
|
25
|
+
u16be(inputs.clientEphPub.length),
|
|
26
|
+
inputs.clientEphPub,
|
|
27
|
+
u16be(inputs.serverEphPub.length),
|
|
28
|
+
inputs.serverEphPub
|
|
29
|
+
]);
|
|
30
|
+
return sha256(body);
|
|
31
|
+
}
|
package/dist/facade.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Client } from "./client.js";
|
|
2
|
+
import type { DirectConnectOptions } from "./direct-client/connect.js";
|
|
3
|
+
import type { TunnelConnectOptions } from "./tunnel-client/connect.js";
|
|
4
|
+
import type { ChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
|
|
5
|
+
import type { DirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
|
|
6
|
+
export type { ChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
|
|
7
|
+
export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
|
|
8
|
+
export type { DirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
|
|
9
|
+
export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
|
|
10
|
+
export type { ClientObserverLike } from "./observability/observer.js";
|
|
11
|
+
export type { Client, ClientPath } from "./client.js";
|
|
12
|
+
export type { FlowersecErrorCode, FlowersecPath, FlowersecStage } from "./utils/errors.js";
|
|
13
|
+
export { FlowersecError } from "./utils/errors.js";
|
|
14
|
+
export type { TunnelConnectOptions } from "./tunnel-client/connect.js";
|
|
15
|
+
export type { DirectConnectOptions } from "./direct-client/connect.js";
|
|
16
|
+
export type ConnectOptions = TunnelConnectOptions | DirectConnectOptions;
|
|
17
|
+
export declare function connectTunnel(grant: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
|
|
18
|
+
export declare function connectDirect(info: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
|
|
19
|
+
export declare function connect(input: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
|
|
20
|
+
export declare function connect(input: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
|
|
21
|
+
export declare function connect(input: unknown, opts: ConnectOptions): Promise<Client>;
|
package/dist/facade.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { connectDirect as connectDirectInternal } from "./direct-client/connect.js";
|
|
2
|
+
import { connectTunnel as connectTunnelInternal } from "./tunnel-client/connect.js";
|
|
3
|
+
import { FlowersecError } from "./utils/errors.js";
|
|
4
|
+
export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
|
|
5
|
+
export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
|
|
6
|
+
export { FlowersecError } from "./utils/errors.js";
|
|
7
|
+
export async function connectTunnel(grant, opts) {
|
|
8
|
+
return await connectTunnelInternal(grant, opts);
|
|
9
|
+
}
|
|
10
|
+
export async function connectDirect(info, opts) {
|
|
11
|
+
return await connectDirectInternal(info, opts);
|
|
12
|
+
}
|
|
13
|
+
function maybeParseJSON(input) {
|
|
14
|
+
if (typeof input !== "string")
|
|
15
|
+
return input;
|
|
16
|
+
const s = input.trim();
|
|
17
|
+
if (s === "")
|
|
18
|
+
return input;
|
|
19
|
+
if (s[0] !== "{" && s[0] !== "[")
|
|
20
|
+
return input;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(s);
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
throw new FlowersecError({
|
|
26
|
+
path: "auto",
|
|
27
|
+
stage: "validate",
|
|
28
|
+
code: "invalid_input",
|
|
29
|
+
message: "invalid JSON string",
|
|
30
|
+
cause: e,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function connect(input, opts) {
|
|
35
|
+
const v = maybeParseJSON(input);
|
|
36
|
+
if (v != null && typeof v === "object") {
|
|
37
|
+
const o = v;
|
|
38
|
+
if (o["ws_url"] !== undefined)
|
|
39
|
+
return await connectDirectInternal(v, opts);
|
|
40
|
+
if (o["grant_client"] !== undefined)
|
|
41
|
+
return await connectTunnelInternal(v, opts);
|
|
42
|
+
if (o["grant_server"] !== undefined)
|
|
43
|
+
return await connectTunnelInternal(v, opts);
|
|
44
|
+
if (o["tunnel_url"] !== undefined)
|
|
45
|
+
return await connectTunnelInternal(v, opts);
|
|
46
|
+
if (o["token"] !== undefined || o["role"] !== undefined)
|
|
47
|
+
return await connectTunnelInternal(v, opts);
|
|
48
|
+
throw new FlowersecError({
|
|
49
|
+
path: "auto",
|
|
50
|
+
stage: "validate",
|
|
51
|
+
code: "invalid_input",
|
|
52
|
+
message: "invalid input: expected DirectConnectInfo (ws_url) or ChannelInitGrant (tunnel_url, grant_client, or grant_server)",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
throw new FlowersecError({
|
|
56
|
+
path: "auto",
|
|
57
|
+
stage: "validate",
|
|
58
|
+
code: "invalid_input",
|
|
59
|
+
message: "invalid input: expected an object or a JSON string",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Endpoint role for tunnel attach. */
|
|
2
|
+
export declare enum Role {
|
|
3
|
+
/** Client endpoint. */
|
|
4
|
+
Role_client = 1,
|
|
5
|
+
/** Server endpoint. */
|
|
6
|
+
Role_server = 2
|
|
7
|
+
}
|
|
8
|
+
/** E2EE cipher suite identifier. */
|
|
9
|
+
export declare enum Suite {
|
|
10
|
+
/** P-256 + HKDF-SHA256 + AES-256-GCM. */
|
|
11
|
+
Suite_P256_HKDF_SHA256_AES_256_GCM = 2,
|
|
12
|
+
/** X25519 + HKDF-SHA256 + AES-256-GCM. */
|
|
13
|
+
Suite_X25519_HKDF_SHA256_AES_256_GCM = 1
|
|
14
|
+
}
|
|
15
|
+
/** Grant issued by controlplane to attach and start E2EE. */
|
|
16
|
+
export interface ChannelInitGrant {
|
|
17
|
+
/** WebSocket URL of the tunnel server. */
|
|
18
|
+
tunnel_url: string;
|
|
19
|
+
/** Channel identifier shared by both endpoints. */
|
|
20
|
+
channel_id: string;
|
|
21
|
+
/** Unix timestamp when the grant expires. */
|
|
22
|
+
channel_init_expire_at_unix_s: number;
|
|
23
|
+
/** Server idle timeout hint in seconds. */
|
|
24
|
+
idle_timeout_seconds: number;
|
|
25
|
+
/** Role for this grant (client or server). */
|
|
26
|
+
role: Role;
|
|
27
|
+
/** Signed tunnel attach token. */
|
|
28
|
+
token: string;
|
|
29
|
+
/** Base64url-encoded 32-byte PSK. */
|
|
30
|
+
e2ee_psk_b64u: string;
|
|
31
|
+
/** Allowed E2EE cipher suites. */
|
|
32
|
+
allowed_suites: Suite[];
|
|
33
|
+
/** Default E2EE cipher suite. */
|
|
34
|
+
default_suite: Suite;
|
|
35
|
+
}
|
|
36
|
+
export declare function assertChannelInitGrant(v: unknown): ChannelInitGrant;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Code generated by idlgen. DO NOT EDIT.
|
|
2
|
+
/** Endpoint role for tunnel attach. */
|
|
3
|
+
export var Role;
|
|
4
|
+
(function (Role) {
|
|
5
|
+
/** Client endpoint. */
|
|
6
|
+
Role[Role["Role_client"] = 1] = "Role_client";
|
|
7
|
+
/** Server endpoint. */
|
|
8
|
+
Role[Role["Role_server"] = 2] = "Role_server";
|
|
9
|
+
})(Role || (Role = {}));
|
|
10
|
+
/** E2EE cipher suite identifier. */
|
|
11
|
+
export var Suite;
|
|
12
|
+
(function (Suite) {
|
|
13
|
+
/** P-256 + HKDF-SHA256 + AES-256-GCM. */
|
|
14
|
+
Suite[Suite["Suite_P256_HKDF_SHA256_AES_256_GCM"] = 2] = "Suite_P256_HKDF_SHA256_AES_256_GCM";
|
|
15
|
+
/** X25519 + HKDF-SHA256 + AES-256-GCM. */
|
|
16
|
+
Suite[Suite["Suite_X25519_HKDF_SHA256_AES_256_GCM"] = 1] = "Suite_X25519_HKDF_SHA256_AES_256_GCM";
|
|
17
|
+
})(Suite || (Suite = {}));
|
|
18
|
+
function isRecord(v) {
|
|
19
|
+
return typeof v === "object" && v != null && !Array.isArray(v);
|
|
20
|
+
}
|
|
21
|
+
function assertString(name, v) {
|
|
22
|
+
if (typeof v !== "string")
|
|
23
|
+
throw new Error(`bad ${name}`);
|
|
24
|
+
return v;
|
|
25
|
+
}
|
|
26
|
+
function assertBoolean(name, v) {
|
|
27
|
+
if (typeof v !== "boolean")
|
|
28
|
+
throw new Error(`bad ${name}`);
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
function assertSafeInt(name, v) {
|
|
32
|
+
if (typeof v !== "number" || !Number.isSafeInteger(v))
|
|
33
|
+
throw new Error(`bad ${name}`);
|
|
34
|
+
return v;
|
|
35
|
+
}
|
|
36
|
+
function assertU32(name, v) {
|
|
37
|
+
const n = assertSafeInt(name, v);
|
|
38
|
+
if (n < 0 || n > 0xffffffff)
|
|
39
|
+
throw new Error(`bad ${name}`);
|
|
40
|
+
return n;
|
|
41
|
+
}
|
|
42
|
+
function assertU16(name, v) {
|
|
43
|
+
const n = assertU32(name, v);
|
|
44
|
+
if (n > 0xffff)
|
|
45
|
+
throw new Error(`bad ${name}`);
|
|
46
|
+
return n;
|
|
47
|
+
}
|
|
48
|
+
function assertU8(name, v) {
|
|
49
|
+
const n = assertU32(name, v);
|
|
50
|
+
if (n > 0xff)
|
|
51
|
+
throw new Error(`bad ${name}`);
|
|
52
|
+
return n;
|
|
53
|
+
}
|
|
54
|
+
function assertU64(name, v) {
|
|
55
|
+
const n = assertSafeInt(name, v);
|
|
56
|
+
if (n < 0)
|
|
57
|
+
throw new Error(`bad ${name}`);
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
function assertI32(name, v) {
|
|
61
|
+
const n = assertSafeInt(name, v);
|
|
62
|
+
if (n < -2147483648 || n > 2147483647)
|
|
63
|
+
throw new Error(`bad ${name}`);
|
|
64
|
+
return n;
|
|
65
|
+
}
|
|
66
|
+
function assertI64(name, v) {
|
|
67
|
+
return assertSafeInt(name, v);
|
|
68
|
+
}
|
|
69
|
+
function assertStringMap(name, v) {
|
|
70
|
+
if (!isRecord(v))
|
|
71
|
+
throw new Error(`bad ${name}`);
|
|
72
|
+
for (const [k, vv] of Object.entries(v)) {
|
|
73
|
+
void k;
|
|
74
|
+
if (typeof vv !== "string")
|
|
75
|
+
throw new Error(`bad ${name}`);
|
|
76
|
+
}
|
|
77
|
+
return v;
|
|
78
|
+
}
|
|
79
|
+
const _RoleValues = new Set([
|
|
80
|
+
1,
|
|
81
|
+
2,
|
|
82
|
+
]);
|
|
83
|
+
function assertRole(name, v) {
|
|
84
|
+
const n = assertSafeInt(name, v);
|
|
85
|
+
if (!_RoleValues.has(n))
|
|
86
|
+
throw new Error(`bad ${name}`);
|
|
87
|
+
return n;
|
|
88
|
+
}
|
|
89
|
+
const _SuiteValues = new Set([
|
|
90
|
+
2,
|
|
91
|
+
1,
|
|
92
|
+
]);
|
|
93
|
+
function assertSuite(name, v) {
|
|
94
|
+
const n = assertSafeInt(name, v);
|
|
95
|
+
if (!_SuiteValues.has(n))
|
|
96
|
+
throw new Error(`bad ${name}`);
|
|
97
|
+
return n;
|
|
98
|
+
}
|
|
99
|
+
export function assertChannelInitGrant(v) {
|
|
100
|
+
if (!isRecord(v))
|
|
101
|
+
throw new Error("bad ChannelInitGrant");
|
|
102
|
+
const o = v;
|
|
103
|
+
if (o["tunnel_url"] === undefined)
|
|
104
|
+
throw new Error("bad ChannelInitGrant.tunnel_url");
|
|
105
|
+
assertString("ChannelInitGrant.tunnel_url", o["tunnel_url"]);
|
|
106
|
+
if (o["channel_id"] === undefined)
|
|
107
|
+
throw new Error("bad ChannelInitGrant.channel_id");
|
|
108
|
+
assertString("ChannelInitGrant.channel_id", o["channel_id"]);
|
|
109
|
+
if (o["channel_init_expire_at_unix_s"] === undefined)
|
|
110
|
+
throw new Error("bad ChannelInitGrant.channel_init_expire_at_unix_s");
|
|
111
|
+
assertI64("ChannelInitGrant.channel_init_expire_at_unix_s", o["channel_init_expire_at_unix_s"]);
|
|
112
|
+
if (o["idle_timeout_seconds"] === undefined)
|
|
113
|
+
throw new Error("bad ChannelInitGrant.idle_timeout_seconds");
|
|
114
|
+
assertI32("ChannelInitGrant.idle_timeout_seconds", o["idle_timeout_seconds"]);
|
|
115
|
+
if (o["role"] === undefined)
|
|
116
|
+
throw new Error("bad ChannelInitGrant.role");
|
|
117
|
+
assertRole("ChannelInitGrant.role", o["role"]);
|
|
118
|
+
if (o["token"] === undefined)
|
|
119
|
+
throw new Error("bad ChannelInitGrant.token");
|
|
120
|
+
assertString("ChannelInitGrant.token", o["token"]);
|
|
121
|
+
if (o["e2ee_psk_b64u"] === undefined)
|
|
122
|
+
throw new Error("bad ChannelInitGrant.e2ee_psk_b64u");
|
|
123
|
+
assertString("ChannelInitGrant.e2ee_psk_b64u", o["e2ee_psk_b64u"]);
|
|
124
|
+
if (o["allowed_suites"] === undefined)
|
|
125
|
+
throw new Error("bad ChannelInitGrant.allowed_suites");
|
|
126
|
+
if (!Array.isArray(o["allowed_suites"]))
|
|
127
|
+
throw new Error("bad ChannelInitGrant.allowed_suites");
|
|
128
|
+
for (let i = 0; i < o["allowed_suites"].length; i++) {
|
|
129
|
+
assertSuite("ChannelInitGrant.allowed_suites[]", o["allowed_suites"][i]);
|
|
130
|
+
}
|
|
131
|
+
if (o["default_suite"] === undefined)
|
|
132
|
+
throw new Error("bad ChannelInitGrant.default_suite");
|
|
133
|
+
assertSuite("ChannelInitGrant.default_suite", o["default_suite"]);
|
|
134
|
+
return o;
|
|
135
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** E2EE cipher suite identifier. */
|
|
2
|
+
export declare enum Suite {
|
|
3
|
+
/** P-256 + HKDF-SHA256 + AES-256-GCM. */
|
|
4
|
+
Suite_P256_HKDF_SHA256_AES_256_GCM = 2,
|
|
5
|
+
/** X25519 + HKDF-SHA256 + AES-256-GCM. */
|
|
6
|
+
Suite_X25519_HKDF_SHA256_AES_256_GCM = 1
|
|
7
|
+
}
|
|
8
|
+
/** Connection info for direct (no tunnel) WebSocket + E2EE sessions. */
|
|
9
|
+
export interface DirectConnectInfo {
|
|
10
|
+
/** WebSocket URL of the direct server. */
|
|
11
|
+
ws_url: string;
|
|
12
|
+
/** Channel identifier. */
|
|
13
|
+
channel_id: string;
|
|
14
|
+
/** Base64url-encoded 32-byte PSK. */
|
|
15
|
+
e2ee_psk_b64u: string;
|
|
16
|
+
/** Unix timestamp when the handshake init window expires. */
|
|
17
|
+
channel_init_expire_at_unix_s: number;
|
|
18
|
+
/** Default E2EE suite identifier. */
|
|
19
|
+
default_suite: Suite;
|
|
20
|
+
}
|
|
21
|
+
export declare function assertDirectConnectInfo(v: unknown): DirectConnectInfo;
|