@floegence/flowersec-core 2.5.2 → 2.5.4
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/framing/jsonframe.d.ts +1 -1
- package/dist/framing/jsonframe.js +3 -1
- package/dist/node/acceptor.js +33 -11
- package/dist/proxy/runtime.js +39 -6
- package/dist/proxy/serviceWorker.d.ts +2 -0
- package/dist/proxy/serviceWorker.js +63 -7
- package/dist/proxy/windowBridge.d.ts +2 -0
- package/dist/proxy/windowBridge.js +143 -42
- package/dist/proxy/wsPatch.d.ts +1 -0
- package/dist/proxy/wsPatch.js +39 -6
- package/dist/rpc/client.js +9 -7
- package/dist/rpc/server.js +44 -20
- package/dist/rpc/validate.d.ts +1 -0
- package/dist/rpc/validate.js +18 -6
- package/dist/transport/webTransportAdapter.js +17 -2
- package/dist/v2/publicSession.js +4 -0
- package/dist/v2/session.d.ts +1 -1
- package/dist/v2/session.js +38 -11
- package/dist/yamux/stream.d.ts +3 -0
- package/dist/yamux/stream.js +17 -4
- package/package.json +3 -3
- package/sbom/cyclonedx.json +6 -6
- package/sbom/spdx.json +11 -11
package/dist/proxy/wsPatch.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type WebSocketPatchOptions = Readonly<{
|
|
|
4
4
|
shouldProxy?: (url: URL) => boolean;
|
|
5
5
|
maxWsFrameBytes?: number;
|
|
6
6
|
maxWsBufferedAmountBytes?: number;
|
|
7
|
+
closeHandshakeTimeoutMs?: number;
|
|
7
8
|
}>;
|
|
8
9
|
export declare function installWebSocketPatch(options: WebSocketPatchOptions): Readonly<{
|
|
9
10
|
uninstall(): void;
|
package/dist/proxy/wsPatch.js
CHANGED
|
@@ -64,6 +64,7 @@ export function installWebSocketPatch(options) {
|
|
|
64
64
|
const runtimeLimits = options.runtime.limits;
|
|
65
65
|
const maxFrameBytes = limit("maxWsFrameBytes", options.maxWsFrameBytes, runtimeLimits.maxWsFrameBytes ?? 1024 * 1024);
|
|
66
66
|
const maxBufferedBytes = limit("maxWsBufferedAmountBytes", options.maxWsBufferedAmountBytes, runtimeLimits.maxWsBufferedAmountBytes ?? 4 * 1024 * 1024);
|
|
67
|
+
const closeHandshakeTimeoutMs = limit("closeHandshakeTimeoutMs", options.closeHandshakeTimeoutMs, 5_000);
|
|
67
68
|
const shouldProxy = options.shouldProxy ?? ((url) => {
|
|
68
69
|
const location = globalThis.location;
|
|
69
70
|
if (location?.hostname === undefined || location.hostname === "")
|
|
@@ -95,6 +96,7 @@ export function installWebSocketPatch(options) {
|
|
|
95
96
|
abort = new AbortController();
|
|
96
97
|
stream;
|
|
97
98
|
writes = Promise.resolve();
|
|
99
|
+
closeTimer;
|
|
98
100
|
constructor(input, protocols) {
|
|
99
101
|
const url = new URL(String(input), globalThis.location?.href);
|
|
100
102
|
if (!shouldProxy(url))
|
|
@@ -152,12 +154,17 @@ export function installWebSocketPatch(options) {
|
|
|
152
154
|
const reasonBytes = encoder.encode(reason);
|
|
153
155
|
if (reasonBytes.length > 123)
|
|
154
156
|
throw new DOMException("WebSocket close reason is too long", "SyntaxError");
|
|
157
|
+
if (this.readyState === ProxyWebSocket.CONNECTING || this.stream === undefined) {
|
|
158
|
+
this.fail();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
155
161
|
this.readyState = ProxyWebSocket.CLOSING;
|
|
156
162
|
const payload = code === undefined ? new Uint8Array() : new Uint8Array([...u16be(code), ...reasonBytes]);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
const stream = this.stream;
|
|
164
|
+
this.closeTimer = setTimeout(() => this.fail(), closeHandshakeTimeoutMs);
|
|
165
|
+
this.writes = this.writes
|
|
166
|
+
.then(async () => await writeFrame(stream, 8, payload, maxFrameBytes))
|
|
167
|
+
.catch(() => this.fail());
|
|
161
168
|
}
|
|
162
169
|
async connect(url, protocols) {
|
|
163
170
|
try {
|
|
@@ -183,10 +190,19 @@ export function installWebSocketPatch(options) {
|
|
|
183
190
|
this.writes = this.writes.then(async () => await writeFrame(stream, 10, frame.payload, maxFrameBytes)).catch(() => this.fail());
|
|
184
191
|
}
|
|
185
192
|
else if (frame.opcode === 8) {
|
|
193
|
+
if (frame.payload.length === 1)
|
|
194
|
+
throw new Error("invalid WebSocket close frame");
|
|
186
195
|
const code = frame.payload.length >= 2 ? readU16(frame.payload) : 1000;
|
|
187
196
|
const reason = frame.payload.length > 2 ? decoder.decode(frame.payload.subarray(2)) : "";
|
|
188
|
-
this.readyState
|
|
189
|
-
this.
|
|
197
|
+
const peerInitiated = this.readyState === ProxyWebSocket.OPEN;
|
|
198
|
+
this.readyState = ProxyWebSocket.CLOSING;
|
|
199
|
+
if (this.closeTimer !== undefined)
|
|
200
|
+
clearTimeout(this.closeTimer);
|
|
201
|
+
if (peerInitiated) {
|
|
202
|
+
this.writes = this.writes.then(async () => await writeFrame(stream, 8, frame.payload, maxFrameBytes));
|
|
203
|
+
}
|
|
204
|
+
await this.writes;
|
|
205
|
+
this.completeClose(code, reason);
|
|
190
206
|
return;
|
|
191
207
|
}
|
|
192
208
|
else if (frame.opcode === 1) {
|
|
@@ -211,12 +227,29 @@ export function installWebSocketPatch(options) {
|
|
|
211
227
|
fail() {
|
|
212
228
|
if (this.readyState === ProxyWebSocket.CLOSED)
|
|
213
229
|
return;
|
|
230
|
+
if (this.closeTimer !== undefined)
|
|
231
|
+
clearTimeout(this.closeTimer);
|
|
214
232
|
this.readyState = ProxyWebSocket.CLOSED;
|
|
215
233
|
this.bufferedAmount = 0;
|
|
216
234
|
this.emit("error", new Event("error"));
|
|
217
235
|
this.emit("close", new CloseEvent("close", { code: 1006, reason: "proxy WebSocket failed", wasClean: false }));
|
|
236
|
+
const stream = this.stream;
|
|
237
|
+
this.stream = undefined;
|
|
238
|
+
this.abort.abort();
|
|
239
|
+
void stream?.reset().catch(() => undefined);
|
|
240
|
+
}
|
|
241
|
+
completeClose(code, reason) {
|
|
242
|
+
if (this.readyState === ProxyWebSocket.CLOSED)
|
|
243
|
+
return;
|
|
244
|
+
if (this.closeTimer !== undefined)
|
|
245
|
+
clearTimeout(this.closeTimer);
|
|
246
|
+
this.readyState = ProxyWebSocket.CLOSED;
|
|
247
|
+
this.bufferedAmount = 0;
|
|
248
|
+
this.emit("close", new CloseEvent("close", { code, reason, wasClean: true }));
|
|
249
|
+
const stream = this.stream;
|
|
218
250
|
this.stream = undefined;
|
|
219
251
|
this.abort.abort();
|
|
252
|
+
void stream?.close().catch(() => stream.reset().catch(() => undefined));
|
|
220
253
|
}
|
|
221
254
|
}
|
|
222
255
|
globalThis.WebSocket = ProxyWebSocket;
|
package/dist/rpc/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
2
|
-
import { assertRpcEnvelope } from "./validate.js";
|
|
2
|
+
import { assertRpcEnvelope, assertRpcTypeId } from "./validate.js";
|
|
3
3
|
// Guard against precision loss when encoding request IDs as numbers.
|
|
4
4
|
const MAX_SAFE_REQUEST_ID = BigInt(Number.MAX_SAFE_INTEGER);
|
|
5
5
|
// RpcClient sends request/response envelopes and dispatches notifications.
|
|
@@ -25,12 +25,13 @@ export class RpcClient {
|
|
|
25
25
|
async call(typeId, payload, signal) {
|
|
26
26
|
if (this.closed)
|
|
27
27
|
throw new Error("rpc client closed");
|
|
28
|
+
const validatedTypeId = assertRpcTypeId(typeId);
|
|
28
29
|
if (this.nextId > MAX_SAFE_REQUEST_ID)
|
|
29
30
|
throw new Error("request id overflow");
|
|
30
31
|
const requestId = this.nextId;
|
|
31
32
|
this.nextId += 1n;
|
|
32
33
|
const env = {
|
|
33
|
-
type_id:
|
|
34
|
+
type_id: validatedTypeId,
|
|
34
35
|
request_id: Number(requestId),
|
|
35
36
|
response_to: 0,
|
|
36
37
|
payload
|
|
@@ -39,7 +40,7 @@ export class RpcClient {
|
|
|
39
40
|
this.pending.set(requestId, { resolve, reject });
|
|
40
41
|
});
|
|
41
42
|
try {
|
|
42
|
-
await writeJsonFrame(this.write, env);
|
|
43
|
+
await writeJsonFrame(this.write, env, DEFAULT_MAX_JSON_FRAME_BYTES);
|
|
43
44
|
}
|
|
44
45
|
catch (e) {
|
|
45
46
|
this.pending.delete(requestId);
|
|
@@ -71,7 +72,7 @@ export class RpcClient {
|
|
|
71
72
|
}
|
|
72
73
|
// onNotify registers a handler for incoming notifications.
|
|
73
74
|
onNotify(typeId, handler) {
|
|
74
|
-
const tid = typeId
|
|
75
|
+
const tid = assertRpcTypeId(typeId);
|
|
75
76
|
const set = this.notifyHandlers.get(tid) ?? new Set();
|
|
76
77
|
set.add(handler);
|
|
77
78
|
this.notifyHandlers.set(tid, set);
|
|
@@ -86,13 +87,14 @@ export class RpcClient {
|
|
|
86
87
|
async notify(typeId, payload) {
|
|
87
88
|
if (this.closed)
|
|
88
89
|
throw new Error("rpc client closed");
|
|
90
|
+
const validatedTypeId = assertRpcTypeId(typeId);
|
|
89
91
|
const env = {
|
|
90
|
-
type_id:
|
|
92
|
+
type_id: validatedTypeId,
|
|
91
93
|
request_id: 0,
|
|
92
94
|
response_to: 0,
|
|
93
95
|
payload
|
|
94
96
|
};
|
|
95
|
-
await writeJsonFrame(this.write, env);
|
|
97
|
+
await writeJsonFrame(this.write, env, DEFAULT_MAX_JSON_FRAME_BYTES);
|
|
96
98
|
}
|
|
97
99
|
async readLoop() {
|
|
98
100
|
try {
|
|
@@ -101,7 +103,7 @@ export class RpcClient {
|
|
|
101
103
|
if (v.response_to === 0) {
|
|
102
104
|
// Notification: response_to=0 and request_id=0.
|
|
103
105
|
if (v.request_id === 0) {
|
|
104
|
-
const set = this.notifyHandlers.get(v.type_id
|
|
106
|
+
const set = this.notifyHandlers.get(v.type_id);
|
|
105
107
|
if (set != null) {
|
|
106
108
|
for (const h of set) {
|
|
107
109
|
try {
|
package/dist/rpc/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
2
|
-
import { assertRpcEnvelope, assertRpcError } from "./validate.js";
|
|
2
|
+
import { assertRpcEnvelope, assertRpcError, assertRpcTypeId } from "./validate.js";
|
|
3
3
|
import { SDK_DEFAULTS } from "../defaults.js";
|
|
4
4
|
const DEFAULT_RPC_SERVER_OPTIONS = Object.freeze({
|
|
5
5
|
maxConcurrentRequests: SDK_DEFAULTS.rpc.maxConcurrentRequests,
|
|
@@ -10,13 +10,13 @@ export class RpcRouter {
|
|
|
10
10
|
handlers = new Map();
|
|
11
11
|
notifyHandlers = new Map();
|
|
12
12
|
register(typeId, handler) {
|
|
13
|
-
this.handlers.set(typeId
|
|
13
|
+
this.handlers.set(assertRpcTypeId(typeId), handler);
|
|
14
14
|
}
|
|
15
15
|
handler(typeId) {
|
|
16
|
-
return this.handlers.get(typeId
|
|
16
|
+
return this.handlers.get(assertRpcTypeId(typeId));
|
|
17
17
|
}
|
|
18
18
|
onNotify(typeId, handler) {
|
|
19
|
-
const normalized = typeId
|
|
19
|
+
const normalized = assertRpcTypeId(typeId);
|
|
20
20
|
const handlers = this.notifyHandlers.get(normalized) ?? new Set();
|
|
21
21
|
handlers.add(handler);
|
|
22
22
|
this.notifyHandlers.set(normalized, handlers);
|
|
@@ -27,7 +27,7 @@ export class RpcRouter {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
async dispatchNotification(typeId, payload) {
|
|
30
|
-
const normalized = typeId
|
|
30
|
+
const normalized = assertRpcTypeId(typeId);
|
|
31
31
|
const requestHandler = this.handlers.get(normalized);
|
|
32
32
|
if (requestHandler !== undefined)
|
|
33
33
|
await requestHandler(payload);
|
|
@@ -76,7 +76,7 @@ export class RpcServer {
|
|
|
76
76
|
if (this.closed)
|
|
77
77
|
throw new Error("rpc server closed");
|
|
78
78
|
await this.writeEnvelope({
|
|
79
|
-
type_id: typeId
|
|
79
|
+
type_id: assertRpcTypeId(typeId),
|
|
80
80
|
request_id: 0,
|
|
81
81
|
response_to: 0,
|
|
82
82
|
payload,
|
|
@@ -93,6 +93,7 @@ export class RpcServer {
|
|
|
93
93
|
return await new Promise(() => undefined);
|
|
94
94
|
}));
|
|
95
95
|
let failure;
|
|
96
|
+
const aborted = abortPromise(signal);
|
|
96
97
|
try {
|
|
97
98
|
while (!this.closed) {
|
|
98
99
|
if (signal?.aborted)
|
|
@@ -101,6 +102,7 @@ export class RpcServer {
|
|
|
101
102
|
readJsonFrame(this.transport.readExactly, DEFAULT_MAX_JSON_FRAME_BYTES),
|
|
102
103
|
this.terminalSignal.then((error) => { throw error; }),
|
|
103
104
|
workerFailure,
|
|
105
|
+
...(aborted === undefined ? [] : [aborted.promise]),
|
|
104
106
|
]);
|
|
105
107
|
const v = assertRpcEnvelope(next);
|
|
106
108
|
if (v.response_to !== 0)
|
|
@@ -126,6 +128,9 @@ export class RpcServer {
|
|
|
126
128
|
failure = err;
|
|
127
129
|
this.terminalError = err;
|
|
128
130
|
}
|
|
131
|
+
finally {
|
|
132
|
+
aborted?.cleanup();
|
|
133
|
+
}
|
|
129
134
|
let closeError;
|
|
130
135
|
try {
|
|
131
136
|
this.close(failure ?? this.terminalError ?? new Error("rpc server closed"));
|
|
@@ -173,19 +178,23 @@ export class RpcServer {
|
|
|
173
178
|
if (work == null)
|
|
174
179
|
return;
|
|
175
180
|
const v = work.envelope;
|
|
176
|
-
const h = this.router.handler(v.type_id);
|
|
177
|
-
let out;
|
|
178
|
-
if (h == null)
|
|
179
|
-
out = { payload: null, error: { code: 404, message: "handler not found" } };
|
|
180
|
-
else {
|
|
181
|
-
try {
|
|
182
|
-
out = await h(v.payload);
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
out = { payload: null, error: { code: 500, message: "internal error" } };
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
181
|
try {
|
|
182
|
+
const h = this.router.handler(v.type_id);
|
|
183
|
+
let out;
|
|
184
|
+
if (h == null)
|
|
185
|
+
out = { payload: null, error: { code: 404, message: "handler not found" } };
|
|
186
|
+
else {
|
|
187
|
+
const outcome = await Promise.race([
|
|
188
|
+
Promise.resolve().then(() => h(v.payload)).then((value) => ({ kind: "completed", value }), () => ({
|
|
189
|
+
kind: "completed",
|
|
190
|
+
value: { payload: null, error: { code: 500, message: "internal error" } },
|
|
191
|
+
})),
|
|
192
|
+
this.terminalSignal.then(() => ({ kind: "terminated" })),
|
|
193
|
+
]);
|
|
194
|
+
if (outcome.kind === "terminated")
|
|
195
|
+
return;
|
|
196
|
+
out = outcome.value;
|
|
197
|
+
}
|
|
189
198
|
if (this.closed)
|
|
190
199
|
return;
|
|
191
200
|
await this.writeResponse(v, out);
|
|
@@ -201,7 +210,12 @@ export class RpcServer {
|
|
|
201
210
|
if (work == null)
|
|
202
211
|
return;
|
|
203
212
|
const v = work.envelope;
|
|
204
|
-
await
|
|
213
|
+
const completed = await Promise.race([
|
|
214
|
+
Promise.resolve().then(() => this.router.dispatchNotification(v.type_id, v.payload)).then(() => true),
|
|
215
|
+
this.terminalSignal.then(() => false),
|
|
216
|
+
]);
|
|
217
|
+
if (!completed)
|
|
218
|
+
return;
|
|
205
219
|
}
|
|
206
220
|
}
|
|
207
221
|
async nextWork(queue, waiters) {
|
|
@@ -236,7 +250,7 @@ export class RpcServer {
|
|
|
236
250
|
await this.writeEnvelope(resp);
|
|
237
251
|
}
|
|
238
252
|
async writeEnvelope(envelope) {
|
|
239
|
-
const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, envelope));
|
|
253
|
+
const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, envelope, DEFAULT_MAX_JSON_FRAME_BYTES));
|
|
240
254
|
this.writeChain = write;
|
|
241
255
|
try {
|
|
242
256
|
await write;
|
|
@@ -247,6 +261,16 @@ export class RpcServer {
|
|
|
247
261
|
}
|
|
248
262
|
}
|
|
249
263
|
}
|
|
264
|
+
function abortPromise(signal) {
|
|
265
|
+
if (signal === undefined || signal.aborted)
|
|
266
|
+
return undefined;
|
|
267
|
+
let onAbort;
|
|
268
|
+
const promise = new Promise((_resolve, reject) => {
|
|
269
|
+
onAbort = () => reject(signal.reason ?? new Error("aborted"));
|
|
270
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
271
|
+
});
|
|
272
|
+
return { promise, cleanup: () => signal.removeEventListener("abort", onAbort) };
|
|
273
|
+
}
|
|
250
274
|
function positiveInteger(value, name) {
|
|
251
275
|
if (!Number.isSafeInteger(value) || value <= 0)
|
|
252
276
|
throw new RangeError(`${name} must be a positive integer`);
|
package/dist/rpc/validate.d.ts
CHANGED
package/dist/rpc/validate.js
CHANGED
|
@@ -6,21 +6,33 @@ const strictDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
|
6
6
|
// The wire format is JSON, so JS numbers are used. For u64 we enforce the safe integer range
|
|
7
7
|
// to avoid silent precision loss on request/response correlation.
|
|
8
8
|
export function assertRpcEnvelope(v) {
|
|
9
|
-
if (typeof v !== "object" || v == null)
|
|
9
|
+
if (typeof v !== "object" || v == null || Array.isArray(v))
|
|
10
10
|
throw new Error("bad rpc envelope");
|
|
11
11
|
const o = v;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const keys = Object.keys(o);
|
|
13
|
+
if (keys.some((key) => key !== "type_id" && key !== "request_id" && key !== "response_to" && key !== "payload" && key !== "error")) {
|
|
14
|
+
throw new Error("bad rpc envelope: shape");
|
|
15
|
+
}
|
|
16
|
+
if (!Object.prototype.hasOwnProperty.call(o, "payload"))
|
|
17
|
+
throw new Error("bad rpc envelope: payload");
|
|
18
|
+
assertRpcTypeId(o.type_id);
|
|
14
19
|
if (!isSafeU64Number(o.request_id))
|
|
15
20
|
throw new Error("bad rpc envelope: request_id");
|
|
16
21
|
if (!isSafeU64Number(o.response_to))
|
|
17
22
|
throw new Error("bad rpc envelope: response_to");
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
if (o.request_id !== 0 && o.response_to !== 0)
|
|
24
|
+
throw new Error("bad rpc envelope: request/response shape");
|
|
25
|
+
if (o.error != null && o.response_to === 0)
|
|
26
|
+
throw new Error("bad rpc envelope: error shape");
|
|
27
|
+
if (o.error != null)
|
|
20
28
|
assertRpcError(o.error);
|
|
21
|
-
}
|
|
22
29
|
return o;
|
|
23
30
|
}
|
|
31
|
+
export function assertRpcTypeId(value) {
|
|
32
|
+
if (!isSafeU32Number(value) || value === 0)
|
|
33
|
+
throw new RangeError("RPC typeId must be a non-zero u32 integer");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
24
36
|
export function assertRpcError(value) {
|
|
25
37
|
if (typeof value !== "object" || value == null)
|
|
26
38
|
throw new Error("bad rpc envelope: error");
|
|
@@ -327,6 +327,7 @@ class WebTransportDatagramAdapter {
|
|
|
327
327
|
this.terminalError = error;
|
|
328
328
|
this.incoming.fail(error);
|
|
329
329
|
void this.writable.abort(error).catch(() => undefined);
|
|
330
|
+
void this.readable.cancel(error).catch(() => undefined);
|
|
330
331
|
}
|
|
331
332
|
assertOpen() {
|
|
332
333
|
if (this.terminalError !== undefined)
|
|
@@ -416,15 +417,26 @@ class IncomingWebTransportStreamQueue {
|
|
|
416
417
|
}
|
|
417
418
|
}
|
|
418
419
|
class IncomingDatagramQueue {
|
|
420
|
+
static MAX_VALUES = 256;
|
|
421
|
+
static MAX_BYTES = 4 * 1024 * 1024;
|
|
419
422
|
values = [];
|
|
423
|
+
bufferedBytes = 0;
|
|
420
424
|
waiters = new Set();
|
|
421
425
|
terminalError;
|
|
422
426
|
push(value) {
|
|
423
427
|
if (this.terminalError !== undefined)
|
|
424
428
|
return;
|
|
425
429
|
const waiter = this.waiters.values().next().value;
|
|
426
|
-
if (waiter === undefined)
|
|
430
|
+
if (waiter === undefined) {
|
|
431
|
+
// DATAGRAMs are lossy by contract. Drop an incoming value once the
|
|
432
|
+
// bounded queue budget is exhausted, while keeping the native reader
|
|
433
|
+
// drained so an unconsumed peer cannot grow memory without bound.
|
|
434
|
+
if (this.values.length >= IncomingDatagramQueue.MAX_VALUES ||
|
|
435
|
+
this.bufferedBytes + value.byteLength > IncomingDatagramQueue.MAX_BYTES)
|
|
436
|
+
return;
|
|
427
437
|
this.values.push(value);
|
|
438
|
+
this.bufferedBytes += value.byteLength;
|
|
439
|
+
}
|
|
428
440
|
else
|
|
429
441
|
waiter.deliver(value);
|
|
430
442
|
}
|
|
@@ -434,8 +446,10 @@ class IncomingDatagramQueue {
|
|
|
434
446
|
if (this.terminalError !== undefined)
|
|
435
447
|
return Promise.reject(this.terminalError);
|
|
436
448
|
const value = this.values.shift();
|
|
437
|
-
if (value !== undefined)
|
|
449
|
+
if (value !== undefined) {
|
|
450
|
+
this.bufferedBytes -= value.byteLength;
|
|
438
451
|
return Promise.resolve(value);
|
|
452
|
+
}
|
|
439
453
|
return new Promise((resolve, reject) => {
|
|
440
454
|
let settled = false;
|
|
441
455
|
const cleanup = () => {
|
|
@@ -470,6 +484,7 @@ class IncomingDatagramQueue {
|
|
|
470
484
|
return;
|
|
471
485
|
this.terminalError = error;
|
|
472
486
|
this.values.length = 0;
|
|
487
|
+
this.bufferedBytes = 0;
|
|
473
488
|
for (const waiter of [...this.waiters])
|
|
474
489
|
waiter.fail(error);
|
|
475
490
|
}
|
package/dist/v2/publicSession.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SessionError } from "./contract.js";
|
|
2
2
|
import { createStreamMetadataV2, streamMetadataValuesV2 } from "./streamMetadata.js";
|
|
3
|
+
import { assertRpcTypeId } from "../rpc/validate.js";
|
|
3
4
|
/** @internal */
|
|
4
5
|
export function projectSessionV2(session) {
|
|
5
6
|
const notificationOwner = {
|
|
@@ -132,6 +133,7 @@ function projectRpcPeerV2(peer, notificationOwner) {
|
|
|
132
133
|
return Object.freeze({
|
|
133
134
|
async call(typeId, payload, decodeResponse, options) {
|
|
134
135
|
try {
|
|
136
|
+
assertRpcTypeId(typeId);
|
|
135
137
|
assertJsonValue(payload);
|
|
136
138
|
const result = await peer.call(typeId, payload, options?.signal);
|
|
137
139
|
if (result.error !== undefined) {
|
|
@@ -146,6 +148,7 @@ function projectRpcPeerV2(peer, notificationOwner) {
|
|
|
146
148
|
},
|
|
147
149
|
async notify(typeId, payload, options) {
|
|
148
150
|
try {
|
|
151
|
+
assertRpcTypeId(typeId);
|
|
149
152
|
assertJsonValue(payload);
|
|
150
153
|
if (options?.signal?.aborted)
|
|
151
154
|
throw options.signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
@@ -156,6 +159,7 @@ function projectRpcPeerV2(peer, notificationOwner) {
|
|
|
156
159
|
}
|
|
157
160
|
},
|
|
158
161
|
onNotify(typeId, decodePayload, handler) {
|
|
162
|
+
assertRpcTypeId(typeId);
|
|
159
163
|
if (notificationOwner.closed)
|
|
160
164
|
return () => undefined;
|
|
161
165
|
const unsubscribe = peer.onNotify(typeId, (payload) => {
|
package/dist/v2/session.d.ts
CHANGED
|
@@ -132,7 +132,7 @@ export declare class SessionV2 implements SessionV2Contract {
|
|
|
132
132
|
private peerResponderFrozen;
|
|
133
133
|
private responderChanged;
|
|
134
134
|
private idleWatchdogStarted;
|
|
135
|
-
private
|
|
135
|
+
private idleTimerCancel;
|
|
136
136
|
private readonly terminationState;
|
|
137
137
|
private readonly rpcRouter;
|
|
138
138
|
constructor(carrier: CarrierSessionV2, control: CarrierStreamV2, controlReader: ExactReader, config: SessionConfigV2, material: HandshakeMaterial);
|
package/dist/v2/session.js
CHANGED
|
@@ -137,7 +137,7 @@ export class SessionV2 {
|
|
|
137
137
|
peerResponderFrozen = false;
|
|
138
138
|
responderChanged = deferred();
|
|
139
139
|
idleWatchdogStarted = false;
|
|
140
|
-
|
|
140
|
+
idleTimerCancel;
|
|
141
141
|
terminationState = deferred();
|
|
142
142
|
rpcRouter;
|
|
143
143
|
constructor(carrier, control, controlReader, config, material) {
|
|
@@ -1017,10 +1017,9 @@ export class SessionV2 {
|
|
|
1017
1017
|
const timeoutMs = sessionIdleTimeoutMs(this.config);
|
|
1018
1018
|
if (timeoutMs === 0)
|
|
1019
1019
|
return;
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
this.idleTimer = undefined;
|
|
1020
|
+
this.idleTimerCancel?.();
|
|
1021
|
+
this.idleTimerCancel = scheduleLongTimeout(() => {
|
|
1022
|
+
this.idleTimerCancel = undefined;
|
|
1024
1023
|
this.fail(new SessionV2Error("timeout", "Flowersec v2 session idle timeout exceeded"));
|
|
1025
1024
|
}, timeoutMs);
|
|
1026
1025
|
}
|
|
@@ -1034,10 +1033,8 @@ export class SessionV2 {
|
|
|
1034
1033
|
this.terminalError = error;
|
|
1035
1034
|
this.lifecycle = "closed";
|
|
1036
1035
|
this.terminationState.resolve({ error });
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
this.idleTimer = undefined;
|
|
1040
|
-
}
|
|
1036
|
+
this.idleTimerCancel?.();
|
|
1037
|
+
this.idleTimerCancel = undefined;
|
|
1041
1038
|
this.incoming.fail(error);
|
|
1042
1039
|
this.outboundPermits.fail(error);
|
|
1043
1040
|
this.inboundPermits.fail(error);
|
|
@@ -1227,6 +1224,10 @@ class EncryptedStreamV2 {
|
|
|
1227
1224
|
if (this.terminalError !== undefined)
|
|
1228
1225
|
return false;
|
|
1229
1226
|
this.terminalError = error;
|
|
1227
|
+
const pendingSendRekey = this.pendingSendRekey;
|
|
1228
|
+
this.pendingSendRekey = undefined;
|
|
1229
|
+
pendingSendRekey?.armed.reject(error);
|
|
1230
|
+
pendingSendRekey?.done.reject(error);
|
|
1230
1231
|
this.opened.reject(error);
|
|
1231
1232
|
this.data.fail(error);
|
|
1232
1233
|
return true;
|
|
@@ -1988,12 +1989,38 @@ function createSessionDeadline(config, phase) {
|
|
|
1988
1989
|
}
|
|
1989
1990
|
function defaultDeadlineFactory(timeoutMs, phase) {
|
|
1990
1991
|
const controller = new AbortController();
|
|
1991
|
-
const
|
|
1992
|
+
const cancelTimer = scheduleLongTimeout(() => {
|
|
1992
1993
|
controller.abort(new SessionV2Error("timeout", `${phase} deadline exceeded`));
|
|
1993
1994
|
}, timeoutMs);
|
|
1994
1995
|
return {
|
|
1995
1996
|
signal: controller.signal,
|
|
1996
|
-
cancel:
|
|
1997
|
+
cancel: cancelTimer,
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
const MAX_NATIVE_TIMEOUT_MS = 2_147_483_647;
|
|
2001
|
+
// Node and browsers clamp setTimeout delays above a signed 32-bit integer.
|
|
2002
|
+
// Keep the public timeout range intact by scheduling the remaining duration in
|
|
2003
|
+
// bounded segments against a monotonic wall-clock deadline.
|
|
2004
|
+
function scheduleLongTimeout(callback, timeoutMs) {
|
|
2005
|
+
const clock = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
2006
|
+
const deadline = clock() + timeoutMs;
|
|
2007
|
+
let timer;
|
|
2008
|
+
let canceled = false;
|
|
2009
|
+
const schedule = () => {
|
|
2010
|
+
if (canceled)
|
|
2011
|
+
return;
|
|
2012
|
+
const remaining = deadline - clock();
|
|
2013
|
+
if (remaining <= 0) {
|
|
2014
|
+
callback();
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
timer = setTimeout(schedule, Math.min(remaining, MAX_NATIVE_TIMEOUT_MS));
|
|
2018
|
+
};
|
|
2019
|
+
schedule();
|
|
2020
|
+
return () => {
|
|
2021
|
+
canceled = true;
|
|
2022
|
+
if (timer !== undefined)
|
|
2023
|
+
clearTimeout(timer);
|
|
1997
2024
|
};
|
|
1998
2025
|
}
|
|
1999
2026
|
function combineSignals(...signals) {
|
package/dist/yamux/stream.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export declare class YamuxStream {
|
|
|
12
12
|
private readWaiters;
|
|
13
13
|
private error;
|
|
14
14
|
private resetTask;
|
|
15
|
+
private closeTask;
|
|
16
|
+
private closeRequested;
|
|
15
17
|
private writeChain;
|
|
16
18
|
private writeQueueBytes;
|
|
17
19
|
private finalized;
|
|
@@ -23,6 +25,7 @@ export declare class YamuxStream {
|
|
|
23
25
|
write(data: Uint8Array): Promise<void>;
|
|
24
26
|
private writeSerial;
|
|
25
27
|
close(): Promise<void>;
|
|
28
|
+
private closeSerial;
|
|
26
29
|
reset(err?: Error): Promise<void>;
|
|
27
30
|
abort(err?: Error): void;
|
|
28
31
|
private fail;
|
package/dist/yamux/stream.js
CHANGED
|
@@ -24,6 +24,8 @@ export class YamuxStream {
|
|
|
24
24
|
// Terminal error (reset/overflow) for the stream.
|
|
25
25
|
error = null;
|
|
26
26
|
resetTask;
|
|
27
|
+
closeTask;
|
|
28
|
+
closeRequested = false;
|
|
27
29
|
writeChain = Promise.resolve();
|
|
28
30
|
writeQueueBytes = 0;
|
|
29
31
|
finalized = false;
|
|
@@ -89,6 +91,8 @@ export class YamuxStream {
|
|
|
89
91
|
}
|
|
90
92
|
// write sends DATA frames, respecting the send window.
|
|
91
93
|
async write(data) {
|
|
94
|
+
if (this.closeRequested)
|
|
95
|
+
throw new Error("stream closed");
|
|
92
96
|
this.ensureWritable();
|
|
93
97
|
const byteCount = data.byteLength;
|
|
94
98
|
const nextQueueBytes = this.writeQueueBytes + byteCount;
|
|
@@ -129,10 +133,19 @@ export class YamuxStream {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
// close sends FIN and transitions to local close.
|
|
132
|
-
|
|
133
|
-
if (this.
|
|
134
|
-
return;
|
|
135
|
-
if (this.state === "reset")
|
|
136
|
+
close() {
|
|
137
|
+
if (this.closeTask !== undefined)
|
|
138
|
+
return this.closeTask;
|
|
139
|
+
if (this.state === "closed" || this.state === "reset" || this.state === "localClose")
|
|
140
|
+
return Promise.resolve();
|
|
141
|
+
this.closeRequested = true;
|
|
142
|
+
const task = this.writeChain.then(async () => await this.closeSerial());
|
|
143
|
+
this.writeChain = task.catch(() => undefined);
|
|
144
|
+
this.closeTask = task;
|
|
145
|
+
return task;
|
|
146
|
+
}
|
|
147
|
+
async closeSerial() {
|
|
148
|
+
if (this.state === "closed" || this.state === "reset" || this.state === "localClose")
|
|
136
149
|
return;
|
|
137
150
|
const wasRemoteClose = this.state === "remoteClose";
|
|
138
151
|
const flags = this.sendFlags() | FLAG_FIN;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/flowersec-core",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.4",
|
|
4
4
|
"description": "Flowersec core TypeScript library for carrier-neutral encrypted sessions and multiplexed streams.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"ws": "^8.21.2"
|
|
78
78
|
},
|
|
79
79
|
"optionalDependencies": {
|
|
80
|
-
"@floegence/flowersec-node-native": "2.5.
|
|
80
|
+
"@floegence/flowersec-node-native": "2.5.4"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@playwright/test": "1.62.1",
|
|
@@ -95,5 +95,5 @@
|
|
|
95
95
|
"vite": "^8.2.1",
|
|
96
96
|
"vitest": "4.1.10"
|
|
97
97
|
},
|
|
98
|
-
"flowersecSourceCommit": "
|
|
98
|
+
"flowersecSourceCommit": "026cb52d116d2a04de50d0f0621fff57c7657120"
|
|
99
99
|
}
|
package/sbom/cyclonedx.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:9db05d70-0170-5669-8eb4-881e06b18415",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
7
|
"component": {
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "@floegence/flowersec-core",
|
|
10
|
-
"version": "2.5.
|
|
11
|
-
"purl": "pkg:npm/%40floegence/flowersec-core@2.5.
|
|
12
|
-
"bom-ref": "pkg:npm/%40floegence/flowersec-core@2.5.
|
|
10
|
+
"version": "2.5.4",
|
|
11
|
+
"purl": "pkg:npm/%40floegence/flowersec-core@2.5.4",
|
|
12
|
+
"bom-ref": "pkg:npm/%40floegence/flowersec-core@2.5.4"
|
|
13
13
|
},
|
|
14
14
|
"properties": [
|
|
15
15
|
{
|
|
16
16
|
"name": "flowersec:source-inventory-sha256",
|
|
17
|
-
"value": "
|
|
17
|
+
"value": "308ce3a2dd421245b940297e16fbb6cae9cc3d79e4835164e4304b5f7acd5ca8"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
},
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
],
|
|
191
191
|
"dependencies": [
|
|
192
192
|
{
|
|
193
|
-
"ref": "pkg:npm/%40floegence/flowersec-core@2.5.
|
|
193
|
+
"ref": "pkg:npm/%40floegence/flowersec-core@2.5.4",
|
|
194
194
|
"dependsOn": [
|
|
195
195
|
"pkg:npm/%40noble/ciphers@2.3.0",
|
|
196
196
|
"pkg:npm/%40noble/curves@2.3.0",
|