@floegence/flowersec-core 2.5.3 → 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 +8 -5
- 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 +1 -0
- package/dist/proxy/windowBridge.js +93 -33
- 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/v2/publicSession.js +4 -0
- package/dist/v2/session.js +4 -0
- 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
|
@@ -9,6 +9,6 @@ type ReadExactlyFn = (n: number) => Promise<Uint8Array>;
|
|
|
9
9
|
type ReadExactlyLike = Readonly<{
|
|
10
10
|
readExactly: (n: number) => Promise<Uint8Array>;
|
|
11
11
|
}>;
|
|
12
|
-
export declare function writeJsonFrame(write: WriteFn | WriteLike, v: unknown): Promise<void>;
|
|
12
|
+
export declare function writeJsonFrame(write: WriteFn | WriteLike, v: unknown, maxBytes?: number): Promise<void>;
|
|
13
13
|
export declare function readJsonFrame(readExactly: ReadExactlyFn | ReadExactlyLike, maxBytes: number): Promise<unknown>;
|
|
14
14
|
export {};
|
|
@@ -13,8 +13,10 @@ function normalizeReadExactly(readExactly) {
|
|
|
13
13
|
return typeof readExactly === "function" ? readExactly : (n) => readExactly.readExactly(n);
|
|
14
14
|
}
|
|
15
15
|
// writeJsonFrame encodes a JSON payload with a 4-byte length prefix.
|
|
16
|
-
export async function writeJsonFrame(write, v) {
|
|
16
|
+
export async function writeJsonFrame(write, v, maxBytes = 0) {
|
|
17
17
|
const json = te.encode(JSON.stringify(v));
|
|
18
|
+
if (maxBytes > 0 && json.length > maxBytes)
|
|
19
|
+
throw new JsonFramingError("frame too large");
|
|
18
20
|
const hdr = u32be(json.length);
|
|
19
21
|
const out = new Uint8Array(4 + json.length);
|
|
20
22
|
out.set(hdr, 0);
|
package/dist/node/acceptor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RpcRouter } from "../rpc/server.js";
|
|
2
|
-
import { assertRpcError } from "../rpc/validate.js";
|
|
2
|
+
import { assertRpcError, assertRpcTypeId } from "../rpc/validate.js";
|
|
3
3
|
import { acceptReceivedSessionV2, receiveSessionAdmissionV2, rejectSessionAdmissionV2, } from "../connector/sessionAcceptor.js";
|
|
4
4
|
import { nodeSessionRuntimeV2 } from "./sessionRuntime.js";
|
|
5
5
|
import { startNodeWebSocketServer, } from "./webSocketServer.js";
|
|
@@ -85,10 +85,13 @@ function registerNotification(state, typeId, handler) {
|
|
|
85
85
|
state.notifications.set(typeId, handler);
|
|
86
86
|
}
|
|
87
87
|
function validateRPCRegistration(typeId, handler) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
try {
|
|
89
|
+
assertRpcTypeId(typeId);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
93
|
+
}
|
|
94
|
+
if (typeof handler !== "function") {
|
|
92
95
|
throw new HandlerRegistrationError("invalid_handler");
|
|
93
96
|
}
|
|
94
97
|
}
|
package/dist/proxy/runtime.js
CHANGED
|
@@ -45,11 +45,34 @@ function normalizeTimeout(input) {
|
|
|
45
45
|
}
|
|
46
46
|
return input;
|
|
47
47
|
}
|
|
48
|
+
class InvalidProxyPathError extends TypeError {
|
|
49
|
+
}
|
|
48
50
|
function normalizePath(input) {
|
|
49
|
-
if (input !== input.trim() || !input.startsWith("/") || input.startsWith("//") || /[\u0000-\u0020]/u.test(input) || input.includes("://")) {
|
|
50
|
-
throw new
|
|
51
|
+
if (input !== input.trim() || !input.startsWith("/") || input.startsWith("//") || /[\u0000-\u0020]/u.test(input) || input.includes("://") || input.includes("#")) {
|
|
52
|
+
throw new InvalidProxyPathError("proxy path must be an origin-relative path");
|
|
51
53
|
}
|
|
52
|
-
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = new URL(input, "https://flowersec.invalid/");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new InvalidProxyPathError("proxy path must be an origin-relative path");
|
|
60
|
+
}
|
|
61
|
+
const pathname = normalizePercentEscapes(parsed.pathname, true).replace(/\/{2,}/gu, "/");
|
|
62
|
+
const search = normalizePercentEscapes(parsed.search, false);
|
|
63
|
+
return pathname + search;
|
|
64
|
+
}
|
|
65
|
+
function normalizePercentEscapes(input, rejectEncodedSeparators) {
|
|
66
|
+
if (/%(?![0-9a-f]{2})/iu.test(input))
|
|
67
|
+
throw new InvalidProxyPathError("proxy path contains invalid percent encoding");
|
|
68
|
+
return input.replace(/%([0-9a-f]{2})/giu, (_match, hex) => {
|
|
69
|
+
const value = Number.parseInt(hex, 16);
|
|
70
|
+
if (rejectEncodedSeparators && (value === 0x2f || value === 0x5c)) {
|
|
71
|
+
throw new InvalidProxyPathError("proxy path contains an encoded separator");
|
|
72
|
+
}
|
|
73
|
+
const character = String.fromCharCode(value);
|
|
74
|
+
return /[A-Za-z0-9\-._~]/u.test(character) ? character : `%${hex.toUpperCase()}`;
|
|
75
|
+
});
|
|
53
76
|
}
|
|
54
77
|
function pathName(path) {
|
|
55
78
|
const query = path.indexOf("?");
|
|
@@ -176,11 +199,19 @@ class StreamAdmission {
|
|
|
176
199
|
return Promise.reject(new SessionError("resource_exhausted"));
|
|
177
200
|
}
|
|
178
201
|
return new Promise((resolve, reject) => {
|
|
202
|
+
let cleaned = false;
|
|
203
|
+
const cleanup = () => {
|
|
204
|
+
if (cleaned)
|
|
205
|
+
return;
|
|
206
|
+
cleaned = true;
|
|
207
|
+
signal?.removeEventListener("abort", onAbort);
|
|
208
|
+
};
|
|
179
209
|
const entry = {
|
|
180
210
|
bytes,
|
|
181
211
|
...(signal === undefined ? {} : { signal }),
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
cleanup,
|
|
213
|
+
resolve: (release) => { cleanup(); resolve(release); },
|
|
214
|
+
reject: (error) => { cleanup(); reject(error); },
|
|
184
215
|
};
|
|
185
216
|
const onAbort = () => {
|
|
186
217
|
const index = this.queue.indexOf(entry);
|
|
@@ -188,7 +219,7 @@ class StreamAdmission {
|
|
|
188
219
|
return;
|
|
189
220
|
this.queue.splice(index, 1);
|
|
190
221
|
this.queuedBytes -= bytes;
|
|
191
|
-
reject(new SessionError("canceled"));
|
|
222
|
+
entry.reject(new SessionError("canceled"));
|
|
192
223
|
};
|
|
193
224
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
194
225
|
this.queue.push(entry);
|
|
@@ -251,6 +282,8 @@ async function* readChunks(reader, maxChunkBytes, maxBodyBytes) {
|
|
|
251
282
|
function publicFailure(error) {
|
|
252
283
|
if (error instanceof ProxyPolicyError)
|
|
253
284
|
return { status: 403, code: "policy_denied", message: "proxy request denied" };
|
|
285
|
+
if (error instanceof InvalidProxyPathError)
|
|
286
|
+
return { status: 400, code: "invalid_request", message: "invalid proxy request" };
|
|
254
287
|
if (error instanceof SessionError && (error.code === "resource_exhausted" || error.code === "closed" || error.code === "going_away")) {
|
|
255
288
|
return { status: 503, code: error.code, message: "proxy service unavailable" };
|
|
256
289
|
}
|
|
@@ -15,6 +15,8 @@ export type ProxyServiceWorkerScriptOptions = Readonly<{
|
|
|
15
15
|
sameOriginOnly?: boolean;
|
|
16
16
|
maxRequestBodyBytes?: number;
|
|
17
17
|
maxInjectHTMLBytes?: number;
|
|
18
|
+
responseMetadataTimeoutMs?: number;
|
|
19
|
+
responseBodyInactivityTimeoutMs?: number;
|
|
18
20
|
passthrough?: ProxyServiceWorkerPassthroughOptions;
|
|
19
21
|
proxyPathPrefix?: string;
|
|
20
22
|
stripProxyPathPrefix?: boolean;
|
|
@@ -17,6 +17,12 @@ function bounded(name, value, fallback, maximum) {
|
|
|
17
17
|
throw new TypeError(`${name} is invalid`);
|
|
18
18
|
return result;
|
|
19
19
|
}
|
|
20
|
+
function optionalBounded(name, value, maximum) {
|
|
21
|
+
const result = value ?? 0;
|
|
22
|
+
if (!Number.isSafeInteger(result) || result < 0 || result > maximum)
|
|
23
|
+
throw new TypeError(`${name} is invalid`);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
20
26
|
function token(name, value, fallback = "") {
|
|
21
27
|
const result = value ?? fallback;
|
|
22
28
|
if (result !== result.trim() || /[\u0000-\u0020\u007f]/u.test(result) || result.length > 512)
|
|
@@ -42,6 +48,8 @@ function normalizeOptions(options) {
|
|
|
42
48
|
sameOriginOnly: options.sameOriginOnly ?? true,
|
|
43
49
|
maxRequestBodyBytes: bounded("maxRequestBodyBytes", options.maxRequestBodyBytes, 64 * 1024 * 1024, 256 * 1024 * 1024),
|
|
44
50
|
maxInjectHTMLBytes: bounded("maxInjectHTMLBytes", options.maxInjectHTMLBytes, 8 * 1024 * 1024, 32 * 1024 * 1024),
|
|
51
|
+
responseMetadataTimeoutMs: bounded("responseMetadataTimeoutMs", options.responseMetadataTimeoutMs, 10_000, 300_000),
|
|
52
|
+
responseBodyInactivityTimeoutMs: optionalBounded("responseBodyInactivityTimeoutMs", options.responseBodyInactivityTimeoutMs, 300_000),
|
|
45
53
|
passthroughPaths: strings("passthrough.paths", options.passthrough?.paths),
|
|
46
54
|
passthroughPrefixes: strings("passthrough.prefixes", options.passthrough?.prefixes),
|
|
47
55
|
proxyPathPrefix,
|
|
@@ -127,6 +135,8 @@ function serviceWorkerMain(config) {
|
|
|
127
135
|
return new Response("proxy runtime unavailable", { status: 503 });
|
|
128
136
|
}
|
|
129
137
|
let body;
|
|
138
|
+
if (request.signal.aborted)
|
|
139
|
+
return new Response("proxy request canceled", { status: 499 });
|
|
130
140
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
131
141
|
const requestBody = await request.clone().arrayBuffer();
|
|
132
142
|
if (requestBody.byteLength > config.maxRequestBodyBytes)
|
|
@@ -138,16 +148,54 @@ function serviceWorkerMain(config) {
|
|
|
138
148
|
let metadata = null;
|
|
139
149
|
let controller = null;
|
|
140
150
|
let finished = false;
|
|
141
|
-
|
|
151
|
+
let remoteAbortSent = false;
|
|
152
|
+
let bodyInactivityTimer;
|
|
153
|
+
const abortRemote = () => {
|
|
154
|
+
if (remoteAbortSent)
|
|
155
|
+
return;
|
|
156
|
+
remoteAbortSent = true;
|
|
157
|
+
try {
|
|
158
|
+
channel.port1.postMessage({ type: "flowersec-proxy:abort" });
|
|
159
|
+
}
|
|
160
|
+
catch { /* Port already failed. */ }
|
|
161
|
+
};
|
|
162
|
+
const cleanup = () => {
|
|
163
|
+
clearTimeout(metadataTimer);
|
|
164
|
+
clearTimeout(bodyInactivityTimer);
|
|
165
|
+
clearInterval(runtimeWatchdog);
|
|
166
|
+
request.signal.removeEventListener("abort", requestAborted);
|
|
167
|
+
channel.port1.close();
|
|
168
|
+
};
|
|
169
|
+
const finishError = (status, message, cancelRemote = false) => {
|
|
142
170
|
if (finished)
|
|
143
171
|
return;
|
|
144
172
|
finished = true;
|
|
173
|
+
if (cancelRemote)
|
|
174
|
+
abortRemote();
|
|
145
175
|
if (controller !== null)
|
|
146
176
|
controller.error(new Error(message));
|
|
147
177
|
else
|
|
148
178
|
resolve(new Response(message, { status }));
|
|
149
|
-
|
|
179
|
+
cleanup();
|
|
180
|
+
};
|
|
181
|
+
const requestAborted = () => finishError(499, "proxy request canceled", true);
|
|
182
|
+
const armBodyInactivityTimer = () => {
|
|
183
|
+
clearTimeout(bodyInactivityTimer);
|
|
184
|
+
if (config.responseBodyInactivityTimeoutMs === 0)
|
|
185
|
+
return;
|
|
186
|
+
bodyInactivityTimer = setTimeout(() => finishError(504, "proxy response body timed out", true), config.responseBodyInactivityTimeoutMs);
|
|
150
187
|
};
|
|
188
|
+
const metadataTimer = setTimeout(() => finishError(504, "proxy response timed out", true), config.responseMetadataTimeoutMs);
|
|
189
|
+
const runtimeWatchdog = setInterval(() => {
|
|
190
|
+
void worker.clients.get(target.id).then((current) => {
|
|
191
|
+
if (current !== null || finished)
|
|
192
|
+
return;
|
|
193
|
+
if (config.windowTarget === "registered_runtime")
|
|
194
|
+
runtimeClientId = "";
|
|
195
|
+
finishError(503, "proxy runtime unavailable", true);
|
|
196
|
+
}).catch(() => finishError(503, "proxy runtime unavailable", true));
|
|
197
|
+
}, 250);
|
|
198
|
+
request.signal.addEventListener("abort", requestAborted, { once: true });
|
|
151
199
|
channel.port1.onmessage = (message) => {
|
|
152
200
|
const value = message.data;
|
|
153
201
|
if (value === null || typeof value !== "object" || finished)
|
|
@@ -162,13 +210,21 @@ function serviceWorkerMain(config) {
|
|
|
162
210
|
headers.append(entry.name, entry.value);
|
|
163
211
|
}
|
|
164
212
|
metadata = { status: value.status, headers };
|
|
213
|
+
clearTimeout(metadataTimer);
|
|
214
|
+
armBodyInactivityTimer();
|
|
165
215
|
const stream = new ReadableStream({
|
|
166
216
|
start(valueController) {
|
|
167
217
|
controller = valueController;
|
|
168
218
|
channel.port1.postMessage({ type: "flowersec-proxy:response_credit" });
|
|
169
219
|
},
|
|
170
220
|
pull() { channel.port1.postMessage({ type: "flowersec-proxy:response_credit" }); },
|
|
171
|
-
cancel() {
|
|
221
|
+
cancel() {
|
|
222
|
+
if (finished)
|
|
223
|
+
return;
|
|
224
|
+
finished = true;
|
|
225
|
+
abortRemote();
|
|
226
|
+
cleanup();
|
|
227
|
+
},
|
|
172
228
|
});
|
|
173
229
|
resolve(new Response(stream, { status: metadata.status, headers: metadata.headers }));
|
|
174
230
|
return;
|
|
@@ -177,19 +233,20 @@ function serviceWorkerMain(config) {
|
|
|
177
233
|
if (controller === null || !(value.data instanceof ArrayBuffer))
|
|
178
234
|
return finishError(502, "invalid proxy response");
|
|
179
235
|
controller.enqueue(new Uint8Array(value.data));
|
|
236
|
+
armBodyInactivityTimer();
|
|
180
237
|
return;
|
|
181
238
|
}
|
|
182
239
|
if (value.type === "flowersec-proxy:response_end") {
|
|
183
240
|
finished = true;
|
|
184
241
|
controller?.close();
|
|
185
|
-
|
|
242
|
+
cleanup();
|
|
186
243
|
return;
|
|
187
244
|
}
|
|
188
245
|
if (value.type === "flowersec-proxy:response_error") {
|
|
189
246
|
finishError(Number.isInteger(value.status) ? value.status : 502, typeof value.message === "string" ? value.message : "proxy request failed");
|
|
190
247
|
}
|
|
191
248
|
};
|
|
192
|
-
channel.port1.onmessageerror = () => finishError(502, "proxy request failed");
|
|
249
|
+
channel.port1.onmessageerror = () => finishError(502, "proxy request failed", true);
|
|
193
250
|
const headers = Array.from(request.headers.entries()).map(([name, value]) => ({ name, value }));
|
|
194
251
|
try {
|
|
195
252
|
target.postMessage({
|
|
@@ -207,8 +264,7 @@ function serviceWorkerMain(config) {
|
|
|
207
264
|
catch {
|
|
208
265
|
if (config.windowTarget === "registered_runtime")
|
|
209
266
|
runtimeClientId = "";
|
|
210
|
-
|
|
211
|
-
resolve(new Response("proxy runtime unavailable", { status: 503 }));
|
|
267
|
+
finishError(503, "proxy runtime unavailable");
|
|
212
268
|
}
|
|
213
269
|
});
|
|
214
270
|
const injection = config.injectHTML;
|
|
@@ -39,12 +39,17 @@ export class MessagePortByteStream {
|
|
|
39
39
|
if (this.terminalError !== undefined)
|
|
40
40
|
throw this.terminalError;
|
|
41
41
|
return await new Promise((resolve, reject) => {
|
|
42
|
-
const
|
|
42
|
+
const cleanup = () => options.signal?.removeEventListener("abort", onAbort);
|
|
43
|
+
const waiter = {
|
|
44
|
+
resolve: (value) => { cleanup(); resolve(value); },
|
|
45
|
+
reject: (error) => { cleanup(); reject(error); },
|
|
46
|
+
};
|
|
43
47
|
const onAbort = () => {
|
|
44
48
|
const index = this.readWaiters.indexOf(waiter);
|
|
45
|
-
if (index
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
if (index < 0)
|
|
50
|
+
return;
|
|
51
|
+
this.readWaiters.splice(index, 1);
|
|
52
|
+
waiter.reject(new SessionError("canceled"));
|
|
48
53
|
};
|
|
49
54
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
50
55
|
this.readWaiters.push(waiter);
|
|
@@ -66,17 +71,19 @@ export class MessagePortByteStream {
|
|
|
66
71
|
const copy = data.slice();
|
|
67
72
|
this.buffered += copy.length;
|
|
68
73
|
await new Promise((resolve, reject) => {
|
|
74
|
+
const onAbort = () => this.settleWrite(id, new SessionError("canceled"));
|
|
69
75
|
this.writeWaiters.set(id, {
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
bytes: copy.length,
|
|
77
|
+
...(options.signal === undefined ? {} : { signal: options.signal, onAbort }),
|
|
78
|
+
resolve,
|
|
79
|
+
reject,
|
|
72
80
|
});
|
|
81
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
73
82
|
try {
|
|
74
83
|
this.port.postMessage({ type: "chunk", id, data: copy.buffer }, [copy.buffer]);
|
|
75
84
|
}
|
|
76
85
|
catch {
|
|
77
|
-
this.
|
|
78
|
-
this.buffered -= copy.length;
|
|
79
|
-
reject(new SessionError("operation_failed"));
|
|
86
|
+
this.settleWrite(id, new SessionError("operation_failed"));
|
|
80
87
|
}
|
|
81
88
|
});
|
|
82
89
|
return data.length;
|
|
@@ -130,11 +137,7 @@ export class MessagePortByteStream {
|
|
|
130
137
|
return;
|
|
131
138
|
}
|
|
132
139
|
if (message.type === "ack" && Number.isSafeInteger(message.id)) {
|
|
133
|
-
|
|
134
|
-
if (waiter !== undefined) {
|
|
135
|
-
this.writeWaiters.delete(message.id);
|
|
136
|
-
waiter.resolve();
|
|
137
|
-
}
|
|
140
|
+
this.settleWrite(message.id);
|
|
138
141
|
return;
|
|
139
142
|
}
|
|
140
143
|
if (message.type === "end") {
|
|
@@ -159,9 +162,8 @@ export class MessagePortByteStream {
|
|
|
159
162
|
this.readBuffered = 0;
|
|
160
163
|
for (const waiter of this.readWaiters.splice(0))
|
|
161
164
|
waiter.resolve(null);
|
|
162
|
-
for (const
|
|
163
|
-
|
|
164
|
-
this.writeWaiters.clear();
|
|
165
|
+
for (const id of [...this.writeWaiters.keys()])
|
|
166
|
+
this.settleWrite(id, new SessionError("closed"));
|
|
165
167
|
this.port.close();
|
|
166
168
|
}
|
|
167
169
|
fail(error) {
|
|
@@ -173,11 +175,22 @@ export class MessagePortByteStream {
|
|
|
173
175
|
this.readBuffered = 0;
|
|
174
176
|
for (const waiter of this.readWaiters.splice(0))
|
|
175
177
|
waiter.reject(error);
|
|
176
|
-
for (const
|
|
177
|
-
|
|
178
|
-
this.writeWaiters.clear();
|
|
178
|
+
for (const id of [...this.writeWaiters.keys()])
|
|
179
|
+
this.settleWrite(id, error);
|
|
179
180
|
this.port.close();
|
|
180
181
|
}
|
|
182
|
+
settleWrite(id, error) {
|
|
183
|
+
const waiter = this.writeWaiters.get(id);
|
|
184
|
+
if (waiter === undefined)
|
|
185
|
+
return;
|
|
186
|
+
this.writeWaiters.delete(id);
|
|
187
|
+
waiter.signal?.removeEventListener("abort", waiter.onAbort);
|
|
188
|
+
this.buffered = Math.max(0, this.buffered - waiter.bytes);
|
|
189
|
+
if (error === undefined)
|
|
190
|
+
waiter.resolve();
|
|
191
|
+
else
|
|
192
|
+
waiter.reject(error);
|
|
193
|
+
}
|
|
181
194
|
}
|
|
182
195
|
function bridgeLimits(maxWsFrameBytes, maxWsBufferedAmountBytes) {
|
|
183
196
|
const wsFrame = maxWsFrameBytes ?? SDK_DEFAULTS.proxy.maxWsFrameBytes;
|
|
@@ -279,33 +292,65 @@ export function registerProxyAppWindow(options) {
|
|
|
279
292
|
dispose: () => { disposed = true; },
|
|
280
293
|
});
|
|
281
294
|
}
|
|
282
|
-
async function bridgeStreams(runtimeStream, port) {
|
|
295
|
+
async function bridgeStreams(runtimeStream, port, signal) {
|
|
283
296
|
const bridge = new MessagePortByteStream(port);
|
|
297
|
+
const controller = new AbortController();
|
|
298
|
+
let resetTask;
|
|
299
|
+
const resetBoth = () => {
|
|
300
|
+
resetTask ??= Promise.allSettled([
|
|
301
|
+
Promise.resolve().then(async () => await runtimeStream.reset()),
|
|
302
|
+
Promise.resolve().then(async () => await bridge.reset()),
|
|
303
|
+
]);
|
|
304
|
+
return resetTask;
|
|
305
|
+
};
|
|
306
|
+
const abort = () => {
|
|
307
|
+
controller.abort(signal?.reason ?? new SessionError("canceled"));
|
|
308
|
+
void resetBoth();
|
|
309
|
+
};
|
|
310
|
+
if (signal?.aborted === true)
|
|
311
|
+
abort();
|
|
312
|
+
else
|
|
313
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
284
314
|
const left = (async () => {
|
|
285
315
|
while (true) {
|
|
286
|
-
const chunk = await runtimeStream.read();
|
|
316
|
+
const chunk = await runtimeStream.read({ signal: controller.signal });
|
|
287
317
|
if (chunk === null) {
|
|
288
318
|
await bridge.closeWrite();
|
|
289
319
|
return;
|
|
290
320
|
}
|
|
291
|
-
await bridge.write(chunk);
|
|
321
|
+
await bridge.write(chunk, { signal: controller.signal });
|
|
292
322
|
}
|
|
293
|
-
})();
|
|
323
|
+
})().catch((error) => { controller.abort(error); void resetBoth(); throw error; });
|
|
294
324
|
const right = (async () => {
|
|
295
325
|
while (true) {
|
|
296
|
-
const chunk = await bridge.read();
|
|
326
|
+
const chunk = await bridge.read({ signal: controller.signal });
|
|
297
327
|
if (chunk === null) {
|
|
298
328
|
await runtimeStream.closeWrite();
|
|
299
329
|
return;
|
|
300
330
|
}
|
|
301
331
|
let offset = 0;
|
|
302
332
|
while (offset < chunk.length)
|
|
303
|
-
offset += await runtimeStream.write(chunk.subarray(offset));
|
|
333
|
+
offset += await runtimeStream.write(chunk.subarray(offset), { signal: controller.signal });
|
|
304
334
|
}
|
|
305
|
-
})();
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
335
|
+
})().catch((error) => { controller.abort(error); void resetBoth(); throw error; });
|
|
336
|
+
try {
|
|
337
|
+
const settled = await Promise.allSettled([left, right]);
|
|
338
|
+
const failure = settled.find((result) => result.status === "rejected");
|
|
339
|
+
if (failure !== undefined) {
|
|
340
|
+
await resetBoth();
|
|
341
|
+
throw failure.reason;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
await Promise.all([runtimeStream.close(), bridge.close()]);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
await resetBoth();
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
signal?.removeEventListener("abort", abort);
|
|
353
|
+
}
|
|
309
354
|
}
|
|
310
355
|
export function registerProxyControllerWindow(options) {
|
|
311
356
|
const target = options.targetWindow ?? globalThis.window;
|
|
@@ -315,6 +360,7 @@ export function registerProxyControllerWindow(options) {
|
|
|
315
360
|
}
|
|
316
361
|
const nonce = capability(options.capabilityNonce);
|
|
317
362
|
let disposed = false;
|
|
363
|
+
const active = new Set();
|
|
318
364
|
const onMessage = (event) => {
|
|
319
365
|
if (disposed || !allowed.has(event.origin) || (options.expectedSource !== undefined && event.source !== options.expectedSource))
|
|
320
366
|
return;
|
|
@@ -331,6 +377,7 @@ export function registerProxyControllerWindow(options) {
|
|
|
331
377
|
void (async () => {
|
|
332
378
|
let canceled = false;
|
|
333
379
|
const openController = new AbortController();
|
|
380
|
+
active.add(openController);
|
|
334
381
|
port.onmessage = (message) => {
|
|
335
382
|
if (message.data?.type === "reset" ||
|
|
336
383
|
message.data?.type === "close") {
|
|
@@ -349,17 +396,30 @@ export function registerProxyControllerWindow(options) {
|
|
|
349
396
|
return;
|
|
350
397
|
}
|
|
351
398
|
port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: true, protocol: opened.protocol });
|
|
352
|
-
await bridgeStreams(opened.stream, port);
|
|
399
|
+
await bridgeStreams(opened.stream, port, openController.signal);
|
|
353
400
|
}
|
|
354
401
|
catch {
|
|
355
|
-
|
|
402
|
+
try {
|
|
403
|
+
port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: false });
|
|
404
|
+
}
|
|
405
|
+
catch { /* Port is already closed. */ }
|
|
356
406
|
port.close();
|
|
357
407
|
}
|
|
408
|
+
finally {
|
|
409
|
+
active.delete(openController);
|
|
410
|
+
}
|
|
358
411
|
})();
|
|
359
412
|
}
|
|
360
413
|
};
|
|
361
414
|
target.addEventListener("message", onMessage);
|
|
362
|
-
return Object.freeze({
|
|
415
|
+
return Object.freeze({
|
|
416
|
+
dispose: () => {
|
|
417
|
+
disposed = true;
|
|
418
|
+
target.removeEventListener("message", onMessage);
|
|
419
|
+
for (const controller of active)
|
|
420
|
+
controller.abort(new SessionError("closed"));
|
|
421
|
+
},
|
|
422
|
+
});
|
|
363
423
|
}
|
|
364
424
|
export async function registerProxyAppWindowWithServiceWorkerControl(options) {
|
|
365
425
|
await registerServiceWorkerAndEnsureControl({
|
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");
|
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.js
CHANGED
|
@@ -1224,6 +1224,10 @@ class EncryptedStreamV2 {
|
|
|
1224
1224
|
if (this.terminalError !== undefined)
|
|
1225
1225
|
return false;
|
|
1226
1226
|
this.terminalError = error;
|
|
1227
|
+
const pendingSendRekey = this.pendingSendRekey;
|
|
1228
|
+
this.pendingSendRekey = undefined;
|
|
1229
|
+
pendingSendRekey?.armed.reject(error);
|
|
1230
|
+
pendingSendRekey?.done.reject(error);
|
|
1227
1231
|
this.opened.reject(error);
|
|
1228
1232
|
this.data.fail(error);
|
|
1229
1233
|
return true;
|
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",
|
package/sbom/spdx.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
5
|
"name": "flowersec-ts",
|
|
6
|
-
"documentNamespace": "https://github.com/floegence/flowersec/sbom/flowersec-ts/
|
|
6
|
+
"documentNamespace": "https://github.com/floegence/flowersec/sbom/flowersec-ts/308ce3a2dd421245b940297e16fbb6cae9cc3d79e4835164e4304b5f7acd5ca8",
|
|
7
7
|
"creationInfo": {
|
|
8
8
|
"created": "1970-01-01T00:00:00Z",
|
|
9
9
|
"creators": [
|
|
@@ -13,19 +13,19 @@
|
|
|
13
13
|
"packages": [
|
|
14
14
|
{
|
|
15
15
|
"name": "@floegence/flowersec-core",
|
|
16
|
-
"SPDXID": "SPDXRef-Package-
|
|
17
|
-
"versionInfo": "2.5.
|
|
16
|
+
"SPDXID": "SPDXRef-Package-972ad62e2707c407a361",
|
|
17
|
+
"versionInfo": "2.5.4",
|
|
18
18
|
"downloadLocation": "NOASSERTION",
|
|
19
19
|
"filesAnalyzed": false,
|
|
20
20
|
"licenseConcluded": "NOASSERTION",
|
|
21
21
|
"licenseDeclared": "NOASSERTION",
|
|
22
22
|
"copyrightText": "NOASSERTION",
|
|
23
|
-
"comment": "Flowersec source inventory SHA-256:
|
|
23
|
+
"comment": "Flowersec source inventory SHA-256: 308ce3a2dd421245b940297e16fbb6cae9cc3d79e4835164e4304b5f7acd5ca8",
|
|
24
24
|
"externalRefs": [
|
|
25
25
|
{
|
|
26
26
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
27
27
|
"referenceType": "purl",
|
|
28
|
-
"referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.5.
|
|
28
|
+
"referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.5.4"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -136,30 +136,30 @@
|
|
|
136
136
|
{
|
|
137
137
|
"spdxElementId": "SPDXRef-DOCUMENT",
|
|
138
138
|
"relationshipType": "DESCRIBES",
|
|
139
|
-
"relatedSpdxElement": "SPDXRef-Package-
|
|
139
|
+
"relatedSpdxElement": "SPDXRef-Package-972ad62e2707c407a361"
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
|
-
"spdxElementId": "SPDXRef-Package-
|
|
142
|
+
"spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
|
|
143
143
|
"relationshipType": "DEPENDS_ON",
|
|
144
144
|
"relatedSpdxElement": "SPDXRef-Package-5ce913a03239b02770fb"
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
|
-
"spdxElementId": "SPDXRef-Package-
|
|
147
|
+
"spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
|
|
148
148
|
"relationshipType": "DEPENDS_ON",
|
|
149
149
|
"relatedSpdxElement": "SPDXRef-Package-01009adf60db02c13634"
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
|
-
"spdxElementId": "SPDXRef-Package-
|
|
152
|
+
"spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
|
|
153
153
|
"relationshipType": "DEPENDS_ON",
|
|
154
154
|
"relatedSpdxElement": "SPDXRef-Package-8815b117c8a1d5f7eeb0"
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
|
-
"spdxElementId": "SPDXRef-Package-
|
|
157
|
+
"spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
|
|
158
158
|
"relationshipType": "DEPENDS_ON",
|
|
159
159
|
"relatedSpdxElement": "SPDXRef-Package-f8b45a289df643042b94"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
|
-
"spdxElementId": "SPDXRef-Package-
|
|
162
|
+
"spdxElementId": "SPDXRef-Package-972ad62e2707c407a361",
|
|
163
163
|
"relationshipType": "DEPENDS_ON",
|
|
164
164
|
"relatedSpdxElement": "SPDXRef-Package-b779412f685822663496"
|
|
165
165
|
},
|