@floegence/flowersec-core 0.23.0 → 0.25.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.
package/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # @floegence/flowersec-core
2
2
 
3
- Flowersec core TypeScript library for the complete portable Flowersec client/server contract in Node.js, plus the browser and Service Worker runtime owned by TypeScript.
4
-
5
- Status: experimental; not audited.
3
+ The TypeScript SDK for Flowersec end-to-end encrypted direct and tunneled sessions. It implements the portable Node.js client and endpoint contract plus the browser and Service Worker runtime owned by TypeScript.
6
4
 
7
5
  ## Install
8
6
 
@@ -10,73 +8,33 @@ Status: experimental; not audited.
10
8
  npm install @floegence/flowersec-core
11
9
  ```
12
10
 
13
- ## Recommended usage
14
-
15
- Browser:
16
-
17
- ```ts
18
- import { connectBrowser } from "@floegence/flowersec-core/browser";
19
- import { requestConnectArtifact } from "@floegence/flowersec-core/controlplane";
11
+ The package is ESM-only and exposes environment-specific entrypoints for browser and Node.js applications.
20
12
 
21
- const artifact = await requestConnectArtifact({
22
- endpointId: "env_demo",
23
- });
24
-
25
- const client = await connectBrowser(artifact);
26
- await client.ping();
27
- client.close();
28
- ```
13
+ ## Cookbook
29
14
 
30
- Node.js:
15
+ Start with the [TypeScript cookbook](https://github.com/floegence/flowersec/tree/main/examples/ts). It contains runnable browser direct/tunnel pages, Node.js clients, the shared demo server, the Service Worker proxy runtime, and manual protocol-stack references.
31
16
 
32
- ```ts
33
- import { connectNode, createNodeReconnectConfig } from "@floegence/flowersec-core/node";
34
- import { requestConnectArtifact } from "@floegence/flowersec-core/controlplane";
35
- import { createControlplaneArtifactSource } from "@floegence/flowersec-core/reconnect";
36
-
37
- const artifact = await requestConnectArtifact({
38
- baseUrl: "https://your-app.example/api/flowersec",
39
- endpointId: "env_demo",
40
- });
41
-
42
- const client = await connectNode(artifact, {
43
- origin: "https://your-app.example",
44
- });
45
- await client.ping();
46
- const rttMs = await client.probeLiveness();
47
- client.close();
48
-
49
- const reconnectConfig = createNodeReconnectConfig({
50
- source: createControlplaneArtifactSource({
51
- baseUrl: "https://your-app.example/api/flowersec",
52
- endpointId: "env_demo",
53
- }),
54
- connect: {
55
- origin: "https://your-app.example",
56
- },
57
- });
58
- ```
17
+ ## Entrypoints
59
18
 
60
- Browser `requestConnectArtifact(...)`, `requestEntryConnectArtifact(...)`, and `ControlplaneRequestError` remain available from `@floegence/flowersec-core/browser` as stable aliases.
19
+ - Browser client: `@floegence/flowersec-core/browser`
20
+ - Node.js client and endpoint transport: `@floegence/flowersec-core/node`
21
+ - Controlplane artifact helpers: `@floegence/flowersec-core/controlplane`
22
+ - Endpoint session serving: `@floegence/flowersec-core/endpoint`
23
+ - RPC: `@floegence/flowersec-core/rpc`
24
+ - Reconnect: `@floegence/flowersec-core/reconnect`
25
+ - HTTP/WebSocket proxy and browser runtime: `@floegence/flowersec-core/proxy`
26
+ - Observability: `@floegence/flowersec-core/observability`
61
27
 
62
- High-level connects use `RequireTLS` by default. `AllowPlaintextForLoopback` permits only literal loopback
63
- targets without DNS resolution. Deliberate non-loopback `ws://` connections must use
64
- `createNetworkPlaintextPolicy(...)` with exact canonical IP literals and
65
- `PlaintextRiskAcceptance.acceptPreE2ECredentialExposure`. The unrestricted `AllowPlaintext` preset is deprecated.
28
+ High-level WebSocket connections require TLS by default. Use `AllowPlaintextForLoopback` only for literal local development targets.
66
29
 
