@floegence/flowersec-core 2.3.6 → 2.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -42
- package/THIRD_PARTY_NOTICES.md +4 -79
- package/dist/cli.js +31 -31
- package/dist/connector/adapters/rawQuicCandidate.d.ts +5 -0
- package/dist/connector/adapters/rawQuicCandidate.js +71 -0
- package/dist/connector/sessionAcceptor.d.ts +22 -2
- package/dist/connector/sessionAcceptor.js +51 -21
- package/dist/connector/sessionConnector.d.ts +2 -0
- package/dist/connector/sessionConnector.js +4 -1
- package/dist/interop/proxyServerPeer.d.ts +1 -0
- package/dist/interop/proxyServerPeer.js +83 -0
- package/dist/interop/serverParityPeer.d.ts +1 -0
- package/dist/interop/serverParityPeer.js +608 -0
- package/dist/node/acceptor.d.ts +31 -19
- package/dist/node/acceptor.js +353 -72
- package/dist/node/connectSession.d.ts +2 -1
- package/dist/node/connectSession.js +22 -12
- package/dist/node/controlplane.d.ts +137 -0
- package/dist/node/controlplane.js +439 -0
- package/dist/node/index.d.ts +8 -2
- package/dist/node/index.js +4 -1
- package/dist/node/nativeTransportAddon.d.ts +94 -0
- package/dist/node/nativeTransportAddon.js +166 -0
- package/dist/node/proxyServer.d.ts +31 -0
- package/dist/node/proxyServer.js +572 -0
- package/dist/node/rawQuicAdapter.d.ts +9 -0
- package/dist/node/rawQuicAdapter.js +107 -0
- package/dist/node/rawQuicServer.d.ts +24 -0
- package/dist/node/rawQuicServer.js +37 -0
- package/dist/node/runtimeCapability.d.ts +8 -1
- package/dist/node/runtimeCapability.js +20 -6
- package/dist/node/tunnelRuntime.d.ts +42 -0
- package/dist/node/tunnelRuntime.js +552 -0
- package/dist/node/webSocketServer.d.ts +24 -0
- package/dist/node/webSocketServer.js +135 -0
- package/dist/public/contract.d.ts +4 -3
- package/dist/transport/webSocketAdapter.d.ts +7 -0
- package/dist/transport/webSocketAdapter.js +136 -0
- package/dist/v2/artifact.js +1 -1
- package/dist/v2/capability.js +1 -1
- package/dist/v2/carrier.d.ts +4 -1
- package/dist/v2/carrier.js +2 -2
- package/dist/v2/handshake.js +7 -7
- package/dist/v2/protocol.js +5 -5
- package/dist/v2/publicSession.js +25 -7
- package/dist/v2/session.d.ts +2 -2
- package/dist/v2/session.js +3 -3
- package/dist/v2/unreliableMessage.js +4 -4
- package/dist/vendor/tr46.js +11 -6
- package/dist/ws-client/binaryTransport.d.ts +3 -0
- package/dist/ws-client/binaryTransport.js +14 -0
- package/package.json +19 -16
- package/sbom/cyclonedx.json +52 -2585
- package/sbom/spdx.json +56 -1826
- package/dist/node/webTransportClient.d.ts +0 -9
- package/dist/node/webTransportClient.js +0 -56
- package/dist/node/webTransportServer.d.ts +0 -22
- package/dist/node/webTransportServer.js +0 -100
package/dist/node/acceptor.d.ts
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
import { type Artifact } from "../public/artifact.js";
|
|
2
1
|
import { type IncomingStream, type JsonValue, type OperationOptions, type Session } from "../public/contract.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
private constructor();
|
|
7
|
-
}
|
|
8
|
-
export type AuthorizationDecision = Readonly<{
|
|
9
|
-
decision: "allow";
|
|
10
|
-
artifact: Artifact;
|
|
11
|
-
}> | Readonly<{
|
|
12
|
-
decision: "reject" | "retry";
|
|
13
|
-
reason: string;
|
|
14
|
-
}>;
|
|
2
|
+
import { type DirectAuthorizationDecision, type RuntimeAuthorizationRequest } from "./controlplane.js";
|
|
3
|
+
export type { RuntimeAuthorizationRequest } from "./controlplane.js";
|
|
4
|
+
export type AuthorizationDecision = DirectAuthorizationDecision;
|
|
15
5
|
export type RPCHandlerResult = Readonly<{
|
|
16
6
|
payload: JsonValue;
|
|
17
7
|
}> | Readonly<{
|
|
@@ -23,6 +13,9 @@ export type RPCHandlerResult = Readonly<{
|
|
|
23
13
|
export type RPCHandler = (payload: JsonValue, request: Readonly<{
|
|
24
14
|
typeId: number;
|
|
25
15
|
}>) => Promise<RPCHandlerResult>;
|
|
16
|
+
export type NotificationHandler = (payload: JsonValue, request: Readonly<{
|
|
17
|
+
typeId: number;
|
|
18
|
+
}>) => Promise<void> | void;
|
|
26
19
|
export type StreamHandler = (incoming: IncomingStream, options: OperationOptions) => Promise<void>;
|
|
27
20
|
export type SessionHandlerOptions = Readonly<{
|
|
28
21
|
maxConcurrentStreams?: number;
|
|
@@ -34,16 +27,35 @@ export declare class SessionHandlersError extends Error {
|
|
|
34
27
|
export declare class SessionHandlers {
|
|
35
28
|
constructor(options?: SessionHandlerOptions);
|
|
36
29
|
handleRPC(typeId: number, handler: RPCHandler): void;
|
|
30
|
+
handleNotification(typeId: number, handler: NotificationHandler): void;
|
|
37
31
|
handleStream(kind: string, handler: StreamHandler): void;
|
|
38
32
|
}
|
|
39
|
-
export type
|
|
33
|
+
export type AcceptorListener = Readonly<{
|
|
34
|
+
carrier: "websocket";
|
|
35
|
+
path: "direct";
|
|
36
|
+
host: string;
|
|
37
|
+
port: number;
|
|
38
|
+
tls?: Readonly<{
|
|
39
|
+
certificate: string;
|
|
40
|
+
privateKey: string;
|
|
41
|
+
}>;
|
|
42
|
+
allowedOrigins: readonly string[];
|
|
43
|
+
}> | Readonly<{
|
|
44
|
+
carrier: "raw_quic";
|
|
45
|
+
path: "direct";
|
|
40
46
|
host: string;
|
|
41
47
|
port: number;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
tls: Readonly<{
|
|
49
|
+
certificate: string;
|
|
50
|
+
privateKey: string;
|
|
51
|
+
}>;
|
|
52
|
+
}>;
|
|
53
|
+
export type AcceptorOptions = Readonly<{
|
|
54
|
+
listeners: readonly AcceptorListener[];
|
|
45
55
|
maxInboundStreams: number;
|
|
56
|
+
cleanupTimeoutMs?: number;
|
|
46
57
|
authorize(request: RuntimeAuthorizationRequest, options: OperationOptions): Promise<AuthorizationDecision>;
|
|
58
|
+
release?(leaseId: string): Promise<void> | void;
|
|
47
59
|
resolveHandlers?(request: RuntimeAuthorizationRequest, options: OperationOptions): Promise<SessionHandlers> | SessionHandlers;
|
|
48
60
|
}>;
|
|
49
61
|
export declare class AcceptedSession {
|
|
@@ -54,10 +66,10 @@ export declare class AcceptedSession {
|
|
|
54
66
|
}
|
|
55
67
|
export declare class Acceptor {
|
|
56
68
|
private constructor();
|
|
57
|
-
|
|
69
|
+
addresses(): readonly Readonly<{
|
|
58
70
|
host: string;
|
|
59
71
|
port: number;
|
|
60
|
-
}
|
|
72
|
+
}>[];
|
|
61
73
|
accept(operation?: OperationOptions): Promise<AcceptedSession>;
|
|
62
74
|
close(): Promise<void>;
|
|
63
75
|
}
|
package/dist/node/acceptor.js
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
import { sha256 } from "@noble/hashes/sha256";
|
|
2
1
|
import { RpcRouter } from "../rpc/server.js";
|
|
3
|
-
import {
|
|
2
|
+
import { acceptReceivedSessionV2, receiveSessionAdmissionV2, rejectSessionAdmissionV2, } from "../connector/sessionAcceptor.js";
|
|
4
3
|
import { nodeSessionRuntimeV2 } from "./sessionRuntime.js";
|
|
5
|
-
import {
|
|
4
|
+
import { startNodeWebSocketServer, } from "./webSocketServer.js";
|
|
5
|
+
import { createNativeRawQuicDriver } from "./nativeTransportAddon.js";
|
|
6
|
+
import { startNodeRawQuicServer, } from "./rawQuicServer.js";
|
|
6
7
|
import { unwrapArtifact } from "../public/artifact.js";
|
|
7
8
|
import { SessionError, } from "../public/contract.js";
|
|
8
9
|
import { projectSessionV2 } from "../v2/publicSession.js";
|
|
9
|
-
import {
|
|
10
|
+
import { runtimeAuthorizationRequestFromDecoded, } from "./controlplane.js";
|
|
10
11
|
const DEFAULT_MAX_CONCURRENT_STREAMS = 64;
|
|
11
12
|
const MAX_CONCURRENT_STREAMS = 128;
|
|
13
|
+
const DEFAULT_CLEANUP_TIMEOUT_MS = 2_000;
|
|
12
14
|
const encoder = new TextEncoder();
|
|
13
|
-
export class RuntimeAuthorizationRequest {
|
|
14
|
-
lookupKey;
|
|
15
|
-
constructor(lookupKey) {
|
|
16
|
-
this.lookupKey = lookupKey;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
15
|
export class SessionHandlersError extends Error {
|
|
20
16
|
code;
|
|
21
17
|
constructor(code) {
|
|
@@ -27,28 +23,51 @@ export class SessionHandlersError extends Error {
|
|
|
27
23
|
export class SessionHandlers {
|
|
28
24
|
constructor(options = {}) {
|
|
29
25
|
const maximum = options.maxConcurrentStreams ?? DEFAULT_MAX_CONCURRENT_STREAMS;
|
|
30
|
-
if (!Number.isSafeInteger(maximum) ||
|
|
26
|
+
if (!Number.isSafeInteger(maximum) ||
|
|
27
|
+
maximum < 1 ||
|
|
28
|
+
maximum > MAX_CONCURRENT_STREAMS) {
|
|
31
29
|
throw new SessionHandlersError("invalid_handler");
|
|
32
30
|
}
|
|
33
31
|
sessionHandlerStates.set(this, {
|
|
34
32
|
maxConcurrentStreams: maximum,
|
|
35
33
|
rpc: new Map(),
|
|
34
|
+
notifications: new Map(),
|
|
36
35
|
streams: new Map(),
|
|
37
36
|
frozen: false,
|
|
38
37
|
});
|
|
39
38
|
}
|
|
40
39
|
handleRPC(typeId, handler) {
|
|
41
40
|
const state = mutableHandlerState(this);
|
|
42
|
-
if (!Number.isSafeInteger(typeId) ||
|
|
41
|
+
if (!Number.isSafeInteger(typeId) ||
|
|
42
|
+
typeId < 1 ||
|
|
43
|
+
typeId > 0xffff_ffff ||
|
|
44
|
+
typeof handler !== "function") {
|
|
43
45
|
throw new SessionHandlersError("invalid_handler");
|
|
44
46
|
}
|
|
45
47
|
if (state.rpc.has(typeId))
|
|
46
48
|
throw new SessionHandlersError("already_registered");
|
|
49
|
+
if (state.notifications.has(typeId))
|
|
50
|
+
throw new SessionHandlersError("already_registered");
|
|
47
51
|
state.rpc.set(typeId, handler);
|
|
48
52
|
}
|
|
53
|
+
handleNotification(typeId, handler) {
|
|
54
|
+
const state = mutableHandlerState(this);
|
|
55
|
+
if (!Number.isSafeInteger(typeId) ||
|
|
56
|
+
typeId < 1 ||
|
|
57
|
+
typeId > 0xffff_ffff ||
|
|
58
|
+
typeof handler !== "function") {
|
|
59
|
+
throw new SessionHandlersError("invalid_handler");
|
|
60
|
+
}
|
|
61
|
+
if (state.rpc.has(typeId) || state.notifications.has(typeId))
|
|
62
|
+
throw new SessionHandlersError("already_registered");
|
|
63
|
+
state.notifications.set(typeId, handler);
|
|
64
|
+
}
|
|
49
65
|
handleStream(kind, handler) {
|
|
50
66
|
const state = mutableHandlerState(this);
|
|
51
|
-
if (kind.length < 1 ||
|
|
67
|
+
if (kind.length < 1 ||
|
|
68
|
+
encoder.encode(kind).length > 255 ||
|
|
69
|
+
kind === "flowersec.rpc.v2" ||
|
|
70
|
+
typeof handler !== "function") {
|
|
52
71
|
throw new SessionHandlersError("invalid_handler");
|
|
53
72
|
}
|
|
54
73
|
if (state.streams.has(kind))
|
|
@@ -76,11 +95,39 @@ function freezeHandlers(handlers, router) {
|
|
|
76
95
|
return { payload: result.payload };
|
|
77
96
|
});
|
|
78
97
|
}
|
|
98
|
+
for (const [typeId, handler] of state.notifications) {
|
|
99
|
+
router.onNotify(typeId, (payload) => {
|
|
100
|
+
void Promise.resolve(handler(payload, Object.freeze({ typeId }))).catch(() => undefined);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
79
103
|
return Object.freeze({
|
|
80
104
|
maxConcurrentStreams: state.maxConcurrentStreams,
|
|
81
105
|
streams: new Map(state.streams),
|
|
82
106
|
});
|
|
83
107
|
}
|
|
108
|
+
/** @internal */
|
|
109
|
+
export function freezeSessionHandlersForConnector(handlers) {
|
|
110
|
+
const router = new RpcRouter();
|
|
111
|
+
freezeHandlers(handlers, router);
|
|
112
|
+
return router;
|
|
113
|
+
}
|
|
114
|
+
/** @internal */
|
|
115
|
+
export function registerSessionStreamsAtomically(handlers, entries) {
|
|
116
|
+
const state = mutableHandlerState(handlers);
|
|
117
|
+
const pending = new Set();
|
|
118
|
+
for (const [kind, handler] of entries) {
|
|
119
|
+
if (kind.length < 1 ||
|
|
120
|
+
encoder.encode(kind).length > 255 ||
|
|
121
|
+
kind === "flowersec.rpc.v2" ||
|
|
122
|
+
typeof handler !== "function")
|
|
123
|
+
throw new SessionHandlersError("invalid_handler");
|
|
124
|
+
if (state.streams.has(kind) || pending.has(kind))
|
|
125
|
+
throw new SessionHandlersError("already_registered");
|
|
126
|
+
pending.add(kind);
|
|
127
|
+
}
|
|
128
|
+
for (const [kind, handler] of entries)
|
|
129
|
+
state.streams.set(kind, handler);
|
|
130
|
+
}
|
|
84
131
|
export class AcceptedSession {
|
|
85
132
|
constructor() { }
|
|
86
133
|
get session() {
|
|
@@ -95,32 +142,44 @@ export class AcceptedSession {
|
|
|
95
142
|
throw new SessionError("canceled");
|
|
96
143
|
const incoming = await this.session.acceptStream(options);
|
|
97
144
|
const handler = state.handlers.streams.get(incoming.kind);
|
|
98
|
-
if (handler === undefined ||
|
|
145
|
+
if (handler === undefined ||
|
|
146
|
+
active.size >= state.handlers.maxConcurrentStreams) {
|
|
99
147
|
await incoming.stream.reset();
|
|
100
148
|
continue;
|
|
101
149
|
}
|
|
102
150
|
const task = (async () => {
|
|
103
151
|
try {
|
|
104
152
|
await handler(incoming, options);
|
|
153
|
+
await incoming.stream.closeWrite();
|
|
105
154
|
}
|
|
106
155
|
catch {
|
|
107
156
|
await incoming.stream.reset().catch(() => undefined);
|
|
108
157
|
}
|
|
109
|
-
|
|
110
|
-
await incoming.stream.close().catch(() => undefined);
|
|
111
|
-
}
|
|
112
|
-
})()
|
|
113
|
-
.finally(() => active.delete(task));
|
|
158
|
+
})().finally(() => active.delete(task));
|
|
114
159
|
active.add(task);
|
|
115
160
|
}
|
|
116
161
|
}
|
|
117
162
|
finally {
|
|
118
|
-
await this.
|
|
163
|
+
await this.close().catch(() => undefined);
|
|
119
164
|
await Promise.allSettled(active);
|
|
120
165
|
}
|
|
121
166
|
}
|
|
122
167
|
async close() {
|
|
123
|
-
|
|
168
|
+
const state = acceptedSessionState(this);
|
|
169
|
+
if (state.closePromise === undefined) {
|
|
170
|
+
state.closePromise = (async () => {
|
|
171
|
+
try {
|
|
172
|
+
await state.session.close();
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
if (state.release !== undefined && state.releasePromise === undefined) {
|
|
176
|
+
state.releasePromise = Promise.resolve().then(() => state.release());
|
|
177
|
+
}
|
|
178
|
+
await state.releasePromise;
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
}
|
|
182
|
+
await state.closePromise;
|
|
124
183
|
}
|
|
125
184
|
}
|
|
126
185
|
const acceptedSessionStates = new WeakMap();
|
|
@@ -130,49 +189,174 @@ function acceptedSessionState(accepted) {
|
|
|
130
189
|
throw new SessionError("operation_failed");
|
|
131
190
|
return state;
|
|
132
191
|
}
|
|
133
|
-
function createAcceptedSession(session, handlers) {
|
|
192
|
+
function createAcceptedSession(session, handlers, release) {
|
|
134
193
|
const accepted = new AcceptedSession();
|
|
135
|
-
|
|
194
|
+
const state = { session, handlers };
|
|
195
|
+
if (release !== undefined)
|
|
196
|
+
state.release = release;
|
|
197
|
+
acceptedSessionStates.set(accepted, state);
|
|
136
198
|
return Object.freeze(accepted);
|
|
137
199
|
}
|
|
138
200
|
export class Acceptor {
|
|
139
201
|
constructor() { }
|
|
140
|
-
|
|
141
|
-
return acceptorState(this).
|
|
202
|
+
addresses() {
|
|
203
|
+
return acceptorState(this).listeners.map((listener) => listener.address());
|
|
142
204
|
}
|
|
143
205
|
async accept(operation = {}) {
|
|
144
206
|
const state = acceptorState(this);
|
|
145
|
-
|
|
207
|
+
state.start();
|
|
208
|
+
return await state.accepted.shift(operation.signal);
|
|
209
|
+
}
|
|
210
|
+
async close() {
|
|
211
|
+
const state = acceptorState(this);
|
|
212
|
+
state.abort.abort();
|
|
213
|
+
state.accepted.close();
|
|
214
|
+
await Promise.all(state.listeners.map(async (listener) => await listener.close()));
|
|
215
|
+
await boundedCleanup(state.tasks, state.cleanupTimeoutMs);
|
|
216
|
+
state.tasks.clear();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function releaseLease(state, leaseId) {
|
|
220
|
+
if (leaseId === undefined || state.options.release === undefined)
|
|
221
|
+
return;
|
|
222
|
+
await state.options.release(leaseId);
|
|
223
|
+
}
|
|
224
|
+
async function authorizeCarrier(state, carrier) {
|
|
225
|
+
const received = await receiveSessionAdmissionV2(carrier, state.abort.signal);
|
|
226
|
+
const decoded = received.decoded;
|
|
227
|
+
const request = runtimeAuthorizationRequestFromDecoded(decoded);
|
|
228
|
+
const decision = await abortableCallback(state.options.authorize(request, { signal: state.abort.signal }), state.abort.signal);
|
|
229
|
+
if (decision.decision !== "allow") {
|
|
230
|
+
return await rejectSessionAdmissionV2(received, {
|
|
231
|
+
accepted: false,
|
|
232
|
+
status: decision.decision === "retry" ? 2 : 1,
|
|
233
|
+
reason: decision.reason,
|
|
234
|
+
}, state.abort.signal);
|
|
235
|
+
}
|
|
236
|
+
const leaseId = "leaseId" in decision && typeof decision.leaseId === "string"
|
|
237
|
+
? decision.leaseId
|
|
238
|
+
: undefined;
|
|
239
|
+
try {
|
|
146
240
|
const router = new RpcRouter();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
241
|
+
const registry = state.options.resolveHandlers === undefined
|
|
242
|
+
? new SessionHandlers()
|
|
243
|
+
: await abortableCallback(Promise.resolve(state.options.resolveHandlers(request, {
|
|
244
|
+
signal: state.abort.signal,
|
|
245
|
+
})), state.abort.signal);
|
|
246
|
+
const leg = {
|
|
247
|
+
received,
|
|
248
|
+
artifact: unwrapArtifact(decision.artifact),
|
|
249
|
+
handlers: freezeHandlers(registry, router),
|
|
250
|
+
router,
|
|
251
|
+
};
|
|
252
|
+
if (leaseId !== undefined)
|
|
253
|
+
leg.leaseId = leaseId;
|
|
254
|
+
return leg;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
await releaseLease(state, leaseId).catch(() => undefined);
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function establishDirect(leg, signal, release) {
|
|
262
|
+
const internal = await acceptReceivedSessionV2(leg.received, leg.artifact, {
|
|
263
|
+
runtime: nodeSessionRuntimeV2,
|
|
264
|
+
rpcRouter: leg.router,
|
|
265
|
+
role: "server",
|
|
266
|
+
signal,
|
|
267
|
+
});
|
|
268
|
+
return createAcceptedSession(projectSessionV2(internal), leg.handlers, release);
|
|
269
|
+
}
|
|
270
|
+
async function processCarrier(state, carrier) {
|
|
271
|
+
const leg = await authorizeCarrier(state, carrier);
|
|
272
|
+
if (leg === undefined)
|
|
273
|
+
return;
|
|
274
|
+
try {
|
|
275
|
+
if (leg.received.decoded.request.pathKind !== "direct" ||
|
|
276
|
+
leg.artifact.path.kind !== "direct") {
|
|
277
|
+
await leg.received.carrier.close().catch(() => undefined);
|
|
170
278
|
throw new SessionError("operation_failed");
|
|
171
279
|
}
|
|
172
|
-
|
|
280
|
+
state.accepted.push(await establishDirect(leg, state.abort.signal, leg.leaseId === undefined ? undefined : () => releaseLease(state, leg.leaseId)));
|
|
173
281
|
}
|
|
174
|
-
|
|
175
|
-
await
|
|
282
|
+
catch (error) {
|
|
283
|
+
await releaseLease(state, leg.leaseId).catch(() => undefined);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async function runAcceptLoop(state) {
|
|
288
|
+
while (!state.abort.signal.aborted) {
|
|
289
|
+
try {
|
|
290
|
+
const carrier = await state.accept({ signal: state.abort.signal });
|
|
291
|
+
const task = processCarrier(state, carrier)
|
|
292
|
+
.catch(async () => {
|
|
293
|
+
await carrier.close().catch(() => undefined);
|
|
294
|
+
})
|
|
295
|
+
.finally(() => state.tasks.delete(task));
|
|
296
|
+
state.tasks.add(task);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (!state.abort.signal.aborted)
|
|
300
|
+
state.accepted.fail(error instanceof Error ? error : new Error(String(error)));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
class AcceptedQueue {
|
|
306
|
+
values = [];
|
|
307
|
+
waiters = new Set();
|
|
308
|
+
failure;
|
|
309
|
+
push(value) {
|
|
310
|
+
if (this.failure !== undefined) {
|
|
311
|
+
void value.close();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const waiter = this.waiters.values().next().value;
|
|
315
|
+
if (waiter === undefined)
|
|
316
|
+
this.values.push(value);
|
|
317
|
+
else {
|
|
318
|
+
this.waiters.delete(waiter);
|
|
319
|
+
waiter.resolve(value);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async shift(signal) {
|
|
323
|
+
if (signal?.aborted === true)
|
|
324
|
+
throw new SessionError("canceled");
|
|
325
|
+
const value = this.values.shift();
|
|
326
|
+
if (value !== undefined)
|
|
327
|
+
return value;
|
|
328
|
+
if (this.failure !== undefined)
|
|
329
|
+
throw this.failure;
|
|
330
|
+
return await new Promise((resolve, reject) => {
|
|
331
|
+
const waiter = {
|
|
332
|
+
resolve: (session) => {
|
|
333
|
+
cleanup();
|
|
334
|
+
resolve(session);
|
|
335
|
+
},
|
|
336
|
+
reject: (error) => {
|
|
337
|
+
cleanup();
|
|
338
|
+
reject(error);
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
const abort = () => {
|
|
342
|
+
this.waiters.delete(waiter);
|
|
343
|
+
reject(new SessionError("canceled"));
|
|
344
|
+
};
|
|
345
|
+
const cleanup = () => signal?.removeEventListener("abort", abort);
|
|
346
|
+
this.waiters.add(waiter);
|
|
347
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
fail(error) {
|
|
351
|
+
if (this.failure !== undefined)
|
|
352
|
+
return;
|
|
353
|
+
this.failure = error;
|
|
354
|
+
for (const waiter of this.waiters)
|
|
355
|
+
waiter.reject(error);
|
|
356
|
+
this.waiters.clear();
|
|
357
|
+
}
|
|
358
|
+
close() {
|
|
359
|
+
this.fail(new SessionError("closed"));
|
|
176
360
|
}
|
|
177
361
|
}
|
|
178
362
|
const acceptorStates = new WeakMap();
|
|
@@ -183,35 +367,132 @@ function acceptorState(acceptor) {
|
|
|
183
367
|
return state;
|
|
184
368
|
}
|
|
185
369
|
export async function createAcceptor(options) {
|
|
186
|
-
if (!Number.isSafeInteger(options.maxInboundStreams) ||
|
|
370
|
+
if (!Number.isSafeInteger(options.maxInboundStreams) ||
|
|
371
|
+
options.maxInboundStreams < 1 ||
|
|
372
|
+
options.maxInboundStreams > 128 ||
|
|
373
|
+
options.listeners.length === 0) {
|
|
187
374
|
throw new TypeError("invalid Flowersec Acceptor options");
|
|
188
375
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
376
|
+
const cleanupTimeoutMs = options.cleanupTimeoutMs ?? DEFAULT_CLEANUP_TIMEOUT_MS;
|
|
377
|
+
if (!Number.isSafeInteger(cleanupTimeoutMs) || cleanupTimeoutMs < 1 || cleanupTimeoutMs > 60_000) {
|
|
378
|
+
throw new TypeError("invalid Flowersec Acceptor cleanup timeout");
|
|
379
|
+
}
|
|
380
|
+
if (options.listeners.some((listener) => listener.path !== "direct")) {
|
|
381
|
+
throw new TypeError("Flowersec Acceptor listeners must use the direct path");
|
|
382
|
+
}
|
|
383
|
+
const listeners = [];
|
|
384
|
+
let rawQuicDriver;
|
|
385
|
+
try {
|
|
386
|
+
for (const listener of options.listeners) {
|
|
387
|
+
if (listener.carrier === "websocket") {
|
|
388
|
+
listeners.push(await startNodeWebSocketServer({
|
|
389
|
+
...listener,
|
|
390
|
+
inboundBidirectionalStreamCapacity: options.maxInboundStreams + 2,
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
rawQuicDriver ??= createNativeRawQuicDriver();
|
|
395
|
+
listeners.push(await startNodeRawQuicServer(rawQuicDriver, {
|
|
396
|
+
...listener,
|
|
397
|
+
inboundBidirectionalStreamCapacity: options.maxInboundStreams + 2,
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
await Promise.allSettled(listeners.map(async (listener) => await listener.close()));
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
let cursor = 0;
|
|
407
|
+
const accept = async (operation) => {
|
|
408
|
+
if (listeners.length === 1)
|
|
409
|
+
return await listeners[0].accept(operation);
|
|
410
|
+
const controller = new AbortController();
|
|
411
|
+
const abort = () => controller.abort(operation.signal?.reason);
|
|
412
|
+
operation.signal?.addEventListener("abort", abort, { once: true });
|
|
413
|
+
try {
|
|
414
|
+
return await Promise.any(listeners.map(async (listener, index) => {
|
|
415
|
+
const selected = listeners[(index + cursor) % listeners.length];
|
|
416
|
+
return await selected.accept({ signal: controller.signal });
|
|
417
|
+
})).finally(() => {
|
|
418
|
+
cursor = (cursor + 1) % listeners.length;
|
|
419
|
+
controller.abort();
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
operation.signal?.removeEventListener("abort", abort);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
198
426
|
const acceptor = new Acceptor();
|
|
199
|
-
|
|
427
|
+
const accepted = new AcceptedQueue();
|
|
428
|
+
const abort = new AbortController();
|
|
429
|
+
const tasks = new Set();
|
|
430
|
+
let started = false;
|
|
431
|
+
const state = {
|
|
432
|
+
listeners,
|
|
433
|
+
options,
|
|
434
|
+
accept,
|
|
435
|
+
accepted,
|
|
436
|
+
abort,
|
|
437
|
+
tasks,
|
|
438
|
+
cleanupTimeoutMs,
|
|
439
|
+
start() {
|
|
440
|
+
if (started)
|
|
441
|
+
return;
|
|
442
|
+
started = true;
|
|
443
|
+
const task = runAcceptLoop(state).finally(() => tasks.delete(task));
|
|
444
|
+
tasks.add(task);
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
acceptorStates.set(acceptor, state);
|
|
200
448
|
return Object.freeze(acceptor);
|
|
201
449
|
}
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
450
|
+
async function boundedCleanup(tasks, timeoutMs) {
|
|
451
|
+
await Promise.race([
|
|
452
|
+
Promise.allSettled([...tasks]).then(() => undefined),
|
|
453
|
+
new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
454
|
+
]);
|
|
455
|
+
}
|
|
456
|
+
async function abortableCallback(promise, signal) {
|
|
457
|
+
if (signal.aborted)
|
|
458
|
+
throw signal.reason instanceof Error ? signal.reason : new SessionError("closed");
|
|
459
|
+
return await new Promise((resolve, reject) => {
|
|
460
|
+
let settled = false;
|
|
461
|
+
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
462
|
+
const abort = () => {
|
|
463
|
+
if (settled)
|
|
464
|
+
return;
|
|
465
|
+
settled = true;
|
|
466
|
+
cleanup();
|
|
467
|
+
reject(signal.reason instanceof Error ? signal.reason : new SessionError("closed"));
|
|
468
|
+
};
|
|
469
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
470
|
+
void promise.then((value) => {
|
|
471
|
+
if (settled)
|
|
472
|
+
return;
|
|
473
|
+
settled = true;
|
|
474
|
+
cleanup();
|
|
475
|
+
resolve(value);
|
|
476
|
+
}, (error) => {
|
|
477
|
+
if (settled)
|
|
478
|
+
return;
|
|
479
|
+
settled = true;
|
|
480
|
+
cleanup();
|
|
481
|
+
reject(error);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
208
484
|
}
|
|
209
485
|
function validRPCError(error) {
|
|
210
|
-
if (!Number.isSafeInteger(error.code) ||
|
|
486
|
+
if (!Number.isSafeInteger(error.code) ||
|
|
487
|
+
error.code < 1 ||
|
|
488
|
+
error.code > 0xffff_ffff) {
|
|
211
489
|
return { code: 500, message: "handler failed" };
|
|
212
490
|
}
|
|
213
|
-
if (error.message !== undefined &&
|
|
491
|
+
if (error.message !== undefined &&
|
|
492
|
+
encoder.encode(error.message).length > 1024) {
|
|
214
493
|
return { code: 500, message: "handler failed" };
|
|
215
494
|
}
|
|
216
|
-
return error.message === undefined
|
|
495
|
+
return error.message === undefined
|
|
496
|
+
? { code: error.code }
|
|
497
|
+
: { code: error.code, message: error.message };
|
|
217
498
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { ArtifactLease } from "../public/artifactLease.js";
|
|
2
2
|
import type { Session } from "../public/contract.js";
|
|
3
3
|
import { type ArtifactSource, type ConnectionController } from "../connectionController.js";
|
|
4
|
+
import { type SessionHandlers } from "./acceptor.js";
|
|
4
5
|
export type SessionTLSOptions = Readonly<{
|
|
5
6
|
ca?: string | Uint8Array;
|
|
6
|
-
serverCertificateHash?: Uint8Array;
|
|
7
7
|
}>;
|
|
8
8
|
export type SessionOptions = Readonly<{
|
|
9
9
|
origin: string;
|
|
10
10
|
signal?: AbortSignal;
|
|
11
11
|
connectTimeoutMs?: number;
|
|
12
12
|
tls?: SessionTLSOptions;
|
|
13
|
+
handlers?: SessionHandlers;
|
|
13
14
|
}>;
|
|
14
15
|
export type ConnectionControllerOptions = Readonly<{
|
|
15
16
|
origin: string;
|