@floegence/flowersec-core 2.5.3 → 3.0.0

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 (119) hide show
  1. package/README.md +46 -18
  2. package/dist/browser/connectSessionV3.d.ts +13 -0
  3. package/dist/browser/connectSessionV3.js +75 -0
  4. package/dist/browser/index.d.ts +3 -2
  5. package/dist/browser/index.js +2 -1
  6. package/dist/browser/v2.d.ts +3 -0
  7. package/dist/browser/v2.js +2 -0
  8. package/dist/facade.d.ts +15 -8
  9. package/dist/facade.js +5 -4
  10. package/dist/framing/jsonframe.d.ts +1 -1
  11. package/dist/framing/jsonframe.js +3 -1
  12. package/dist/interop/proxyServerPeer.js +1 -1
  13. package/dist/interop/serverParityPeer.js +8 -1
  14. package/dist/node/acceptor.d.ts +10 -0
  15. package/dist/node/acceptor.js +53 -7
  16. package/dist/node/acceptorV3.d.ts +57 -0
  17. package/dist/node/acceptorV3.js +227 -0
  18. package/dist/node/connectSessionV3.d.ts +21 -0
  19. package/dist/node/connectSessionV3.js +73 -0
  20. package/dist/node/index.d.ts +15 -10
  21. package/dist/node/index.js +6 -5
  22. package/dist/node/nativeTransportAddon.d.ts +39 -1
  23. package/dist/node/nativeTransportAddon.js +29 -5
  24. package/dist/node/rawQuicAdapterV3.d.ts +10 -0
  25. package/dist/node/rawQuicAdapterV3.js +76 -0
  26. package/dist/node/rawQuicServerV3.d.ts +24 -0
  27. package/dist/node/rawQuicServerV3.js +34 -0
  28. package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
  29. package/dist/node/runtimeAuthorizationV3.js +126 -0
  30. package/dist/node/tunnelRuntimeV3.d.ts +57 -0
  31. package/dist/node/tunnelRuntimeV3.js +857 -0
  32. package/dist/node/v2.d.ts +11 -0
  33. package/dist/node/v2.js +6 -0
  34. package/dist/node/webSocketServerV3.d.ts +27 -0
  35. package/dist/node/webSocketServerV3.js +219 -0
  36. package/dist/proxy/runtime.js +39 -6
  37. package/dist/proxy/serviceWorker.d.ts +2 -0
  38. package/dist/proxy/serviceWorker.js +63 -7
  39. package/dist/proxy/windowBridge.d.ts +1 -0
  40. package/dist/proxy/windowBridge.js +93 -33
  41. package/dist/proxy/wsPatch.d.ts +1 -0
  42. package/dist/proxy/wsPatch.js +39 -6
  43. package/dist/public/streamHandlers.d.ts +3 -0
  44. package/dist/public/streamHandlers.js +6 -1
  45. package/dist/rpc/client.js +9 -7
  46. package/dist/rpc/server.js +44 -20
  47. package/dist/rpc/validate.d.ts +1 -0
  48. package/dist/rpc/validate.js +18 -6
  49. package/dist/v2/index.d.ts +14 -0
  50. package/dist/v2/index.js +7 -0
  51. package/dist/v2/protocol.js +23 -32
  52. package/dist/v2/publicSession.js +4 -0
  53. package/dist/v2/session.d.ts +5 -0
  54. package/dist/v2/session.js +94 -35
  55. package/dist/v3/admissionError.d.ts +4 -0
  56. package/dist/v3/admissionError.js +8 -0
  57. package/dist/v3/artifact.d.ts +145 -0
  58. package/dist/v3/artifact.js +1162 -0
  59. package/dist/v3/artifactLease.d.ts +17 -0
  60. package/dist/v3/artifactLease.js +109 -0
  61. package/dist/v3/browserRuntime.d.ts +37 -0
  62. package/dist/v3/browserRuntime.js +197 -0
  63. package/dist/v3/browserSessionRuntime.d.ts +2 -0
  64. package/dist/v3/browserSessionRuntime.js +21 -0
  65. package/dist/v3/capability.d.ts +32 -0
  66. package/dist/v3/capability.js +345 -0
  67. package/dist/v3/carrier.d.ts +84 -0
  68. package/dist/v3/carrier.js +411 -0
  69. package/dist/v3/connectionController.d.ts +87 -0
  70. package/dist/v3/connectionController.js +805 -0
  71. package/dist/v3/contract.d.ts +65 -0
  72. package/dist/v3/contract.js +3 -0
  73. package/dist/v3/controller.d.ts +43 -0
  74. package/dist/v3/controller.js +303 -0
  75. package/dist/v3/handshake.d.ts +81 -0
  76. package/dist/v3/handshake.js +471 -0
  77. package/dist/v3/jcs.d.ts +6 -0
  78. package/dist/v3/jcs.js +69 -0
  79. package/dist/v3/jsonPreflight.d.ts +1 -0
  80. package/dist/v3/jsonPreflight.js +251 -0
  81. package/dist/v3/nodeRuntime.d.ts +40 -0
  82. package/dist/v3/nodeRuntime.js +315 -0
  83. package/dist/v3/nodeSessionRuntime.d.ts +2 -0
  84. package/dist/v3/nodeSessionRuntime.js +13 -0
  85. package/dist/v3/protocol.d.ts +99 -0
  86. package/dist/v3/protocol.js +773 -0
  87. package/dist/v3/publicApi.d.ts +12 -0
  88. package/dist/v3/publicApi.js +46 -0
  89. package/dist/v3/publicSession.d.ts +1 -0
  90. package/dist/v3/publicSession.js +281 -0
  91. package/dist/v3/retryDisposition.d.ts +16 -0
  92. package/dist/v3/retryDisposition.js +30 -0
  93. package/dist/v3/runtimeAdapters.d.ts +9 -0
  94. package/dist/v3/runtimeAdapters.js +109 -0
  95. package/dist/v3/security.d.ts +34 -0
  96. package/dist/v3/security.js +100 -0
  97. package/dist/v3/serverAdmission.d.ts +38 -0
  98. package/dist/v3/serverAdmission.js +223 -0
  99. package/dist/v3/session.d.ts +318 -0
  100. package/dist/v3/session.js +2289 -0
  101. package/dist/v3/sessionConnector.d.ts +37 -0
  102. package/dist/v3/sessionConnector.js +440 -0
  103. package/dist/v3/streamLifetimeLedger.d.ts +31 -0
  104. package/dist/v3/streamLifetimeLedger.js +131 -0
  105. package/dist/v3/streamMetadata.d.ts +1 -0
  106. package/dist/v3/streamMetadata.js +7 -0
  107. package/dist/v3/transportConstants.d.ts +56 -0
  108. package/dist/v3/transportConstants.js +59 -0
  109. package/dist/v3/unreliableMessage.d.ts +44 -0
  110. package/dist/v3/unreliableMessage.js +284 -0
  111. package/dist/v3/webSocketCarrier.d.ts +31 -0
  112. package/dist/v3/webSocketCarrier.js +370 -0
  113. package/dist/v3/webTransportCarrier.d.ts +27 -0
  114. package/dist/v3/webTransportCarrier.js +534 -0
  115. package/dist/yamux/stream.d.ts +3 -0
  116. package/dist/yamux/stream.js +17 -4
  117. package/package.json +4 -4
  118. package/sbom/cyclonedx.json +6 -6
  119. package/sbom/spdx.json +11 -11
