@floegence/flowersec-core 0.21.0 → 0.22.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.
Files changed (40) hide show
  1. package/README.md +9 -1
  2. package/dist/client-connect/connectCore.d.ts +1 -1
  3. package/dist/client-connect/connectCore.js +5 -4
  4. package/dist/controlplane/channelInit.d.ts +27 -0
  5. package/dist/controlplane/channelInit.js +116 -0
  6. package/dist/controlplane/http.d.ts +16 -0
  7. package/dist/controlplane/http.js +59 -0
  8. package/dist/controlplane/index.d.ts +4 -0
  9. package/dist/controlplane/index.js +4 -0
  10. package/dist/controlplane/issuer.d.ts +15 -0
  11. package/dist/controlplane/issuer.js +53 -0
  12. package/dist/controlplane/request.js +2 -1
  13. package/dist/controlplane/token.d.ts +33 -0
  14. package/dist/controlplane/token.js +119 -0
  15. package/dist/defaults.d.ts +46 -0
  16. package/dist/defaults.js +46 -0
  17. package/dist/endpoint/index.d.ts +83 -0
  18. package/dist/endpoint/index.js +384 -0
  19. package/dist/endpoint/node.d.ts +9 -0
  20. package/dist/endpoint/node.js +51 -0
  21. package/dist/framing/jsonframe.js +2 -1
  22. package/dist/node/index.d.ts +3 -0
  23. package/dist/node/index.js +3 -0
  24. package/dist/proxy/constants.js +4 -3
  25. package/dist/proxy/controllerWindow.js +23 -3
  26. package/dist/proxy/headerPolicy.js +1 -0
  27. package/dist/proxy/runtime.d.ts +1 -0
  28. package/dist/proxy/runtime.js +38 -1
  29. package/dist/proxy/server.d.ts +27 -0
  30. package/dist/proxy/server.js +480 -0
  31. package/dist/proxy/serviceWorker.js +23 -5
  32. package/dist/proxy/windowBridgeProtocol.d.ts +1 -0
  33. package/dist/reconnect/index.js +33 -22
  34. package/dist/rpc/server.d.ts +9 -2
  35. package/dist/rpc/server.js +33 -10
  36. package/dist/yamux/session.d.ts +6 -1
  37. package/dist/yamux/session.js +12 -6
  38. package/dist/yamux/stream.d.ts +1 -0
  39. package/dist/yamux/stream.js +19 -4
  40. package/package.json +5 -1
@@ -1,24 +1,25 @@
1
1
  import { getClientTermination } from "../client-connect/termination.js";
2
2
  import { emitObserverDiagnostic, withObserverContext } from "../observability/observer.js";
3
+ import { SDK_DEFAULTS } from "../defaults.js";
3
4
  export { createArtifactResolver, createControlplaneArtifactSource } from "./artifactControlplane.js";
4
5
  function normalizeAutoReconnect(cfg) {
5
6
  if (!cfg?.enabled) {
6
7
  return {
7
8
  enabled: false,
8
9
  maxAttempts: 1,
9
- initialDelayMs: 500,
10
- maxDelayMs: 10_000,
11
- factor: 1.8,
12
- jitterRatio: 0.2,
10
+ initialDelayMs: SDK_DEFAULTS.reconnect.initialDelayMs,
11
+ maxDelayMs: SDK_DEFAULTS.reconnect.maxDelayMs,
12
+ factor: SDK_DEFAULTS.reconnect.factor,
13
+ jitterRatio: SDK_DEFAULTS.reconnect.jitterRatio,
13
14
  };
14
15
  }
15
16
  return {
16
17
  enabled: true,
17
- maxAttempts: Math.max(1, cfg.maxAttempts ?? 5),
18
- initialDelayMs: Math.max(0, cfg.initialDelayMs ?? 500),
19
- maxDelayMs: Math.max(0, cfg.maxDelayMs ?? 10_000),
20
- factor: Math.max(1, cfg.factor ?? 1.8),
21
- jitterRatio: Math.max(0, cfg.jitterRatio ?? 0.2),
18
+ maxAttempts: Math.max(1, cfg.maxAttempts ?? SDK_DEFAULTS.reconnect.maxAttempts),
19
+ initialDelayMs: Math.max(0, cfg.initialDelayMs ?? SDK_DEFAULTS.reconnect.initialDelayMs),
20
+ maxDelayMs: Math.max(0, cfg.maxDelayMs ?? SDK_DEFAULTS.reconnect.maxDelayMs),
21
+ factor: Math.max(1, cfg.factor ?? SDK_DEFAULTS.reconnect.factor),
22
+ jitterRatio: Math.max(0, cfg.jitterRatio ?? SDK_DEFAULTS.reconnect.jitterRatio),
22
23
  };
23
24
  }
