@floegence/flowersec-core 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ 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";
@@ -177,14 +177,7 @@ export async function connectCore(args) {
177
177
  secure.close();
178
178
  throw new FlowersecError({ path: args.path, stage: "yamux", code: "open_stream_failed", message: "open rpc stream failed", cause: e });
179
179
  }
180
- const reader = new ByteReader(async () => {
181
- try {
182
- return await rpcStream.read();
183
- }
184
- catch {
185
- return null;
186
- }
187
- });
180
+ const reader = new ByteReader(() => rpcStream.read());
188
181
  const readExactly = (n) => reader.readExactly(n);
189
182
  const write = (b) => rpcStream.write(b);
190
183
  try {
@@ -252,9 +245,28 @@ export async function connectCore(args) {
252
245
  mux,
253
246
  rpc,
254
247
  ping,
255
- openStream: async (kind) => {
248
+ openStream: async (kind, opts = {}) => {
256
249
  if (kind == null || kind === "")
257
250
  throw new FlowersecError({ path: args.path, stage: "validate", code: "missing_stream_kind", message: "missing stream kind" });
251
+ if (opts.signal?.aborted) {
252
+ throw new FlowersecError({
253
+ path: args.path,
254
+ stage: "yamux",
255
+ code: "canceled",
256
+ message: "open stream aborted",
257
+ cause: opts.signal.reason,
258
+ });
259
+ }
260
+ const abortReason = (signal) => {
261
+ const r = signal.reason;
262
+ if (r instanceof Error)
263
+ return r;
264
+ if (typeof r === "string" && r !== "")
265
+ return new AbortError(r);
266
+ return new AbortError("aborted");
267
+ };
268
+ const signal = opts.signal;
269
+ let abortListener;
258
270
  let s;
259
271
  try {
260
272
  s = await mux.openStream();
@@ -262,10 +274,59 @@ export async function connectCore(args) {
262
274
  catch (e) {
263
275
  throw new FlowersecError({ path: args.path, stage: "yamux", code: "open_stream_failed", message: "open stream failed", cause: e });
264
276
  }
277
+ if (signal != null) {
278
+ abortListener = () => {
279
+ try {
280
+ s.reset(abortReason(signal));
281
+ }
282
+ catch {
283
+ // ignore
284
+ }
285
+ };
286
+ signal.addEventListener("abort", abortListener, { once: true });
287
+ if (signal.aborted)
288
+ abortListener();
289
+ }
290
+ if (signal?.aborted) {
291
+ if (abortListener != null)
292
+ signal.removeEventListener("abort", abortListener);
293
+ try {
294
+ await s.close();
295
+ }
296
+ catch {
297
+ // ignore
298
+ }
299
+ throw new FlowersecError({
300
+ path: args.path,
301
+ stage: "yamux",
302
+ code: "canceled",
303
+ message: "open stream aborted",
304
+ cause: signal.reason,
305
+ });
306
+ }
265
307
  try {
266
308
  await writeStreamHello((b) => s.write(b), kind);
267
309
  }
268
310
  catch (err) {
311
+ if (signal?.aborted) {
312
+ if (abortListener != null)
313
+ signal.removeEventListener("abort", abortListener);
314
+ try {
315
+ await s.close();
316
+ }
317
+ catch {
318
+ // ignore
319
+ }
320
+ throw new FlowersecError({
321
+ path: args.path,
322
+ stage: "yamux",
323
+ code: "canceled",
324
+ message: "open stream aborted",
325
+ cause: signal.reason,
326
+ });
327
+ }
328
+ if (signal != null && abortListener != null)
329
+ signal.removeEventListener("abort", abortListener);
269
330
  try {
270
331
  await s.close();
271
332
  }
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
+ }
@@ -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.2",
3
+ "version": "0.2.0",
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>;