67
- ## Node endpoint and controlplane
30
+ ## Node Proxy Server
68
31
 
69
- `@floegence/flowersec-core/node` exports the high-level endpoint APIs for accepted direct WebSockets and server-role tunnel grants. `@floegence/flowersec-core/endpoint` provides portable session and RPC serving, while `@floegence/flowersec-core/controlplane` provides bounded artifact envelopes, FST2 tokens, issuer rotation, and channel initialization.
32
+ The Node entrypoint exports `serveProxySession(...)`, `serveProxyStream(...)`, and `ProxyServerOptions`. Request bodies remain bounded per request by `maxBodyBytes`; `maxBufferedRequestBodyBytes` additionally caps the total buffered request-body bytes owned by one proxy session. WebSocket frames remain bounded by `maxWsFrameBytes`; `maxWsQueuedBytes` additionally caps upstream-to-Yamux queued bytes per connection, while the reverse direction waits for each Node `ws` send callback.
70
33
 
71
- ## Proxy
34
+ The defaults keep one session request-body budget equal to `maxBodyBytes` and one queued WebSocket frame plus its proxy header. Raise either limit only when the deployment has a matching memory budget.
72
35
 
73
- `@floegence/flowersec-core/proxy` contains both portable HTTP/1 and WebSocket proxy protocols and the TypeScript-owned browser runtime. Node endpoint servers use `serveProxySession(...)`; browser applications use the Service Worker or controller bridge helpers without changing the portable stream contract.
36
+ ## Runtime Boundaries
74
37
 
75
- ## Docs
38
+ TypeScript owns browser and Service Worker integration. Shared tunnel, proxy gateway, and helper binaries remain Go-owned.
76
39
 