24
25
  function backoffDelayMs(attemptIndex, cfg) {
@@ -138,8 +139,9 @@ export function createReconnectManager() {
138
139
  }
139
140
  token += 1;
140
141
  const nextToken = token;
142
+ const reconnectPromise = startConnectLoop(nextToken, cfg);
141
143
  setState({ status: "connecting", error, client: null });
142
- void connectWithRetry(nextToken, cfg).catch(() => {
144
+ void reconnectPromise.catch(() => {
143
145
  // connectWithRetry updates state; keep errors observable via state().
144
146
  });
145
147
  };
@@ -262,6 +264,22 @@ export function createReconnectManager() {
262
264
  }
263
265
  }
264
266
  };
267
+ const startConnectLoop = (t, cfg) => {
268
+ let resolveLoop;
269
+ let rejectLoop;
270
+ const loop = new Promise((resolve, reject) => {
271
+ resolveLoop = resolve;
272
+ rejectLoop = reject;
273
+ });
274
+ let promise;
275
+ promise = loop.finally(() => {
276
+ if (activeConnectPromise === promise)
277
+ activeConnectPromise = null;
278
+ });
279
+ activeConnectPromise = promise;
280
+ void connectWithRetry(t, cfg).then(resolveLoop, rejectLoop);
281
+ return promise;
282
+ };
265
283
  const connect = async (cfg) => {
266
284
  cancelRetrySleep();
267
285
  abortActiveAttempt();
@@ -277,25 +295,18 @@ export function createReconnectManager() {
277
295
  // ignore
278
296
  }
279
297
  }
298
+ const connectPromise = startConnectLoop(t, cfg);
280
299
  setState({ status: "connecting", error: null, client: null });
281
- const p = connectWithRetry(t, cfg);
282
- activeConnectPromise = p;
283
- try {
284
- await p;
285
- }
286
- finally {
287
- if (activeConnectPromise === p)
288
- activeConnectPromise = null;
289
- }
300
+ await connectPromise;
290
301
  };
