@floegence/flowersec-core 0.1.3 → 0.2.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/dist/client-connect/connectCore.js +131 -30
- package/dist/client.d.ts +3 -1
- package/dist/framing/index.d.ts +1 -0
- package/dist/framing/index.js +1 -0
- package/dist/framing/jsonframe.d.ts +14 -0
- package/dist/{rpc/framing.js → framing/jsonframe.js} +14 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/rpc/client.js +2 -2
- package/dist/rpc/index.d.ts +0 -1
- package/dist/rpc/index.js +0 -1
- package/dist/rpc/server.js +2 -2
- package/dist/streamhello/streamHello.js +1 -1
- package/dist/streamio/index.d.ts +15 -0
- package/dist/streamio/index.js +58 -0
- package/dist/tunnel-client/connect.js +10 -4
- package/dist/yamux/byteReader.js +2 -1
- package/dist/yamux/errors.d.ts +4 -0
- package/dist/yamux/errors.js +10 -0
- package/dist/yamux/index.d.ts +1 -0
- package/dist/yamux/index.js +1 -0
- package/dist/yamux/stream.d.ts +1 -1
- package/dist/yamux/stream.js +2 -2
- package/package.json +9 -1
- package/dist/rpc/framing.d.ts +0 -4
|
@@ -5,15 +5,13 @@ import { RpcClient } from "../rpc/client.js";
|
|
|
5
5
|
import { writeStreamHello } from "../streamhello/streamHello.js";
|
|
6
6
|
import { normalizeObserver, nowSeconds } from "../observability/observer.js";
|
|
7
7
|
import { base64urlDecode } from "../utils/base64url.js";
|
|
8
|
-
import { FlowersecError, throwIfAborted } from "../utils/errors.js";
|
|
8
|
+
import { AbortError, FlowersecError, throwIfAborted } from "../utils/errors.js";
|
|
9
9
|
import { WebSocketBinaryTransport, WsCloseError } from "../ws-client/binaryTransport.js";
|
|
10
10
|
import { OriginMismatchError, WsFactoryRequiredError, classifyConnectError, classifyHandshakeError, createWebSocket, waitOpen, withAbortAndTimeout, } from "./common.js";
|
|
11
11
|
import { isTunnelAttachCloseReason } from "./tunnelAttachCloseReason.js";
|
|
12
12
|
export async function connectCore(args) {
|
|
13
13
|
const observer = normalizeObserver(args.opts.observer);
|
|
14
14
|
const signal = args.opts.signal;
|
|
15
|
-
const connectTimeoutMs = args.opts.connectTimeoutMs ?? 10_000;
|
|
16
|
-
const handshakeTimeoutMs = args.opts.handshakeTimeoutMs ?? 10_000;
|
|
17
15
|
const connectStart = nowSeconds();
|
|
18
16
|
const origin = args.opts.origin;
|
|
19
17
|
if (origin == null || origin === "") {
|
|
@@ -23,6 +21,41 @@ export async function connectCore(args) {
|
|
|
23
21
|
const code = args.path === "tunnel" ? "missing_tunnel_url" : "missing_ws_url";
|
|
24
22
|
throw new FlowersecError({ path: args.path, stage: "validate", code, message: "missing websocket url" });
|
|
25
23
|
}
|
|
24
|
+
const invalidOption = (message) => {
|
|
25
|
+
throw new FlowersecError({ path: args.path, stage: "validate", code: "invalid_option", message });
|
|
26
|
+
};
|
|
27
|
+
const connectTimeoutMs = args.opts.connectTimeoutMs ?? 10_000;
|
|
28
|
+
if (!Number.isFinite(connectTimeoutMs) || connectTimeoutMs < 0) {
|
|
29
|
+
invalidOption("connectTimeoutMs must be a non-negative number");
|
|
30
|
+
}
|
|
31
|
+
const handshakeTimeoutMs = args.opts.handshakeTimeoutMs ?? 10_000;
|
|
32
|
+
if (!Number.isFinite(handshakeTimeoutMs) || handshakeTimeoutMs < 0) {
|
|
33
|
+
invalidOption("handshakeTimeoutMs must be a non-negative number");
|
|
34
|
+
}
|
|
35
|
+
const keepaliveIntervalMs = args.opts.keepaliveIntervalMs ?? 0;
|
|
36
|
+
if (!Number.isFinite(keepaliveIntervalMs) || keepaliveIntervalMs < 0) {
|
|
37
|
+
invalidOption("keepaliveIntervalMs must be a non-negative number");
|
|
38
|
+
}
|
|
39
|
+
const clientFeatures = args.opts.clientFeatures ?? 0;
|
|
40
|
+
if (!Number.isSafeInteger(clientFeatures) || clientFeatures < 0 || clientFeatures > 0xffffffff) {
|
|
41
|
+
invalidOption("clientFeatures must be a uint32");
|
|
42
|
+
}
|
|
43
|
+
const maxHandshakePayload = args.opts.maxHandshakePayload ?? 0;
|
|
44
|
+
if (!Number.isSafeInteger(maxHandshakePayload) || maxHandshakePayload < 0) {
|
|
45
|
+
invalidOption("maxHandshakePayload must be a non-negative integer");
|
|
46
|
+
}
|
|
47
|
+
const maxRecordBytes = args.opts.maxRecordBytes ?? 0;
|
|
48
|
+
if (!Number.isSafeInteger(maxRecordBytes) || maxRecordBytes < 0) {
|
|
49
|
+
invalidOption("maxRecordBytes must be a non-negative integer");
|
|
50
|
+
}
|
|
51
|
+
const maxBufferedBytes = args.opts.maxBufferedBytes ?? 0;
|
|
52
|
+
if (!Number.isSafeInteger(maxBufferedBytes) || maxBufferedBytes < 0) {
|
|
53
|
+
invalidOption("maxBufferedBytes must be a non-negative integer");
|
|
54
|
+
}
|
|
55
|
+
const maxWsQueuedBytes = args.opts.maxWsQueuedBytes ?? 0;
|
|
56
|
+
if (!Number.isSafeInteger(maxWsQueuedBytes) || maxWsQueuedBytes < 0) {
|
|
57
|
+
invalidOption("maxWsQueuedBytes must be a non-negative integer");
|
|
58
|
+
}
|
|
26
59
|
let ws;
|
|
27
60
|
try {
|
|
28
61
|
ws = createWebSocket(args.wsUrl, origin, args.opts.wsFactory);
|
|
@@ -39,7 +72,7 @@ export async function connectCore(args) {
|
|
|
39
72
|
// Install close/error/message listeners before waiting for "open" to avoid a gap where a peer close
|
|
40
73
|
// (for example a tunnel attach rejection with a reason token) can be missed and misclassified as a handshake timeout.
|
|
41
74
|
const transport = new WebSocketBinaryTransport(ws, {
|
|
42
|
-
...(
|
|
75
|
+
...(maxWsQueuedBytes > 0 ? { maxQueuedBytes: maxWsQueuedBytes } : {}),
|
|
43
76
|
observer,
|
|
44
77
|
});
|
|
45
78
|
try {
|
|
@@ -116,14 +149,11 @@ export async function connectCore(args) {
|
|
|
116
149
|
const handshakeStart = nowSeconds();
|
|
117
150
|
let secure;
|
|
118
151
|
try {
|
|
119
|
-
const maxHandshakePayload = args.opts.maxHandshakePayload ?? 0;
|
|
120
|
-
const maxRecordBytes = args.opts.maxRecordBytes ?? 0;
|
|
121
|
-
const maxBufferedBytes = args.opts.maxBufferedBytes ?? 0;
|
|
122
152
|
secure = await withAbortAndTimeout(clientHandshake(transport, {
|
|
123
153
|
channelId: args.channelId,
|
|
124
154
|
suite,
|
|
125
155
|
psk,
|
|
126
|
-
clientFeatures
|
|
156
|
+
clientFeatures,
|
|
127
157
|
maxHandshakePayload: maxHandshakePayload > 0 ? maxHandshakePayload : 8 * 1024,
|
|
128
158
|
maxRecordBytes: maxRecordBytes > 0 ? maxRecordBytes : (1 << 20),
|
|
129
159
|
...(maxBufferedBytes > 0 ? { maxBufferedBytes } : {}),
|
|
@@ -177,14 +207,7 @@ export async function connectCore(args) {
|
|
|
177
207
|
secure.close();
|
|
178
208
|
throw new FlowersecError({ path: args.path, stage: "yamux", code: "open_stream_failed", message: "open rpc stream failed", cause: e });
|
|
179
209
|
}
|
|
180
|
-
const reader = new ByteReader(
|
|
181
|
-
try {
|
|
182
|
-
return await rpcStream.read();
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
});
|
|
210
|
+
const reader = new ByteReader(() => rpcStream.read());
|
|
188
211
|
const readExactly = (n) => reader.readExactly(n);
|
|
189
212
|
const write = (b) => rpcStream.write(b);
|
|
190
213
|
try {
|
|
@@ -216,7 +239,6 @@ export async function connectCore(args) {
|
|
|
216
239
|
throw new FlowersecError({ path: args.path, stage: "secure", code: "ping_failed", message: "ping failed", cause: e });
|
|
217
240
|
}
|
|
218
241
|
};
|
|
219
|
-
const keepaliveIntervalMs = Math.max(0, args.opts.keepaliveIntervalMs ?? 0);
|
|
220
242
|
let keepaliveTimer;
|
|
221
243
|
let keepaliveInFlight = false;
|
|
222
244
|
const stopKeepalive = () => {
|
|
@@ -225,6 +247,27 @@ export async function connectCore(args) {
|
|
|
225
247
|
clearInterval(keepaliveTimer);
|
|
226
248
|
keepaliveTimer = undefined;
|
|
227
249
|
};
|
|
250
|
+
const closeAll = () => {
|
|
251
|
+
stopKeepalive();
|
|
252
|
+
try {
|
|
253
|
+
rpc.close();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// ignore
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
mux.close();
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// ignore
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
secure.close();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// ignore
|
|
269
|
+
}
|
|
270
|
+
};
|
|
228
271
|
if (keepaliveIntervalMs > 0) {
|
|
229
272
|
keepaliveTimer = setInterval(() => {
|
|
230
273
|
if (keepaliveInFlight)
|
|
@@ -232,12 +275,7 @@ export async function connectCore(args) {
|
|
|
232
275
|
keepaliveInFlight = true;
|
|
233
276
|
ping()
|
|
234
277
|
.catch(() => {
|
|
235
|
-
|
|
236
|
-
ws.close();
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
// ignore
|
|
240
|
-
}
|
|
278
|
+
closeAll();
|
|
241
279
|
})
|
|
242
280
|
.finally(() => {
|
|
243
281
|
keepaliveInFlight = false;
|
|
@@ -252,9 +290,28 @@ export async function connectCore(args) {
|
|
|
252
290
|
mux,
|
|
253
291
|
rpc,
|
|
254
292
|
ping,
|
|
255
|
-
openStream: async (kind) => {
|
|
293
|
+
openStream: async (kind, opts = {}) => {
|
|
256
294
|
if (kind == null || kind === "")
|
|
257
295
|
throw new FlowersecError({ path: args.path, stage: "validate", code: "missing_stream_kind", message: "missing stream kind" });
|
|
296
|
+
if (opts.signal?.aborted) {
|
|
297
|
+
throw new FlowersecError({
|
|
298
|
+
path: args.path,
|
|
299
|
+
stage: "yamux",
|
|
300
|
+
code: "canceled",
|
|
301
|
+
message: "open stream aborted",
|
|
302
|
+
cause: opts.signal.reason,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const abortReason = (signal) => {
|
|
306
|
+
const r = signal.reason;
|
|
307
|
+
if (r instanceof Error)
|
|
308
|
+
return r;
|
|
309
|
+
if (typeof r === "string" && r !== "")
|
|
310
|
+
return new AbortError(r);
|
|
311
|
+
return new AbortError("aborted");
|
|
312
|
+
};
|
|
313
|
+
const signal = opts.signal;
|
|
314
|
+
let abortListener;
|
|
258
315
|
let s;
|
|
259
316
|
try {
|
|
260
317
|
s = await mux.openStream();
|
|
@@ -262,10 +319,59 @@ export async function connectCore(args) {
|
|
|
262
319
|
catch (e) {
|
|
263
320
|
throw new FlowersecError({ path: args.path, stage: "yamux", code: "open_stream_failed", message: "open stream failed", cause: e });
|
|
264
321
|
}
|
|
322
|
+
if (signal != null) {
|
|
323
|
+
abortListener = () => {
|
|
324
|
+
try {
|
|
325
|
+
s.reset(abortReason(signal));
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// ignore
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
332
|
+
if (signal.aborted)
|
|
333
|
+
abortListener();
|
|
334
|
+
}
|
|
335
|
+
if (signal?.aborted) {
|
|
336
|
+
if (abortListener != null)
|
|
337
|
+
signal.removeEventListener("abort", abortListener);
|
|
338
|
+
try {
|
|
339
|
+
await s.close();
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// ignore
|
|
343
|
+
}
|
|
344
|
+
throw new FlowersecError({
|
|
345
|
+
path: args.path,
|
|
346
|
+
stage: "yamux",
|
|
347
|
+
code: "canceled",
|
|
348
|
+
message: "open stream aborted",
|
|
349
|
+
cause: signal.reason,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
265
352
|
try {
|
|
266
353
|
await writeStreamHello((b) => s.write(b), kind);
|
|
267
354
|
}
|
|
268
355
|
catch (err) {
|
|
356
|
+
if (signal?.aborted) {
|
|
357
|
+
if (abortListener != null)
|
|
358
|
+
signal.removeEventListener("abort", abortListener);
|
|
359
|
+
try {
|
|
360
|
+
await s.close();
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
// ignore
|
|
364
|
+
}
|
|
365
|
+
throw new FlowersecError({
|
|
366
|
+
path: args.path,
|
|
367
|
+
stage: "yamux",
|
|
368
|
+
code: "canceled",
|
|
369
|
+
message: "open stream aborted",
|
|
370
|
+
cause: signal.reason,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (signal != null && abortListener != null)
|
|
374
|
+
signal.removeEventListener("abort", abortListener);
|
|
269
375
|
try {
|
|
270
376
|
await s.close();
|
|
271
377
|
}
|
|
@@ -282,12 +388,7 @@ export async function connectCore(args) {
|
|
|
282
388
|
}
|
|
283
389
|
return s;
|
|
284
390
|
},
|
|
285
|
-
close:
|
|
286
|
-
stopKeepalive();
|
|
287
|
-
rpc.close();
|
|
288
|
-
mux.close();
|
|
289
|
-
secure.close();
|
|
290
|
-
},
|
|
391
|
+
close: closeAll,
|
|
291
392
|
};
|
|
292
393
|
}
|
|
293
394
|
catch (e) {
|
package/dist/client.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export type Client = Readonly<{
|
|
|
7
7
|
path: ClientPath;
|
|
8
8
|
endpointInstanceId?: string;
|
|
9
9
|
rpc: RpcClient;
|
|
10
|
-
openStream: (kind: string
|
|
10
|
+
openStream: (kind: string, opts?: Readonly<{
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}>) => Promise<YamuxStream>;
|
|
11
13
|
ping: () => Promise<void>;
|
|
12
14
|
close: () => void;
|
|
13
15
|
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./jsonframe.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./jsonframe.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const DEFAULT_MAX_JSON_FRAME_BYTES: number;
|
|
2
|
+
export declare class JsonFramingError extends Error {
|
|
3
|
+
}
|
|
4
|
+
type WriteFn = (b: Uint8Array) => Promise<void>;
|
|
5
|
+
type WriteLike = Readonly<{
|
|
6
|
+
write: (b: Uint8Array) => Promise<void>;
|
|
7
|
+
}>;
|
|
8
|
+
type ReadExactlyFn = (n: number) => Promise<Uint8Array>;
|
|
9
|
+
type ReadExactlyLike = Readonly<{
|
|
10
|
+
readExactly: (n: number) => Promise<Uint8Array>;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function writeJsonFrame(write: WriteFn | WriteLike, v: unknown): Promise<void>;
|
|
13
|
+
export declare function readJsonFrame(readExactly: ReadExactlyFn | ReadExactlyLike, maxBytes: number): Promise<unknown>;
|
|
14
|
+
export {};
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { readU32be, u32be } from "../utils/bin.js";
|
|
2
|
+
export const DEFAULT_MAX_JSON_FRAME_BYTES = 1 << 20;
|
|
2
3
|
const te = new TextEncoder();
|
|
3
4
|
const td = new TextDecoder();
|
|
4
|
-
//
|
|
5
|
-
export class
|
|
5
|
+
// JsonFramingError marks malformed or oversized frames.
|
|
6
|
+
export class JsonFramingError extends Error {
|
|
7
|
+
}
|
|
8
|
+
function normalizeWrite(write) {
|
|
9
|
+
return typeof write === "function" ? write : (b) => write.write(b);
|
|
10
|
+
}
|
|
11
|
+
function normalizeReadExactly(readExactly) {
|
|
12
|
+
return typeof readExactly === "function" ? readExactly : (n) => readExactly.readExactly(n);
|
|
6
13
|
}
|
|
7
14
|
// writeJsonFrame encodes a JSON payload with a 4-byte length prefix.
|
|
8
15
|
export async function writeJsonFrame(write, v) {
|
|
@@ -11,14 +18,15 @@ export async function writeJsonFrame(write, v) {
|
|
|
11
18
|
const out = new Uint8Array(4 + json.length);
|
|
12
19
|
out.set(hdr, 0);
|
|
13
20
|
out.set(json, 4);
|
|
14
|
-
await write(out);
|
|
21
|
+
await normalizeWrite(write)(out);
|
|
15
22
|
}
|
|
16
23
|
// readJsonFrame reads and parses a length-prefixed JSON payload.
|
|
17
24
|
export async function readJsonFrame(readExactly, maxBytes) {
|
|
18
|
-
const
|
|
25
|
+
const read = normalizeReadExactly(readExactly);
|
|
26
|
+
const hdr = await read(4);
|
|
19
27
|
const n = readU32be(hdr, 0);
|
|
20
28
|
if (maxBytes > 0 && n > maxBytes)
|
|
21
|
-
throw new
|
|
22
|
-
const payload = await
|
|
29
|
+
throw new JsonFramingError("frame too large");
|
|
30
|
+
const payload = await read(n);
|
|
23
31
|
return JSON.parse(td.decode(payload));
|
|
24
32
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "./utils/base64url.js";
|
|
2
2
|
export * from "./utils/bin.js";
|
|
3
3
|
export * from "./utils/errors.js";
|
|
4
|
+
export * from "./framing/index.js";
|
|
5
|
+
export * from "./streamio/index.js";
|
|
4
6
|
export * from "./e2ee/constants.js";
|
|
5
7
|
export * from "./e2ee/framing.js";
|
|
6
8
|
export * from "./e2ee/transcript.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "./utils/base64url.js";
|
|
2
2
|
export * from "./utils/bin.js";
|
|
3
3
|
export * from "./utils/errors.js";
|
|
4
|
+
export * from "./framing/index.js";
|
|
5
|
+
export * from "./streamio/index.js";
|
|
4
6
|
export * from "./e2ee/constants.js";
|
|
5
7
|
export * from "./e2ee/framing.js";
|
|
6
8
|
export * from "./e2ee/transcript.js";
|
package/dist/rpc/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizeObserver, nowSeconds } from "../observability/observer.js";
|
|
2
|
-
import { readJsonFrame, writeJsonFrame } from "
|
|
2
|
+
import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
3
3
|
import { assertRpcEnvelope } from "./validate.js";
|
|
4
4
|
// Guard against precision loss when encoding request IDs as numbers.
|
|
5
5
|
const MAX_SAFE_REQUEST_ID = BigInt(Number.MAX_SAFE_INTEGER);
|
|
@@ -108,7 +108,7 @@ export class RpcClient {
|
|
|
108
108
|
async readLoop() {
|
|
109
109
|
try {
|
|
110
110
|
while (!this.closed) {
|
|
111
|
-
const v = assertRpcEnvelope(await readJsonFrame(this.readExactly,
|
|
111
|
+
const v = assertRpcEnvelope(await readJsonFrame(this.readExactly, DEFAULT_MAX_JSON_FRAME_BYTES));
|
|
112
112
|
if (v.response_to === 0) {
|
|
113
113
|
// Notification: response_to=0 and request_id=0.
|
|
114
114
|
if (v.request_id === 0) {
|
package/dist/rpc/index.d.ts
CHANGED
package/dist/rpc/index.js
CHANGED
package/dist/rpc/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readJsonFrame, writeJsonFrame } from "
|
|
1
|
+
import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
2
2
|
import { assertRpcEnvelope } from "./validate.js";
|
|
3
3
|
// RpcServer dispatches request envelopes to registered handlers.
|
|
4
4
|
export class RpcServer {
|
|
@@ -21,7 +21,7 @@ export class RpcServer {
|
|
|
21
21
|
while (!this.closed) {
|
|
22
22
|
if (signal?.aborted)
|
|
23
23
|
throw signal.reason ?? new Error("aborted");
|
|
24
|
-
const v = assertRpcEnvelope(await readJsonFrame(this.readExactly,
|
|
24
|
+
const v = assertRpcEnvelope(await readJsonFrame(this.readExactly, DEFAULT_MAX_JSON_FRAME_BYTES));
|
|
25
25
|
if (v.response_to !== 0)
|
|
26
26
|
continue;
|
|
27
27
|
if (v.request_id === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readJsonFrame, writeJsonFrame } from "../
|
|
1
|
+
import { readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
2
2
|
// writeStreamHello sends the initial stream greeting.
|
|
3
3
|
export async function writeStreamHello(write, kind) {
|
|
4
4
|
const h = { kind, v: 1 };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ByteReader } from "../yamux/byteReader.js";
|
|
2
|
+
import type { YamuxStream } from "../yamux/stream.js";
|
|
3
|
+
export declare function readMaybe(stream: YamuxStream): Promise<Uint8Array | null>;
|
|
4
|
+
export declare function createByteReader(stream: YamuxStream, opts?: Readonly<{
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}>): ByteReader;
|
|
7
|
+
export declare function readExactly(reader: ByteReader, n: number, opts?: Readonly<{
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
}>): Promise<Uint8Array>;
|
|
10
|
+
export type ReadNBytesOptions = Readonly<{
|
|
11
|
+
chunkSize?: number;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
onProgress?: (read: number) => void;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function readNBytes(reader: ByteReader, n: number, opts?: ReadNBytesOptions): Promise<Uint8Array>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AbortError, throwIfAborted } from "../utils/errors.js";
|
|
2
|
+
import { ByteReader } from "../yamux/byteReader.js";
|
|
3
|
+
function abortReasonToError(signal) {
|
|
4
|
+
const r = signal.reason;
|
|
5
|
+
if (r instanceof Error)
|
|
6
|
+
return r;
|
|
7
|
+
if (typeof r === "string" && r !== "")
|
|
8
|
+
return new AbortError(r);
|
|
9
|
+
return new AbortError("aborted");
|
|
10
|
+
}
|
|
11
|
+
function bindAbortToStream(stream, signal) {
|
|
12
|
+
const onAbort = () => {
|
|
13
|
+
try {
|
|
14
|
+
stream.reset(abortReasonToError(signal));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Best-effort cancel.
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
if (signal.aborted) {
|
|
21
|
+
onAbort();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
25
|
+
}
|
|
26
|
+
// readMaybe reads the next chunk or null on EOF.
|
|
27
|
+
export async function readMaybe(stream) {
|
|
28
|
+
return await stream.read();
|
|
29
|
+
}
|
|
30
|
+
// createByteReader adapts a YamuxStream to a ByteReader (EOF is handled by YamuxStream.read()).
|
|
31
|
+
export function createByteReader(stream, opts = {}) {
|
|
32
|
+
if (opts.signal != null)
|
|
33
|
+
bindAbortToStream(stream, opts.signal);
|
|
34
|
+
return new ByteReader(() => stream.read());
|
|
35
|
+
}
|
|
36
|
+
// readExactly reads n bytes (or throws on EOF/error). If signal aborts, the caller is expected to reset/close the stream.
|
|
37
|
+
export async function readExactly(reader, n, opts = {}) {
|
|
38
|
+
throwIfAborted(opts.signal, "read aborted");
|
|
39
|
+
return await reader.readExactly(n);
|
|
40
|
+
}
|
|
41
|
+
// readNBytes reads exactly n bytes and returns them as a single contiguous buffer.
|
|
42
|
+
export async function readNBytes(reader, n, opts = {}) {
|
|
43
|
+
const total = Math.max(0, Math.floor(n));
|
|
44
|
+
const out = new Uint8Array(total);
|
|
45
|
+
if (total === 0)
|
|
46
|
+
return out;
|
|
47
|
+
const chunkSize = Math.max(1, Math.floor(opts.chunkSize ?? 64 * 1024));
|
|
48
|
+
let off = 0;
|
|
49
|
+
while (off < total) {
|
|
50
|
+
throwIfAborted(opts.signal, "read aborted");
|
|
51
|
+
const take = Math.min(chunkSize, total - off);
|
|
52
|
+
const chunk = await reader.readExactly(take);
|
|
53
|
+
out.set(chunk, off);
|
|
54
|
+
off += chunk.length;
|
|
55
|
+
opts.onProgress?.(off);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
@@ -105,7 +105,7 @@ export async function connectTunnel(grant, opts) {
|
|
|
105
105
|
endpoint_instance_id: endpointInstanceId
|
|
106
106
|
};
|
|
107
107
|
const attachJson = JSON.stringify(attach);
|
|
108
|
-
const keepaliveIntervalMs = opts.keepaliveIntervalMs
|
|
108
|
+
const keepaliveIntervalMs = opts.keepaliveIntervalMs ?? defaultKeepaliveIntervalMs(idleTimeoutSeconds);
|
|
109
109
|
return await connectCore({
|
|
110
110
|
path: "tunnel",
|
|
111
111
|
wsUrl: checkedGrant.tunnel_url,
|
|
@@ -119,7 +119,13 @@ export async function connectTunnel(grant, opts) {
|
|
|
119
119
|
function defaultKeepaliveIntervalMs(idleTimeoutSeconds) {
|
|
120
120
|
if (!Number.isFinite(idleTimeoutSeconds) || idleTimeoutSeconds <= 0)
|
|
121
121
|
return 0;
|
|
122
|
-
const idleMs = idleTimeoutSeconds * 1000;
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
const idleMs = Math.floor(idleTimeoutSeconds * 1000);
|
|
123
|
+
if (idleMs <= 0)
|
|
124
|
+
return 0;
|
|
125
|
+
let interval = Math.floor(idleMs / 2);
|
|
126
|
+
if (interval < 500)
|
|
127
|
+
interval = 500;
|
|
128
|
+
if (interval >= idleMs)
|
|
129
|
+
interval = Math.floor(idleMs / 2);
|
|
130
|
+
return interval;
|
|
125
131
|
}
|
package/dist/yamux/byteReader.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StreamEOFError } from "./errors.js";
|
|
1
2
|
// ByteReader buffers incoming chunks and supports exact reads.
|
|
2
3
|
export class ByteReader {
|
|
3
4
|
readChunk;
|
|
@@ -15,7 +16,7 @@ export class ByteReader {
|
|
|
15
16
|
while (this.buffered < n) {
|
|
16
17
|
const chunk = await this.readChunk();
|
|
17
18
|
if (chunk == null)
|
|
18
|
-
throw new
|
|
19
|
+
throw new StreamEOFError();
|
|
19
20
|
if (chunk.length === 0)
|
|
20
21
|
continue;
|
|
21
22
|
this.chunks.push(chunk);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// StreamEOFError marks end-of-stream for yamux reads.
|
|
2
|
+
export class StreamEOFError extends Error {
|
|
3
|
+
constructor(message = "eof") {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "StreamEOFError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function isStreamEOFError(e) {
|
|
9
|
+
return e instanceof StreamEOFError;
|
|
10
|
+
}
|
package/dist/yamux/index.d.ts
CHANGED
package/dist/yamux/index.js
CHANGED
package/dist/yamux/stream.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare class YamuxStream {
|
|
|
15
15
|
open(): Promise<void>;
|
|
16
16
|
onData(data: Uint8Array, flags: number): void;
|
|
17
17
|
onWindowUpdate(delta: number, flags: number): void;
|
|
18
|
-
read(): Promise<Uint8Array>;
|
|
18
|
+
read(): Promise<Uint8Array | null>;
|
|
19
19
|
write(data: Uint8Array): Promise<void>;
|
|
20
20
|
close(): Promise<void>;
|
|
21
21
|
reset(err: Error): void;
|
package/dist/yamux/stream.js
CHANGED
|
@@ -54,7 +54,7 @@ export class YamuxStream {
|
|
|
54
54
|
this.sendWindow += delta >>> 0;
|
|
55
55
|
this.session.notifySendWindow(this.id);
|
|
56
56
|
}
|
|
57
|
-
// read resolves with the next data chunk or throws on
|
|
57
|
+
// read resolves with the next data chunk, null on EOF, or throws on reset/errors.
|
|
58
58
|
async read() {
|
|
59
59
|
while (true) {
|
|
60
60
|
if (this.error != null)
|
|
@@ -66,7 +66,7 @@ export class YamuxStream {
|
|
|
66
66
|
return b;
|
|
67
67
|
}
|
|
68
68
|
if (this.state === "closed" || this.state === "remoteClose")
|
|
69
|
-
|
|
69
|
+
return null;
|
|
70
70
|
await new Promise((resolve) => this.readWaiters.push(resolve));
|
|
71
71
|
}
|
|
72
72
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/flowersec-core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,6 +36,14 @@
|
|
|
36
36
|
"types": "./dist/browser/index.d.ts",
|
|
37
37
|
"default": "./dist/browser/index.js"
|
|
38
38
|
},
|
|
39
|
+
"./framing": {
|
|
40
|
+
"types": "./dist/framing/index.d.ts",
|
|
41
|
+
"default": "./dist/framing/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./streamio": {
|
|
44
|
+
"types": "./dist/streamio/index.d.ts",
|
|
45
|
+
"default": "./dist/streamio/index.js"
|
|
46
|
+
},
|
|
39
47
|
"./rpc": {
|
|
40
48
|
"types": "./dist/rpc/index.d.ts",
|
|
41
49
|
"default": "./dist/rpc/index.js"
|
package/dist/rpc/framing.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export declare class RpcFramingError extends Error {
|
|
2
|
-
}
|
|
3
|
-
export declare function writeJsonFrame(write: (b: Uint8Array) => Promise<void>, v: unknown): Promise<void>;
|
|
4
|
-
export declare function readJsonFrame(readExactly: (n: number) => Promise<Uint8Array>, maxBytes: number): Promise<unknown>;
|