77
- - Frontend quickstart: `docs/FRONTEND_QUICKSTART.md`
78
- - Integration guide: `docs/INTEGRATION_GUIDE.md`
79
- - API surface contract: `docs/API_SURFACE.md`
80
- - Controlplane artifact fetch: `docs/CONTROLPLANE_ARTIFACT_FETCH.md`
81
- - Error model: `docs/ERROR_MODEL.md`
82
- - Migration guide: `docs/V0_20_MIGRATION.md`
40
+ Review the shared [API contract](../docs/API_CONTRACT.md), [protocol](../docs/PROTOCOL.md), [threat model](../docs/THREAT_MODEL.md), and [error model](../docs/ERROR_MODEL.md).
@@ -23,7 +23,7 @@ export type ConnectOptionsBase = Readonly<{
23
23
  maxHandshakePayload?: number;
24
24
  /** Maximum encrypted record size on the wire (0 uses default). */
25
25
  maxRecordBytes?: number;
26
- /** Maximum buffered plaintext bytes in the secure channel (0 uses default). */
26
+ /** Maximum queued inbound plaintext bytes in the secure channel (0 uses default). */
27
27
  maxBufferedBytes?: number;
28
28
  /** Maximum queued outbound plaintext bytes in the secure channel (default 4 MiB; 0 uses default). */
29
29
  maxOutboundBufferedBytes?: number;
@@ -39,9 +39,9 @@ export type ConnectOptionsBase = Readonly<{
39
39
  observer?: ClientObserverLike;
40
40
  /** Policy evaluated before any WebSocket network activity. */
41
41
  transportSecurityPolicy?: TransportSecurityPolicy;
42
- /** Experimental scope validators keyed by scope name. */
42
+ /** Scope validators keyed by scope name. */
43
43
  scopeResolvers?: ConnectScopeResolverMap;
44
- /** Experimental migration switch for optional scope failures. */
44
+ /** Explicit compatibility switch for optional scope failures. */
45
45
  relaxedOptionalScopeValidation?: boolean;
46
46
  /** Acknowledged Yamux liveness checks, or false to disable automatic checks. */
47
47
  liveness?: false | LivenessOptions;
@@ -303,10 +303,11 @@ export async function connectCore(args) {
303
303
  return await mux.probeLiveness(liveness.timeoutMs);
304
304
  }
305
305
  catch (e) {
306
- if (isYamuxPingTimeoutError(e)) {
306
+ const timedOut = isYamuxPingTimeoutError(e);
307
+ if (timedOut) {
307
308
  emitObserverDiagnostic(args.opts.observer, { path: args.path, stage: "yamux", code_domain: "event", code: "liveness_timeout", result: "fail" });
308
309
  }
309
- const error = new FlowersecError({ path: args.path, stage: "yamux", code: "ping_failed", message: "liveness probe failed", cause: e });
310
+ const error = new FlowersecError({ path: args.path, stage: "yamux", code: timedOut ? "timeout" : "ping_failed", message: "liveness probe failed", cause: e });
310
311
  reportTermination(error);
311
312
  throw error;
312
313
  }
@@ -355,8 +356,9 @@ export async function connectCore(args) {
355
356
  },
356
357
  probeLiveness,
357
358
  openStream: async (kind, opts = {}) => {
358
- if (kind == null || kind === "")
359
- throw new FlowersecError({ path: args.path, stage: "validate", code: "missing_stream_kind", message: "missing stream kind" });
359
+ const streamKind = kind?.trim() ?? "";
360
+ if (streamKind === "")
361
+ throw new FlowersecError({ path: args.path, stage: "rpc", code: "missing_stream_kind", message: "missing stream kind" });
360
362
  if (opts.signal?.aborted) {
361
363
  throw new FlowersecError({
362
364
  path: args.path,
@@ -415,7 +417,7 @@ export async function connectCore(args) {
415
417
  });
416
418
  }
417
419
  try {
418
- await writeStreamHello((b) => s.write(b), kind);
420
+ await writeStreamHello((b) => s.write(b), streamKind);
419
421
  }
420
422
  catch (err) {
421
423
  if (signal?.aborted) {
@@ -35,7 +35,7 @@ export async function enforceTransportSecurity(args) {
35
35
  const policy = args.policy ?? RequireTLS;
36
36
  try {
37
37
  if (typeof policy === "function") {
38
- allowed = await policy(input);
38
+ allowed = await policy(input) === true;
39
39
  }
40
40
  else {
41
41
  allowed = evaluatePreset(policy, target);
@@ -1,7 +1,7 @@
1
1
  import { type TokenPayload } from "./token.js";
2
2
  export declare class IssuerKeyset {
3
- private kid;
4
- private signingSeed;
3
+ private active;
4
+ private readonly verificationKeys;
5
5
  constructor(kid: string, signingSeed: Uint8Array);
6
6
  static random(kid: string): IssuerKeyset;
7
7
  currentKID(): string;
@@ -9,7 +9,10 @@ export declare class IssuerKeyset {
9
9
  sign(payload: Omit<TokenPayload, "kid"> & Readonly<{
10
10
  kid?: string;
11
11
  }>): string;
12
+ addVerificationKey(kid: string, publicKey: Uint8Array): void;
12
13
  rotate(kid: string, signingSeed: Uint8Array): void;
14
+ retireVerificationKey(kid: string): void;
13
15
  exportTunnelKeyset(): Uint8Array;
14
16
  dispose(): void;
17
+ private requireActive;
15
18
  }
@@ -2,13 +2,13 @@ import { ed25519 } from "@noble/curves/ed25519";
2
2
  import { base64urlEncode } from "../utils/base64url.js";
3
3
  import { signToken } from "./token.js";
4
4
  export class IssuerKeyset {
5
- kid;
6
- signingSeed;
5
+ active;
6
+ verificationKeys = new Map();
7
7
  constructor(kid, signingSeed) {
8
- this.kid = normalizeKid(kid);
9
- if (signingSeed.length !== 32)
10
- throw new TypeError("Ed25519 signing seed must be 32 bytes");
11
- this.signingSeed = signingSeed.slice();
8
+ const normalizedKID = normalizeKid(kid);
9
+ const retainedSeed = copySigningSeed(signingSeed);
10
+ this.active = { kid: normalizedKID, signingSeed: retainedSeed };
11
+ this.verificationKeys.set(normalizedKID, ed25519.getPublicKey(retainedSeed));
12
12
  }
13
13
  static random(kid) {
14
14
  const seed = crypto.getRandomValues(new Uint8Array(32));
@@ -20,30 +20,90 @@ export class IssuerKeyset {
20
20
  }
21
21
  }
22
22
  currentKID() {
23
- return this.kid;
23
+ return this.requireActive().kid;
24
24
  }
25
25
  publicKeys() {
26
- return new Map([[this.kid, ed25519.getPublicKey(this.signingSeed)]]);
26
+ this.requireActive();
27
+ return new Map([...this.verificationKeys.entries()]
28
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
29
+ .map(([kid, publicKey]) => [kid, publicKey.slice()]));
27
30
  }
28
31
  sign(payload) {
29
- return signToken(this.signingSeed, { ...payload, kid: this.kid });
32
+ const active = this.requireActive();
33
+ return signToken(active.signingSeed, { ...payload, kid: active.kid });
34
+ }
35
+ addVerificationKey(kid, publicKey) {
36
+ this.requireActive();
37
+ const normalizedKID = normalizeKid(kid);
38
+ const retainedKey = copyPublicKey(publicKey);
39
+ const existing = this.verificationKeys.get(normalizedKID);
40
+ if (existing != null) {
41
+ if (!equalBytes(existing, retainedKey))
42
+ throw new Error(`key ID ${normalizedKID} is already bound to a different public key`);
43
+ return;
44
+ }
45
+ this.verificationKeys.set(normalizedKID, retainedKey);
30
46
  }
31
47
  rotate(kid, signingSeed) {
48
+ const active = this.requireActive();
32
49
  const nextKID = normalizeKid(kid);
33
- if (signingSeed.length !== 32)
34
- throw new TypeError("Ed25519 signing seed must be 32 bytes");
35
- const nextSeed = signingSeed.slice();
36
- this.signingSeed.fill(0);
37
- this.kid = nextKID;
38
- this.signingSeed = nextSeed;
50
+ const nextSeed = copySigningSeed(signingSeed);
51
+ const nextPublicKey = ed25519.getPublicKey(nextSeed);
52
+ const prepublishedKey = this.verificationKeys.get(nextKID);
53
+ if (prepublishedKey == null) {
54
+ nextSeed.fill(0);
55
+ throw new Error(`key ID ${nextKID} must be prepublished before rotation`);
56
+ }
57
+ if (!equalBytes(prepublishedKey, nextPublicKey)) {
58
+ nextSeed.fill(0);
59
+ throw new Error(`key ID ${nextKID} is bound to a different public key`);
60
+ }
61
+ this.active = { kid: nextKID, signingSeed: nextSeed };
62
+ active.signingSeed.fill(0);
63
+ }
64
+ retireVerificationKey(kid) {
65
+ const active = this.requireActive();
66
+ const normalizedKID = normalizeKid(kid);
67
+ if (normalizedKID === active.kid)
68
+ throw new Error("the active signing key cannot be retired");
69
+ if (!this.verificationKeys.delete(normalizedKID))
70
+ throw new Error(`key ID ${normalizedKID} is not published`);
39
71
  }
40
72
  exportTunnelKeyset() {
41
73
  const keys = [...this.publicKeys()].map(([kid, publicKey]) => ({ kid, pubkey_b64u: base64urlEncode(publicKey) }));
42
74
  return new TextEncoder().encode(JSON.stringify({ keys }, null, 2));
43
75
  }
44
76
  dispose() {
45
- this.signingSeed.fill(0);
77
+ const active = this.active;
78
+ if (active == null)
79
+ return;
80
+ active.signingSeed.fill(0);
81
+ this.active = undefined;
82
+ this.verificationKeys.clear();
46
83
  }
84
+ requireActive() {
85
+ if (this.active == null)
86
+ throw new Error("issuer keyset is disposed");
87
+ return this.active;
88
+ }
89
+ }
90
+ function copySigningSeed(signingSeed) {
91
+ if (signingSeed.length !== 32)
92
+ throw new TypeError("Ed25519 signing seed must be 32 bytes");
93
+ return signingSeed.slice();
94
+ }
95
+ function copyPublicKey(publicKey) {
96
+ if (publicKey.length !== 32)
97
+ throw new TypeError("Ed25519 public key must be 32 bytes");
98
+ return publicKey.slice();
99
+ }
100
+ function equalBytes(left, right) {
101
+ if (left.length !== right.length)
102
+ return false;
103
+ let diff = 0;
104
+ for (let index = 0; index < left.length; index++)
105
+ diff |= left[index] ^ right[index];
106
+ return diff === 0;
47
107
  }
48
108
  function normalizeKid(kid) {
49
109
  const value = kid.trim();
@@ -8,6 +8,7 @@ export declare const SDK_DEFAULTS: Readonly<{
8
8
  maxHandshakePayloadBytes: number;
9
9
  maxRecordBytes: number;
10
10
  outboundRecordChunkBytes: number;
11
+ maxInboundBufferedBytes: number;
11
12
  maxOutboundBufferedBytes: number;
12
13
  }>;
13
14
  yamux: Readonly<{
@@ -15,6 +16,7 @@ export declare const SDK_DEFAULTS: Readonly<{
15
16
  maxInboundStreams: 32;
16
17
  maxFrameBytes: number;
17
18
  preferredOutboundFrameBytes: number;
19
+ maxStreamWriteQueueBytes: number;
18
20
  maxStreamReceiveBytes: number;
19
21
  maxSessionReceiveBytes: number;
20
22
  }>;
package/dist/defaults.js CHANGED
@@ -8,6 +8,7 @@ export const SDK_DEFAULTS = Object.freeze({
8
8
  maxHandshakePayloadBytes: 8 * 1024,
9
9
  maxRecordBytes: 1024 * 1024,
10
10
  outboundRecordChunkBytes: 64 * 1024,
11
+ maxInboundBufferedBytes: 4 * 1024 * 1024,
11
12
  maxOutboundBufferedBytes: 4 * 1024 * 1024,
12
13
  }),
13
14
  yamux: Object.freeze({
@@ -15,6 +16,7 @@ export const SDK_DEFAULTS = Object.freeze({
15
16
  maxInboundStreams: 32,
16
17
  maxFrameBytes: 256 * 1024,
17
18
  preferredOutboundFrameBytes: 64 * 1024,
19
+ maxStreamWriteQueueBytes: 4 * 1024 * 1024,
18
20
  maxStreamReceiveBytes: 256 * 1024,
19
21
  maxSessionReceiveBytes: 16 * 1024 * 1024,
20
22
  }),
@@ -16,7 +16,7 @@ export type HandshakeClientOptions = Readonly<{
16
16
  maxRecordBytes: number;
17
17
  /** Preferred plaintext bytes per outbound record. */
18
18
  outboundRecordChunkBytes?: number;
19
- /** Maximum buffered plaintext bytes for the secure channel. */
19
+ /** Maximum queued inbound plaintext bytes for the secure channel. */
20
20
  maxBufferedBytes?: number;
21
21
  /** Maximum queued outbound plaintext bytes for the secure channel. */
22
22
  maxOutboundBufferedBytes?: number;
@@ -44,7 +44,7 @@ export type HandshakeServerOptions = Readonly<{
44
44
  maxRecordBytes: number;
45
45
  /** Preferred plaintext bytes per outbound record. */
46
46
  outboundRecordChunkBytes?: number;
47
- /** Maximum buffered plaintext bytes for the secure channel. */
47
+ /** Maximum queued inbound plaintext bytes for the secure channel. */
48
48
  maxBufferedBytes?: number;
49
49
  /** Maximum queued outbound plaintext bytes for the secure channel. */
50
50
  maxOutboundBufferedBytes?: number;
@@ -31,7 +31,7 @@ export declare class SecureChannel {
31
31
  private readonly transport;
32
32
  private readonly maxRecordBytes;
33
33
  private readonly outboundRecordChunkBytes;
34
- private readonly maxBufferedBytes;
34
+ private readonly maxInboundBufferedBytes;
35
35
  private readonly maxOutboundBufferedBytes;
36
36
  private sendKey;
37
37
  private recvKey;
@@ -50,7 +50,7 @@ export declare class SecureChannel {
50
50
  private sendQueueBytes;
51
51
  private sendClosed;
52
52
  private sendErr;
53
- private readonly recvQueue;
53
+ private recvQueue;
54
54
  private recvQueueHead;
55
55
  private recvQueueBytes;
56
56
  private recvWaiters;
@@ -1,6 +1,7 @@
1
1
  import { RECORD_FLAG_APP, RECORD_FLAG_PING, RECORD_FLAG_REKEY } from "./constants.js";
2
2
  import { decryptRecord, encryptRecord, maxPlaintextBytes } from "./record.js";
3
3
  import { deriveRekeyKey } from "./kdf.js";
4
+ import { SDK_DEFAULTS } from "../defaults.js";
4
5
  const maxRecordSeq = (1n << 64n) - 1n;
5
6
  class RecordSeqExhaustedError extends Error {
6
7
  constructor() {
@@ -15,8 +16,7 @@ export class SecureChannel {
15
16
  // Maximum allowed bytes per record frame.
16
17
  maxRecordBytes;
17
18
  outboundRecordChunkBytes;
18
- // Upper bound for buffered plaintext in memory.
19
- maxBufferedBytes;
19
+ maxInboundBufferedBytes;
20
20
  maxOutboundBufferedBytes;
21
21
  // Active encryption keys and nonce prefixes for the current epoch.
22
22
  sendKey;
@@ -52,16 +52,16 @@ export class SecureChannel {
52
52
  this.transport = args.transport;
53
53
  this.maxRecordBytes = args.maxRecordBytes;
54
54
  const maxPlain = Math.max(1, maxPlaintextBytes(this.maxRecordBytes));
55
- this.outboundRecordChunkBytes = args.outboundRecordChunkBytes ?? Math.min(64 * 1024, maxPlain);
55
+ this.outboundRecordChunkBytes = args.outboundRecordChunkBytes ?? Math.min(SDK_DEFAULTS.e2ee.outboundRecordChunkBytes, maxPlain);
56
56
  if (!Number.isSafeInteger(this.outboundRecordChunkBytes) || this.outboundRecordChunkBytes <= 0 || this.outboundRecordChunkBytes > maxPlain) {
57
57
  throw new RangeError("outboundRecordChunkBytes must be a positive integer within the record plaintext limit");
58
58
  }
59
- this.maxBufferedBytes = Math.max(0, args.maxBufferedBytes ?? 4 * (1 << 20));
60
- const maxOutboundBufferedBytes = args.maxOutboundBufferedBytes ?? 4 * (1 << 20);
59
+ this.maxInboundBufferedBytes = Math.max(0, args.maxBufferedBytes ?? SDK_DEFAULTS.e2ee.maxInboundBufferedBytes);
60
+ const maxOutboundBufferedBytes = args.maxOutboundBufferedBytes ?? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes;
61
61
  if (!Number.isSafeInteger(maxOutboundBufferedBytes) || maxOutboundBufferedBytes < 0) {
62
62
  throw new RangeError("maxOutboundBufferedBytes must be a non-negative safe integer");
63
63
  }
64
- this.maxOutboundBufferedBytes = maxOutboundBufferedBytes === 0 ? 4 * (1 << 20) : maxOutboundBufferedBytes;
64
+ this.maxOutboundBufferedBytes = maxOutboundBufferedBytes === 0 ? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes : maxOutboundBufferedBytes;
65
65
  this.sendKey = args.sendKey;
66
66
  this.recvKey = args.recvKey;
67
67
  this.sendNoncePrefix = args.sendNoncePrefix;
@@ -87,7 +87,11 @@ export class SecureChannel {
87
87
  if (this.recvQueueHead < this.recvQueue.length) {
88
88
  const b = this.recvQueue[this.recvQueueHead];
89
89
  this.recvQueueHead++;
90
- if (this.recvQueueHead > 1024 && this.recvQueueHead * 2 > this.recvQueue.length) {
90
+ if (this.recvQueueHead === this.recvQueue.length) {
91
+ this.recvQueue = [];
92
+ this.recvQueueHead = 0;
93
+ }
94
+ else if (this.recvQueueHead > 1024 && this.recvQueueHead * 2 > this.recvQueue.length) {
91
95
  this.recvQueue.splice(0, this.recvQueueHead);
92
96
  this.recvQueueHead = 0;
93
97
  }
@@ -171,7 +175,11 @@ export class SecureChannel {
171
175
  return null;
172
176
  const req = this.sendQueue[this.sendQueueHead];
173
177
  this.sendQueueHead++;
174
- if (this.sendQueueHead > 1024 && this.sendQueueHead * 2 > this.sendQueue.length) {
178
+ if (this.sendQueueHead === this.sendQueue.length) {
179
+ this.sendQueue = [];
180
+ this.sendQueueHead = 0;
181
+ }
182
+ else if (this.sendQueueHead > 1024 && this.sendQueueHead * 2 > this.sendQueue.length) {
175
183
  this.sendQueue.splice(0, this.sendQueueHead);
176
184
  this.sendQueueHead = 0;
177
185
  }
@@ -182,7 +190,11 @@ export class SecureChannel {
182
190
  return undefined;
183
191
  const w = this.sendWaiters[this.sendWaitersHead];
184
192
  this.sendWaitersHead++;
185
- if (this.sendWaitersHead > 1024 && this.sendWaitersHead * 2 > this.sendWaiters.length) {
193
+ if (this.sendWaitersHead === this.sendWaiters.length) {
194
+ this.sendWaiters = [];
195
+ this.sendWaitersHead = 0;
196
+ }
197
+ else if (this.sendWaitersHead > 1024 && this.sendWaitersHead * 2 > this.sendWaiters.length) {
186
198
  this.sendWaiters.splice(0, this.sendWaitersHead);
187
199
  this.sendWaitersHead = 0;
188
200
  }
@@ -284,7 +296,7 @@ export class SecureChannel {
284
296
  }
285
297
  this.recvSeq = seq + 1n;
286
298
  if (flags === RECORD_FLAG_APP) {
287
- if (this.maxBufferedBytes > 0 && this.recvQueueBytes + plaintext.length > this.maxBufferedBytes) {
299
+ if (this.maxInboundBufferedBytes > 0 && this.recvQueueBytes + plaintext.length > this.maxInboundBufferedBytes) {
288
300
  throw new Error("recv buffer exceeded");
289
301
  }
290
302
  this.recvQueue.push(plaintext);