@@ -0,0 +1,857 @@
1
+ import { createAdmissionReasonRegistryV3, receiveSessionAdmissionV3, rejectSessionAdmissionV3, } from "../v3/serverAdmission.js";
2
+ import { configureServerWebSocketCarrierRoleV3 } from "../v3/webSocketCarrier.js";
3
+ import { AdmissionStatusV3, encodeFSA3ResponseV3, } from "../v3/artifact.js";
4
+ import { startNodeWebSocketListenerV3 } from "./webSocketServerV3.js";
5
+ import { createNativeRawQuicDriverV3 } from "./nativeTransportAddon.js";
6
+ import { startNodeRawQuicListenerV3 } from "./rawQuicServerV3.js";
7
+ import { consumeTunnelAuthorizationGrantV3, inspectTunnelAuthorizationGrantV3, retireTunnelAuthorizationGrantV3, runtimeAuthorizationRequestV3FromDecoded, validateTunnelAuthorizationGrantV3, } from "./runtimeAuthorizationV3.js";
8
+ const DEFAULT_PAIR_TIMEOUT_MS = 10_000;
9
+ const DEFAULT_ADMISSION_TIMEOUT_MS = 10_000;
10
+ const DEFAULT_CLEANUP_TIMEOUT_MS = 2_000;
11
+ const DEFAULT_MAX_CONCURRENT_ADMISSIONS = 1_024;
12
+ const DEFAULT_MAX_PENDING_LEGS = 1_024;
13
+ const DEFAULT_MAX_ACTIVE_PAIRS = 1_024;
14
+ const DEFAULT_MAX_CONCURRENT_STREAMS = 128;
15
+ const ID = /^[A-Za-z0-9._~-]{1,128}$/u;
16
+ const BUILT_IN_ADMISSION_REASONS = [
17
+ "authorization_expired",
18
+ "capacity",
19
+ "credential_replay",
20
+ "expired_artifact",
21
+ "invalid_credential",
22
+ "pair_mismatch",
23
+ "pair_timeout",
24
+ "replaced",
25
+ "replacement_denied",
26
+ ];
27
+ export class TunnelRuntimeV3 {
28
+ constructor() { }
29
+ async start() {
30
+ await startTunnelRuntimeV3(tunnelRuntimeStateV3(this));
31
+ }
32
+ addresses() {
33
+ return tunnelRuntimeStateV3(this).listeners.map((listener) => listener.address());
34
+ }
35
+ async close() {
36
+ const state = tunnelRuntimeStateV3(this);
37
+ const existing = closes.get(state);
38
+ if (existing !== undefined)
39
+ return await existing;
40
+ const closing = (async () => {
41
+ await Promise.resolve();
42
+ state.abort.abort(new Error("Flowersec tunnel runtime closed"));
43
+ for (const generation of [...state.generations.values()])
44
+ finishGeneration(state, generation);
45
+ const starting = starts.get(state);
46
+ if (starting !== undefined)
47
+ await Promise.allSettled([starting]);
48
+ await Promise.allSettled(state.listeners.map(async (listener) => await listener.close()));
49
+ await boundedCleanup(state.tasks, state.limits.cleanupTimeoutMs, () => {
50
+ for (const controller of state.releaseControllers) {
51
+ controller.abort(new Error("Flowersec tunnel lease cleanup timed out"));
52
+ }
53
+ });
54
+ })();
55
+ closes.set(state, closing);
56
+ return await closing;
57
+ }
58
+ }
59
+ const tunnelRuntimeStatesV3 = new WeakMap();
60
+ function tunnelRuntimeStateV3(runtime) {
61
+ const state = tunnelRuntimeStatesV3.get(runtime);
62
+ if (state === undefined)
63
+ throw new Error("invalid Flowersec TunnelRuntimeV3");
64
+ return state;
65
+ }
66
+ export function createTunnelRuntimeV3(options) {
67
+ const limits = validateOptions(options);
68
+ const admissionReasons = createAdmissionReasonRegistryV3(BUILT_IN_ADMISSION_REASONS, options.admissionReasons);
69
+ const runtime = new TunnelRuntimeV3();
70
+ const state = {
71
+ listeners: [],
72
+ options,
73
+ admissionReasons,
74
+ limits,
75
+ abort: new AbortController(),
76
+ generations: new Map(),
77
+ usedCredentials: new Map(),
78
+ legGenerations: new WeakMap(),
79
+ admissionSlots: new AdmissionSlots(limits.maxConcurrentAdmissions),
80
+ authorizerSlots: new Slots(limits.maxConcurrentAdmissions),
81
+ releaseControllers: new Set(),
82
+ tasks: new Set(),
83
+ counts: { pendingLegs: 0, activePairs: 0 },
84
+ };
85
+ tunnelRuntimeStatesV3.set(runtime, state);
86
+ return Object.freeze(runtime);
87
+ }
88
+ const starts = new WeakMap();
89
+ const closes = new WeakMap();
90
+ async function startTunnelRuntimeV3(state) {
91
+ if (state.abort.signal.aborted)
92
+ throw new Error("Flowersec tunnel runtime is closed");
93
+ let started = starts.get(state);
94
+ if (started === undefined) {
95
+ started = (async () => {
96
+ let rawQuicDriver;
97
+ try {
98
+ for (const listener of state.options.listeners) {
99
+ if (state.abort.signal.aborted)
100
+ throw new Error("Flowersec tunnel runtime is closed");
101
+ const running = listener.carrier === "websocket"
102
+ ? await startNodeWebSocketListenerV3({
103
+ ...listener,
104
+ path: "tunnel",
105
+ inboundBidirectionalStreamCapacity: state.options.maxInboundStreams + 2,
106
+ maxPendingSessions: state.limits.maxConcurrentAdmissions,
107
+ pendingSessionTimeoutMs: state.limits.admissionTimeoutMs,
108
+ cleanupTimeoutMs: state.limits.cleanupTimeoutMs,
109
+ })
110
+ : await startNodeRawQuicListenerV3(rawQuicDriver ??= createNativeRawQuicDriverV3(), {
111
+ ...listener,
112
+ path: "tunnel",
113
+ inboundBidirectionalStreamCapacity: state.options.maxInboundStreams + 2,
114
+ });
115
+ if (state.abort.signal.aborted) {
116
+ await running.close().catch(() => undefined);
117
+ throw new Error("Flowersec tunnel runtime is closed");
118
+ }
119
+ state.listeners.push(running);
120
+ }
121
+ }
122
+ catch (error) {
123
+ await Promise.allSettled(state.listeners.splice(0).map(async (listener) => await listener.close()));
124
+ throw error;
125
+ }
126
+ for (const listener of state.listeners)
127
+ track(state, runListener(state, listener));
128
+ })();
129
+ starts.set(state, started);
130
+ }
131
+ await started;
132
+ }
133
+ async function runListener(state, listener) {
134
+ while (!state.abort.signal.aborted) {
135
+ let carrier;
136
+ try {
137
+ carrier = await listener.accept({ signal: state.abort.signal });
138
+ }
139
+ catch {
140
+ return;
141
+ }
142
+ const releaseAdmission = state.admissionSlots.tryAcquire();
143
+ if (releaseAdmission === undefined) {
144
+ carrier.abort({ code: 6, reason: "tunnel admission capacity" });
145
+ continue;
146
+ }
147
+ track(state, processCarrier(state, carrier).finally(releaseAdmission));
148
+ }
149
+ }
150
+ async function processCarrier(state, carrier) {
151
+ const admission = new AbortController();
152
+ const unlinkRuntime = linkAbort(state.abort.signal, admission);
153
+ const deadline = setTimeout(() => {
154
+ admission.abort(new Error("Flowersec tunnel admission timed out"));
155
+ }, state.limits.admissionTimeoutMs);
156
+ try {
157
+ const received = await receiveSessionAdmissionV3(carrier, admission.signal);
158
+ const decoded = received.decoded;
159
+ const authorizationRequest = runtimeAuthorizationRequestV3FromDecoded(decoded);
160
+ if (carrier.path !== "tunnel" || decoded.request.pathKind !== "tunnel" ||
161
+ decoded.request.candidates.find((candidate) => candidate.id === decoded.request.chosen_candidate_id)?.carrier !== carrier.kind) {
162
+ await reject(received, "invalid_credential", false, state.admissionReasons, admission.signal);
163
+ return;
164
+ }
165
+ const releaseAuthorizer = await state.authorizerSlots.acquire(admission.signal);
166
+ let authorizerStarted = false;
167
+ let decision;
168
+ try {
169
+ decision = await abortableCallback(() => {
170
+ authorizerStarted = true;
171
+ try {
172
+ // Admission cancellation must not free this slot while an
173
+ // application-owned authorization Promise is still running.
174
+ return state.options.authorize(authorizationRequest, { signal: admission.signal }).finally(releaseAuthorizer);
175
+ }
176
+ catch (error) {
177
+ releaseAuthorizer();
178
+ throw error;
179
+ }
180
+ }, admission.signal, (lateDecision) => {
181
+ if (lateDecision !== undefined && lateDecision.decision === "allow") {
182
+ releaseDecision(state, lateDecision);
183
+ }
184
+ });
185
+ }
186
+ finally {
187
+ if (!authorizerStarted)
188
+ releaseAuthorizer();
189
+ }
190
+ if (decision.decision !== "allow") {
191
+ await reject(received, decision.reason, decision.decision === "retry", state.admissionReasons, admission.signal);
192
+ return;
193
+ }
194
+ const leg = validatedLeg(state, received, decoded.request, authorizationRequest, decision);
195
+ if (leg === undefined) {
196
+ const grant = inspectTunnelAuthorizationGrantV3(decision.grant);
197
+ releaseDecision(state, decision);
198
+ const expired = grant !== undefined &&
199
+ grant.expiresAtUnixSeconds <= Math.floor(Date.now() / 1_000);
200
+ await reject(received, expired ? "expired_artifact" : "invalid_credential", expired, state.admissionReasons, admission.signal);
201
+ return;
202
+ }
203
+ configureRole(leg);
204
+ registerLeg(state, leg);
205
+ }
206
+ catch {
207
+ carrier.abort({ code: 6, reason: "tunnel admission failed" });
208
+ }
209
+ finally {
210
+ clearTimeout(deadline);
211
+ unlinkRuntime();
212
+ }
213
+ }
214
+ function validatedLeg(state, received, request, authorizationRequest, authorization) {
215
+ const now = Math.floor(Date.now() / 1_000);
216
+ const candidate = validateTunnelAuthorizationGrantV3(authorization.grant, authorizationRequest);
217
+ if (candidate === undefined ||
218
+ !ID.test(candidate.credentialId) || candidate.credentialId !== authorizationRequest.lookupKey() ||
219
+ !ID.test(candidate.leaseId) || !Number.isSafeInteger(candidate.expiresAtUnixSeconds) ||
220
+ candidate.expiresAtUnixSeconds <= now || !ID.test(candidate.expectedPeerEndpointInstanceId) ||
221
+ candidate.expectedPeerEndpointInstanceId === request.endpoint_instance_id)
222
+ return undefined;
223
+ const grant = consumeTunnelAuthorizationGrantV3(authorization.grant, authorizationRequest);
224
+ if (grant === undefined ||
225
+ grant !== candidate)
226
+ return undefined;
227
+ let released = false;
228
+ return {
229
+ received,
230
+ request,
231
+ authorization: grant,
232
+ release() {
233
+ if (released)
234
+ return;
235
+ released = true;
236
+ trackRelease(state, grant.leaseId);
237
+ },
238
+ };
239
+ }
240
+ function registerLeg(state, leg) {
241
+ pruneCredentials(state);
242
+ if (state.usedCredentials.has(leg.authorization.credentialId)) {
243
+ leg.release();
244
+ track(state, reject(leg.received, "credential_replay", false, state.admissionReasons, state.abort.signal));
245
+ return;
246
+ }
247
+ state.usedCredentials.set(leg.authorization.credentialId, leg.authorization.expiresAtUnixSeconds);
248
+ const key = pairKey(leg.request);
249
+ let generation = state.generations.get(key);
250
+ if (generation !== undefined && !generation.finished &&
251
+ (generation.active || generation.roles.has(leg.request.role))) {
252
+ if (!leg.authorization.allowReplacement) {
253
+ leg.release();
254
+ track(state, reject(leg.received, "replacement_denied", false, state.admissionReasons, state.abort.signal));
255
+ return;
256
+ }
257
+ replaceGeneration(state, generation);
258
+ generation = undefined;
259
+ }
260
+ if (generation === undefined || generation.finished) {
261
+ if (state.counts.pendingLegs >= state.limits.maxPendingLegs) {
262
+ leg.release();
263
+ track(state, reject(leg.received, "capacity", true, state.admissionReasons, state.abort.signal));
264
+ return;
265
+ }
266
+ generation = {
267
+ key,
268
+ abort: new AbortController(),
269
+ roles: new Map(),
270
+ active: false,
271
+ finished: false,
272
+ };
273
+ state.generations.set(key, generation);
274
+ }
275
+ const peer = generation.roles.get(leg.request.role === 1 ? 2 : 1);
276
+ if (peer !== undefined && !mirrored(peer, leg)) {
277
+ leg.release();
278
+ track(state, reject(leg.received, "pair_mismatch", false, state.admissionReasons, state.abort.signal));
279
+ return;
280
+ }
281
+ generation.roles.set(leg.request.role, leg);
282
+ state.legGenerations.set(leg, generation);
283
+ track(state, watchTermination(state, leg));
284
+ if (generation.roles.size === 1) {
285
+ state.counts.pendingLegs += 1;
286
+ const expiresFirst = leg.authorization.expiresAtUnixSeconds * 1_000 <= Date.now() + state.limits.pairTimeoutMs;
287
+ const timeout = Math.min(state.limits.pairTimeoutMs, Math.max(1, leg.authorization.expiresAtUnixSeconds * 1_000 - Date.now()));
288
+ generation.timer = setTimeout(() => rejectGeneration(state, generation, expiresFirst ? "expired_artifact" : "pair_timeout", true), timeout);
289
+ return;
290
+ }
291
+ const now = Math.floor(Date.now() / 1_000);
292
+ if (firstAuthorizationExpired(generation, now)) {
293
+ rejectGeneration(state, generation, "expired_artifact", true);
294
+ return;
295
+ }
296
+ if (state.counts.activePairs >= state.limits.maxActivePairs) {
297
+ rejectGeneration(state, generation, "capacity", true);
298
+ return;
299
+ }
300
+ if (generation.timer !== undefined)
301
+ clearTimeout(generation.timer);
302
+ state.counts.pendingLegs -= 1;
303
+ state.counts.activePairs += 1;
304
+ generation.active = true;
305
+ track(state, activatePair(state, generation));
306
+ }
307
+ async function activatePair(state, generation) {
308
+ const first = generation.roles.get(1);
309
+ const second = generation.roles.get(2);
310
+ if (first === undefined || second === undefined) {
311
+ finishGeneration(state, generation);
312
+ return;
313
+ }
314
+ try {
315
+ await respondTunnelPairAdmissionsV3(first.received, second.received, generation.abort.signal, state.limits.admissionTimeoutMs);
316
+ await bridgePair(state, first.received.carrier, second.received.carrier, generation.abort.signal);
317
+ }
318
+ catch {
319
+ // Pair teardown below is the observable failure boundary for both endpoints.
320
+ }
321
+ finally {
322
+ finishGeneration(state, generation);
323
+ }
324
+ }
325
+ async function watchTermination(state, leg) {
326
+ try {
327
+ await leg.received.carrier.waitTermination();
328
+ }
329
+ catch {
330
+ // Carrier termination has the same generation cleanup below.
331
+ }
332
+ const generation = state.legGenerations.get(leg);
333
+ if (generation !== undefined && !generation.finished)
334
+ finishGeneration(state, generation);
335
+ }
336
+ function configureRole(leg) {
337
+ if (leg.received.carrier.kind === "websocket") {
338
+ configureServerWebSocketCarrierRoleV3(leg.received.carrier, leg.request.role === 2);
339
+ }
340
+ }
341
+ async function bridgePair(state, first, second, signal) {
342
+ const controller = new AbortController();
343
+ const cancel = () => controller.abort(signal.reason);
344
+ signal.addEventListener("abort", cancel, { once: true });
345
+ const tasks = new Set();
346
+ const slots = new Slots(state.limits.maxConcurrentStreams);
347
+ const start = (task, fatal = true) => {
348
+ tasks.add(task);
349
+ void task.catch(() => { if (fatal)
350
+ controller.abort(); }).finally(() => tasks.delete(task));
351
+ };
352
+ try {
353
+ const [controlIn, controlOut] = await activateTunnelControlStreamsV3(first, second, controller.signal, state.limits.admissionTimeoutMs);
354
+ start(spliceTunnelStreamsV3(controlIn, controlOut, controller.signal, state.limits.cleanupTimeoutMs));
355
+ start(bridgeStreams(first, second, slots, controller.signal, start));
356
+ start(bridgeStreams(second, first, slots, controller.signal, start));
357
+ if (first.unreliableDatagrams !== undefined && second.unreliableDatagrams !== undefined) {
358
+ start(bridgeDatagrams(first.unreliableDatagrams, second.unreliableDatagrams, controller.signal));
359
+ start(bridgeDatagrams(second.unreliableDatagrams, first.unreliableDatagrams, controller.signal));
360
+ }
361
+ await aborted(controller.signal);
362
+ }
363
+ finally {
364
+ signal.removeEventListener("abort", cancel);
365
+ controller.abort();
366
+ first.abort({ code: 1, reason: "tunnel bridge closed" });
367
+ second.abort({ code: 1, reason: "tunnel bridge closed" });
368
+ await boundedCleanup(tasks, state.limits.cleanupTimeoutMs);
369
+ }
370
+ }
371
+ /** @internal */
372
+ export async function activateTunnelControlStreamsV3(first, second, signal, timeoutMs) {
373
+ let partialIncoming;
374
+ try {
375
+ return await runBoundedPhase(signal, timeoutMs, "Flowersec tunnel activation timed out", async (activationSignal) => {
376
+ const incoming = await first.acceptStream({ signal: activationSignal });
377
+ partialIncoming = incoming;
378
+ if (activationSignal.aborted) {
379
+ incoming.abort(asError(activationSignal.reason));
380
+ throw asError(activationSignal.reason);
381
+ }
382
+ try {
383
+ const outgoing = await second.openStream({ signal: activationSignal });
384
+ if (activationSignal.aborted) {
385
+ incoming.abort(asError(activationSignal.reason));
386
+ outgoing.abort(asError(activationSignal.reason));
387
+ throw asError(activationSignal.reason);
388
+ }
389
+ partialIncoming = undefined;
390
+ return [incoming, outgoing];
391
+ }
392
+ catch (error) {
393
+ incoming.abort(asError(error));
394
+ partialIncoming = undefined;
395
+ throw error;
396
+ }
397
+ }, ([incoming, outgoing]) => {
398
+ incoming.abort(new Error("Flowersec tunnel activation completed after timeout"));
399
+ outgoing.abort(new Error("Flowersec tunnel activation completed after timeout"));
400
+ });
401
+ }
402
+ catch (error) {
403
+ partialIncoming?.abort(asError(error));
404
+ throw error;
405
+ }
406
+ }
407
+ async function bridgeStreams(source, target, slots, signal, start) {
408
+ while (!signal.aborted) {
409
+ const incoming = await source.acceptStream({ signal });
410
+ const release = await slots.acquire(signal);
411
+ let outgoing;
412
+ try {
413
+ outgoing = await target.openStream({ signal });
414
+ }
415
+ catch (error) {
416
+ release();
417
+ if (signal.aborted) {
418
+ incoming.abort(asError(error));
419
+ throw error;
420
+ }
421
+ await incoming.reset().catch(() => undefined);
422
+ continue;
423
+ }
424
+ start(spliceTunnelStreamsV3(incoming, outgoing, signal).finally(release), false);
425
+ }
426
+ }
427
+ export async function spliceTunnelStreamsV3(left, right, signal, halfCloseGraceMs = 0) {
428
+ const closePair = halfCloseGraceMs > 0;
429
+ const abort = () => {
430
+ left.abort(asError(signal.reason));
431
+ right.abort(asError(signal.reason));
432
+ };
433
+ signal.addEventListener("abort", abort, { once: true });
434
+ try {
435
+ let signalHalfClose;
436
+ const halfClosed = new Promise((resolve) => {
437
+ signalHalfClose = () => resolve({ outcome: "half_closed" });
438
+ });
439
+ const copies = [
440
+ settledCopy(0, copyStream(left, right, signal, closePair ? signalHalfClose : undefined)),
441
+ settledCopy(1, copyStream(right, left, signal, closePair ? signalHalfClose : undefined)),
442
+ ];
443
+ const first = await Promise.race(closePair ? [...copies, halfClosed] : copies);
444
+ if (first.outcome === "rejected")
445
+ throw first.error;
446
+ if (closePair) {
447
+ const results = await awaitWithin(Promise.all(copies), halfCloseGraceMs, "Flowersec tunnel control half-close timed out");
448
+ const failure = results.find((result) => result.outcome === "rejected");
449
+ if (failure?.outcome === "rejected")
450
+ throw failure.error;
451
+ throw new Error("Flowersec tunnel control stream closed");
452
+ }
453
+ if (first.outcome === "half_closed")
454
+ throw new Error("invalid tunnel stream half-close state");
455
+ const second = await copies[first.index === 0 ? 1 : 0];
456
+ if (second.outcome === "rejected")
457
+ throw second.error;
458
+ }
459
+ catch (error) {
460
+ if (closePair || signal.aborted) {
461
+ left.abort(asError(error));
462
+ right.abort(asError(error));
463
+ }
464
+ else {
465
+ await Promise.allSettled([left.reset(), right.reset()]);
466
+ }
467
+ throw error;
468
+ }
469
+ finally {
470
+ signal.removeEventListener("abort", abort);
471
+ }
472
+ }
473
+ async function settledCopy(index, copy) {
474
+ try {
475
+ await copy;
476
+ return { index, outcome: "fulfilled" };
477
+ }
478
+ catch (error) {
479
+ return { index, outcome: "rejected", error };
480
+ }
481
+ }
482
+ async function copyStream(source, target, signal, onEOF) {
483
+ while (true) {
484
+ const chunk = await source.read({ signal });
485
+ if (chunk === null) {
486
+ onEOF?.();
487
+ await target.closeWrite();
488
+ return;
489
+ }
490
+ let offset = 0;
491
+ while (offset < chunk.length) {
492
+ const written = await target.write(chunk.subarray(offset), { signal });
493
+ if (written < 1 || written > chunk.length - offset)
494
+ throw new Error("invalid tunnel stream write");
495
+ offset += written;
496
+ }
497
+ }
498
+ }
499
+ async function bridgeDatagrams(source, target, signal) {
500
+ while (!signal.aborted) {
501
+ const message = await source.receive({ signal });
502
+ if (message.length <= target.maxDatagramSize)
503
+ await target.send(message, { signal });
504
+ }
505
+ }
506
+ function mirrored(first, second) {
507
+ const a = first.request;
508
+ const b = second.request;
509
+ return a.role !== b.role &&
510
+ a.channel_id === b.channel_id &&
511
+ a.profile === b.profile &&
512
+ a.rendezvous_group_id === b.rendezvous_group_id &&
513
+ a.session_contract_hash_b64u === b.session_contract_hash_b64u &&
514
+ a.candidate_set_hash_b64u === b.candidate_set_hash_b64u &&
515
+ a.listener_audience === b.listener_audience &&
516
+ first.authorization.expectedPeerEndpointInstanceId === b.endpoint_instance_id &&
517
+ second.authorization.expectedPeerEndpointInstanceId === a.endpoint_instance_id;
518
+ }
519
+ function pairKey(request) {
520
+ return JSON.stringify([
521
+ request.profile,
522
+ request.channel_id,
523
+ request.rendezvous_group_id,
524
+ request.listener_audience,
525
+ request.session_contract_hash_b64u,
526
+ request.candidate_set_hash_b64u,
527
+ ]);
528
+ }
529
+ function firstAuthorizationExpired(generation, nowUnixSeconds) {
530
+ for (const leg of generation.roles.values()) {
531
+ if (leg.authorization.expiresAtUnixSeconds <= nowUnixSeconds)
532
+ return true;
533
+ }
534
+ return false;
535
+ }
536
+ function rejectGeneration(state, generation, reason, retryable) {
537
+ if (generation.finished)
538
+ return;
539
+ detachGeneration(state, generation);
540
+ track(state, (async () => {
541
+ try {
542
+ await rejectTunnelPairAdmissionsV3([...generation.roles.values()].map(({ received }) => received), reason, retryable, state.admissionReasons, state.abort.signal, state.limits.admissionTimeoutMs);
543
+ }
544
+ catch {
545
+ // Pair teardown below owns the observable rejection boundary.
546
+ }
547
+ finally {
548
+ generation.abort.abort(new Error("Flowersec tunnel pair rejected"));
549
+ for (const leg of generation.roles.values()) {
550
+ leg.received.carrier.abort({ code: 6, reason: "tunnel pair rejected" });
551
+ leg.release();
552
+ }
553
+ }
554
+ })());
555
+ }
556
+ function replaceGeneration(state, generation) {
557
+ if (generation.finished)
558
+ return;
559
+ const error = new Error("Flowersec tunnel pair replaced");
560
+ generation.abort.abort(error);
561
+ for (const leg of generation.roles.values()) {
562
+ leg.received.stream.abort(error);
563
+ leg.received.carrier.abort({ code: 6, reason: "tunnel pair replaced" });
564
+ leg.release();
565
+ }
566
+ detachGeneration(state, generation);
567
+ }
568
+ /** @internal */
569
+ export async function rejectTunnelPairAdmissionsV3(admissions, reason, retryable, admissionReasons, signal, timeoutMs) {
570
+ await runBoundedPhase(signal, timeoutMs, "Flowersec tunnel admission rejection timed out", async (responseSignal) => {
571
+ await Promise.all(admissions.map(async (received) => {
572
+ await reject(received, reason, retryable, admissionReasons, responseSignal);
573
+ }));
574
+ });
575
+ }
576
+ function finishGeneration(state, generation) {
577
+ if (generation.finished)
578
+ return;
579
+ detachGeneration(state, generation);
580
+ generation.abort.abort(new Error("Flowersec tunnel pair closed"));
581
+ for (const leg of generation.roles.values()) {
582
+ leg.received.carrier.abort({ code: 1, reason: "tunnel pair closed" });
583
+ leg.release();
584
+ }
585
+ }
586
+ function detachGeneration(state, generation) {
587
+ generation.finished = true;
588
+ if (generation.timer !== undefined)
589
+ clearTimeout(generation.timer);
590
+ if (generation.active)
591
+ state.counts.activePairs -= 1;
592
+ else if (generation.roles.size > 0)
593
+ state.counts.pendingLegs -= 1;
594
+ if (state.generations.get(generation.key) === generation)
595
+ state.generations.delete(generation.key);
596
+ }
597
+ export async function respondTunnelPairAdmissionsV3(first, second, signal, timeoutMs) {
598
+ await runBoundedPhase(signal, timeoutMs, "Flowersec tunnel admission response timed out", async (responseSignal) => {
599
+ await Promise.all([
600
+ acceptAdmission(first, responseSignal),
601
+ acceptAdmission(second, responseSignal),
602
+ ]);
603
+ });
604
+ }
605
+ async function acceptAdmission(received, signal) {
606
+ const response = encodeFSA3ResponseV3({ status: AdmissionStatusV3.Success, reason: "" });
607
+ let offset = 0;
608
+ while (offset < response.length) {
609
+ const written = await received.stream.write(response.subarray(offset), { signal });
610
+ if (written < 1 || written > response.length - offset)
611
+ throw new Error("invalid admission write");
612
+ offset += written;
613
+ }
614
+ await abortableCallback(async () => await received.stream.closeWrite(), signal);
615
+ }
616
+ async function reject(received, reason, retryable, admissionReasons, signal) {
617
+ try {
618
+ await rejectSessionAdmissionV3(received, {
619
+ accepted: false,
620
+ status: retryable ? AdmissionStatusV3.Retryable : AdmissionStatusV3.Reject,
621
+ reason,
622
+ }, admissionReasons, signal);
623
+ }
624
+ catch {
625
+ // Rejection intentionally closes the carrier and reports through FSA3.
626
+ }
627
+ }
628
+ function validateOptions(options) {
629
+ const limits = {
630
+ maxPendingLegs: options.maxPendingLegs ?? DEFAULT_MAX_PENDING_LEGS,
631
+ maxActivePairs: options.maxActivePairs ?? DEFAULT_MAX_ACTIVE_PAIRS,
632
+ maxConcurrentStreams: options.maxConcurrentStreams ?? DEFAULT_MAX_CONCURRENT_STREAMS,
633
+ maxConcurrentAdmissions: options.maxConcurrentAdmissions ?? DEFAULT_MAX_CONCURRENT_ADMISSIONS,
634
+ pairTimeoutMs: options.pairTimeoutMs ?? DEFAULT_PAIR_TIMEOUT_MS,
635
+ admissionTimeoutMs: options.admissionTimeoutMs ?? DEFAULT_ADMISSION_TIMEOUT_MS,
636
+ cleanupTimeoutMs: options.cleanupTimeoutMs ?? DEFAULT_CLEANUP_TIMEOUT_MS,
637
+ };
638
+ if (options.listeners.length === 0 || !Number.isSafeInteger(options.maxInboundStreams) ||
639
+ options.maxInboundStreams < 1 || options.maxInboundStreams > 128 || typeof options.authorize !== "function" ||
640
+ !boundedInteger(limits.maxPendingLegs, 1, 65_536) || !boundedInteger(limits.maxActivePairs, 1, 65_536) ||
641
+ !boundedInteger(limits.maxConcurrentStreams, 1, 128) ||
642
+ !boundedInteger(limits.maxConcurrentAdmissions, 1, 65_536) ||
643
+ !boundedInteger(limits.pairTimeoutMs, 1, 60_000) ||
644
+ !boundedInteger(limits.admissionTimeoutMs, 1, 60_000) ||
645
+ !boundedInteger(limits.cleanupTimeoutMs, 1, 60_000)) {
646
+ throw new TypeError("invalid Flowersec TunnelRuntimeV3 options");
647
+ }
648
+ return limits;
649
+ }
650
+ function boundedInteger(value, minimum, maximum) {
651
+ return Number.isSafeInteger(value) && value >= minimum && value <= maximum;
652
+ }
653
+ function pruneCredentials(state) {
654
+ const now = Math.floor(Date.now() / 1_000);
655
+ for (const [credential, expiry] of state.usedCredentials) {
656
+ if (expiry <= now)
657
+ state.usedCredentials.delete(credential);
658
+ }
659
+ }
660
+ function releaseDecision(state, decision) {
661
+ const grant = retireTunnelAuthorizationGrantV3(decision.grant);
662
+ if (grant === undefined || !ID.test(grant.leaseId))
663
+ return;
664
+ trackRelease(state, grant.leaseId);
665
+ }
666
+ function trackRelease(state, leaseId) {
667
+ if (state.options.release === undefined)
668
+ return;
669
+ const controller = new AbortController();
670
+ state.releaseControllers.add(controller);
671
+ const release = Promise.resolve()
672
+ .then(async () => await state.options.release(leaseId, { signal: controller.signal }))
673
+ .then(() => undefined);
674
+ const timer = setTimeout(() => {
675
+ controller.abort(new Error("Flowersec tunnel lease cleanup timed out"));
676
+ }, state.limits.cleanupTimeoutMs);
677
+ track(state, Promise.race([release, aborted(controller.signal)])
678
+ .then(() => undefined)
679
+ .finally(() => {
680
+ clearTimeout(timer);
681
+ state.releaseControllers.delete(controller);
682
+ }));
683
+ }
684
+ function track(state, task) {
685
+ state.tasks.add(task);
686
+ void task.catch(() => undefined).finally(() => state.tasks.delete(task));
687
+ }
688
+ async function boundedCleanup(tasks, timeoutMs, onTimeout) {
689
+ let timer;
690
+ const timeout = new Promise((resolve) => {
691
+ timer = setTimeout(() => {
692
+ onTimeout?.();
693
+ resolve();
694
+ }, timeoutMs);
695
+ });
696
+ await Promise.race([Promise.allSettled([...tasks]).then(() => undefined), timeout]);
697
+ if (timer !== undefined)
698
+ clearTimeout(timer);
699
+ }
700
+ async function abortableCallback(callback, signal, onLateValue) {
701
+ if (signal.aborted)
702
+ throw asError(signal.reason);
703
+ return await new Promise((resolve, reject) => {
704
+ let settled = false;
705
+ const cleanup = () => signal.removeEventListener("abort", abort);
706
+ const abort = () => {
707
+ if (settled)
708
+ return;
709
+ settled = true;
710
+ cleanup();
711
+ reject(asError(signal.reason));
712
+ };
713
+ signal.addEventListener("abort", abort, { once: true });
714
+ let promise;
715
+ try {
716
+ promise = callback();
717
+ }
718
+ catch (error) {
719
+ settled = true;
720
+ cleanup();
721
+ reject(error);
722
+ return;
723
+ }
724
+ void promise.then((value) => {
725
+ if (settled) {
726
+ onLateValue?.(value);
727
+ return;
728
+ }
729
+ settled = true;
730
+ cleanup();
731
+ resolve(value);
732
+ }, (error) => {
733
+ if (settled)
734
+ return;
735
+ settled = true;
736
+ cleanup();
737
+ reject(error);
738
+ });
739
+ });
740
+ }
741
+ async function runBoundedPhase(parent, timeoutMs, timeoutMessage, operation, onLateValue) {
742
+ const controller = new AbortController();
743
+ const unlink = linkAbort(parent, controller);
744
+ const timer = setTimeout(() => controller.abort(new Error(timeoutMessage)), timeoutMs);
745
+ try {
746
+ return await abortableCallback(async () => await operation(controller.signal), controller.signal, onLateValue);
747
+ }
748
+ finally {
749
+ clearTimeout(timer);
750
+ unlink();
751
+ }
752
+ }
753
+ async function awaitWithin(promise, timeoutMs, timeoutMessage) {
754
+ let timer;
755
+ const timeout = new Promise((_resolve, reject) => {
756
+ timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
757
+ });
758
+ try {
759
+ return await Promise.race([promise, timeout]);
760
+ }
761
+ finally {
762
+ if (timer !== undefined)
763
+ clearTimeout(timer);
764
+ }
765
+ }
766
+ function linkAbort(source, target) {
767
+ const abort = () => target.abort(source.reason);
768
+ source.addEventListener("abort", abort, { once: true });
769
+ if (source.aborted)
770
+ abort();
771
+ return () => source.removeEventListener("abort", abort);
772
+ }
773
+ function aborted(signal) {
774
+ if (signal.aborted)
775
+ return Promise.resolve();
776
+ return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
777
+ }
778
+ function asError(error) {
779
+ return error instanceof Error ? error : new Error(String(error));
780
+ }
781
+ class Slots {
782
+ maximum;
783
+ active = 0;
784
+ waiters = [];
785
+ constructor(maximum) {
786
+ this.maximum = maximum;
787
+ }
788
+ async acquire(signal) {
789
+ if (signal.aborted)
790
+ throw asError(signal.reason);
791
+ if (this.active >= this.maximum) {
792
+ await new Promise((resolve, reject) => {
793
+ let settled = false;
794
+ const ready = () => {
795
+ if (settled)
796
+ return;
797
+ settled = true;
798
+ cleanup();
799
+ // Reserve the released slot before waking the waiter. A new caller
800
+ // can otherwise claim it during the microtask turn.
801
+ this.active += 1;
802
+ resolve();
803
+ };
804
+ const abort = () => {
805
+ if (settled)
806
+ return;
807
+ settled = true;
808
+ cleanup();
809
+ reject(asError(signal.reason));
810
+ };
811
+ const cleanup = () => {
812
+ const index = this.waiters.findIndex((waiter) => waiter.grant === ready);
813
+ if (index >= 0)
814
+ this.waiters.splice(index, 1);
815
+ signal.removeEventListener("abort", abort);
816
+ };
817
+ this.waiters.push({ grant: ready });
818
+ signal.addEventListener("abort", abort, { once: true });
819
+ });
820
+ }
821
+ else {
822
+ this.active += 1;
823
+ }
824
+ let released = false;
825
+ return () => {
826
+ if (released)
827
+ return;
828
+ released = true;
829
+ this.active -= 1;
830
+ while (this.waiters.length > 0) {
831
+ const waiter = this.waiters.shift();
832
+ waiter.grant();
833
+ if (this.active <= this.maximum)
834
+ break;
835
+ }
836
+ };
837
+ }
838
+ }
839
+ class AdmissionSlots {
840
+ maximum;
841
+ active = 0;
842
+ constructor(maximum) {
843
+ this.maximum = maximum;
844
+ }
845
+ tryAcquire() {
846
+ if (this.active >= this.maximum)
847
+ return undefined;
848
+ this.active += 1;
849
+ let released = false;
850
+ return () => {
851
+ if (released)
852
+ return;
853
+ released = true;
854
+ this.active -= 1;
855
+ };
856
+ }
857
+ }