@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.
Files changed (58) hide show
  1. package/README.md +31 -42
  2. package/THIRD_PARTY_NOTICES.md +4 -79
  3. package/dist/cli.js +31 -31
  4. package/dist/connector/adapters/rawQuicCandidate.d.ts +5 -0
  5. package/dist/connector/adapters/rawQuicCandidate.js +71 -0
  6. package/dist/connector/sessionAcceptor.d.ts +22 -2
  7. package/dist/connector/sessionAcceptor.js +51 -21
  8. package/dist/connector/sessionConnector.d.ts +2 -0
  9. package/dist/connector/sessionConnector.js +4 -1
  10. package/dist/interop/proxyServerPeer.d.ts +1 -0
  11. package/dist/interop/proxyServerPeer.js +83 -0
  12. package/dist/interop/serverParityPeer.d.ts +1 -0
  13. package/dist/interop/serverParityPeer.js +608 -0
  14. package/dist/node/acceptor.d.ts +31 -19
  15. package/dist/node/acceptor.js +353 -72
  16. package/dist/node/connectSession.d.ts +2 -1
  17. package/dist/node/connectSession.js +22 -12
  18. package/dist/node/controlplane.d.ts +137 -0
  19. package/dist/node/controlplane.js +439 -0
  20. package/dist/node/index.d.ts +8 -2
  21. package/dist/node/index.js +4 -1
  22. package/dist/node/nativeTransportAddon.d.ts +94 -0
  23. package/dist/node/nativeTransportAddon.js +166 -0
  24. package/dist/node/proxyServer.d.ts +31 -0
  25. package/dist/node/proxyServer.js +572 -0
  26. package/dist/node/rawQuicAdapter.d.ts +9 -0
  27. package/dist/node/rawQuicAdapter.js +107 -0
  28. package/dist/node/rawQuicServer.d.ts +24 -0
  29. package/dist/node/rawQuicServer.js +37 -0
  30. package/dist/node/runtimeCapability.d.ts +8 -1
  31. package/dist/node/runtimeCapability.js +20 -6
  32. package/dist/node/tunnelRuntime.d.ts +42 -0
  33. package/dist/node/tunnelRuntime.js +552 -0
  34. package/dist/node/webSocketServer.d.ts +24 -0
  35. package/dist/node/webSocketServer.js +135 -0
  36. package/dist/public/contract.d.ts +4 -3
  37. package/dist/transport/webSocketAdapter.d.ts +7 -0
  38. package/dist/transport/webSocketAdapter.js +136 -0
  39. package/dist/v2/artifact.js +1 -1
  40. package/dist/v2/capability.js +1 -1
  41. package/dist/v2/carrier.d.ts +4 -1
  42. package/dist/v2/carrier.js +2 -2
  43. package/dist/v2/handshake.js +7 -7
  44. package/dist/v2/protocol.js +5 -5
  45. package/dist/v2/publicSession.js +25 -7
  46. package/dist/v2/session.d.ts +2 -2
  47. package/dist/v2/session.js +3 -3
  48. package/dist/v2/unreliableMessage.js +4 -4
  49. package/dist/vendor/tr46.js +11 -6
  50. package/dist/ws-client/binaryTransport.d.ts +3 -0
  51. package/dist/ws-client/binaryTransport.js +14 -0
  52. package/package.json +19 -16
  53. package/sbom/cyclonedx.json +52 -2585
  54. package/sbom/spdx.json +56 -1826
  55. package/dist/node/webTransportClient.d.ts +0 -9
  56. package/dist/node/webTransportClient.js +0 -56
  57. package/dist/node/webTransportServer.d.ts +0 -22
  58. 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,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 || 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
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
- finally {
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.session.close().catch(() => undefined);
163
+ await this.close().catch(() => undefined);
119
164
  await Promise.allSettled(active);
120
165
  }
121
166
  }
122
167
  async close() {
123
- 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;
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
- acceptedSessionStates.set(accepted, { session, handlers });
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
- address() {
141
- return acceptorState(this).server.address();
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
- 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 {
146
240
  const router = new RpcRouter();
147
- let handlers;
148
- const internal = await acceptNativeSessionV2(carrier, async (decoded, signal) => {
149
- const request = runtimeAuthorizationRequest(decoded);
150
- const decision = await state.options.authorize(request, signal === undefined ? {} : { signal });
151
- if (decision.decision !== "allow") {
152
- return {
153
- accepted: false,
154
- status: decision.decision === "retry" ? 2 : 1,
155
- reason: decision.reason,
156
- };
157
- }
158
- const registry = state.options.resolveHandlers === undefined
159
- ? new SessionHandlers()
160
- : await state.options.resolveHandlers(request, signal === undefined ? {} : { signal });
161
- handlers = freezeHandlers(registry, router);
162
- return { accepted: true, artifact: unwrapArtifact(decision.artifact) };
163
- }, {
164
- runtime: nodeSessionRuntimeV2,
165
- rpcRouter: router,
166
- ...(operation.signal === undefined ? {} : { signal: operation.signal }),
167
- });
168
- if (handlers === undefined) {
169
- 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);
170
278
  throw new SessionError("operation_failed");
171
279
  }
172
- return createAcceptedSession(projectSessionV2(internal), handlers);
280
+ state.accepted.push(await establishDirect(leg, state.abort.signal, leg.leaseId === undefined ? undefined : () => releaseLease(state, leg.leaseId)));
173
281
  }
174
- async close() {
175
- 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"));
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) || 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) {
187
374
  throw new TypeError("invalid Flowersec Acceptor options");
188
375
  }
189
- const server = await startNodeWebTransportServerV2({
190
- host: options.host,
191
- port: options.port,
192
- path: options.path,
193
- certificate: options.certificate,
194
- privateKey: options.privateKey,
195
- carrierPath: "direct",
196
- inboundBidirectionalStreamCapacity: options.maxInboundStreams + 2,
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
- 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);
200
448
  return Object.freeze(acceptor);
201
449
  }
202
- function runtimeAuthorizationRequest(decoded) {
203
- const credential = decoded.request.pathKind === "direct"
204
- ? decoded.request.routing_token
205
- : decoded.request.attach_token;
206
- const lookupKey = base64urlEncode(sha256(encoder.encode(credential)));
207
- 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
+ });
208
484
  }
209
485
  function validRPCError(error) {
210
- 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) {
211
489
  return { code: 500, message: "handler failed" };
212
490
  }
213
- if (error.message !== undefined && encoder.encode(error.message).length > 1024) {
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 ? { 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 };
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;