@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 +20 -62
- package/dist/client-connect/connectCore.d.ts +3 -3
- package/dist/client-connect/connectCore.js +7 -5
- package/dist/client-connect/transportSecurity.js +1 -1
- package/dist/controlplane/issuer.d.ts +5 -2
- package/dist/controlplane/issuer.js +76 -16
- 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.js +203 -38
- package/dist/proxy/appWindow.js +93 -7
- package/dist/proxy/controllerWindow.js +219 -78
- package/dist/proxy/portStream.d.ts +2 -1
- package/dist/proxy/portStream.js +173 -74
- package/dist/proxy/server.d.ts +2 -0
- package/dist/proxy/server.js +346 -105
- package/dist/proxy/windowBridgeProtocol.d.ts +4 -3
- package/dist/proxy/windowBridgeProtocol.js +1 -1
- package/dist/reconnect/index.js +62 -25
- package/dist/yamux/byteReader.d.ts +2 -0
- package/dist/yamux/byteReader.js +28 -24
- package/dist/yamux/session.js +1 -1
- package/package.json +4 -1
package/dist/reconnect/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getClientTermination } from "../client-connect/termination.js";
|
|
2
2
|
import { emitObserverDiagnostic, withObserverContext } from "../observability/observer.js";
|
|
3
3
|
import { SDK_DEFAULTS } from "../defaults.js";
|
|
4
|
+
import { AbortError, FlowersecError } from "../utils/errors.js";
|
|
4
5
|
export { createArtifactResolver, createControlplaneArtifactSource } from "./artifactControlplane.js";
|
|
5
6
|
function normalizeAutoReconnect(cfg) {
|
|
6
7
|
const settings = {
|
|
@@ -33,6 +34,26 @@ function backoffDelayMs(attemptIndex, cfg) {
|
|
|
33
34
|
const jitter = cfg.jitterRatio <= 0 ? 0 : base * cfg.jitterRatio * (Math.random() * 2 - 1);
|
|
34
35
|
return Math.max(0, Math.round(base + jitter));
|
|
35
36
|
}
|
|
37
|
+
const TERMINAL_RECONNECT_CODES = new Set([
|
|
38
|
+
"invalid_input",
|
|
39
|
+
"invalid_option",
|
|
40
|
+
"role_mismatch",
|
|
41
|
+
"transport_policy_denied",
|
|
42
|
+
"invalid_psk",
|
|
43
|
+
"invalid_suite",
|
|
44
|
+
"missing_grant",
|
|
45
|
+
"missing_connect_info",
|
|
46
|
+
"missing_tunnel_url",
|
|
47
|
+
"missing_ws_url",
|
|
48
|
+
"missing_channel_id",
|
|
49
|
+
"missing_token",
|
|
50
|
+
"missing_init_exp",
|
|
51
|
+
]);
|
|
52
|
+
function isTerminalConnectError(error) {
|
|
53
|
+
if (error instanceof AbortError || error.name === "AbortError")
|
|
54
|
+
return true;
|
|
55
|
+
return error instanceof FlowersecError && (error.code === "canceled" || TERMINAL_RECONNECT_CODES.has(error.code));
|
|
56
|
+
}
|
|
36
57
|
function isSameConfig(a, b) {
|
|
37
58
|
if (a == null)
|
|
38
59
|
return false;
|
|
@@ -101,6 +122,30 @@ export function createReconnectManager() {
|
|
|
101
122
|
resolve();
|
|
102
123
|
}, ms);
|
|
103
124
|
});
|
|
125
|
+
const finishWithError = (cfg, error, currentAttemptSeq) => {
|
|
126
|
+
setState({ status: "error", error, client: null });
|
|
127
|
+
emitObserverDiagnostic(withObserverContext(cfg.observer, { attemptSeq: currentAttemptSeq }), {
|
|
128
|
+
path: "auto",
|
|
129
|
+
stage: "reconnect",
|
|
130
|
+
code_domain: "event",
|
|
131
|
+
code: "reconnect_exhausted",
|
|
132
|
+
result: "fail",
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
const scheduleRetry = async (t, cfg, error, settings, failedAttemptIndex, currentAttemptSeq) => {
|
|
136
|
+
if (t !== token || active !== cfg)
|
|
137
|
+
return false;
|
|
138
|
+
setState({ status: "connecting", error, client: null });
|
|
139
|
+
emitObserverDiagnostic(withObserverContext(cfg.observer, { attemptSeq: currentAttemptSeq }), {
|
|
140
|
+
path: "auto",
|
|
141
|
+
stage: "reconnect",
|
|
142
|
+
code_domain: "event",
|
|
143
|
+
code: "reconnect_scheduled",
|
|
144
|
+
result: "retry",
|
|
145
|
+
});
|
|
146
|
+
await sleep(backoffDelayMs(failedAttemptIndex, settings));
|
|
147
|
+
return t === token && active === cfg;
|
|
148
|
+
};
|
|
104
149
|
const disconnectInternal = () => {
|
|
105
150
|
cancelRetrySleep();
|
|
106
151
|
abortActiveAttempt();
|
|
@@ -145,8 +190,7 @@ export function createReconnectManager() {
|
|
|
145
190
|
}
|
|
146
191
|
token += 1;
|
|
147
192
|
const nextToken = token;
|
|
148
|
-
const reconnectPromise = startConnectLoop(nextToken, cfg);
|
|
149
|
-
setState({ status: "connecting", error, client: null });
|
|
193
|
+
const reconnectPromise = startConnectLoop(nextToken, cfg, error);
|
|
150
194
|
void reconnectPromise.catch(() => {
|
|
151
195
|
// connectWithRetry updates state; keep errors observable via state().
|
|
152
196
|
});
|
|
@@ -184,9 +228,17 @@ export function createReconnectManager() {
|
|
|
184
228
|
attemptAbort = new AbortController();
|
|
185
229
|
return await cfg.connectOnce({ signal: attemptAbort.signal, observer: createObserver(t, cfg, currentAttemptSeq) ?? {} });
|
|
186
230
|
};
|
|
187
|
-
const connectWithRetry = async (t, cfg) => {
|
|
231
|
+
const connectWithRetry = async (t, cfg, initialFailure) => {
|
|
188
232
|
const ar = normalizeAutoReconnect(cfg.autoReconnect);
|
|
189
233
|
let attempts = 0;
|
|
234
|
+
if (initialFailure != null) {
|
|
235
|
+
if (isTerminalConnectError(initialFailure)) {
|
|
236
|
+
finishWithError(cfg, initialFailure, attemptSeq);
|
|
237
|
+
throw initialFailure;
|
|
238
|
+
}
|
|
239
|
+
if (!await scheduleRetry(t, cfg, initialFailure, ar, 0, attemptSeq))
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
190
242
|
for (;;) {
|
|
191
243
|
if (t !== token)
|
|
192
244
|
return;
|
|
@@ -245,32 +297,17 @@ export function createReconnectManager() {
|
|
|
245
297
|
return;
|
|
246
298
|
if (active !== cfg)
|
|
247
299
|
return;
|
|
248
|
-
const canRetry = ar.enabled && attempts < ar.maxAttempts;
|
|
300
|
+
const canRetry = ar.enabled && attempts < ar.maxAttempts && !isTerminalConnectError(e);
|
|
249
301
|
if (!canRetry) {
|
|
250
|
-
|
|
251
|
-
emitObserverDiagnostic(withObserverContext(cfg.observer, { attemptSeq }), {
|
|
252
|
-
path: "auto",
|
|
253
|
-
stage: "reconnect",
|
|
254
|
-
code_domain: "event",
|
|
255
|
-
code: "reconnect_exhausted",
|
|
256
|
-
result: "fail",
|
|
257
|
-
});
|
|
302
|
+
finishWithError(cfg, e, attemptSeq);
|
|
258
303
|
throw e;
|
|
259
304
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
emitObserverDiagnostic(withObserverContext(cfg.observer, { attemptSeq }), {
|
|
263
|
-
path: "auto",
|
|
264
|
-
stage: "reconnect",
|
|
265
|
-
code_domain: "event",
|
|
266
|
-
code: "reconnect_scheduled",
|
|
267
|
-
result: "retry",
|
|
268
|
-
});
|
|
269
|
-
await sleep(delay);
|
|
305
|
+
if (!await scheduleRetry(t, cfg, e, ar, attempts - 1, attemptSeq))
|
|
306
|
+
return;
|
|
270
307
|
}
|
|
271
308
|
}
|
|
272
309
|
};
|
|
273
|
-
const startConnectLoop = (t, cfg) => {
|
|
310
|
+
const startConnectLoop = (t, cfg, initialFailure) => {
|
|
274
311
|
let resolveLoop;
|
|
275
312
|
let rejectLoop;
|
|
276
313
|
const loop = new Promise((resolve, reject) => {
|
|
@@ -283,7 +320,7 @@ export function createReconnectManager() {
|
|
|
283
320
|
activeConnectPromise = null;
|
|
284
321
|
});
|
|
285
322
|
activeConnectPromise = promise;
|
|
286
|
-
void connectWithRetry(t, cfg).then(resolveLoop, rejectLoop);
|
|
323
|
+
void connectWithRetry(t, cfg, initialFailure).then(resolveLoop, rejectLoop);
|
|
287
324
|
return promise;
|
|
288
325
|
};
|
|
289
326
|
const connect = async (cfg) => {
|
|
@@ -300,7 +337,7 @@ export function createReconnectManager() {
|
|
|
300
337
|
setState({ status: "error", error: closeError, client: null });
|
|
301
338
|
throw closeError;
|
|
302
339
|
}
|
|
303
|
-
const connectPromise = startConnectLoop(t, cfg);
|
|
340
|
+
const connectPromise = startConnectLoop(t, cfg, null);
|
|
304
341
|
setState({ status: "connecting", error: null, client: null });
|
|
305
342
|
await connectPromise;
|
|
306
343
|
};
|
package/dist/yamux/byteReader.js
CHANGED
|
@@ -23,25 +23,7 @@ export class ByteReader {
|
|
|
23
23
|
this.buffered += chunk.length;
|
|
24
24
|
}
|
|
25
25
|
const out = new Uint8Array(n);
|
|
26
|
-
|
|
27
|
-
while (outOff < n) {
|
|
28
|
-
const head = this.chunks[this.chunkHead];
|
|
29
|
-
const avail = head.length - this.headOff;
|
|
30
|
-
const need = n - outOff;
|
|
31
|
-
const take = Math.min(avail, need);
|
|
32
|
-
out.set(head.subarray(this.headOff, this.headOff + take), outOff);
|
|
33
|
-
outOff += take;
|
|
34
|
-
this.headOff += take;
|
|
35
|
-
this.buffered -= take;
|
|
36
|
-
if (this.headOff === head.length) {
|
|
37
|
-
this.chunkHead++;
|
|
38
|
-
this.headOff = 0;
|
|
39
|
-
if (this.chunkHead > 1024 && this.chunkHead * 2 > this.chunks.length) {
|
|
40
|
-
this.chunks.splice(0, this.chunkHead);
|
|
41
|
-
this.chunkHead = 0;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
26
|
+
this.consumeAvailable(n, out);
|
|
45
27
|
return out;
|
|
46
28
|
}
|
|
47
29
|
// discardExactly consumes bytes without allocating a contiguous output buffer.
|
|
@@ -59,19 +41,41 @@ export class ByteReader {
|
|
|
59
41
|
this.chunks.push(chunk);
|
|
60
42
|
this.buffered += chunk.length;
|
|
61
43
|
}
|
|
44
|
+
remaining -= this.consumeAvailable(remaining);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// bufferedBytes returns the number of bytes currently buffered.
|
|
48
|
+
bufferedBytes() {
|
|
49
|
+
return this.buffered;
|
|
50
|
+
}
|
|
51
|
+
consumeAvailable(maxBytes, output) {
|
|
52
|
+
let consumed = 0;
|
|
53
|
+
while (consumed < maxBytes && this.buffered > 0) {
|
|
62
54
|
const head = this.chunks[this.chunkHead];
|
|
63
|
-
const take = Math.min(
|
|
55
|
+
const take = Math.min(maxBytes - consumed, head.length - this.headOff);
|
|
56
|
+
if (output != null)
|
|
57
|
+
output.set(head.subarray(this.headOff, this.headOff + take), consumed);
|
|
64
58
|
this.headOff += take;
|
|
65
59
|
this.buffered -= take;
|
|
66
|
-
|
|
60
|
+
consumed += take;
|
|
67
61
|
if (this.headOff === head.length) {
|
|
68
62
|
this.chunkHead++;
|
|
69
63
|
this.headOff = 0;
|
|
70
64
|
}
|
|
71
65
|
}
|
|
66
|
+
this.compactConsumedChunks();
|
|
67
|
+
return consumed;
|
|
72
68
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
compactConsumedChunks() {
|
|
70
|
+
if (this.buffered === 0) {
|
|
71
|
+
this.chunks.length = 0;
|
|
72
|
+
this.chunkHead = 0;
|
|
73
|
+
this.headOff = 0;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (this.chunkHead > 1024 && this.chunkHead * 2 > this.chunks.length) {
|
|
77
|
+
this.chunks.splice(0, this.chunkHead);
|
|
78
|
+
this.chunkHead = 0;
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
}
|
package/dist/yamux/session.js
CHANGED
|
@@ -11,7 +11,7 @@ export const DEFAULT_YAMUX_LIMITS = Object.freeze({
|
|
|
11
11
|
preferredOutboundFrameBytes: SDK_DEFAULTS.yamux.preferredOutboundFrameBytes,
|
|
12
12
|
maxStreamReceiveBytes: SDK_DEFAULTS.yamux.maxStreamReceiveBytes,
|
|
13
13
|
maxSessionReceiveBytes: SDK_DEFAULTS.yamux.maxSessionReceiveBytes,
|
|
14
|
-
maxStreamWriteQueueBytes: SDK_DEFAULTS.
|
|
14
|
+
maxStreamWriteQueueBytes: SDK_DEFAULTS.yamux.maxStreamWriteQueueBytes,
|
|
15
15
|
});
|
|
16
16
|
// YamuxSession multiplexes multiple streams over a single byte stream.
|
|
17
17
|
export class YamuxSession {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/flowersec-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -121,6 +121,8 @@
|
|
|
121
121
|
"build": "tsc -p tsconfig.build.json",
|
|
122
122
|
"bench": "vitest bench --run",
|
|
123
123
|
"test": "npm run build && vitest run",
|
|
124
|
+
"test:browser": "npm run build && playwright test",
|
|
125
|
+
"ensure:browser": "node ./scripts/ensure-playwright-chromium.mjs",
|
|
124
126
|
"test:coverage": "npm run build && vitest run --coverage",
|
|
125
127
|
"lint": "eslint .",
|
|
126
128
|
"verify:package": "node ./scripts/verify-package-exports.mjs",
|
|
@@ -136,6 +138,7 @@
|
|
|
136
138
|
"devDependencies": {
|
|
137
139
|
"@types/node": "^24.0.0",
|
|
138
140
|
"@types/ws": "^8.5.12",
|
|
141
|
+
"@playwright/test": "1.58.2",
|
|
139
142
|
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
|
140
143
|
"@typescript-eslint/parser": "^8.18.0",
|
|
141
144
|
"@vitest/coverage-v8": "4.1.0",
|