@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.
@@ -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
- ...(args.opts.maxWsQueuedBytes != null && args.opts.maxWsQueuedBytes > 0 ? { maxQueuedBytes: args.opts.maxWsQueuedBytes } : {}),
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: args.opts.clientFeatures ?? 0,
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(async () => {
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
- try {
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) => Promise<YamuxStream>;
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
- // RpcFramingError marks malformed or oversized frames.
5
- export class RpcFramingError extends Error {
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 hdr = await readExactly(4);
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 RpcFramingError("frame too large");
22
- const payload = await readExactly(n);
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";
@@ -1,5 +1,5 @@
1
1
  import { normalizeObserver, nowSeconds } from "../observability/observer.js";
2
- import { readJsonFrame, writeJsonFrame } from "./framing.js";
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, 1 << 20));
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) {
@@ -1,4 +1,3 @@
1
- export * from "./framing.js";
2
1
  export * from "./caller.js";
3
2
  export * from "./client.js";
4
3
  export * from "./server.js";
package/dist/rpc/index.js CHANGED
@@ -1,4 +1,3 @@
1
- export * from "./framing.js";
2
1
  export * from "./caller.js";
3
2
  export * from "./client.js";
4
3
  export * from "./server.js";
@@ -1,4 +1,4 @@
1
- import { readJsonFrame, writeJsonFrame } from "./framing.js";
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, 1 << 20));
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 "../rpc/framing.js";
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 !== undefined ? Math.max(0, opts.keepaliveIntervalMs) : defaultKeepaliveIntervalMs(idleTimeoutSeconds);
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
- const half = Math.floor(idleMs / 2);
124
- return Math.max(500, half);
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
  }
@@ -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 Error("eof");
19
+ throw new StreamEOFError();
19
20
  if (chunk.length === 0)
20
21
  continue;
21
22
  this.chunks.push(chunk);
@@ -0,0 +1,4 @@
1
+ export declare class StreamEOFError extends Error {
2
+ constructor(message?: string);
3
+ }
4
+ export declare function isStreamEOFError(e: unknown): e is StreamEOFError;
@@ -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
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./constants.js";
2
2
  export * from "./byteReader.js";
3
+ export * from "./errors.js";
3
4
  export * from "./header.js";
4
5
  export * from "./session.js";
5
6
  export * from "./stream.js";
@@ -1,5 +1,6 @@
1
1
  export * from "./constants.js";
2
2
  export * from "./byteReader.js";
3
+ export * from "./errors.js";
3
4
  export * from "./header.js";
4
5
  export * from "./session.js";
5
6
  export * from "./stream.js";
@@ -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;
@@ -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 EOF/reset.
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
- throw new Error("eof");
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",
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"
@@ -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>;