@floegence/flowersec-core 2.3.5 → 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.
Files changed (61) hide show
  1. package/README.md +33 -42
  2. package/THIRD_PARTY_NOTICES.md +4 -79
  3. package/dist/cli.js +31 -31
  4. package/dist/connectionController.d.ts +1 -0
  5. package/dist/connectionController.js +3 -0
  6. package/dist/connector/adapters/rawQuicCandidate.d.ts +5 -0
  7. package/dist/connector/adapters/rawQuicCandidate.js +71 -0
  8. package/dist/connector/sessionAcceptor.d.ts +22 -2
  9. package/dist/connector/sessionAcceptor.js +51 -21
  10. package/dist/connector/sessionConnector.d.ts +2 -0
  11. package/dist/connector/sessionConnector.js +4 -1
  12. package/dist/interop/proxyServerPeer.d.ts +1 -0
  13. package/dist/interop/proxyServerPeer.js +83 -0
  14. package/dist/interop/serverParityPeer.d.ts +1 -0
  15. package/dist/interop/serverParityPeer.js +608 -0
  16. package/dist/node/acceptor.d.ts +31 -19
  17. package/dist/node/acceptor.js +360 -70
  18. package/dist/node/connectSession.d.ts +2 -1
  19. package/dist/node/connectSession.js +22 -12
  20. package/dist/node/controlplane.d.ts +137 -0
  21. package/dist/node/controlplane.js +439 -0
  22. package/dist/node/index.d.ts +8 -2
  23. package/dist/node/index.js +4 -1
  24. package/dist/node/nativeTransportAddon.d.ts +94 -0
  25. package/dist/node/nativeTransportAddon.js +166 -0
  26. package/dist/node/proxyServer.d.ts +31 -0
  27. package/dist/node/proxyServer.js +572 -0
  28. package/dist/node/rawQuicAdapter.d.ts +9 -0
  29. package/dist/node/rawQuicAdapter.js +107 -0
  30. package/dist/node/rawQuicServer.d.ts +24 -0
  31. package/dist/node/rawQuicServer.js +37 -0
  32. package/dist/node/runtimeCapability.d.ts +8 -1
  33. package/dist/node/runtimeCapability.js +20 -6
  34. package/dist/node/tunnelRuntime.d.ts +42 -0
  35. package/dist/node/tunnelRuntime.js +552 -0
  36. package/dist/node/webSocketServer.d.ts +24 -0
  37. package/dist/node/webSocketServer.js +135 -0
  38. package/dist/public/contract.d.ts +4 -3
  39. package/dist/transport/webSocketAdapter.d.ts +7 -0
  40. package/dist/transport/webSocketAdapter.js +136 -0
  41. package/dist/v2/artifact.js +1 -1
  42. package/dist/v2/capability.js +1 -1
  43. package/dist/v2/carrier.d.ts +4 -1
  44. package/dist/v2/carrier.js +2 -2
  45. package/dist/v2/handshake.js +7 -7
  46. package/dist/v2/protocol.js +5 -5
  47. package/dist/v2/publicSession.js +25 -7
  48. package/dist/v2/retryDisposition.js +4 -4
  49. package/dist/v2/session.d.ts +2 -2
  50. package/dist/v2/session.js +3 -3
  51. package/dist/v2/unreliableMessage.js +4 -4
  52. package/dist/vendor/tr46.js +11 -6
  53. package/dist/ws-client/binaryTransport.d.ts +3 -0
  54. package/dist/ws-client/binaryTransport.js +14 -0
  55. package/package.json +19 -16
  56. package/sbom/cyclonedx.json +52 -2585
  57. package/sbom/spdx.json +56 -1826
  58. package/dist/node/webTransportClient.d.ts +0 -9
  59. package/dist/node/webTransportClient.js +0 -56
  60. package/dist/node/webTransportServer.d.ts +0 -22
  61. package/dist/node/webTransportServer.js +0 -100
@@ -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
- export declare class RuntimeAuthorizationRequest {
4
- readonly lookupKey: string;
5
- private readonly requestBrand;
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 AcceptorOptions = Readonly<{
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
- path: string;
43
- certificate: string;
44
- privateKey: string;
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
- address(): Readonly<{
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
  }
@@ -1,21 +1,17 @@
1
- import { sha256 } from "@noble/hashes/sha256";
2
1
  import { RpcRouter } from "../rpc/server.js";
3
- import { acceptNativeSessionV2 } from "../connector/sessionAcceptor.js";
2
+ import { acceptReceivedSessionV2, receiveSessionAdmissionV2, rejectSessionAdmissionV2, } from "../connector/sessionAcceptor.js";
4
3
  import { nodeSessionRuntimeV2 } from "./sessionRuntime.js";
5
- import { startNodeWebTransportServerV2, } from "./webTransportServer.js";
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 { base64urlEncode } from "../utils/base64url.js";
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) || maximum < 1 || maximum > MAX_CONCURRENT_STREAMS) {
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) || typeId < 1 || typeId > 0xffff_ffff || typeof handler !== "function") {
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 || encoder.encode(kind).length > 255 || kind === "flowersec.rpc.v2" || typeof handler !== "function") {
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,23 +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 || active.size >= state.handlers.maxConcurrentStreams) {
145
+ if (handler === undefined ||
146
+ active.size >= state.handlers.maxConcurrentStreams) {
99
147
  await incoming.stream.reset();
100
148
  continue;
101
149
  }
102
- const task = handler(incoming, options)
103
- .finally(() => incoming.stream.close())
104
- .finally(() => active.delete(task));
150
+ const task = (async () => {
151
+ try {
152
+ await handler(incoming, options);
153
+ await incoming.stream.closeWrite();
154
+ }
155
+ catch {
156
+ await incoming.stream.reset().catch(() => undefined);
157
+ }
158
+ })().finally(() => active.delete(task));
105
159
  active.add(task);
106
160
  }