291
302
  const connectIfNeeded = async (cfg) => {
292
303
  if (isSameConfig(active, cfg)) {
293
- if (s.status === "connected" && s.client)
294
- return;
295
- if (s.status === "connecting" && activeConnectPromise) {
304
+ if (activeConnectPromise) {
296
305
  await activeConnectPromise;
297
306
  return;
298
307
  }
308
+ if (s.status === "connected" && s.client)
309
+ return;
299
310
  }
300
311
  await connect(cfg);
301
312
  };
@@ -13,9 +13,14 @@ export type RpcServerTransport = Readonly<{
13
13
  write(bytes: Uint8Array): Promise<void>;
14
14
  close(error: unknown): void;
15
15
  }>;
16
+ export declare class RpcRouter {
17
+ private readonly handlers;
18
+ register(typeId: number, handler: RpcHandler): void;
19
+ handler(typeId: number): RpcHandler | undefined;
20
+ }
16
21
  export declare class RpcServer {
17
22
  private readonly transport;
18
- private readonly handlers;
23
+ private readonly router;
19
24
  private closed;
20
25
  private readonly options;
21
26
  private readonly requests;
@@ -27,8 +32,9 @@ export declare class RpcServer {
27
32
  private readonly terminalSignal;
28
33
  private signalTerminal;
29
34
  private transportClosed;
30
- constructor(transport: RpcServerTransport, options?: RpcServerOptions);
35
+ constructor(transport: RpcServerTransport, options?: RpcServerOptions, router?: RpcRouter);
31
36
  register(typeId: number, h: RpcHandler): void;
37
+ notify(typeId: number, payload: unknown): Promise<void>;
32
38
  serve(signal?: AbortSignal): Promise<void>;
33
39
  close(error?: unknown): void;
34
40
  private fail;
@@ -37,4 +43,5 @@ export declare class RpcServer {
37
43
  private nextWork;
38
44
  private wakeOne;
39
45
  private writeResponse;
46
+ private writeEnvelope;
40
47
  }
@@ -1,15 +1,24 @@
1
1
  import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
2
2
  import { assertRpcEnvelope } from "./validate.js";
3
+ import { SDK_DEFAULTS } from "../defaults.js";
3
4
  const DEFAULT_RPC_SERVER_OPTIONS = Object.freeze({
4
- maxConcurrentRequests: 32,
5
- maxQueuedRequests: 128,
6
- maxQueuedNotifications: 128,
5
+ maxConcurrentRequests: SDK_DEFAULTS.rpc.maxConcurrentRequests,
6
+ maxQueuedRequests: SDK_DEFAULTS.rpc.maxQueuedRequests,
7
+ maxQueuedNotifications: SDK_DEFAULTS.rpc.maxQueuedNotifications,
7
8
  });
9
+ export class RpcRouter {
10
+ handlers = new Map();
11
+ register(typeId, handler) {
12
+ this.handlers.set(typeId >>> 0, handler);
13
+ }
14
+ handler(typeId) {
15
+ return this.handlers.get(typeId >>> 0);
16
+ }
17
+ }
8
18
  // RpcServer dispatches request envelopes to registered handlers.
9
19
  export class RpcServer {
10
20
  transport;
11
- // Registered handlers keyed by type ID.
12
- handlers = new Map();
21
+ router;
13
22
  // Closed flag to stop the serve loop.
14
23
  closed = false;
15
24
  options;
@@ -22,8 +31,9 @@ export class RpcServer {
22
31
  terminalSignal;
23
32
  signalTerminal;
24
33
  transportClosed = false;
25
- constructor(transport, options = {}) {
34
+ constructor(transport, options = {}, router = new RpcRouter()) {
26
35
  this.transport = transport;
36
+ this.router = router;
27
37
  this.terminalSignal = new Promise((resolve) => { this.signalTerminal = resolve; });
28
38
  this.options = {
29
39
  maxConcurrentRequests: positiveInteger(options.maxConcurrentRequests ?? DEFAULT_RPC_SERVER_OPTIONS.maxConcurrentRequests, "maxConcurrentRequests"),
@@ -33,7 +43,17 @@ export class RpcServer {
33
43
  }
34
44
  // register binds a handler to a type ID.
35
45
  register(typeId, h) {
36
- this.handlers.set(typeId >>> 0, h);
46
+ this.router.register(typeId, h);
47
+ }
48
+ async notify(typeId, payload) {
49
+ if (this.closed)
50
+ throw new Error("rpc server closed");
51
+ await this.writeEnvelope({
52
+ type_id: typeId >>> 0,
53
+ request_id: 0,
54
+ response_to: 0,
55
+ payload,
56
+ });
37
57
  }
38
58
  // serve handles request/response frames until closed or aborted.
39
59
  async serve(signal) {
@@ -106,7 +126,7 @@ export class RpcServer {
106
126
  if (work == null)
107
127
  return;
108
128
  const v = work.envelope;
109
- const h = this.handlers.get(v.type_id >>> 0);
129
+ const h = this.router.handler(v.type_id);
110
130
  let out;
111
131
  if (h == null)
112
132
  out = { payload: null, error: { code: 404, message: "handler not found" } };
@@ -129,7 +149,7 @@ export class RpcServer {
129
149
  if (work == null)
130
150
  return;
131
151
  const v = work.envelope;
132
- const h = this.handlers.get(v.type_id >>> 0);
152
+ const h = this.router.handler(v.type_id);
133
153
  if (h == null)
134
154
  continue;
135
155
  try {
@@ -158,7 +178,10 @@ export class RpcServer {
158
178
  payload: out.payload,
159
179
  ...(out.error != null ? { error: out.error } : {}),
160
180
  };
161
- const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, resp));
181
+ await this.writeEnvelope(resp);
182
+ }
183
+ async writeEnvelope(envelope) {
184
+ const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, envelope));
162
185
  this.writeChain = write.catch(() => { });
163
186
  await write;
164
187
  }
@@ -12,8 +12,11 @@ export type YamuxLimits = Readonly<{
12
12
  preferredOutboundFrameBytes: number;
13
13
  maxStreamReceiveBytes: number;
14
14
  maxSessionReceiveBytes: number;
15
+ /** Maximum bytes retained by pending write calls on one stream. */
16
+ maxStreamWriteQueueBytes?: number;
15
17
  }>;
16
- export declare const DEFAULT_YAMUX_LIMITS: YamuxLimits;
18
+ type ResolvedYamuxLimits = Readonly<Required<YamuxLimits>>;
19
+ export declare const DEFAULT_YAMUX_LIMITS: ResolvedYamuxLimits;
17
20
  export type ByteDuplex = {
18
21
  /** Reads the next chunk from the underlying connection. */
19
22
  read(): Promise<Uint8Array>;
@@ -60,6 +63,7 @@ export declare class YamuxSession {
60
63
  getStream(id: number): YamuxStream | undefined;
61
64
  writeRaw(chunk: Uint8Array): Promise<void>;
62
65
  outboundFrameBytes(): number;
66
+ streamWriteQueueBytes(): number;
63
67
  releaseReceiveBytes(bytes: number): void;
64
68
  probeLiveness(timeoutMs?: number): Promise<number>;
65
69
  private startLivenessProbe;
@@ -79,3 +83,4 @@ export declare class YamuxSession {
79
83
  private isInboundStreamIdValid;
80
84
  private diagnostic;
81
85
  }
86
+ export {};
@@ -3,13 +3,15 @@ import { decodeHeader, encodeHeader, HEADER_LEN } from "./header.js";
3
3
  import { FLAG_ACK, FLAG_RST, FLAG_SYN, TYPE_DATA, TYPE_GO_AWAY, TYPE_PING, TYPE_WINDOW_UPDATE, YAMUX_VERSION } from "./constants.js";
4
4
  import { YamuxStream } from "./stream.js";
5
5
  import { YamuxResourceExhaustedError } from "./errors.js";
6
+ import { SDK_DEFAULTS } from "../defaults.js";
6
7
  export const DEFAULT_YAMUX_LIMITS = Object.freeze({
7
- maxActiveStreams: 64,
8
- maxInboundStreams: 32,
9
- maxFrameBytes: 256 * 1024,
10
- preferredOutboundFrameBytes: 64 * 1024,
11
- maxStreamReceiveBytes: 256 * 1024,
12
- maxSessionReceiveBytes: 16 * (1 << 20),
8
+ maxActiveStreams: SDK_DEFAULTS.yamux.maxActiveStreams,
9
+ maxInboundStreams: SDK_DEFAULTS.yamux.maxInboundStreams,
10
+ maxFrameBytes: SDK_DEFAULTS.yamux.maxFrameBytes,
11
+ preferredOutboundFrameBytes: SDK_DEFAULTS.yamux.preferredOutboundFrameBytes,
12
+ maxStreamReceiveBytes: SDK_DEFAULTS.yamux.maxStreamReceiveBytes,
13
+ maxSessionReceiveBytes: SDK_DEFAULTS.yamux.maxSessionReceiveBytes,
14
+ maxStreamWriteQueueBytes: SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes,
13
15
  });
14
16
  // YamuxSession multiplexes multiple streams over a single byte stream.
15
17
  export class YamuxSession {
@@ -92,6 +94,9 @@ export class YamuxSession {
92
94
  outboundFrameBytes() {
93
95
  return this.limits.preferredOutboundFrameBytes;
94
96
  }
97
+ streamWriteQueueBytes() {
98
+ return this.limits.maxStreamWriteQueueBytes;
99
+ }
95
100
  releaseReceiveBytes(bytes) {
96
101
  this.sessionReceiveBytes = Math.max(0, this.sessionReceiveBytes - Math.max(0, bytes));
97
102
  }
@@ -436,6 +441,7 @@ function normalizeYamuxLimits(input) {
436
441
  preferredOutboundFrameBytes: input?.preferredOutboundFrameBytes ?? Math.min(DEFAULT_YAMUX_LIMITS.preferredOutboundFrameBytes, maxFrameBytes),
437
442
  maxStreamReceiveBytes: input?.maxStreamReceiveBytes ?? DEFAULT_YAMUX_LIMITS.maxStreamReceiveBytes,
438
443
  maxSessionReceiveBytes: input?.maxSessionReceiveBytes ?? DEFAULT_YAMUX_LIMITS.maxSessionReceiveBytes,
444
+ maxStreamWriteQueueBytes: input?.maxStreamWriteQueueBytes ?? DEFAULT_YAMUX_LIMITS.maxStreamWriteQueueBytes,
439
445
  };
440
446
  for (const [name, value] of Object.entries(limits)) {
441
447
  if (!Number.isSafeInteger(value) || value <= 0)
@@ -13,6 +13,7 @@ export declare class YamuxStream {
13
13
  private error;
14
14
  private resetTask;
15
15
  private writeChain;
16
+ private writeQueueBytes;
16
17
  private finalized;
17
18
  constructor(session: YamuxSession, id: number, state: StreamState);
18
19
  open(): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { concatBytes } from "../utils/bin.js";
2
2
  import { encodeHeader } from "./header.js";
3
3
  import { DEFAULT_MAX_STREAM_WINDOW, FLAG_ACK, FLAG_FIN, FLAG_RST, FLAG_SYN, TYPE_DATA, TYPE_WINDOW_UPDATE } from "./constants.js";
4
+ import { YamuxResourceExhaustedError } from "./errors.js";
4
5
  // YamuxStream manages per-stream flow control and state transitions.
5
6
  export class YamuxStream {
6
7
  // Stream identifier within the session.
@@ -24,6 +25,7 @@ export class YamuxStream {
24
25
  error = null;
25
26
  resetTask;
26
27
  writeChain = Promise.resolve();
28
+ writeQueueBytes = 0;
27
29
  finalized = false;
28
30
  constructor(session, id, state) {
29
31
  this.session = session;
@@ -87,10 +89,23 @@ export class YamuxStream {
87
89
  }
88
90
  // write sends DATA frames, respecting the send window.
89
91
  async write(data) {
90
- const payload = data.slice();
91
- const write = this.writeChain.then(() => this.writeSerial(payload));
92
- this.writeChain = write.catch(() => { });
93
- await write;
92
+ this.ensureWritable();
93
+ const byteCount = data.byteLength;
94
+ const nextQueueBytes = this.writeQueueBytes + byteCount;
95
+ const limit = this.session.streamWriteQueueBytes();
96
+ if (!Number.isSafeInteger(nextQueueBytes) || nextQueueBytes > limit) {
97
+ throw new YamuxResourceExhaustedError("stream_write_queue_bytes", nextQueueBytes, limit);
98
+ }
99
+ this.writeQueueBytes = nextQueueBytes;
100
+ try {
101
+ const payload = data.slice();
102
+ const write = this.writeChain.then(() => this.writeSerial(payload));
103
+ this.writeChain = write.catch(() => { });
104
+ await write;
105
+ }
106
+ finally {
107
+ this.writeQueueBytes = Math.max(0, this.writeQueueBytes - byteCount);
108
+ }
94
109
  }
95
110
  async writeSerial(data) {
96
111
  this.ensureWritable();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,6 +40,10 @@
40
40
  "types": "./dist/controlplane/index.d.ts",
41
41
  "default": "./dist/controlplane/index.js"
42
42
  },
43
+ "./endpoint": {
44
+ "types": "./dist/endpoint/index.d.ts",
45
+ "default": "./dist/endpoint/index.js"
46
+ },
43
47
  "./framing": {
44
48
  "types": "./dist/framing/index.d.ts",
45
49
  "default": "./dist/framing/index.js"