@floegence/flowersec-core 0.20.0 → 0.20.2
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/dist/client-connect/connectCore.d.ts +2 -0
- package/dist/client-connect/connectCore.js +45 -5
- package/dist/client-connect/termination.d.ts +6 -0
- package/dist/client-connect/termination.js +7 -0
- package/dist/e2ee/handshake.d.ts +4 -0
- package/dist/e2ee/handshake.js +14 -1
- package/dist/e2ee/secureChannel.d.ts +6 -1
- package/dist/e2ee/secureChannel.js +37 -12
- package/dist/proxy/appWindow.d.ts +1 -0
- package/dist/proxy/appWindow.js +17 -5
- package/dist/proxy/controllerWindow.js +87 -14
- package/dist/proxy/headerPolicy.js +10 -1
- package/dist/proxy/integration.d.ts +4 -0
- package/dist/proxy/integration.js +12 -0
- package/dist/proxy/portStream.d.ts +3 -1
- package/dist/proxy/portStream.js +40 -3
- package/dist/proxy/runtime.d.ts +8 -0
- package/dist/proxy/runtime.js +170 -4
- package/dist/proxy/runtimeScope.d.ts +8 -0
- package/dist/proxy/windowBridgeProtocol.d.ts +8 -0
- package/dist/proxy/windowBridgeProtocol.js +2 -0
- package/dist/proxy/wsPatch.d.ts +1 -0
- package/dist/proxy/wsPatch.js +56 -12
- package/dist/reconnect/index.js +9 -0
- package/dist/rpc/client.d.ts +2 -0
- package/dist/rpc/client.js +11 -0
- package/dist/yamux/session.d.ts +8 -1
- package/dist/yamux/session.js +77 -26
- package/package.json +1 -1
package/dist/proxy/portStream.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE, PROXY_WINDOW_STREAM_END_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, } from "./windowBridgeProtocol.js";
|
|
1
|
+
import { PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE, PROXY_WINDOW_STREAM_END_MSG_TYPE, PROXY_WINDOW_STREAM_RESET_MSG_TYPE, PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE, } from "./windowBridgeProtocol.js";
|
|
2
2
|
function cloneChunk(chunk) {
|
|
3
3
|
const out = new Uint8Array(chunk.byteLength);
|
|
4
4
|
out.set(chunk);
|
|
5
5
|
return out.buffer;
|
|
6
6
|
}
|
|
7
|
-
export function createMessagePortBackedStream(port) {
|
|
7
|
+
export function createMessagePortBackedStream(port, opts = {}) {
|
|
8
8
|
let closed = false;
|
|
9
9
|
let error = null;
|
|
10
10
|
const queue = [];
|
|
11
11
|
const waiters = [];
|
|
12
|
+
const pendingWrites = new Map();
|
|
13
|
+
let nextWriteId = 1;
|
|
12
14
|
const resolveWaiter = (value) => {
|
|
13
15
|
const waiter = waiters.shift();
|
|
14
16
|
if (waiter) {
|
|
@@ -29,6 +31,9 @@ export function createMessagePortBackedStream(port) {
|
|
|
29
31
|
while (resolveWaiter(err)) {
|
|
30
32
|
// Drain waiters.
|
|
31
33
|
}
|
|
34
|
+
for (const pending of pendingWrites.values())
|
|
35
|
+
pending.reject(err);
|
|
36
|
+
pendingWrites.clear();
|
|
32
37
|
};
|
|
33
38
|
port.onmessage = (ev) => {
|
|
34
39
|
const data = ev.data;
|
|
@@ -46,8 +51,22 @@ export function createMessagePortBackedStream(port) {
|
|
|
46
51
|
case PROXY_WINDOW_STREAM_END_MSG_TYPE:
|
|
47
52
|
case PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE:
|
|
48
53
|
closed = true;
|
|
54
|
+
for (const pending of pendingWrites.values())
|
|
55
|
+
pending.reject(new Error("stream is closed"));
|
|
56
|
+
pendingWrites.clear();
|
|
49
57
|
pushValue(null);
|
|
50
58
|
return;
|
|
59
|
+
case PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE: {
|
|
60
|
+
const writeId = data.writeId;
|
|
61
|
+
if (!Number.isSafeInteger(writeId) || writeId <= 0)
|
|
62
|
+
return;
|
|
63
|
+
const pending = pendingWrites.get(writeId);
|
|
64
|
+
if (pending == null)
|
|
65
|
+
return;
|
|
66
|
+
pendingWrites.delete(writeId);
|
|
67
|
+
pending.resolve();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
51
70
|
case PROXY_WINDOW_STREAM_RESET_MSG_TYPE: {
|
|
52
71
|
closed = true;
|
|
53
72
|
const message = String(data.message ?? "stream reset");
|
|
@@ -87,12 +106,30 @@ export function createMessagePortBackedStream(port) {
|
|
|
87
106
|
if (closed)
|
|
88
107
|
throw new Error("stream is closed");
|
|
89
108
|
const ab = cloneChunk(chunk);
|
|
90
|
-
|
|
109
|
+
if (opts.writeAcknowledgements !== true) {
|
|
110
|
+
port.postMessage({ type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, data: ab }, [ab]);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const writeId = nextWriteId++;
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
pendingWrites.set(writeId, { resolve, reject });
|
|
116
|
+
try {
|
|
117
|
+
port.postMessage({ type: PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE, data: ab, writeId }, [ab]);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
pendingWrites.delete(writeId);
|
|
121
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
91
124
|
},
|
|
92
125
|
async close() {
|
|
93
126
|
if (closed)
|
|
94
127
|
return;
|
|
95
128
|
closed = true;
|
|
129
|
+
const closeError = new Error("stream is closed");
|
|
130
|
+
for (const pending of pendingWrites.values())
|
|
131
|
+
pending.reject(closeError);
|
|
132
|
+
pendingWrites.clear();
|
|
96
133
|
try {
|
|
97
134
|
port.postMessage({ type: PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE });
|
|
98
135
|
}
|
package/dist/proxy/runtime.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export type ProxyRuntimeLimits = Readonly<{
|
|
|
15
15
|
maxChunkBytes: number;
|
|
16
16
|
maxBodyBytes: number;
|
|
17
17
|
maxWsFrameBytes: number;
|
|
18
|
+
maxWsBufferedAmountBytes: number;
|
|
19
|
+
maxConcurrentHttpStreams: number;
|
|
20
|
+
maxQueuedHttpRequests: number;
|
|
21
|
+
maxQueuedHttpBodyBytes: number;
|
|
18
22
|
}>;
|
|
19
23
|
export type ProxyRuntime = Readonly<{
|
|
20
24
|
limits: ProxyRuntimeLimits;
|
|
@@ -40,6 +44,10 @@ export type ProxyRuntimeOptions = Readonly<{
|
|
|
40
44
|
maxChunkBytes?: number;
|
|
41
45
|
maxBodyBytes?: number;
|
|
42
46
|
maxWsFrameBytes?: number;
|
|
47
|
+
maxWsBufferedAmountBytes?: number;
|
|
48
|
+
maxConcurrentHttpStreams?: number;
|
|
49
|
+
maxQueuedHttpRequests?: number;
|
|
50
|
+
maxQueuedHttpBodyBytes?: number;
|
|
43
51
|
timeoutMs?: number;
|
|
44
52
|
extraRequestHeaders?: readonly string[];
|
|
45
53
|
extraResponseHeaders?: readonly string[];
|
package/dist/proxy/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@ import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../
|
|
|
2
2
|
import { createByteReader } from "../streamio/index.js";
|
|
3
3
|
import { base64urlEncode } from "../utils/base64url.js";
|
|
4
4
|
import { readU32be, u32be } from "../utils/bin.js";
|
|
5
|
+
import { AbortError, FlowersecError, isFlowersecError } from "../utils/errors.js";
|
|
5
6
|
import { CookieJar } from "./cookieJar.js";
|
|
6
7
|
import { DEFAULT_MAX_BODY_BYTES, DEFAULT_MAX_CHUNK_BYTES, DEFAULT_MAX_WS_FRAME_BYTES, PROXY_KIND_HTTP1, PROXY_KIND_WS, PROXY_PROTOCOL_VERSION } from "./constants.js";
|
|
7
8
|
import { filterRequestHeaders, filterResponseHeaders, filterWsOpenHeaders } from "./headerPolicy.js";
|
|
@@ -129,6 +130,141 @@ function normalizeMaxBytes(name, v, defaultValue) {
|
|
|
129
130
|
return defaultValue;
|
|
130
131
|
return n;
|
|
131
132
|
}
|
|
133
|
+
const DEFAULT_MAX_CONCURRENT_HTTP_STREAMS = 24;
|
|
134
|
+
const DEFAULT_MAX_QUEUED_HTTP_REQUESTS = 128;
|
|
135
|
+
const DEFAULT_MAX_QUEUED_HTTP_BODY_BYTES = 64 * (1 << 20);
|
|
136
|
+
const DEFAULT_MAX_WS_BUFFERED_AMOUNT_BYTES = 4 * (1 << 20);
|
|
137
|
+
function normalizePositiveLimit(name, value, defaultValue) {
|
|
138
|
+
if (value == null)
|
|
139
|
+
return defaultValue;
|
|
140
|
+
if (!Number.isFinite(value) || !Number.isSafeInteger(value) || value <= 0) {
|
|
141
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
function normalizeNonNegativeLimit(name, value, defaultValue) {
|
|
146
|
+
if (value == null)
|
|
147
|
+
return defaultValue;
|
|
148
|
+
if (!Number.isFinite(value) || !Number.isSafeInteger(value) || value < 0) {
|
|
149
|
+
throw new Error(`${name} must be a non-negative safe integer`);
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
class HttpStreamAdmission {
|
|
154
|
+
path;
|
|
155
|
+
maxConcurrent;
|
|
156
|
+
maxQueued;
|
|
157
|
+
maxQueuedBodyBytes;
|
|
158
|
+
active = 0;
|
|
159
|
+
pending = [];
|
|
160
|
+
pendingBodyBytes = 0;
|
|
161
|
+
closed = false;
|
|
162
|
+
constructor(path, maxConcurrent, maxQueued, maxQueuedBodyBytes) {
|
|
163
|
+
this.path = path;
|
|
164
|
+
this.maxConcurrent = maxConcurrent;
|
|
165
|
+
this.maxQueued = maxQueued;
|
|
166
|
+
this.maxQueuedBodyBytes = maxQueuedBodyBytes;
|
|
167
|
+
}
|
|
168
|
+
acquire(bodyBytes, signal) {
|
|
169
|
+
if (this.closed)
|
|
170
|
+
return Promise.reject(this.closedError());
|
|
171
|
+
if (signal?.aborted)
|
|
172
|
+
return Promise.reject(this.abortedError());
|
|
173
|
+
if (this.active < this.maxConcurrent && this.pending.length === 0) {
|
|
174
|
+
this.active++;
|
|
175
|
+
return Promise.resolve(this.createRelease());
|
|
176
|
+
}
|
|
177
|
+
if (this.pending.length >= this.maxQueued) {
|
|
178
|
+
return Promise.reject(new FlowersecError({
|
|
179
|
+
path: this.path,
|
|
180
|
+
stage: "yamux",
|
|
181
|
+
code: "resource_exhausted",
|
|
182
|
+
message: "proxy runtime HTTP request queue is full",
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
if (this.pendingBodyBytes + bodyBytes > this.maxQueuedBodyBytes) {
|
|
186
|
+
return Promise.reject(new FlowersecError({
|
|
187
|
+
path: this.path,
|
|
188
|
+
stage: "yamux",
|
|
189
|
+
code: "resource_exhausted",
|
|
190
|
+
message: "proxy runtime HTTP request body queue is full",
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const waiter = {
|
|
195
|
+
bodyBytes,
|
|
196
|
+
resolve,
|
|
197
|
+
reject,
|
|
198
|
+
...(signal === undefined ? {} : { signal }),
|
|
199
|
+
};
|
|
200
|
+
waiter.onAbort = () => {
|
|
201
|
+
const index = this.pending.indexOf(waiter);
|
|
202
|
+
if (index < 0)
|
|
203
|
+
return;
|
|
204
|
+
this.pending.splice(index, 1);
|
|
205
|
+
this.pendingBodyBytes = Math.max(0, this.pendingBodyBytes - waiter.bodyBytes);
|
|
206
|
+
this.cleanupWaiter(waiter);
|
|
207
|
+
reject(this.abortedError());
|
|
208
|
+
};
|
|
209
|
+
signal?.addEventListener("abort", waiter.onAbort, { once: true });
|
|
210
|
+
this.pending.push(waiter);
|
|
211
|
+
this.pendingBodyBytes += bodyBytes;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
close() {
|
|
215
|
+
if (this.closed)
|
|
216
|
+
return;
|
|
217
|
+
this.closed = true;
|
|
218
|
+
for (const waiter of this.pending.splice(0)) {
|
|
219
|
+
this.pendingBodyBytes = Math.max(0, this.pendingBodyBytes - waiter.bodyBytes);
|
|
220
|
+
this.cleanupWaiter(waiter);
|
|
221
|
+
waiter.reject(this.closedError());
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
assertOpen() {
|
|
225
|
+
if (this.closed)
|
|
226
|
+
throw this.closedError();
|
|
227
|
+
}
|
|
228
|
+
createRelease() {
|
|
229
|
+
let released = false;
|
|
230
|
+
return () => {
|
|
231
|
+
if (released)
|
|
232
|
+
return;
|
|
233
|
+
released = true;
|
|
234
|
+
this.active = Math.max(0, this.active - 1);
|
|
235
|
+
this.drain();
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
drain() {
|
|
239
|
+
while (!this.closed && this.active < this.maxConcurrent && this.pending.length > 0) {
|
|
240
|
+
const waiter = this.pending.shift();
|
|
241
|
+
this.pendingBodyBytes = Math.max(0, this.pendingBodyBytes - waiter.bodyBytes);
|
|
242
|
+
this.cleanupWaiter(waiter);
|
|
243
|
+
if (waiter.signal?.aborted) {
|
|
244
|
+
waiter.reject(this.abortedError());
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
this.active++;
|
|
248
|
+
waiter.resolve(this.createRelease());
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
cleanupWaiter(waiter) {
|
|
252
|
+
if (waiter.onAbort != null) {
|
|
253
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
abortedError() {
|
|
257
|
+
return new AbortError("proxy HTTP request canceled while waiting for stream admission");
|
|
258
|
+
}
|
|
259
|
+
closedError() {
|
|
260
|
+
return new FlowersecError({
|
|
261
|
+
path: this.path,
|
|
262
|
+
stage: "close",
|
|
263
|
+
code: "not_connected",
|
|
264
|
+
message: "proxy runtime is disposed",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
132
268
|
async function writeChunkFrames(stream, body, chunkSize, maxBodyBytes) {
|
|
133
269
|
if (maxBodyBytes > 0 && body.length > maxBodyBytes)
|
|
134
270
|
throw new Error("request body too large");
|
|
@@ -169,6 +305,11 @@ export function createProxyRuntime(opts) {
|
|
|
169
305
|
const maxChunkBytes = normalizeMaxBytes("maxChunkBytes", opts.maxChunkBytes, DEFAULT_MAX_CHUNK_BYTES);
|
|
170
306
|
const maxBodyBytes = normalizeMaxBytes("maxBodyBytes", opts.maxBodyBytes, DEFAULT_MAX_BODY_BYTES);
|
|
171
307
|
const maxWsFrameBytes = normalizeMaxBytes("maxWsFrameBytes", opts.maxWsFrameBytes, DEFAULT_MAX_WS_FRAME_BYTES);
|
|
308
|
+
const maxWsBufferedAmountBytes = normalizeMaxBytes("maxWsBufferedAmountBytes", opts.maxWsBufferedAmountBytes, DEFAULT_MAX_WS_BUFFERED_AMOUNT_BYTES);
|
|
309
|
+
const maxConcurrentHttpStreams = normalizePositiveLimit("maxConcurrentHttpStreams", opts.maxConcurrentHttpStreams, DEFAULT_MAX_CONCURRENT_HTTP_STREAMS);
|
|
310
|
+
const maxQueuedHttpRequests = normalizeNonNegativeLimit("maxQueuedHttpRequests", opts.maxQueuedHttpRequests, DEFAULT_MAX_QUEUED_HTTP_REQUESTS);
|
|
311
|
+
const maxQueuedHttpBodyBytes = normalizeNonNegativeLimit("maxQueuedHttpBodyBytes", opts.maxQueuedHttpBodyBytes, DEFAULT_MAX_QUEUED_HTTP_BODY_BYTES);
|
|
312
|
+
const httpStreamAdmission = new HttpStreamAdmission(client.path, maxConcurrentHttpStreams, maxQueuedHttpRequests, maxQueuedHttpBodyBytes);
|
|
172
313
|
const timeoutMs = normalizeTimeoutMs(opts.timeoutMs);
|
|
173
314
|
const extraRequestHeaders = opts.extraRequestHeaders ?? [];
|
|
174
315
|
const extraResponseHeaders = opts.extraResponseHeaders ?? [];
|
|
@@ -203,6 +344,7 @@ export function createProxyRuntime(opts) {
|
|
|
203
344
|
const dispatchFetch = (req, port) => {
|
|
204
345
|
const ac = new AbortController();
|
|
205
346
|
let stream = null;
|
|
347
|
+
let releaseAdmission = null;
|
|
206
348
|
port.onmessage = (ev) => {
|
|
207
349
|
const m = ev.data;
|
|
208
350
|
if (m && typeof m === "object" && m.type === "flowersec-proxy:abort") {
|
|
@@ -215,6 +357,11 @@ export function createProxyRuntime(opts) {
|
|
|
215
357
|
assertPathPolicyAllows("http", path, pathPolicy);
|
|
216
358
|
const requestID = req.id.trim() !== "" ? req.id : randomB64u(18);
|
|
217
359
|
const externalOrigin = externalOriginOverride ?? normalizeExternalOrigin(req.external_origin);
|
|
360
|
+
const body = req.body != null ? new Uint8Array(req.body) : new Uint8Array();
|
|
361
|
+
if (maxBodyBytes > 0 && body.length > maxBodyBytes)
|
|
362
|
+
throw new Error("request body too large");
|
|
363
|
+
releaseAdmission = await httpStreamAdmission.acquire(body.byteLength, ac.signal);
|
|
364
|
+
httpStreamAdmission.assertOpen();
|
|
218
365
|
stream = await client.openStream(PROXY_KIND_HTTP1, { signal: ac.signal });
|
|
219
366
|
const reader = createByteReader(stream, { signal: ac.signal });
|
|
220
367
|
const filteredReqHeaders = filterRequestHeaders(req.headers, { extraAllowed: extraRequestHeaders });
|
|
@@ -229,7 +376,6 @@ export function createProxyRuntime(opts) {
|
|
|
229
376
|
...(externalOrigin === undefined ? {} : { external_origin: externalOrigin }),
|
|
230
377
|
timeout_ms: timeoutMs
|
|
231
378
|
});
|
|
232
|
-
const body = req.body != null ? new Uint8Array(req.body) : new Uint8Array();
|
|
233
379
|
await writeChunkFrames(stream, body, Math.min(64 * 1024, maxChunkBytes), maxBodyBytes);
|
|
234
380
|
const respMeta = (await readJsonFrame(reader, maxJsonFrameBytes));
|
|
235
381
|
if (respMeta.v !== PROXY_PROTOCOL_VERSION || respMeta.request_id !== requestID) {
|
|
@@ -264,8 +410,17 @@ export function createProxyRuntime(opts) {
|
|
|
264
410
|
}
|
|
265
411
|
catch (e) {
|
|
266
412
|
const msg = e instanceof Error ? e.message : String(e);
|
|
267
|
-
const
|
|
268
|
-
|
|
413
|
+
const code = isFlowersecError(e) ? e.code : undefined;
|
|
414
|
+
const status = e instanceof ProxyRuntimePolicyError
|
|
415
|
+
? e.status
|
|
416
|
+
: code === "resource_exhausted" || code === "not_connected"
|
|
417
|
+
? 503
|
|
418
|
+
: 502;
|
|
419
|
+
port.postMessage({
|
|
420
|
+
type: "flowersec-proxy:response_error",
|
|
421
|
+
status,
|
|
422
|
+
message: msg,
|
|
423
|
+
});
|
|
269
424
|
try {
|
|
270
425
|
stream?.reset(new Error(msg));
|
|
271
426
|
}
|
|
@@ -274,6 +429,7 @@ export function createProxyRuntime(opts) {
|
|
|
274
429
|
}
|
|
275
430
|
}
|
|
276
431
|
finally {
|
|
432
|
+
releaseAdmission?.();
|
|
277
433
|
try {
|
|
278
434
|
port.close();
|
|
279
435
|
}
|
|
@@ -312,10 +468,20 @@ export function createProxyRuntime(opts) {
|
|
|
312
468
|
return { stream, protocol: resp.protocol ?? "" };
|
|
313
469
|
}
|
|
314
470
|
return {
|
|
315
|
-
limits: {
|
|
471
|
+
limits: {
|
|
472
|
+
maxJsonFrameBytes,
|
|
473
|
+
maxChunkBytes,
|
|
474
|
+
maxBodyBytes,
|
|
475
|
+
maxWsFrameBytes,
|
|
476
|
+
maxWsBufferedAmountBytes,
|
|
477
|
+
maxConcurrentHttpStreams,
|
|
478
|
+
maxQueuedHttpRequests,
|
|
479
|
+
maxQueuedHttpBodyBytes,
|
|
480
|
+
},
|
|
316
481
|
dispatchFetch,
|
|
317
482
|
openWebSocketStream,
|
|
318
483
|
dispose: () => {
|
|
484
|
+
httpStreamAdmission.close();
|
|
319
485
|
sw?.removeEventListener("message", onMessage);
|
|
320
486
|
sw?.removeEventListener("controllerchange", registerRuntime);
|
|
321
487
|
}
|
|
@@ -36,12 +36,20 @@ export declare function resolveRuntimeLimitsFromScope(scope: ProxyRuntimeScopeV1
|
|
|
36
36
|
maxChunkBytes?: number;
|
|
37
37
|
maxBodyBytes?: number;
|
|
38
38
|
maxWsFrameBytes?: number;
|
|
39
|
+
maxWsBufferedAmountBytes?: number;
|
|
40
|
+
maxConcurrentHttpStreams?: number;
|
|
41
|
+
maxQueuedHttpRequests?: number;
|
|
42
|
+
maxQueuedHttpBodyBytes?: number;
|
|
39
43
|
timeoutMs?: number;
|
|
40
44
|
}> | undefined): Readonly<{
|
|
41
45
|
maxJsonFrameBytes?: number;
|
|
42
46
|
maxChunkBytes?: number;
|
|
43
47
|
maxBodyBytes?: number;
|
|
44
48
|
maxWsFrameBytes?: number;
|
|
49
|
+
maxWsBufferedAmountBytes?: number;
|
|
50
|
+
maxConcurrentHttpStreams?: number;
|
|
51
|
+
maxQueuedHttpRequests?: number;
|
|
52
|
+
maxQueuedHttpBodyBytes?: number;
|
|
45
53
|
timeoutMs?: number;
|
|
46
54
|
}> | undefined;
|
|
47
55
|
export declare function resolvePresetInputFromScope(scope: ProxyRuntimeScopeV1, presetOverride: ProxyPresetInput | undefined): ProxyPresetInput | undefined;
|
|
@@ -3,8 +3,10 @@ export declare const PROXY_WINDOW_FETCH_FORWARD_MSG_TYPE = "flowersec-proxy:wind
|
|
|
3
3
|
export declare const PROXY_WINDOW_FETCH_MSG_TYPE = "flowersec-proxy:fetch";
|
|
4
4
|
export declare const PROXY_WINDOW_WS_OPEN_MSG_TYPE = "flowersec-proxy:ws_open";
|
|
5
5
|
export declare const PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE = "flowersec-proxy:ws_open_ack";
|
|
6
|
+
export declare const PROXY_WINDOW_WS_WRITE_ACK_CAPABILITY = "stream_write_ack_v1";
|
|
6
7
|
export declare const PROXY_WINDOW_WS_ERROR_MSG_TYPE = "flowersec-proxy:ws_error";
|
|
7
8
|
export declare const PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE = "flowersec-proxy:stream_chunk";
|
|
9
|
+
export declare const PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE = "flowersec-proxy:stream_write_ack";
|
|
8
10
|
export declare const PROXY_WINDOW_STREAM_END_MSG_TYPE = "flowersec-proxy:stream_end";
|
|
9
11
|
export declare const PROXY_WINDOW_STREAM_RESET_MSG_TYPE = "flowersec-proxy:stream_reset";
|
|
10
12
|
export declare const PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE = "flowersec-proxy:stream_close";
|
|
@@ -34,6 +36,7 @@ export type ProxyWindowWsOpenMsg = Readonly<{
|
|
|
34
36
|
export type ProxyWindowWsOpenAckMsg = Readonly<{
|
|
35
37
|
type: typeof PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE;
|
|
36
38
|
protocol: string;
|
|
39
|
+
capabilities?: readonly string[];
|
|
37
40
|
}>;
|
|
38
41
|
export type ProxyWindowWsErrorMsg = Readonly<{
|
|
39
42
|
type: typeof PROXY_WINDOW_WS_ERROR_MSG_TYPE;
|
|
@@ -42,6 +45,11 @@ export type ProxyWindowWsErrorMsg = Readonly<{
|
|
|
42
45
|
export type ProxyWindowStreamChunkMsg = Readonly<{
|
|
43
46
|
type: typeof PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE;
|
|
44
47
|
data: ArrayBuffer;
|
|
48
|
+
writeId?: number;
|
|
49
|
+
}>;
|
|
50
|
+
export type ProxyWindowStreamWriteAckMsg = Readonly<{
|
|
51
|
+
type: typeof PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE;
|
|
52
|
+
writeId: number;
|
|
45
53
|
}>;
|
|
46
54
|
export type ProxyWindowStreamEndMsg = Readonly<{
|
|
47
55
|
type: typeof PROXY_WINDOW_STREAM_END_MSG_TYPE;
|
|
@@ -2,8 +2,10 @@ export const PROXY_WINDOW_FETCH_FORWARD_MSG_TYPE = "flowersec-proxy:window_fetch
|
|
|
2
2
|
export const PROXY_WINDOW_FETCH_MSG_TYPE = "flowersec-proxy:fetch";
|
|
3
3
|
export const PROXY_WINDOW_WS_OPEN_MSG_TYPE = "flowersec-proxy:ws_open";
|
|
4
4
|
export const PROXY_WINDOW_WS_OPEN_ACK_MSG_TYPE = "flowersec-proxy:ws_open_ack";
|
|
5
|
+
export const PROXY_WINDOW_WS_WRITE_ACK_CAPABILITY = "stream_write_ack_v1";
|
|
5
6
|
export const PROXY_WINDOW_WS_ERROR_MSG_TYPE = "flowersec-proxy:ws_error";
|
|
6
7
|
export const PROXY_WINDOW_STREAM_CHUNK_MSG_TYPE = "flowersec-proxy:stream_chunk";
|
|
8
|
+
export const PROXY_WINDOW_STREAM_WRITE_ACK_MSG_TYPE = "flowersec-proxy:stream_write_ack";
|
|
7
9
|
export const PROXY_WINDOW_STREAM_END_MSG_TYPE = "flowersec-proxy:stream_end";
|
|
8
10
|
export const PROXY_WINDOW_STREAM_RESET_MSG_TYPE = "flowersec-proxy:stream_reset";
|
|
9
11
|
export const PROXY_WINDOW_STREAM_CLOSE_MSG_TYPE = "flowersec-proxy:stream_close";
|
package/dist/proxy/wsPatch.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type WebSocketPatchOptions = Readonly<{
|
|
|
13
13
|
}>;
|
|
14
14
|
shouldProxy?: (url: URL) => boolean;
|
|
15
15
|
maxWsFrameBytes?: number;
|
|
16
|
+
maxWsBufferedAmountBytes?: number;
|
|
16
17
|
}>;
|
|
17
18
|
export declare function installWebSocketPatch(opts: WebSocketPatchOptions): Readonly<{
|
|
18
19
|
uninstall: () => void;
|
package/dist/proxy/wsPatch.js
CHANGED
|
@@ -85,6 +85,22 @@ export function installWebSocketPatch(opts) {
|
|
|
85
85
|
if (maxWsFrameBytesFloor < 0)
|
|
86
86
|
throw new Error("maxWsFrameBytes must be >= 0");
|
|
87
87
|
const maxWsFrameBytes = maxWsFrameBytesFloor === 0 ? runtimeMaxWsFrameBytes : maxWsFrameBytesFloor;
|
|
88
|
+
const defaultMaxWsBufferedAmountBytes = 4 * (1 << 20);
|
|
89
|
+
const runtimeMaxWsBufferedAmountBytesRaw = runtime.limits?.maxWsBufferedAmountBytes;
|
|
90
|
+
if (runtimeMaxWsBufferedAmountBytesRaw !== undefined &&
|
|
91
|
+
(!Number.isSafeInteger(runtimeMaxWsBufferedAmountBytesRaw) || runtimeMaxWsBufferedAmountBytesRaw < 0)) {
|
|
92
|
+
throw new Error("runtime maxWsBufferedAmountBytes must be a non-negative safe integer");
|
|
93
|
+
}
|
|
94
|
+
const runtimeMaxWsBufferedAmountBytes = runtimeMaxWsBufferedAmountBytesRaw == null || runtimeMaxWsBufferedAmountBytesRaw === 0
|
|
95
|
+
? defaultMaxWsBufferedAmountBytes
|
|
96
|
+
: runtimeMaxWsBufferedAmountBytesRaw;
|
|
97
|
+
const maxWsBufferedAmountBytesRaw = opts.maxWsBufferedAmountBytes ?? runtimeMaxWsBufferedAmountBytes;
|
|
98
|
+
if (!Number.isSafeInteger(maxWsBufferedAmountBytesRaw) || maxWsBufferedAmountBytesRaw < 0) {
|
|
99
|
+
throw new Error("maxWsBufferedAmountBytes must be a non-negative safe integer");
|
|
100
|
+
}
|
|
101
|
+
const maxWsBufferedAmountBytes = maxWsBufferedAmountBytesRaw === 0
|
|
102
|
+
? runtimeMaxWsBufferedAmountBytes
|
|
103
|
+
: maxWsBufferedAmountBytesRaw;
|
|
88
104
|
class PatchedWebSocket {
|
|
89
105
|
static CONNECTING = 0;
|
|
90
106
|
static OPEN = 1;
|
|
@@ -119,36 +135,60 @@ export function installWebSocketPatch(opts) {
|
|
|
119
135
|
removeEventListener(type, listener) {
|
|
120
136
|
this.listeners.off(type, listener);
|
|
121
137
|
}
|
|
122
|
-
queueWriteFrame(stream, op, payload) {
|
|
138
|
+
queueWriteFrame(stream, op, payload, bufferedBytes = 0) {
|
|
123
139
|
this.writeChain = this.writeChain
|
|
124
|
-
.then(() =>
|
|
125
|
-
.
|
|
140
|
+
.then(async () => {
|
|
141
|
+
if (this.readyState === PatchedWebSocket.CLOSED)
|
|
142
|
+
return;
|
|
143
|
+
const resolved = typeof payload === "function" ? await payload() : payload;
|
|
144
|
+
await writeWSFrame(stream, op, resolved, maxWsFrameBytes);
|
|
145
|
+
})
|
|
146
|
+
.catch((e) => this.fail(e))
|
|
147
|
+
.finally(() => {
|
|
148
|
+
this.bufferedAmount = Math.max(0, this.bufferedAmount - bufferedBytes);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
reserveBufferedAmount(bytes) {
|
|
152
|
+
if (this.bufferedAmount + bytes > maxWsBufferedAmountBytes) {
|
|
153
|
+
this.fail(new Error("WebSocket bufferedAmount limit exceeded"));
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
this.bufferedAmount += bytes;
|
|
157
|
+
return true;
|
|
126
158
|
}
|
|
127
159
|
send(data) {
|
|
128
160
|
if (this.readyState !== PatchedWebSocket.OPEN || this.stream == null) {
|
|
129
161
|
throw new Error("WebSocket is not open");
|
|
130
162
|
}
|
|
131
163
|
const s = this.stream;
|
|
132
|
-
const sendBytes = (op,
|
|
133
|
-
this.
|
|
164
|
+
const sendBytes = (op, byteLength, copyPayload) => {
|
|
165
|
+
if (!this.reserveBufferedAmount(byteLength))
|
|
166
|
+
return;
|
|
167
|
+
try {
|
|
168
|
+
this.queueWriteFrame(s, op, copyPayload(), byteLength);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
this.bufferedAmount = Math.max(0, this.bufferedAmount - byteLength);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
134
174
|
};
|
|
135
175
|
if (typeof data === "string") {
|
|
136
|
-
|
|
176
|
+
const payload = te.encode(data);
|
|
177
|
+
sendBytes(1, payload.byteLength, () => payload);
|
|
137
178
|
return;
|
|
138
179
|
}
|
|
139
180
|
if (data instanceof ArrayBuffer) {
|
|
140
|
-
sendBytes(2, new Uint8Array(data));
|
|
181
|
+
sendBytes(2, data.byteLength, () => new Uint8Array(data).slice());
|
|
141
182
|
return;
|
|
142
183
|
}
|
|
143
184
|
if (ArrayBuffer.isView(data)) {
|
|
144
|
-
sendBytes(2, new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
185
|
+
sendBytes(2, data.byteLength, () => new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice());
|
|
145
186
|
return;
|
|
146
187
|
}
|
|
147
188
|
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.catch((e) => this.fail(e));
|
|
189
|
+
if (!this.reserveBufferedAmount(data.size))
|
|
190
|
+
return;
|
|
191
|
+
this.queueWriteFrame(s, 2, async () => new Uint8Array(await data.arrayBuffer()), data.size);
|
|
152
192
|
return;
|
|
153
193
|
}
|
|
154
194
|
throw new Error("unsupported WebSocket send payload");
|
|
@@ -260,10 +300,14 @@ export function installWebSocketPatch(opts) {
|
|
|
260
300
|
}
|
|
261
301
|
}
|
|
262
302
|
fail(e) {
|
|
303
|
+
if (this.readyState === PatchedWebSocket.CLOSED)
|
|
304
|
+
return;
|
|
263
305
|
this.readyState = PatchedWebSocket.CLOSED;
|
|
264
306
|
const msg = e instanceof Error ? e.message : String(e);
|
|
307
|
+
this.bufferedAmount = 0;
|
|
265
308
|
this.emit("error", { type: "error", message: msg });
|
|
266
309
|
this.emit("close", { type: "close", code: 1006, reason: msg, wasClean: false });
|
|
310
|
+
this.stream = null;
|
|
267
311
|
try {
|
|
268
312
|
this.ac.abort(msg);
|
|
269
313
|
}
|
package/dist/reconnect/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getClientTermination } from "../client-connect/termination.js";
|
|
1
2
|
import { emitObserverDiagnostic, withObserverContext } from "../observability/observer.js";
|
|
2
3
|
export { createArtifactResolver, createControlplaneArtifactSource } from "./artifactControlplane.js";
|
|
3
4
|
function normalizeAutoReconnect(cfg) {
|
|
@@ -213,6 +214,14 @@ export function createReconnectManager() {
|
|
|
213
214
|
return;
|
|
214
215
|
}
|
|
215
216
|
setState({ status: "connected", client, error: null });
|
|
217
|
+
const termination = getClientTermination(client);
|
|
218
|
+
if (termination != null) {
|
|
219
|
+
void termination.then(({ error }) => {
|
|
220
|
+
if (s.client !== client)
|
|
221
|
+
return;
|
|
222
|
+
startReconnect(t, cfg, error);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
216
225
|
emitObserverDiagnostic(withObserverContext(cfg.observer, { attemptSeq }), {
|
|
217
226
|
path: "auto",
|
|
218
227
|
stage: "reconnect",
|
package/dist/rpc/client.d.ts
CHANGED
|
@@ -8,8 +8,10 @@ export declare class RpcClient {
|
|
|
8
8
|
private readonly notifyHandlers;
|
|
9
9
|
private closed;
|
|
10
10
|
private readonly observer;
|
|
11
|
+
private readonly onTerminal;
|
|
11
12
|
constructor(readExactly: (n: number) => Promise<Uint8Array>, write: (b: Uint8Array) => Promise<void>, opts?: Readonly<{
|
|
12
13
|
observer?: ClientObserverLike;
|
|
14
|
+
onTerminal?: (error: Error) => void;
|
|
13
15
|
}>);
|
|
14
16
|
call(typeId: number, payload: unknown, signal?: AbortSignal): Promise<{
|
|
15
17
|
payload: unknown;
|
package/dist/rpc/client.js
CHANGED
|
@@ -17,10 +17,12 @@ export class RpcClient {
|
|
|
17
17
|
closed = false;
|
|
18
18
|
// Observer for RPC events.
|
|
19
19
|
observer;
|
|
20
|
+
onTerminal;
|
|
20
21
|
constructor(readExactly, write, opts = {}) {
|
|
21
22
|
this.readExactly = readExactly;
|
|
22
23
|
this.write = write;
|
|
23
24
|
this.observer = normalizeObserver(opts.observer);
|
|
25
|
+
this.onTerminal = opts.onTerminal;
|
|
24
26
|
void this.readLoop();
|
|
25
27
|
}
|
|
26
28
|
// call sends a request and awaits a response or abort.
|
|
@@ -136,10 +138,19 @@ export class RpcClient {
|
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
catch (e) {
|
|
141
|
+
const unexpected = !this.closed;
|
|
139
142
|
this.closed = true;
|
|
140
143
|
for (const [, p] of this.pending)
|
|
141
144
|
p.reject(e);
|
|
142
145
|
this.pending.clear();
|
|
146
|
+
if (unexpected) {
|
|
147
|
+
try {
|
|
148
|
+
this.onTerminal?.(e instanceof Error ? e : new Error(String(e)));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Lifecycle callbacks must not escape the read loop.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
143
154
|
}
|
|
144
155
|
}
|
|
145
156
|
}
|
package/dist/yamux/session.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export type YamuxSessionOptions = Readonly<{
|
|
|
33
33
|
limits?: Partial<YamuxLimits>;
|
|
34
34
|
/** Optional generic resource diagnostic callback. */
|
|
35
35
|
onDiagnostic?: (event: YamuxDiagnostic) => void;
|
|
36
|
+
/** Internal lifecycle callback for unexpected session termination. */
|
|
37
|
+
onTerminal?: (error: Error) => void;
|
|
36
38
|
}>;
|
|
37
39
|
export declare class YamuxSession {
|
|
38
40
|
private readonly conn;
|
|
@@ -41,6 +43,7 @@ export declare class YamuxSession {
|
|
|
41
43
|
private readonly onIncomingStream;
|
|
42
44
|
private readonly limits;
|
|
43
45
|
private readonly onDiagnostic;
|
|
46
|
+
private readonly onTerminal;
|
|
44
47
|
private readonly client;
|
|
45
48
|
private nextStreamId;
|
|
46
49
|
private closed;
|
|
@@ -51,7 +54,9 @@ export declare class YamuxSession {
|
|
|
51
54
|
private readonly pingWaiters;
|
|
52
55
|
private activeProbe;
|
|
53
56
|
constructor(conn: ByteDuplex, opts: YamuxSessionOptions);
|
|
54
|
-
openStream(
|
|
57
|
+
openStream(opts?: Readonly<{
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
}>): Promise<YamuxStream>;
|
|
55
60
|
getStream(id: number): YamuxStream | undefined;
|
|
56
61
|
writeRaw(chunk: Uint8Array): Promise<void>;
|
|
57
62
|
outboundFrameBytes(): number;
|
|
@@ -64,6 +69,8 @@ export declare class YamuxSession {
|
|
|
64
69
|
onStreamEstablished(_streamId: number): void;
|
|
65
70
|
onStreamClosed(streamId: number): void;
|
|
66
71
|
close(): void;
|
|
72
|
+
private fail;
|
|
73
|
+
private closeInternal;
|
|
67
74
|
private wakeSendWindowWaiters;
|
|
68
75
|
private readLoop;
|
|
69
76
|
private handlePing;
|