107
161
  }
108
162
  finally {
109
- await this.session.close().catch(() => undefined);
163
+ await this.close().catch(() => undefined);
110
164
  await Promise.allSettled(active);
111
165
  }
112
166
  }
113
167
  async close() {
114
- await this.session.close();
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;
115
183
  }
116
184
  }
117
185
  const acceptedSessionStates = new WeakMap();
@@ -121,49 +189,174 @@ function acceptedSessionState(accepted) {
121
189
  throw new SessionError("operation_failed");
122
190
  return state;
123
191
  }
124
- function createAcceptedSession(session, handlers) {
192
+ function createAcceptedSession(session, handlers, release) {
125
193
  const accepted = new AcceptedSession();
126
- acceptedSessionStates.set(accepted, { session, handlers });
194
+ const state = { session, handlers };
195
+ if (release !== undefined)
196
+ state.release = release;
197
+ acceptedSessionStates.set(accepted, state);
127
198
  return Object.freeze(accepted);
128
199
  }
129
200
  export class Acceptor {
130
201
  constructor() { }
131
- address() {
132
- return acceptorState(this).server.address();
202
+ addresses() {
203
+ return acceptorState(this).listeners.map((listener) => listener.address());
133
204
  }
134
205
  async accept(operation = {}) {
135
206
  const state = acceptorState(this);
136
- const carrier = await state.server.accept(operation);
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 {
137
240
  const router = new RpcRouter();
138
- let handlers;
139
- const internal = await acceptNativeSessionV2(carrier, async (decoded, signal) => {
140
- const request = runtimeAuthorizationRequest(decoded);
141
- const decision = await state.options.authorize(request, signal === undefined ? {} : { signal });
142
- if (decision.decision !== "allow") {
143
- return {
144
- accepted: false,
145
- status: decision.decision === "retry" ? 2 : 1,
146
- reason: decision.reason,
147
- };
148
- }
149
- const registry = state.options.resolveHandlers === undefined
150
- ? new SessionHandlers()
151
- : await state.options.resolveHandlers(request, signal === undefined ? {} : { signal });
152
- handlers = freezeHandlers(registry, router);
153
- return { accepted: true, artifact: unwrapArtifact(decision.artifact) };
154
- }, {
155
- runtime: nodeSessionRuntimeV2,
156
- rpcRouter: router,
157
- ...(operation.signal === undefined ? {} : { signal: operation.signal }),
158
- });
159
- if (handlers === undefined) {
160
- await internal.close().catch(() => undefined);
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);
161
278
  throw new SessionError("operation_failed");
162
279
  }
163
- return createAcceptedSession(projectSessionV2(internal), handlers);
280
+ state.accepted.push(await establishDirect(leg, state.abort.signal, leg.leaseId === undefined ? undefined : () => releaseLease(state, leg.leaseId)));
164
281
  }
165
- async close() {
166
- await acceptorState(this).server.close();
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"));
167
360
  }
168
361
  }
169
362
  const acceptorStates = new WeakMap();
@@ -174,35 +367,132 @@ function acceptorState(acceptor) {
174
367
  return state;
175
368
  }
176
369
  export async function createAcceptor(options) {
177
- if (!Number.isSafeInteger(options.maxInboundStreams) || options.maxInboundStreams < 1 || options.maxInboundStreams > 128) {
370
+ if (!Number.isSafeInteger(options.maxInboundStreams) ||
371
+ options.maxInboundStreams < 1 ||
372
+ options.maxInboundStreams > 128 ||
373
+ options.listeners.length === 0) {
178
374
  throw new TypeError("invalid Flowersec Acceptor options");
179
375
  }
180
- const server = await startNodeWebTransportServerV2({
181
- host: options.host,
182
- port: options.port,
183
- path: options.path,
184
- certificate: options.certificate,
185
- privateKey: options.privateKey,
186
- carrierPath: "direct",
187
- inboundBidirectionalStreamCapacity: options.maxInboundStreams + 2,
188
- });
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
+ };
189
426
  const acceptor = new Acceptor();
190
- acceptorStates.set(acceptor, { server, options });
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);
191
448
  return Object.freeze(acceptor);
192
449
  }
193
- function runtimeAuthorizationRequest(decoded) {
194
- const credential = decoded.request.pathKind === "direct"
195
- ? decoded.request.routing_token
196
- : decoded.request.attach_token;
197
- const lookupKey = base64urlEncode(sha256(encoder.encode(credential)));
198
- return new RuntimeAuthorizationRequest(lookupKey);
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
+ });
199
484
  }
200
485
  function validRPCError(error) {
201
- if (!Number.isSafeInteger(error.code) || error.code < 1 || error.code > 0xffff_ffff) {
486
+ if (!Number.isSafeInteger(error.code) ||
487
+ error.code < 1 ||
488
+ error.code > 0xffff_ffff) {
202
489
  return { code: 500, message: "handler failed" };
203
490
  }
204
- if (error.message !== undefined && encoder.encode(error.message).length > 1024) {
491
+ if (error.message !== undefined &&
492
+ encoder.encode(error.message).length > 1024) {
205
493
  return { code: 500, message: "handler failed" };
206
494
  }
207
- return error.message === undefined ? { code: error.code } : { code: error.code, message: error.message };
495
+ return error.message === undefined
496
+ ? { code: error.code }
497
+ : { code: error.code, message: error.message };
208
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;