@floegence/flowersec-core 0.22.1 → 0.24.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 +17 -65
- package/YAMUX_ALIGNMENT.md +4 -4
- package/dist/client-connect/connectCore.d.ts +3 -3
- package/dist/client-connect/connectCore.js +120 -106
- package/dist/client.d.ts +2 -0
- package/dist/controlplane/issuer.d.ts +5 -2
- package/dist/controlplane/issuer.js +76 -16
- package/dist/controlplane/request.js +3 -1
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +2 -0
- package/dist/e2ee/handshake.d.ts +2 -2
- package/dist/e2ee/secureChannel.d.ts +2 -2
- package/dist/e2ee/secureChannel.js +22 -10
- package/dist/endpoint/index.d.ts +1 -0
- package/dist/endpoint/index.js +26 -5
- package/dist/proxy/controllerWindow.js +4 -5
- package/dist/proxy/runtime.js +10 -19
- package/dist/reconnect/index.js +141 -88
- 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/byteReader.d.ts +2 -0
- package/dist/yamux/byteReader.js +28 -24
- package/dist/yamux/errors.d.ts +8 -0
- package/dist/yamux/errors.js +18 -0
- package/dist/yamux/session.js +8 -5
- package/dist/yamux/stream.d.ts +1 -1
- package/dist/yamux/stream.js +3 -3
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# @floegence/flowersec-core
|
|
2
2
|
|
|
3
|
-
|
|
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,27 @@ Status: experimental; not audited.
|
|
|
10
8
|
npm install @floegence/flowersec-core
|
|
11
9
|
```
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Browser:
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { connectBrowser } from "@floegence/flowersec-core/browser";
|
|
19
|
-
import { requestConnectArtifact } from "@floegence/flowersec-core/controlplane";
|
|
20
|
-
|
|
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
|
-
```
|
|
29
|
-
|
|
30
|
-
Node.js:
|
|
31
|
-
|
|
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
|
-
```
|
|
11
|
+
The package is ESM-only and exposes environment-specific entrypoints for browser and Node.js applications.
|
|
59
12
|
|
|
60
|
-
|
|
13
|
+
## Cookbook
|
|
61
14
|
|
|
62
|
-
|
|
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.
|
|
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.
|
|
66
16
|
|
|
67
|
-
##
|
|
17
|
+
## Entrypoints
|
|
68
18
|
|
|
69
|
-
|
|
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`
|
|
70
27
|
|
|
71
|
-
|
|
28
|
+
High-level WebSocket connections require TLS by default. Use `AllowPlaintextForLoopback` only for literal local development targets.
|
|
72
29
|
|
|
73
|
-
|
|
30
|
+
## Runtime Boundaries
|
|
74
31
|
|
|
75
|
-
|
|
32
|
+
TypeScript owns browser and Service Worker integration. Shared tunnel, proxy gateway, and helper binaries remain Go-owned.
|
|
76
33
|
|
|
77
|
-
|
|
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`
|
|
34
|
+
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).
|
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
|
|
@@ -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
|
|
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
|
-
/**
|
|
42
|
+
/** Scope validators keyed by scope name. */
|
|
43
43
|
scopeResolvers?: ConnectScopeResolverMap;
|
|
44
|
-
/**
|
|
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;
|
|
@@ -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,13 @@ export async function connectCore(args) {
|
|
|
316
303
|
return await mux.probeLiveness(liveness.timeoutMs);
|
|
317
304
|
}
|
|
318
305
|
catch (e) {
|
|
319
|
-
|
|
306
|
+
const timedOut = isYamuxPingTimeoutError(e);
|
|
307
|
+
if (timedOut) {
|
|
320
308
|
emitObserverDiagnostic(args.opts.observer, { path: args.path, stage: "yamux", code_domain: "event", code: "liveness_timeout", result: "fail" });
|
|
321
309
|
}
|
|
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 });
|
|
310
|
+
const error = new FlowersecError({ path: args.path, stage: "yamux", code: timedOut ? "timeout" : "ping_failed", message: "liveness probe failed", cause: e });
|
|
311
|
+
reportTermination(error);
|
|
312
|
+
throw error;
|
|
335
313
|
}
|
|
336
314
|
};
|
|
337
315
|
let livenessTimer;
|
|
@@ -344,24 +322,7 @@ export async function connectCore(args) {
|
|
|
344
322
|
};
|
|
345
323
|
closeAll = () => {
|
|
346
324
|
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
|
-
}
|
|
325
|
+
runSyncCleanups([() => rpc.close(), () => mux.close(), () => secure.close()]);
|
|
365
326
|
};
|
|
366
327
|
if (liveness.intervalMs > 0) {
|
|
367
328
|
livenessTimer = setInterval(() => {
|
|
@@ -385,10 +346,19 @@ export async function connectCore(args) {
|
|
|
385
346
|
mux,
|
|
386
347
|
rpc,
|
|
387
348
|
ping,
|
|
349
|
+
rekey: async () => {
|
|
350
|
+
try {
|
|
351
|
+
await secure.rekeyNow();
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
throw new FlowersecError({ path: args.path, stage: "secure", code: "rekey_failed", message: "rekey failed", cause: error });
|
|
355
|
+
}
|
|
356
|
+
},
|
|
388
357
|
probeLiveness,
|
|
389
358
|
openStream: async (kind, opts = {}) => {
|
|
390
|
-
|
|
391
|
-
|
|
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" });
|
|
392
362
|
if (opts.signal?.aborted) {
|
|
393
363
|
throw new FlowersecError({
|
|
394
364
|
path: args.path,
|
|
@@ -408,6 +378,7 @@ export async function connectCore(args) {
|
|
|
408
378
|
};
|
|
409
379
|
const signal = opts.signal;
|
|
410
380
|
let abortListener;
|
|
381
|
+
let abortReset;
|
|
411
382
|
let s;
|
|
412
383
|
try {
|
|
413
384
|
s = await mux.openStream(signal === undefined ? {} : { signal });
|
|
@@ -421,12 +392,7 @@ export async function connectCore(args) {
|
|
|
421
392
|
}
|
|
422
393
|
if (signal != null) {
|
|
423
394
|
abortListener = () => {
|
|
424
|
-
|
|
425
|
-
s.reset(abortReason(signal));
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
// ignore
|
|
429
|
-
}
|
|
395
|
+
abortReset ??= captureAsyncCleanup(() => Promise.resolve(s.reset(abortReason(signal))));
|
|
430
396
|
};
|
|
431
397
|
signal.addEventListener("abort", abortListener, { once: true });
|
|
432
398
|
if (signal.aborted)
|
|
@@ -435,55 +401,52 @@ export async function connectCore(args) {
|
|
|
435
401
|
if (signal?.aborted) {
|
|
436
402
|
if (abortListener != null)
|
|
437
403
|
signal.removeEventListener("abort", abortListener);
|
|
438
|
-
|
|
439
|
-
await
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
404
|
+
const cleanupError = await captureAsyncCleanup(async () => {
|
|
405
|
+
const resetError = abortReset == null ? undefined : await abortReset;
|
|
406
|
+
const closeError = await captureAsyncCleanup(() => s.close());
|
|
407
|
+
const combined = errorsFrom([resetError, closeError]);
|
|
408
|
+
if (combined != null)
|
|
409
|
+
throw combined;
|
|
410
|
+
});
|
|
444
411
|
throw new FlowersecError({
|
|
445
412
|
path: args.path,
|
|
446
413
|
stage: "yamux",
|
|
447
414
|
code: "canceled",
|
|
448
415
|
message: "open stream aborted",
|
|
449
|
-
cause: signal.reason,
|
|
416
|
+
cause: causeWithCleanup(signal.reason, cleanupError, "aborted stream cleanup failed"),
|
|
450
417
|
});
|
|
451
418
|
}
|
|
452
419
|
try {
|
|
453
|
-
await writeStreamHello((b) => s.write(b),
|
|
420
|
+
await writeStreamHello((b) => s.write(b), streamKind);
|
|
454
421
|
}
|
|
455
422
|
catch (err) {
|
|
456
423
|
if (signal?.aborted) {
|
|
457
424
|
if (abortListener != null)
|
|
458
425
|
signal.removeEventListener("abort", abortListener);
|
|
459
|
-
|
|
460
|
-
await
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
426
|
+
const cleanupError = await captureAsyncCleanup(async () => {
|
|
427
|
+
const resetError = abortReset == null ? undefined : await abortReset;
|
|
428
|
+
const closeError = await captureAsyncCleanup(() => s.close());
|
|
429
|
+
const combined = errorsFrom([resetError, closeError]);
|
|
430
|
+
if (combined != null)
|
|
431
|
+
throw combined;
|
|
432
|
+
});
|
|
465
433
|
throw new FlowersecError({
|
|
466
434
|
path: args.path,
|
|
467
435
|
stage: "yamux",
|
|
468
436
|
code: "canceled",
|
|
469
437
|
message: "open stream aborted",
|
|
470
|
-
cause: signal.reason,
|
|
438
|
+
cause: causeWithCleanup(signal.reason, cleanupError, "aborted stream cleanup failed"),
|
|
471
439
|
});
|
|
472
440
|
}
|
|
473
441
|
if (signal != null && abortListener != null)
|
|
474
442
|
signal.removeEventListener("abort", abortListener);
|
|
475
|
-
|
|
476
|
-
await s.close();
|
|
477
|
-
}
|
|
478
|
-
catch {
|
|
479
|
-
// ignore
|
|
480
|
-
}
|
|
443
|
+
const cleanupError = await captureAsyncCleanup(() => s.close());
|
|
481
444
|
throw new FlowersecError({
|
|
482
445
|
path: args.path,
|
|
483
446
|
stage: "rpc",
|
|
484
447
|
code: "stream_hello_failed",
|
|
485
448
|
message: "stream hello failed",
|
|
486
|
-
cause: err,
|
|
449
|
+
cause: causeWithCleanup(err, cleanupError, "stream hello cleanup failed"),
|
|
487
450
|
});
|
|
488
451
|
}
|
|
489
452
|
if (signal != null && abortListener != null) {
|
|
@@ -499,14 +462,65 @@ export async function connectCore(args) {
|
|
|
499
462
|
return client;
|
|
500
463
|
}
|
|
501
464
|
catch (e) {
|
|
465
|
+
const cleanupError = captureSyncCleanup(() => transport.close());
|
|
466
|
+
throw cleanupError == null ? e : errorWithCleanup(e, cleanupError, "transport cleanup failed");
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function runSyncCleanups(actions) {
|
|
470
|
+
const failures = [];
|
|
471
|
+
for (const action of actions) {
|
|
502
472
|
try {
|
|
503
|
-
|
|
473
|
+
action();
|
|
504
474
|
}
|
|
505
|
-
catch {
|
|
506
|
-
|
|
475
|
+
catch (error) {
|
|
476
|
+
failures.push(errorValue(error));
|
|
507
477
|
}
|
|
508
|
-
throw e;
|
|
509
478
|
}
|
|
479
|
+
if (failures.length > 0)
|
|
480
|
+
throw new AggregateError(failures, "Flowersec cleanup failed");
|
|
481
|
+
}
|
|
482
|
+
function captureSyncCleanup(action) {
|
|
483
|
+
try {
|
|
484
|
+
action();
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
return error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function captureAsyncCleanup(action) {
|
|
492
|
+
try {
|
|
493
|
+
await action();
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
return error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function causeWithCleanup(primary, cleanup, message) {
|
|
501
|
+
if (cleanup === undefined)
|
|
502
|
+
return primary;
|
|
503
|
+
return new AggregateError([errorValue(primary), errorValue(cleanup)], message);
|
|
504
|
+
}
|
|
505
|
+
function errorWithCleanup(primary, cleanup, message) {
|
|
506
|
+
const cause = causeWithCleanup(primary, cleanup, message);
|
|
507
|
+
if (primary instanceof FlowersecError) {
|
|
508
|
+
return new FlowersecError({
|
|
509
|
+
path: primary.path,
|
|
510
|
+
stage: primary.stage,
|
|
511
|
+
code: primary.code,
|
|
512
|
+
message,
|
|
513
|
+
cause,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
return new AggregateError([errorValue(primary), errorValue(cleanup)], message);
|
|
517
|
+
}
|
|
518
|
+
function errorsFrom(values) {
|
|
519
|
+
const failures = values.filter((value) => value !== undefined).map(errorValue);
|
|
520
|
+
return failures.length === 0 ? undefined : new AggregateError(failures, "Flowersec cleanup failed");
|
|
521
|
+
}
|
|
522
|
+
function errorValue(error) {
|
|
523
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
510
524
|
}
|
|
511
525
|
function validateLimitObject(input, name, invalid) {
|
|
512
526
|
if (input === undefined)
|
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;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type TokenPayload } from "./token.js";
|
|
2
2
|
export declare class IssuerKeyset {
|
|
3
|
-
private
|
|
4
|
-
private
|
|
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
|
}
|