@floegence/flowersec-core 0.22.0 → 0.23.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 +3 -1
- package/YAMUX_ALIGNMENT.md +4 -4
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/client-connect/connectCore.js +115 -103
- package/dist/client-connect/transportSecurity.d.ts +10 -0
- package/dist/client-connect/transportSecurity.js +89 -0
- package/dist/client.d.ts +2 -0
- package/dist/controlplane/request.js +3 -1
- package/dist/endpoint/index.d.ts +1 -0
- package/dist/endpoint/index.js +8 -0
- package/dist/facade.d.ts +2 -2
- package/dist/facade.js +1 -1
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +1 -1
- package/dist/proxy/controllerWindow.js +4 -5
- package/dist/proxy/runtime.js +10 -19
- package/dist/reconnect/index.js +79 -63
- package/dist/rpc/server.d.ts +1 -0
- package/dist/rpc/server.js +49 -20
- package/dist/streamio/index.js +3 -6
- package/dist/utils/errors.d.ts +1 -1
- package/dist/yamux/errors.d.ts +8 -0
- package/dist/yamux/errors.js +18 -0
- package/dist/yamux/session.js +7 -4
- package/dist/yamux/stream.d.ts +1 -1
- package/dist/yamux/stream.js +3 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -60,7 +60,9 @@ const reconnectConfig = createNodeReconnectConfig({
|
|
|
60
60
|
Browser `requestConnectArtifact(...)`, `requestEntryConnectArtifact(...)`, and `ControlplaneRequestError` remain available from `@floegence/flowersec-core/browser` as stable aliases.
|
|
61
61
|
|
|
62
62
|
High-level connects use `RequireTLS` by default. `AllowPlaintextForLoopback` permits only literal loopback
|
|
63
|
-
targets without DNS resolution
|
|
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.
|
|
64
66
|
|
|
65
67
|
## Node endpoint and controlplane
|
|
66
68
|
|
package/YAMUX_ALIGNMENT.md
CHANGED
|
@@ -77,19 +77,19 @@ Source: `flowersec-ts/src/yamux/constants.ts`, `flowersec-ts/src/yamux/header.ts
|
|
|
77
77
|
The TS Yamux implementation is exercised in real interop scenarios:
|
|
78
78
|
|
|
79
79
|
- **TS client ↔ Go server (minimal Yamux over TCP)**:
|
|
80
|
-
- Test: `flowersec-
|
|
81
|
-
- Go
|
|
80
|
+
- Test: `flowersec-go/internal/cmd/flowersec-interop` with the TypeScript JSONL harness
|
|
81
|
+
- Go reference: production `client`, `endpoint`, and Yamux-backed session APIs
|
|
82
82
|
- Covers: window update race, RST handling, concurrent open/close, session close.
|
|
83
83
|
- Notes: opt-in via `YAMUX_INTEROP=1` (runs Go harnesses).
|
|
84
84
|
- Notes: sizes scale via `YAMUX_INTEROP_SCALE` (e.g. `2` => 20 streams / 1 MiB per stream).
|
|
85
85
|
- Notes: client-initiated RST scenarios run when `YAMUX_INTEROP_CLIENT_RST=1`.
|
|
86
86
|
- Notes: window-update and concurrent-open/close stress runs when `YAMUX_INTEROP_STRESS=1`.
|
|
87
87
|
- **TS client ↔ Go server (full chain: E2EE + tunnel + Yamux)**:
|
|
88
|
-
- Test: `
|
|
88
|
+
- Test: the Direct/Tunnel variants in `stability/interop_matrix.json`
|
|
89
89
|
- Go harness: `flowersec-go/internal/cmd/flowersec-e2e-harness/main.go` with `-scenario`
|
|
90
90
|
- Notes: reduced stream counts/payload sizes to keep end-to-end runtime bounded.
|
|
91
91
|
- **Layered close/reset probes (memory + WS + full chain)**:
|
|
92
|
-
- Test: `
|
|
92
|
+
- Test: the Go-reference smoke and stress profiles in `testdata/interop/v1/profiles.json`
|
|
93
93
|
- Covers: session-close wakeup, FIN/RST delivery across SecureChannel + WebSocketBinaryTransport,
|
|
94
94
|
and a full-chain FIN/RST probe on `rst_mid_write_go`.
|
|
95
95
|
- Notes: gated by `YAMUX_INTEROP=1`; the full-chain probe additionally requires
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { ConnectBrowserOptions, DirectConnectBrowserOptions, TunnelConnectBrowserOptions } from "./connect.js";
|
|
2
2
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
3
|
-
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
4
|
-
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "../client-connect/transportSecurity.js";
|
|
3
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
4
|
+
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, NetworkPlaintextPolicyOptions, } from "../client-connect/transportSecurity.js";
|
|
5
5
|
export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
|
|
6
6
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
7
7
|
export type { ConnectArtifactRequestConfig, ControlplaneConfig, EntryConnectArtifactRequestConfig, EntryControlplaneConfig, } from "./controlplane.js";
|
package/dist/browser/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
2
|
-
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
2
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
3
|
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
4
4
|
export { ControlplaneRequestError, requestChannelGrant, requestConnectArtifact, requestEntryChannelGrant, requestEntryConnectArtifact, } from "./controlplane.js";
|
|
5
5
|
export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
|
|
@@ -14,7 +14,7 @@ import { prepareChannelId } from "./contract.js";
|
|
|
14
14
|
import { isTunnelAttachCloseReason } from "./tunnelAttachCloseReason.js";
|
|
15
15
|
import { enforceTransportSecurity } from "./transportSecurity.js";
|
|
16
16
|
import { maxPlaintextBytes } from "../e2ee/record.js";
|
|
17
|
-
import { isYamuxResourceExhaustedError } from "../yamux/errors.js";
|
|
17
|
+
import { isYamuxPingTimeoutError, isYamuxResourceExhaustedError } from "../yamux/errors.js";
|
|
18
18
|
import { registerClientTermination } from "./termination.js";
|
|
19
19
|
export async function connectCore(args) {
|
|
20
20
|
const observer = normalizeObserver(args.opts.observer, { path: args.path });
|
|
@@ -143,12 +143,6 @@ export async function connectCore(args) {
|
|
|
143
143
|
catch (err) {
|
|
144
144
|
const reason = classifyConnectError(err);
|
|
145
145
|
observer.onConnect(args.path, "fail", reason, nowSeconds() - connectStart);
|
|
146
|
-
try {
|
|
147
|
-
transport.close();
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
// ignore
|
|
151
|
-
}
|
|
152
146
|
const code = reason === "timeout" ? "timeout" : reason === "canceled" ? "canceled" : "dial_failed";
|
|
153
147
|
throw new FlowersecError({
|
|
154
148
|
path: args.path,
|
|
@@ -167,12 +161,6 @@ export async function connectCore(args) {
|
|
|
167
161
|
catch (err) {
|
|
168
162
|
observer.onAttach("fail", "send_failed");
|
|
169
163
|
attachState = "failed";
|
|
170
|
-
try {
|
|
171
|
-
transport.close();
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
// ignore
|
|
175
|
-
}
|
|
176
164
|
throw new FlowersecError({ path: args.path, stage: "attach", code: "attach_failed", message: "attach failed", cause: err });
|
|
177
165
|
}
|
|
178
166
|
}
|
|
@@ -202,7 +190,6 @@ export async function connectCore(args) {
|
|
|
202
190
|
catch (err) {
|
|
203
191
|
const handshakeElapsedSeconds = nowSeconds() - handshakeStart;
|
|
204
192
|
const handshakeCode = classifyHandshakeError(err);
|
|
205
|
-
transport.close();
|
|
206
193
|
if (args.path === "tunnel" && err instanceof WsCloseError) {
|
|
207
194
|
const reason = err.reason;
|
|
208
195
|
if (isTunnelAttachCloseReason(reason)) {
|
|
@@ -233,17 +220,20 @@ export async function connectCore(args) {
|
|
|
233
220
|
});
|
|
234
221
|
let terminationReported = false;
|
|
235
222
|
let closeAll = () => {
|
|
236
|
-
|
|
237
|
-
secure.close();
|
|
238
|
-
}
|
|
239
|
-
catch { /* ignore */ }
|
|
223
|
+
runSyncCleanups([() => secure.close()]);
|
|
240
224
|
};
|
|
241
225
|
const reportTermination = (error) => {
|
|
242
226
|
if (terminationReported)
|
|
243
227
|
return;
|
|
244
228
|
terminationReported = true;
|
|
245
|
-
|
|
246
|
-
|
|
229
|
+
let terminalError = error;
|
|
230
|
+
try {
|
|
231
|
+
closeAll();
|
|
232
|
+
}
|
|
233
|
+
catch (cleanupError) {
|
|
234
|
+
terminalError = errorWithCleanup(error, cleanupError, "session termination cleanup failed");
|
|
235
|
+
}
|
|
236
|
+
resolveTermination({ error: terminalError });
|
|
247
237
|
};
|
|
248
238
|
const mux = new YamuxSession(conn, {
|
|
249
239
|
client: true,
|
|
@@ -261,23 +251,21 @@ export async function connectCore(args) {
|
|
|
261
251
|
}),
|
|
262
252
|
});
|
|
263
253
|
closeAll = () => {
|
|
264
|
-
|
|
265
|
-
mux.close();
|
|
266
|
-
}
|
|
267
|
-
catch { /* ignore */ }
|
|
268
|
-
try {
|
|
269
|
-
secure.close();
|
|
270
|
-
}
|
|
271
|
-
catch { /* ignore */ }
|
|
254
|
+
runSyncCleanups([() => mux.close(), () => secure.close()]);
|
|
272
255
|
};
|
|
273
256
|
let rpcStream;
|
|
274
257
|
try {
|
|
275
258
|
rpcStream = await mux.openStream(signal === undefined ? {} : { signal });
|
|
276
259
|
}
|
|
277
260
|
catch (e) {
|
|
278
|
-
mux.close();
|
|
279
|
-
|
|
280
|
-
|
|
261
|
+
const cleanupError = captureSyncCleanup(() => runSyncCleanups([() => mux.close(), () => secure.close()]));
|
|
262
|
+
throw new FlowersecError({
|
|
263
|
+
path: args.path,
|
|
264
|
+
stage: "yamux",
|
|
265
|
+
code: "open_stream_failed",
|
|
266
|
+
message: "open rpc stream failed",
|
|
267
|
+
cause: causeWithCleanup(e, cleanupError, "RPC stream setup cleanup failed"),
|
|
268
|
+
});
|
|
281
269
|
}
|
|
282
270
|
const reader = new ByteReader(() => rpcStream.read());
|
|
283
271
|
const readExactly = (n) => reader.readExactly(n);
|
|
@@ -286,20 +274,19 @@ export async function connectCore(args) {
|
|
|
286
274
|
await writeStreamHello(write, "rpc");
|
|
287
275
|
}
|
|
288
276
|
catch (e) {
|
|
289
|
-
|
|
290
|
-
await rpcStream.close();
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
secure.close();
|
|
277
|
+
const cleanupError = await captureAsyncCleanup(async () => {
|
|
278
|
+
const streamCloseError = await captureAsyncCleanup(() => rpcStream.close());
|
|
279
|
+
const stackCloseError = captureSyncCleanup(() => runSyncCleanups([() => mux.close(), () => secure.close()]));
|
|
280
|
+
const combined = errorsFrom([streamCloseError, stackCloseError]);
|
|
281
|
+
if (combined != null)
|
|
282
|
+
throw combined;
|
|
283
|
+
});
|
|
297
284
|
throw new FlowersecError({
|
|
298
285
|
path: args.path,
|
|
299
286
|
stage: "rpc",
|
|
300
287
|
code: "stream_hello_failed",
|
|
301
288
|
message: "rpc stream hello failed",
|
|
302
|
-
cause: e,
|
|
289
|
+
cause: causeWithCleanup(e, cleanupError, "RPC bootstrap cleanup failed"),
|
|
303
290
|
});
|
|
304
291
|
}
|
|
305
292
|
const rpc = new RpcClient(readExactly, write, { observer, onTerminal: reportTermination });
|
|
@@ -316,22 +303,12 @@ export async function connectCore(args) {
|
|
|
316
303
|
return await mux.probeLiveness(liveness.timeoutMs);
|
|
317
304
|
}
|
|
318
305
|
catch (e) {
|
|
319
|
-
if (
|
|
306
|
+
if (isYamuxPingTimeoutError(e)) {
|
|
320
307
|
emitObserverDiagnostic(args.opts.observer, { path: args.path, stage: "yamux", code_domain: "event", code: "liveness_timeout", result: "fail" });
|
|
321
308
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
catch { /* ignore */ }
|
|
326
|
-
try {
|
|
327
|
-
mux.close();
|
|
328
|
-
}
|
|
329
|
-
catch { /* ignore */ }
|
|
330
|
-
try {
|
|
331
|
-
secure.close();
|
|
332
|
-
}
|
|
333
|
-
catch { /* ignore */ }
|
|
334
|
-
throw new FlowersecError({ path: args.path, stage: "yamux", code: "ping_failed", message: "liveness probe failed", cause: e });
|
|
309
|
+
const error = new FlowersecError({ path: args.path, stage: "yamux", code: "ping_failed", message: "liveness probe failed", cause: e });
|
|
310
|
+
reportTermination(error);
|
|
311
|
+
throw error;
|
|
335
312
|
}
|
|
336
313
|
};
|
|
337
314
|
let livenessTimer;
|
|
@@ -344,24 +321,7 @@ export async function connectCore(args) {
|
|
|
344
321
|
};
|
|
345
322
|
closeAll = () => {
|
|
346
323
|
stopLiveness();
|
|
347
|
-
|
|
348
|
-
rpc.close();
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
// ignore
|
|
352
|
-
}
|
|
353
|
-
try {
|
|
354
|
-
mux.close();
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
// ignore
|
|
358
|
-
}
|
|
359
|
-
try {
|
|
360
|
-
secure.close();
|
|
361
|
-
}
|
|
362
|
-
catch {
|
|
363
|
-
// ignore
|
|
364
|
-
}
|
|
324
|
+
runSyncCleanups([() => rpc.close(), () => mux.close(), () => secure.close()]);
|
|
365
325
|
};
|
|
366
326
|
if (liveness.intervalMs > 0) {
|
|
367
327
|
livenessTimer = setInterval(() => {
|
|
@@ -385,6 +345,14 @@ export async function connectCore(args) {
|
|
|
385
345
|
mux,
|
|
386
346
|
rpc,
|
|
387
347
|
ping,
|
|
348
|
+
rekey: async () => {
|
|
349
|
+
try {
|
|
350
|
+
await secure.rekeyNow();
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
throw new FlowersecError({ path: args.path, stage: "secure", code: "rekey_failed", message: "rekey failed", cause: error });
|
|
354
|
+
}
|
|
355
|
+
},
|
|
388
356
|
probeLiveness,
|
|
389
357
|
openStream: async (kind, opts = {}) => {
|
|
390
358
|
if (kind == null || kind === "")
|
|
@@ -408,6 +376,7 @@ export async function connectCore(args) {
|
|
|
408
376
|
};
|
|
409
377
|
const signal = opts.signal;
|
|
410
378
|
let abortListener;
|
|
379
|
+
let abortReset;
|
|
411
380
|
let s;
|
|
412
381
|
try {
|
|
413
382
|
s = await mux.openStream(signal === undefined ? {} : { signal });
|
|
@@ -421,12 +390,7 @@ export async function connectCore(args) {
|
|
|
421
390
|
}
|
|
422
391
|
if (signal != null) {
|
|
423
392
|
abortListener = () => {
|
|
424
|
-
|
|
425
|
-
s.reset(abortReason(signal));
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
// ignore
|
|
429
|
-
}
|
|
393
|
+
abortReset ??= captureAsyncCleanup(() => Promise.resolve(s.reset(abortReason(signal))));
|
|
430
394
|
};
|
|
431
395
|
signal.addEventListener("abort", abortListener, { once: true });
|
|
432
396
|
if (signal.aborted)
|
|
@@ -435,18 +399,19 @@ export async function connectCore(args) {
|
|
|
435
399
|
if (signal?.aborted) {
|
|
436
400
|
if (abortListener != null)
|
|
437
401
|
signal.removeEventListener("abort", abortListener);
|
|
438
|
-
|
|
439
|
-
await
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
402
|
+
const cleanupError = await captureAsyncCleanup(async () => {
|
|
403
|
+
const resetError = abortReset == null ? undefined : await abortReset;
|
|
404
|
+
const closeError = await captureAsyncCleanup(() => s.close());
|
|
405
|
+
const combined = errorsFrom([resetError, closeError]);
|
|
406
|
+
if (combined != null)
|
|
407
|
+
throw combined;
|
|
408
|
+
});
|
|
444
409
|
throw new FlowersecError({
|
|
445
410
|
path: args.path,
|
|
446
411
|
stage: "yamux",
|
|
447
412
|
code: "canceled",
|
|
448
413
|
message: "open stream aborted",
|
|
449
|
-
cause: signal.reason,
|
|
414
|
+
cause: causeWithCleanup(signal.reason, cleanupError, "aborted stream cleanup failed"),
|
|
450
415
|
});
|
|
451
416
|
}
|
|
452
417
|
try {
|
|
@@ -456,34 +421,30 @@ export async function connectCore(args) {
|
|
|
456
421
|
if (signal?.aborted) {
|
|
457
422
|
if (abortListener != null)
|
|
458
423
|
signal.removeEventListener("abort", abortListener);
|
|
459
|
-
|
|
460
|
-
await
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
424
|
+
const cleanupError = await captureAsyncCleanup(async () => {
|
|
425
|
+
const resetError = abortReset == null ? undefined : await abortReset;
|
|
426
|
+
const closeError = await captureAsyncCleanup(() => s.close());
|
|
427
|
+
const combined = errorsFrom([resetError, closeError]);
|
|
428
|
+
if (combined != null)
|
|
429
|
+
throw combined;
|
|
430
|
+
});
|
|
465
431
|
throw new FlowersecError({
|
|
466
432
|
path: args.path,
|
|
467
433
|
stage: "yamux",
|
|
468
434
|
code: "canceled",
|
|
469
435
|
message: "open stream aborted",
|
|
470
|
-
cause: signal.reason,
|
|
436
|
+
cause: causeWithCleanup(signal.reason, cleanupError, "aborted stream cleanup failed"),
|
|
471
437
|
});
|
|
472
438
|
}
|
|
473
439
|
if (signal != null && abortListener != null)
|
|
474
440
|
signal.removeEventListener("abort", abortListener);
|
|
475
|
-
|
|
476
|
-
await s.close();
|
|
477
|
-
}
|
|
478
|
-
catch {
|
|
479
|
-
// ignore
|
|
480
|
-
}
|
|
441
|
+
const cleanupError = await captureAsyncCleanup(() => s.close());
|
|
481
442
|
throw new FlowersecError({
|
|
482
443
|
path: args.path,
|
|
483
444
|
stage: "rpc",
|
|
484
445
|
code: "stream_hello_failed",
|
|
485
446
|
message: "stream hello failed",
|
|
486
|
-
cause: err,
|
|
447
|
+
cause: causeWithCleanup(err, cleanupError, "stream hello cleanup failed"),
|
|
487
448
|
});
|
|
488
449
|
}
|
|
489
450
|
if (signal != null && abortListener != null) {
|
|
@@ -499,14 +460,65 @@ export async function connectCore(args) {
|
|
|
499
460
|
return client;
|
|
500
461
|
}
|
|
501
462
|
catch (e) {
|
|
463
|
+
const cleanupError = captureSyncCleanup(() => transport.close());
|
|
464
|
+
throw cleanupError == null ? e : errorWithCleanup(e, cleanupError, "transport cleanup failed");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function runSyncCleanups(actions) {
|
|
468
|
+
const failures = [];
|
|
469
|
+
for (const action of actions) {
|
|
502
470
|
try {
|
|
503
|
-
|
|
471
|
+
action();
|
|
504
472
|
}
|
|
505
|
-
catch {
|
|
506
|
-
|
|
473
|
+
catch (error) {
|
|
474
|
+
failures.push(errorValue(error));
|
|
507
475
|
}
|
|
508
|
-
throw e;
|
|
509
476
|
}
|
|
477
|
+
if (failures.length > 0)
|
|
478
|
+
throw new AggregateError(failures, "Flowersec cleanup failed");
|
|
479
|
+
}
|
|
480
|
+
function captureSyncCleanup(action) {
|
|
481
|
+
try {
|
|
482
|
+
action();
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
return error;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function captureAsyncCleanup(action) {
|
|
490
|
+
try {
|
|
491
|
+
await action();
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
return error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function causeWithCleanup(primary, cleanup, message) {
|
|
499
|
+
if (cleanup === undefined)
|
|
500
|
+
return primary;
|
|
501
|
+
return new AggregateError([errorValue(primary), errorValue(cleanup)], message);
|
|
502
|
+
}
|
|
503
|
+
function errorWithCleanup(primary, cleanup, message) {
|
|
504
|
+
const cause = causeWithCleanup(primary, cleanup, message);
|
|
505
|
+
if (primary instanceof FlowersecError) {
|
|
506
|
+
return new FlowersecError({
|
|
507
|
+
path: primary.path,
|
|
508
|
+
stage: primary.stage,
|
|
509
|
+
code: primary.code,
|
|
510
|
+
message,
|
|
511
|
+
cause,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return new AggregateError([errorValue(primary), errorValue(cleanup)], message);
|
|
515
|
+
}
|
|
516
|
+
function errorsFrom(values) {
|
|
517
|
+
const failures = values.filter((value) => value !== undefined).map(errorValue);
|
|
518
|
+
return failures.length === 0 ? undefined : new AggregateError(failures, "Flowersec cleanup failed");
|
|
519
|
+
}
|
|
520
|
+
function errorValue(error) {
|
|
521
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
510
522
|
}
|
|
511
523
|
function validateLimitObject(input, name, invalid) {
|
|
512
524
|
if (input === undefined)
|
|
@@ -2,7 +2,16 @@ import type { ClientPath } from "../client.js";
|
|
|
2
2
|
import { type ClientObserverLike } from "../observability/observer.js";
|
|
3
3
|
export declare const RequireTLS: "require_tls";
|
|
4
4
|
export declare const AllowPlaintextForLoopback: "allow_plaintext_for_loopback";
|
|
5
|
+
/** @deprecated Use RequireTLS, AllowPlaintextForLoopback, or createNetworkPlaintextPolicy. */
|
|
5
6
|
export declare const AllowPlaintext: "allow_plaintext";
|
|
7
|
+
export declare const PlaintextRiskAcceptance: {
|
|
8
|
+
readonly acceptPreE2ECredentialExposure: "accept_pre_e2ee_credential_exposure";
|
|
9
|
+
};
|
|
10
|
+
export type PlaintextRiskAcceptance = typeof PlaintextRiskAcceptance[keyof typeof PlaintextRiskAcceptance];
|
|
11
|
+
export type NetworkPlaintextPolicyOptions = Readonly<{
|
|
12
|
+
allowedHosts: readonly string[];
|
|
13
|
+
riskAcceptance: PlaintextRiskAcceptance;
|
|
14
|
+
}>;
|
|
6
15
|
export type TransportSecurityPolicyInput = Readonly<{
|
|
7
16
|
path: ClientPath;
|
|
8
17
|
scheme: "ws" | "wss";
|
|
@@ -11,6 +20,7 @@ export type TransportSecurityPolicyInput = Readonly<{
|
|
|
11
20
|
}>;
|
|
12
21
|
export type TransportSecurityPolicyPreset = typeof RequireTLS | typeof AllowPlaintextForLoopback | typeof AllowPlaintext;
|
|
13
22
|
export type TransportSecurityPolicy = TransportSecurityPolicyPreset | ((input: TransportSecurityPolicyInput) => boolean | Promise<boolean>);
|
|
23
|
+
export declare function createNetworkPlaintextPolicy(options: NetworkPlaintextPolicyOptions): TransportSecurityPolicy;
|
|
14
24
|
export declare function enforceTransportSecurity(args: Readonly<{
|
|
15
25
|
rawUrl: string;
|
|
16
26
|
path: ClientPath;
|
|
@@ -2,7 +2,21 @@ import { FlowersecError } from "../utils/errors.js";
|
|
|
2
2
|
import { emitObserverDiagnostic } from "../observability/observer.js";
|
|
3
3
|
export const RequireTLS = "require_tls";
|
|
4
4
|
export const AllowPlaintextForLoopback = "allow_plaintext_for_loopback";
|
|
5
|
+
/** @deprecated Use RequireTLS, AllowPlaintextForLoopback, or createNetworkPlaintextPolicy. */
|
|
5
6
|
export const AllowPlaintext = "allow_plaintext";
|
|
7
|
+
export const PlaintextRiskAcceptance = {
|
|
8
|
+
acceptPreE2ECredentialExposure: "accept_pre_e2ee_credential_exposure",
|
|
9
|
+
};
|
|
10
|
+
export function createNetworkPlaintextPolicy(options) {
|
|
11
|
+
if (options?.riskAcceptance !== PlaintextRiskAcceptance.acceptPreE2ECredentialExposure) {
|
|
12
|
+
throw new Error("network plaintext policy requires explicit pre-E2EE credential exposure acceptance");
|
|
13
|
+
}
|
|
14
|
+
if (!Array.isArray(options.allowedHosts) || options.allowedHosts.length === 0) {
|
|
15
|
+
throw new Error("network plaintext policy requires at least one allowed host");
|
|
16
|
+
}
|
|
17
|
+
const allowedHosts = new Set(options.allowedHosts.map(canonicalNetworkPlaintextHost));
|
|
18
|
+
return (input) => input.scheme === "wss" || (input.scheme === "ws" && allowedHosts.has(input.host));
|
|
19
|
+
}
|
|
6
20
|
export async function enforceTransportSecurity(args) {
|
|
7
21
|
let target;
|
|
8
22
|
try {
|
|
@@ -105,6 +119,81 @@ function isLiteralLoopbackHost(host) {
|
|
|
105
119
|
}
|
|
106
120
|
return octets[0] === 127;
|
|
107
121
|
}
|
|
122
|
+
function canonicalNetworkPlaintextHost(rawHost) {
|
|
123
|
+
const host = String(rawHost ?? "").trim();
|
|
124
|
+
if (host === "" || host !== host.toLowerCase() || /[@/?#%\[\]]/.test(host)) {
|
|
125
|
+
throw new Error(`invalid network plaintext allowed host ${JSON.stringify(rawHost)}`);
|
|
126
|
+
}
|
|
127
|
+
if (host.includes(":")) {
|
|
128
|
+
return canonicalNetworkIPv6Host(host);
|
|
129
|
+
}
|
|
130
|
+
const octets = canonicalIPv4Octets(host);
|
|
131
|
+
const firstOctet = octets[0];
|
|
132
|
+
const secondOctet = octets[1];
|
|
133
|
+
if (firstOctet == null || secondOctet == null) {
|
|
134
|
+
throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(rawHost)}`);
|
|
135
|
+
}
|
|
136
|
+
if (firstOctet === 127 || (firstOctet === 169 && secondOctet === 254) || firstOctet >= 224 || octets.every((value) => value === 0)) {
|
|
137
|
+
throw new Error(`network plaintext allowed host must be a non-loopback unicast IP literal: ${JSON.stringify(rawHost)}`);
|
|
138
|
+
}
|
|
139
|
+
return host;
|
|
140
|
+
}
|
|
141
|
+
function canonicalIPv4Octets(host) {
|
|
142
|
+
const parts = host.split(".");
|
|
143
|
+
if (parts.length !== 4) {
|
|
144
|
+
throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
|
|
145
|
+
}
|
|
146
|
+
const octets = parts.map((part) => {
|
|
147
|
+
if (!/^(0|[1-9]\d{0,2})$/.test(part)) {
|
|
148
|
+
throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
|
|
149
|
+
}
|
|
150
|
+
const value = Number(part);
|
|
151
|
+
if (value > 255) {
|
|
152
|
+
throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
});
|
|
156
|
+
return octets;
|
|
157
|
+
}
|
|
158
|
+
function canonicalNetworkIPv6Host(host) {
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = new URL(`http://[${host}]/`);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
|
|
165
|
+
}
|
|
166
|
+
const canonical = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
167
|
+
if (canonical !== host) {
|
|
168
|
+
throw new Error(`network plaintext allowed host must be a canonical IP literal: ${JSON.stringify(host)}`);
|
|
169
|
+
}
|
|
170
|
+
const words = expandIPv6Words(canonical);
|
|
171
|
+
const unspecified = words.every((word) => word === 0);
|
|
172
|
+
const loopback = words.slice(0, 7).every((word) => word === 0) && words[7] === 1;
|
|
173
|
+
const mappedIPv4 = words.slice(0, 5).every((word) => word === 0) && words[5] === 0xffff;
|
|
174
|
+
const multicast = (words[0] & 0xff00) === 0xff00;
|
|
175
|
+
const linkLocal = (words[0] & 0xffc0) === 0xfe80;
|
|
176
|
+
if (unspecified || loopback || mappedIPv4 || multicast || linkLocal) {
|
|
177
|
+
throw new Error(`network plaintext allowed host must be a non-loopback unicast IP literal: ${JSON.stringify(host)}`);
|
|
178
|
+
}
|
|
179
|
+
return canonical;
|
|
180
|
+
}
|
|
181
|
+
function expandIPv6Words(host) {
|
|
182
|
+
const halves = host.split("::");
|
|
183
|
+
if (halves.length > 2)
|
|
184
|
+
throw new Error("invalid IPv6 host");
|
|
185
|
+
const left = halves[0] === "" ? [] : halves[0].split(":");
|
|
186
|
+
const right = halves.length === 1 || halves[1] === "" ? [] : halves[1].split(":");
|
|
187
|
+
const missing = 8 - left.length - right.length;
|
|
188
|
+
if ((halves.length === 1 && missing !== 0) || (halves.length === 2 && missing < 1)) {
|
|
189
|
+
throw new Error("invalid IPv6 host");
|
|
190
|
+
}
|
|
191
|
+
const words = [...left, ...Array.from({ length: missing }, () => "0"), ...right].map((part) => Number.parseInt(part, 16));
|
|
192
|
+
if (words.length !== 8 || words.some((word) => !Number.isInteger(word) || word < 0 || word > 0xffff)) {
|
|
193
|
+
throw new Error("invalid IPv6 host");
|
|
194
|
+
}
|
|
195
|
+
return words;
|
|
196
|
+
}
|
|
108
197
|
function detectRuntime() {
|
|
109
198
|
if (typeof window !== "undefined" && typeof window.document !== "undefined")
|
|
110
199
|
return "browser";
|
package/dist/client.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type Client = Readonly<{
|
|
|
11
11
|
signal?: AbortSignal;
|
|
12
12
|
}>) => Promise<YamuxStream>;
|
|
13
13
|
ping: () => Promise<void>;
|
|
14
|
+
/** Emits an authenticated rekey record and advances the E2EE send key. */
|
|
15
|
+
rekey: () => Promise<void>;
|
|
14
16
|
/** Performs an acknowledged Yamux round trip and returns RTT in milliseconds. */
|
|
15
17
|
probeLiveness: () => Promise<number>;
|
|
16
18
|
close: () => void;
|
|
@@ -77,7 +77,9 @@ async function readControlplaneText(response, maxBytes) {
|
|
|
77
77
|
try {
|
|
78
78
|
await reader.cancel();
|
|
79
79
|
}
|
|
80
|
-
catch {
|
|
80
|
+
catch {
|
|
81
|
+
// The size violation is authoritative; cancellation is secondary cleanup.
|
|
82
|
+
}
|
|
81
83
|
throw new ControlplaneResponseTooLargeError(maxBytes);
|
|
82
84
|
}
|
|
83
85
|
text += decoder.decode(chunk.value, { stream: true });
|
package/dist/endpoint/index.d.ts
CHANGED
package/dist/endpoint/index.js
CHANGED
|
@@ -85,6 +85,14 @@ export class Session {
|
|
|
85
85
|
probeLiveness(timeoutMs = SDK_DEFAULTS.transport.handshakeTimeoutMs) {
|
|
86
86
|
return this.mux.probeLiveness(timeoutMs);
|
|
87
87
|
}
|
|
88
|
+
async rekey() {
|
|
89
|
+
try {
|
|
90
|
+
await this.secure.rekeyNow();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw new FlowersecError({ path: this.path, stage: "secure", code: "rekey_failed", message: "endpoint rekey failed", cause: error });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
88
96
|
close() {
|
|
89
97
|
this.fail(new Error("endpoint session closed"));
|
|
90
98
|
this.mux.close();
|
package/dist/facade.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ export type { WebSocketLimits } from "./ws-client/binaryTransport.js";
|
|
|
17
17
|
export type { YamuxLimits } from "./yamux/session.js";
|
|
18
18
|
export type { FlowersecErrorCode, FlowersecPath, FlowersecStage } from "./utils/errors.js";
|
|
19
19
|
export { FlowersecError } from "./utils/errors.js";
|
|
20
|
-
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "./client-connect/transportSecurity.js";
|
|
21
|
-
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "./client-connect/transportSecurity.js";
|
|
20
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
|
|
21
|
+
export type { NetworkPlaintextPolicyOptions, TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "./client-connect/transportSecurity.js";
|
|
22
22
|
export type { TunnelConnectOptions } from "./tunnel-client/connect.js";
|
|
23
23
|
export type { DirectConnectOptions } from "./direct-client/connect.js";
|
|
24
24
|
export type ConnectOptions = TunnelConnectOptions | DirectConnectOptions;
|
package/dist/facade.js
CHANGED
|
@@ -6,7 +6,7 @@ export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
|
|
|
6
6
|
export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
|
|
7
7
|
export { assertConnectArtifact } from "./connect/artifact.js";
|
|
8
8
|
export { FlowersecError } from "./utils/errors.js";
|
|
9
|
-
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "./client-connect/transportSecurity.js";
|
|
9
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "./client-connect/transportSecurity.js";
|
|
10
10
|
export async function connectTunnel(grant, opts) {
|
|
11
11
|
return await connectTunnelInternal(grant, opts);
|
|
12
12
|
}
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
|
-
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
|
-
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, } from "../client-connect/transportSecurity.js";
|
|
2
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
|
+
export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, NetworkPlaintextPolicyOptions, } from "../client-connect/transportSecurity.js";
|
|
4
4
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
5
5
|
export type { DirectNodeReconnectConfig, NodeReconnectConfig, TunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
|
6
6
|
export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
package/dist/node/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
|
-
export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
2
|
+
export { AllowPlaintext, AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
|
|
3
3
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
4
4
|
export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
|
5
5
|
export * from "../endpoint/index.js";
|
|
@@ -78,11 +78,10 @@ function bridgeWebSocket(runtime, msg, port) {
|
|
|
78
78
|
terminal = true;
|
|
79
79
|
acceptingWrites = false;
|
|
80
80
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
81
|
-
|
|
82
|
-
stream
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Best-effort.
|
|
81
|
+
if (stream != null) {
|
|
82
|
+
void Promise.resolve(stream.reset(err)).catch(() => {
|
|
83
|
+
// The bridge error is already delivered through the terminal response.
|
|
84
|
+
});
|
|
86
85
|
}
|
|
87
86
|
try {
|
|
88
87
|
ac.abort(err.message);
|
package/dist/proxy/runtime.js
CHANGED
|
@@ -416,13 +416,13 @@ export function createProxyRuntime(opts) {
|
|
|
416
416
|
}
|
|
417
417
|
if (!respMeta.ok) {
|
|
418
418
|
const msg = respMeta.error?.message ?? "upstream error";
|
|
419
|
-
port.postMessage({
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
419
|
+
port.postMessage({
|
|
420
|
+
type: "flowersec-proxy:response_error",
|
|
421
|
+
status: 502,
|
|
422
|
+
...(respMeta.error?.code === undefined ? {} : { code: respMeta.error.code }),
|
|
423
|
+
message: msg,
|
|
424
|
+
});
|
|
425
|
+
await stream.reset(new Error(msg));
|
|
426
426
|
stream = null;
|
|
427
427
|
return;
|
|
428
428
|
}
|
|
@@ -455,14 +455,10 @@ export function createProxyRuntime(opts) {
|
|
|
455
455
|
port.postMessage({
|
|
456
456
|
type: "flowersec-proxy:response_error",
|
|
457
457
|
status,
|
|
458
|
+
...(code === undefined ? {} : { code }),
|
|
458
459
|
message: msg,
|
|
459
460
|
});
|
|
460
|
-
|
|
461
|
-
stream?.reset(new Error(msg));
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
// Best-effort.
|
|
465
|
-
}
|
|
461
|
+
await stream?.reset(new Error(msg));
|
|
466
462
|
}
|
|
467
463
|
finally {
|
|
468
464
|
wakeResponseCreditWaiter();
|
|
@@ -494,12 +490,7 @@ export function createProxyRuntime(opts) {
|
|
|
494
490
|
const resp = (await readJsonFrame(reader, maxJsonFrameBytes));
|
|
495
491
|
if (resp.v !== PROXY_PROTOCOL_VERSION || resp.ok !== true) {
|
|
496
492
|
const msg = resp.error?.message ?? "upstream ws open failed";
|
|
497
|
-
|
|
498
|
-
stream.reset(new Error(msg));
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
// Best-effort.
|
|
502
|
-
}
|
|
493
|
+
await stream.reset(new Error(msg));
|
|
503
494
|
throw new Error(msg);
|
|
504
495
|
}
|
|
505
496
|
return { stream, protocol: resp.protocol ?? "" };
|
package/dist/reconnect/index.js
CHANGED
|
@@ -3,24 +3,30 @@ import { emitObserverDiagnostic, withObserverContext } from "../observability/ob
|
|
|
3
3
|
import { SDK_DEFAULTS } from "../defaults.js";
|
|
4
4
|
export { createArtifactResolver, createControlplaneArtifactSource } from "./artifactControlplane.js";
|
|
5
5
|
function normalizeAutoReconnect(cfg) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
jitterRatio: SDK_DEFAULTS.reconnect.jitterRatio,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
return {
|
|
17
|
-
enabled: true,
|
|
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),
|
|
6
|
+
const settings = {
|
|
7
|
+
enabled: cfg?.enabled === true,
|
|
8
|
+
maxAttempts: cfg?.enabled === true ? (cfg.maxAttempts ?? SDK_DEFAULTS.reconnect.maxAttempts) : 1,
|
|
9
|
+
initialDelayMs: cfg?.initialDelayMs ?? SDK_DEFAULTS.reconnect.initialDelayMs,
|
|
10
|
+
maxDelayMs: cfg?.maxDelayMs ?? SDK_DEFAULTS.reconnect.maxDelayMs,
|
|
11
|
+
factor: cfg?.factor ?? SDK_DEFAULTS.reconnect.factor,
|
|
12
|
+
jitterRatio: cfg?.jitterRatio ?? SDK_DEFAULTS.reconnect.jitterRatio,
|
|
23
13
|
};
|
|
14
|
+
if (!Number.isSafeInteger(settings.maxAttempts) || settings.maxAttempts < 1) {
|
|
15
|
+
throw new TypeError("autoReconnect.maxAttempts must be a positive integer");
|
|
16
|
+
}
|
|
17
|
+
if (!Number.isFinite(settings.initialDelayMs) || settings.initialDelayMs < 0) {
|
|
18
|
+
throw new TypeError("autoReconnect.initialDelayMs must be a non-negative finite number");
|
|
19
|
+
}
|
|
20
|
+
if (!Number.isFinite(settings.maxDelayMs) || settings.maxDelayMs < 0) {
|
|
21
|
+
throw new TypeError("autoReconnect.maxDelayMs must be a non-negative finite number");
|
|
22
|
+
}
|
|
23
|
+
if (!Number.isFinite(settings.factor) || settings.factor < 1) {
|
|
24
|
+
throw new TypeError("autoReconnect.factor must be a finite number greater than or equal to one");
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isFinite(settings.jitterRatio) || settings.jitterRatio < 0 || settings.jitterRatio > 1) {
|
|
27
|
+
throw new TypeError("autoReconnect.jitterRatio must be a finite number between zero and one");
|
|
28
|
+
}
|
|
29
|
+
return settings;
|
|
24
30
|
}
|
|
25
31
|
function backoffDelayMs(attemptIndex, cfg) {
|
|
26
32
|
const base = Math.min(cfg.maxDelayMs, cfg.initialDelayMs * Math.pow(cfg.factor, attemptIndex));
|
|
@@ -73,13 +79,19 @@ export function createReconnectManager() {
|
|
|
73
79
|
retryResolve = null;
|
|
74
80
|
};
|
|
75
81
|
const abortActiveAttempt = () => {
|
|
82
|
+
attemptAbort?.abort("canceled");
|
|
83
|
+
attemptAbort = null;
|
|
84
|
+
};
|
|
85
|
+
const closeClient = (value) => {
|
|
86
|
+
if (value == null)
|
|
87
|
+
return null;
|
|
76
88
|
try {
|
|
77
|
-
|
|
89
|
+
value.close();
|
|
90
|
+
return null;
|
|
78
91
|
}
|
|
79
|
-
catch {
|
|
80
|
-
|
|
92
|
+
catch (error) {
|
|
93
|
+
return asError(error);
|
|
81
94
|
}
|
|
82
|
-
attemptAbort = null;
|
|
83
95
|
};
|
|
84
96
|
const sleep = (ms) => new Promise((resolve) => {
|
|
85
97
|
retryResolve = resolve;
|
|
@@ -96,13 +108,10 @@ export function createReconnectManager() {
|
|
|
96
108
|
activeConnectPromise = null;
|
|
97
109
|
token += 1;
|
|
98
110
|
attemptSeq = 0;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
catch {
|
|
104
|
-
// ignore
|
|
105
|
-
}
|
|
111
|
+
const closeError = closeClient(s.client);
|
|
112
|
+
if (closeError != null) {
|
|
113
|
+
setState({ status: "error", error: closeError, client: null });
|
|
114
|
+
throw closeError;
|
|
106
115
|
}
|
|
107
116
|
setState({ status: "disconnected", error: null, client: null });
|
|
108
117
|
};
|
|
@@ -115,27 +124,24 @@ export function createReconnectManager() {
|
|
|
115
124
|
return;
|
|
116
125
|
const ar = normalizeAutoReconnect(cfg.autoReconnect);
|
|
117
126
|
if (!ar.enabled) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// ignore
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
setState({ status: "error", error, client: null });
|
|
127
|
+
const closeError = closeClient(s.client);
|
|
128
|
+
const finalError = closeError == null
|
|
129
|
+
? error
|
|
130
|
+
: new AggregateError([error, closeError], "reconnect termination and client close failed");
|
|
131
|
+
setState({ status: "error", error: finalError, client: null });
|
|
127
132
|
return;
|
|
128
133
|
}
|
|
129
134
|
// Restart the connection loop in the background.
|
|
130
135
|
cancelRetrySleep();
|
|
131
136
|
abortActiveAttempt();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
137
|
+
const closeError = closeClient(s.client);
|
|
138
|
+
if (closeError != null) {
|
|
139
|
+
setState({
|
|
140
|
+
status: "error",
|
|
141
|
+
error: new AggregateError([error, closeError], "reconnect termination and client close failed"),
|
|
142
|
+
client: null,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
139
145
|
}
|
|
140
146
|
token += 1;
|
|
141
147
|
const nextToken = token;
|
|
@@ -198,21 +204,15 @@ export function createReconnectManager() {
|
|
|
198
204
|
try {
|
|
199
205
|
const client = await connectOnce(t, cfg, attemptSeq);
|
|
200
206
|
if (t !== token) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
catch {
|
|
205
|
-
// ignore
|
|
206
|
-
}
|
|
207
|
+
const closeError = closeClient(client);
|
|
208
|
+
if (closeError != null)
|
|
209
|
+
throw new ReconnectCleanupError(closeError);
|
|
207
210
|
return;
|
|
208
211
|
}
|
|
209
212
|
if (active !== cfg) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
catch {
|
|
214
|
-
// ignore
|
|
215
|
-
}
|
|
213
|
+
const closeError = closeClient(client);
|
|
214
|
+
if (closeError != null)
|
|
215
|
+
throw new ReconnectCleanupError(closeError);
|
|
216
216
|
return;
|
|
217
217
|
}
|
|
218
218
|
setState({ status: "connected", client, error: null });
|
|
@@ -222,6 +222,10 @@ export function createReconnectManager() {
|
|
|
222
222
|
if (s.client !== client)
|
|
223
223
|
return;
|
|
224
224
|
startReconnect(t, cfg, error);
|
|
225
|
+
}, (error) => {
|
|
226
|
+
if (s.client !== client)
|
|
227
|
+
return;
|
|
228
|
+
startReconnect(t, cfg, asError(error));
|
|
225
229
|
});
|
|
226
230
|
}
|
|
227
231
|
emitObserverDiagnostic(withObserverContext(cfg.observer, { attemptSeq }), {
|
|
@@ -234,6 +238,8 @@ export function createReconnectManager() {
|
|
|
234
238
|
return;
|
|
235
239
|
}
|
|
236
240
|
catch (err) {
|
|
241
|
+
if (err instanceof ReconnectCleanupError)
|
|
242
|
+
throw err.cleanupError;
|
|
237
243
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
238
244
|
if (t !== token)
|
|
239
245
|
return;
|
|
@@ -281,19 +287,18 @@ export function createReconnectManager() {
|
|
|
281
287
|
return promise;
|
|
282
288
|
};
|
|
283
289
|
const connect = async (cfg) => {
|
|
290
|
+
normalizeAutoReconnect(cfg.autoReconnect);
|
|
284
291
|
cancelRetrySleep();
|
|
285
292
|
abortActiveAttempt();
|
|
286
293
|
token += 1;
|
|
287
294
|
const t = token;
|
|
288
295
|
active = cfg;
|
|
289
296
|
attemptSeq = 0;
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ignore
|
|
296
|
-
}
|
|
297
|
+
const closeError = closeClient(s.client);
|
|
298
|
+
if (closeError != null) {
|
|
299
|
+
active = null;
|
|
300
|
+
setState({ status: "error", error: closeError, client: null });
|
|
301
|
+
throw closeError;
|
|
297
302
|
}
|
|
298
303
|
const connectPromise = startConnectLoop(t, cfg);
|
|
299
304
|
setState({ status: "connecting", error: null, client: null });
|
|
@@ -333,3 +338,14 @@ export function createReconnectManager() {
|
|
|
333
338
|
disconnect,
|
|
334
339
|
};
|
|
335
340
|
}
|
|
341
|
+
class ReconnectCleanupError extends Error {
|
|
342
|
+
cleanupError;
|
|
343
|
+
constructor(cleanupError) {
|
|
344
|
+
super("reconnect cleanup failed", { cause: cleanupError });
|
|
345
|
+
this.cleanupError = cleanupError;
|
|
346
|
+
this.name = "ReconnectCleanupError";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function asError(value) {
|
|
350
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
351
|
+
}
|
package/dist/rpc/server.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export declare class RpcServer {
|
|
|
32
32
|
private readonly terminalSignal;
|
|
33
33
|
private signalTerminal;
|
|
34
34
|
private transportClosed;
|
|
35
|
+
private admittedRequests;
|
|
35
36
|
constructor(transport: RpcServerTransport, options?: RpcServerOptions, router?: RpcRouter);
|
|
36
37
|
register(typeId: number, h: RpcHandler): void;
|
|
37
38
|
notify(typeId: number, payload: unknown): Promise<void>;
|
package/dist/rpc/server.js
CHANGED
|
@@ -31,6 +31,7 @@ export class RpcServer {
|
|
|
31
31
|
terminalSignal;
|
|
32
32
|
signalTerminal;
|
|
33
33
|
transportClosed = false;
|
|
34
|
+
admittedRequests = 0;
|
|
34
35
|
constructor(transport, options = {}, router = new RpcRouter()) {
|
|
35
36
|
this.transport = transport;
|
|
36
37
|
this.router = router;
|
|
@@ -57,11 +58,15 @@ export class RpcServer {
|
|
|
57
58
|
}
|
|
58
59
|
// serve handles request/response frames until closed or aborted.
|
|
59
60
|
async serve(signal) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const workers = Array.from({ length: this.options.maxConcurrentRequests }, () => this.requestWorker());
|
|
62
|
+
workers.push(this.notificationWorker());
|
|
63
|
+
const workerFailure = Promise.race(workers.map(async (worker) => {
|
|
64
|
+
await worker;
|
|
65
|
+
if (!this.closed)
|
|
66
|
+
throw new Error("rpc worker ended before server shutdown");
|
|
67
|
+
return await new Promise(() => undefined);
|
|
68
|
+
}));
|
|
69
|
+
let failure;
|
|
65
70
|
try {
|
|
66
71
|
while (!this.closed) {
|
|
67
72
|
if (signal?.aborted)
|
|
@@ -69,6 +74,7 @@ export class RpcServer {
|
|
|
69
74
|
const next = await Promise.race([
|
|
70
75
|
readJsonFrame(this.transport.readExactly, DEFAULT_MAX_JSON_FRAME_BYTES),
|
|
71
76
|
this.terminalSignal.then((error) => { throw error; }),
|
|
77
|
+
workerFailure,
|
|
72
78
|
]);
|
|
73
79
|
const v = assertRpcEnvelope(next);
|
|
74
80
|
if (v.response_to !== 0)
|
|
@@ -81,23 +87,38 @@ export class RpcServer {
|
|
|
81
87
|
this.wakeOne(this.notificationWaiters);
|
|
82
88
|
continue;
|
|
83
89
|
}
|
|
84
|
-
if (this.
|
|
90
|
+
if (this.admittedRequests >= this.options.maxConcurrentRequests + this.options.maxQueuedRequests) {
|
|
85
91
|
await this.writeResponse(v, { payload: null, error: { code: 429, message: "server overloaded" } });
|
|
86
92
|
continue;
|
|
87
93
|
}
|
|
94
|
+
this.admittedRequests += 1;
|
|
88
95
|
this.requests.push({ envelope: v });
|
|
89
96
|
this.wakeOne(this.requestWaiters);
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
99
|
catch (err) {
|
|
100
|
+
failure = err;
|
|
93
101
|
this.terminalError = err;
|
|
94
|
-
this.close(err);
|
|
95
|
-
throw err;
|
|
96
102
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
let closeError;
|
|
104
|
+
try {
|
|
105
|
+
this.close(failure ?? this.terminalError ?? new Error("rpc server closed"));
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
closeError = error;
|
|
100
109
|
}
|
|
110
|
+
const settled = await Promise.allSettled(workers);
|
|
111
|
+
const workerErrors = settled
|
|
112
|
+
.filter((result) => result.status === "rejected")
|
|
113
|
+
.map((result) => result.reason)
|
|
114
|
+
.filter((error) => error !== failure);
|
|
115
|
+
const errors = [failure, closeError, ...workerErrors].filter((error) => error !== undefined);
|
|
116
|
+
if (errors.length === 1)
|
|
117
|
+
throw errors[0];
|
|
118
|
+
if (errors.length > 1)
|
|
119
|
+
throw new AggregateError(errors, "rpc server and cleanup failed");
|
|
120
|
+
if (this.terminalError !== undefined)
|
|
121
|
+
throw this.terminalError;
|
|
101
122
|
}
|
|
102
123
|
// close stops the serve loop and closes the underlying RPC stream.
|
|
103
124
|
close(error = new Error("rpc server closed")) {
|
|
@@ -138,9 +159,14 @@ export class RpcServer {
|
|
|
138
159
|
out = { payload: null, error: { code: 500, message: "internal error" } };
|
|
139
160
|
}
|
|
140
161
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
162
|
+
try {
|
|
163
|
+
if (this.closed)
|
|
164
|
+
return;
|
|
165
|
+
await this.writeResponse(v, out);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
this.admittedRequests = Math.max(0, this.admittedRequests - 1);
|
|
169
|
+
}
|
|
144
170
|
}
|
|
145
171
|
}
|
|
146
172
|
async notificationWorker() {
|
|
@@ -152,10 +178,7 @@ export class RpcServer {
|
|
|
152
178
|
const h = this.router.handler(v.type_id);
|
|
153
179
|
if (h == null)
|
|
154
180
|
continue;
|
|
155
|
-
|
|
156
|
-
await h(v.payload);
|
|
157
|
-
}
|
|
158
|
-
catch { /* Notification failures are isolated. */ }
|
|
181
|
+
await h(v.payload);
|
|
159
182
|
}
|
|
160
183
|
}
|
|
161
184
|
async nextWork(queue, waiters) {
|
|
@@ -182,8 +205,14 @@ export class RpcServer {
|
|
|
182
205
|
}
|
|
183
206
|
async writeEnvelope(envelope) {
|
|
184
207
|
const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, envelope));
|
|
185
|
-
this.writeChain = write
|
|
186
|
-
|
|
208
|
+
this.writeChain = write;
|
|
209
|
+
try {
|
|
210
|
+
await write;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
this.fail(error);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
187
216
|
}
|
|
188
217
|
}
|
|
189
218
|
function positiveInteger(value, name) {
|
package/dist/streamio/index.js
CHANGED
|
@@ -11,12 +11,9 @@ function abortReasonToError(signal) {
|
|
|
11
11
|
}
|
|
12
12
|
function bindAbortToStream(stream, signal) {
|
|
13
13
|
const onAbort = () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// Best-effort cancel.
|
|
19
|
-
}
|
|
14
|
+
void Promise.resolve(stream.reset(abortReasonToError(signal))).catch(() => {
|
|
15
|
+
// The read/write operation reports the authoritative abort to its caller.
|
|
16
|
+
});
|
|
20
17
|
};
|
|
21
18
|
if (signal.aborted) {
|
|
22
19
|
onAbort();
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare class AbortError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
export type FlowersecPath = "auto" | "tunnel" | "direct";
|
|
8
8
|
export type FlowersecStage = "validate" | "connect" | "attach" | "handshake" | "secure" | "yamux" | "rpc" | "close";
|
|
9
|
-
export type FlowersecErrorCode = "timeout" | "canceled" | "invalid_version" | "invalid_input" | "invalid_option" | "invalid_endpoint_instance_id" | "invalid_psk" | "invalid_suite" | "missing_grant" | "missing_connect_info" | "missing_conn" | "missing_handler" | "missing_stream_kind" | "role_mismatch" | "missing_tunnel_url" | "missing_ws_url" | "missing_origin" | "missing_channel_id" | "missing_token" | "missing_init_exp" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "auth_tag_mismatch" | "resolve_failed" | "transport_policy_denied" | "credential_commit_failed" | "random_failed" | "upgrade_failed" | "dial_failed" | "attach_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "init_exp_mismatch" | "idle_timeout_mismatch" | "token_replay" | "tenant_mismatch" | "policy_denied" | "policy_error" | "replace_rate_limited" | "handshake_failed" | "ping_failed" | "mux_failed" | "accept_stream_failed" | "open_stream_failed" | "stream_hello_failed" | "rpc_failed" | "resource_exhausted" | "not_connected";
|
|
9
|
+
export type FlowersecErrorCode = "timeout" | "canceled" | "invalid_version" | "invalid_input" | "invalid_option" | "invalid_endpoint_instance_id" | "invalid_psk" | "invalid_suite" | "missing_grant" | "missing_connect_info" | "missing_conn" | "missing_handler" | "missing_stream_kind" | "role_mismatch" | "missing_tunnel_url" | "missing_ws_url" | "missing_origin" | "missing_channel_id" | "missing_token" | "missing_init_exp" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "auth_tag_mismatch" | "resolve_failed" | "transport_policy_denied" | "credential_commit_failed" | "random_failed" | "upgrade_failed" | "dial_failed" | "attach_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "init_exp_mismatch" | "idle_timeout_mismatch" | "token_replay" | "tenant_mismatch" | "policy_denied" | "policy_error" | "replace_rate_limited" | "handshake_failed" | "ping_failed" | "rekey_failed" | "mux_failed" | "accept_stream_failed" | "open_stream_failed" | "stream_hello_failed" | "rpc_failed" | "resource_exhausted" | "not_connected";
|
|
10
10
|
export declare class FlowersecError extends Error {
|
|
11
11
|
readonly code: FlowersecErrorCode;
|
|
12
12
|
readonly stage: FlowersecStage;
|
package/dist/yamux/errors.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ export declare class StreamEOFError extends Error {
|
|
|
2
2
|
constructor(message?: string);
|
|
3
3
|
}
|
|
4
4
|
export declare function isStreamEOFError(e: unknown): e is StreamEOFError;
|
|
5
|
+
export declare class YamuxStreamResetError extends Error {
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
8
|
+
export declare function isYamuxStreamResetError(error: unknown): error is YamuxStreamResetError;
|
|
5
9
|
export declare class YamuxResourceExhaustedError extends Error {
|
|
6
10
|
readonly resource: string;
|
|
7
11
|
readonly current: number;
|
|
@@ -9,3 +13,7 @@ export declare class YamuxResourceExhaustedError extends Error {
|
|
|
9
13
|
constructor(resource: string, current: number, limit: number);
|
|
10
14
|
}
|
|
11
15
|
export declare function isYamuxResourceExhaustedError(error: unknown): error is YamuxResourceExhaustedError;
|
|
16
|
+
export declare class YamuxPingTimeoutError extends Error {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
export declare function isYamuxPingTimeoutError(error: unknown): error is YamuxPingTimeoutError;
|
package/dist/yamux/errors.js
CHANGED
|
@@ -8,6 +8,15 @@ export class StreamEOFError extends Error {
|
|
|
8
8
|
export function isStreamEOFError(e) {
|
|
9
9
|
return e instanceof StreamEOFError;
|
|
10
10
|
}
|
|
11
|
+
export class YamuxStreamResetError extends Error {
|
|
12
|
+
constructor() {
|
|
13
|
+
super("yamux stream reset");
|
|
14
|
+
this.name = "YamuxStreamResetError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isYamuxStreamResetError(error) {
|
|
18
|
+
return error instanceof YamuxStreamResetError;
|
|
19
|
+
}
|
|
11
20
|
export class YamuxResourceExhaustedError extends Error {
|
|
12
21
|
resource;
|
|
13
22
|
current;
|
|
@@ -23,3 +32,12 @@ export class YamuxResourceExhaustedError extends Error {
|
|
|
23
32
|
export function isYamuxResourceExhaustedError(error) {
|
|
24
33
|
return error instanceof YamuxResourceExhaustedError;
|
|
25
34
|
}
|
|
35
|
+
export class YamuxPingTimeoutError extends Error {
|
|
36
|
+
constructor() {
|
|
37
|
+
super("yamux ping timeout");
|
|
38
|
+
this.name = "YamuxPingTimeoutError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function isYamuxPingTimeoutError(error) {
|
|
42
|
+
return error instanceof YamuxPingTimeoutError;
|
|
43
|
+
}
|
package/dist/yamux/session.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ByteReader } from "./byteReader.js";
|
|
|
2
2
|
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
|
-
import { YamuxResourceExhaustedError } from "./errors.js";
|
|
5
|
+
import { YamuxPingTimeoutError, YamuxResourceExhaustedError } from "./errors.js";
|
|
6
6
|
import { SDK_DEFAULTS } from "../defaults.js";
|
|
7
7
|
export const DEFAULT_YAMUX_LIMITS = Object.freeze({
|
|
8
8
|
maxActiveStreams: SDK_DEFAULTS.yamux.maxActiveStreams,
|
|
@@ -129,7 +129,7 @@ export class YamuxSession {
|
|
|
129
129
|
return await new Promise((resolve, reject) => {
|
|
130
130
|
const timer = setTimeout(() => {
|
|
131
131
|
this.pingWaiters.delete(opaque);
|
|
132
|
-
const error = new
|
|
132
|
+
const error = new YamuxPingTimeoutError();
|
|
133
133
|
reject(error);
|
|
134
134
|
this.fail(error);
|
|
135
135
|
}, timeoutMs);
|
|
@@ -229,8 +229,11 @@ export class YamuxSession {
|
|
|
229
229
|
this.pingWaiters.clear();
|
|
230
230
|
const streams = Array.from(this.streams.values());
|
|
231
231
|
this.streams.clear();
|
|
232
|
-
for (const s of streams)
|
|
233
|
-
s.reset(new Error("session closed"))
|
|
232
|
+
for (const s of streams) {
|
|
233
|
+
void Promise.resolve(s.reset(new Error("session closed"))).catch(() => {
|
|
234
|
+
// Session shutdown has already made the stream terminal.
|
|
235
|
+
});
|
|
236
|
+
}
|
|
234
237
|
}
|
|
235
238
|
wakeSendWindowWaiters() {
|
|
236
239
|
for (const [streamId, ws] of this.sendWindowWaiters) {
|
package/dist/yamux/stream.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare class YamuxStream {
|
|
|
23
23
|
write(data: Uint8Array): Promise<void>;
|
|
24
24
|
private writeSerial;
|
|
25
25
|
close(): Promise<void>;
|
|
26
|
-
reset(err
|
|
26
|
+
reset(err?: Error): Promise<void>;
|
|
27
27
|
private fail;
|
|
28
28
|
bufferedReceiveBytes(): number;
|
|
29
29
|
releaseBufferedReceiveBytes(): number;
|
package/dist/yamux/stream.js
CHANGED
|
@@ -1,7 +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
|
+
import { YamuxResourceExhaustedError, YamuxStreamResetError } from "./errors.js";
|
|
5
5
|
// YamuxStream manages per-stream flow control and state transitions.
|
|
6
6
|
export class YamuxStream {
|
|
7
7
|
// Stream identifier within the session.
|
|
@@ -152,7 +152,7 @@ export class YamuxStream {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
// reset tears down the stream and notifies the peer.
|
|
155
|
-
reset(err) {
|
|
155
|
+
reset(err = new Error("stream reset")) {
|
|
156
156
|
if (this.resetTask != null)
|
|
157
157
|
return this.resetTask;
|
|
158
158
|
if (!this.fail(err))
|
|
@@ -206,7 +206,7 @@ export class YamuxStream {
|
|
|
206
206
|
w();
|
|
207
207
|
}
|
|
208
208
|
if ((flags & FLAG_RST) !== 0) {
|
|
209
|
-
if (this.fail(new
|
|
209
|
+
if (this.fail(new YamuxStreamResetError()))
|
|
210
210
|
this.session.onStreamClosed(this.id);
|
|
211
211
|
}
|
|
212
212
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/flowersec-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -120,8 +120,8 @@
|
|
|
120
120
|
"scripts": {
|
|
121
121
|
"build": "tsc -p tsconfig.build.json",
|
|
122
122
|
"bench": "vitest bench --run",
|
|
123
|
-
"test": "vitest run",
|
|
124
|
-
"test:coverage": "vitest run --coverage",
|
|
123
|
+
"test": "npm run build && vitest run",
|
|
124
|
+
"test:coverage": "npm run build && vitest run --coverage",
|
|
125
125
|
"lint": "eslint .",
|
|
126
126
|
"verify:package": "node ./scripts/verify-package-exports.mjs",
|
|
127
127
|
"prepack": "npm run build",
|