@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
@@ -0,0 +1,83 @@
1
+ import { authorizeRuntime, createAcceptor, createEndpointSet, Issuer, parseArtifact, ProxyServer, SessionHandlers, } from "../node/index.js";
2
+ const ORIGIN = "https://app.example";
3
+ const args = process.argv.slice(2);
4
+ if (args.length !== 2 || args[0] !== "--upstream" || args[1] === undefined) {
5
+ throw new Error("usage: proxyServerPeer --upstream http://127.0.0.1:<port>");
6
+ }
7
+ void run(args[1]).catch((error) => {
8
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
9
+ process.exitCode = 1;
10
+ });
11
+ async function run(upstream) {
12
+ const parsedUpstream = new URL(upstream);
13
+ if (parsedUpstream.protocol !== "http:" || parsedUpstream.hostname !== "127.0.0.1" || parsedUpstream.pathname !== "/") {
14
+ throw new Error("proxy peer upstream must be a loopback HTTP origin");
15
+ }
16
+ const proxy = new ProxyServer({
17
+ upstream,
18
+ upstreamOrigin: upstream,
19
+ allowedOrigins: [ORIGIN],
20
+ maxConcurrentStreams: 4,
21
+ maxJsonFrameBytes: 4096,
22
+ maxChunkBytes: 8,
23
+ maxBodyBytes: 8,
24
+ maxWebSocketFrameBytes: 32,
25
+ defaultHTTPRequestTimeoutMs: 1_000,
26
+ maxHTTPRequestTimeoutMs: 1_000,
27
+ extraRequestHeaders: ["cookie", "origin", "x-request-id"],
28
+ extraResponseHeaders: ["x-visible"],
29
+ blockedResponseHeaders: ["location"],
30
+ extraWebSocketHeaders: ["x-request-id"],
31
+ forbiddenCookieNames: ["secret"],
32
+ forbiddenCookieNamePrefixes: ["private_"],
33
+ onError: (error) => console.error(error instanceof Error ? error.message : String(error)),
34
+ });
35
+ const handlers = new SessionHandlers({ maxConcurrentStreams: 4 });
36
+ proxy.register(handlers);
37
+ let record;
38
+ const acceptor = await createAcceptor({
39
+ listeners: [{
40
+ carrier: "websocket",
41
+ path: "direct",
42
+ host: "127.0.0.1",
43
+ port: 0,
44
+ allowedOrigins: [ORIGIN],
45
+ }],
46
+ maxInboundStreams: 8,
47
+ authorize: async (request) => {
48
+ if (record === undefined)
49
+ throw new Error("authorization record is unavailable");
50
+ const artifact = parseArtifact(record.artifactJSON);
51
+ authorizeRuntime(request, record, "proxy-matrix-node");
52
+ return { decision: "allow", artifact };
53
+ },
54
+ resolveHandlers: () => handlers,
55
+ });
56
+ try {
57
+ const address = acceptor.addresses()[0];
58
+ if (address === undefined)
59
+ throw new Error("Node ProxyServer listener did not bind");
60
+ const issued = new Issuer().issueDirect({
61
+ session: { channelId: "browser-proxy-node", maxInboundStreams: 8 },
62
+ endpoints: createEndpointSet(`ws://127.0.0.1:${address.port}/flowersec/v2/direct`),
63
+ rendezvousGroupId: "browser-proxy-node",
64
+ listenerAudience: "browser-proxy-matrix",
65
+ upstreamAddress: `${address.host}:${address.port}`,
66
+ });
67
+ record = issued.authorizationRecord();
68
+ const acceptedPromise = acceptor.accept();
69
+ process.stdout.write(`${JSON.stringify({
70
+ runtime: "node-typescript",
71
+ artifact_json: new TextDecoder().decode(issued.artifactJSON()),
72
+ origin: ORIGIN,
73
+ })}\n`);
74
+ const accepted = await acceptedPromise;
75
+ const serving = accepted.serve().catch((error) => error);
76
+ await accepted.session.waitTermination().catch(() => undefined);
77
+ await serving;
78
+ }
79
+ finally {
80
+ await proxy.close();
81
+ await acceptor.close();
82
+ }
83
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,608 @@
1
+ import { createInterface } from "node:readline";
2
+ import { createAcceptor, createArtifactLease, createEndpointSet, createStreamMetadata, createTunnelRuntime, connect, Issuer, parseArtifact, SessionError, SessionHandlers, } from "../node/index.js";
3
+ const RUNTIME = "node-typescript";
4
+ const ORIGIN = "https://client.example";
5
+ const ECHO_RPC = 7001;
6
+ const NOTIFY_RPC = 7002;
7
+ const COMPLETE_RPC = 7003;
8
+ const DATAGRAM_READY_RPC = 7005;
9
+ const ECHO_KIND = "parity.echo";
10
+ const RESET_KIND = "parity.reset";
11
+ const encoder = new TextEncoder();
12
+ const decoder = new TextDecoder();
13
+ const TEST_CERT_DER_B64 = "MIIBjzCCAUGgAwIBAgIUW8hQEpQsUJN9a6qqF2g6hsNpSm8wBQYDK2VwMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNjA3MjAxOTAxMjFaFw0zNjA3MTcxOTAxMjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAqMAUGAytlcAMhAAihki/Jec+1EaC6E6PsSxjMYFAazrgkNiUIlbj/+A/0o4GkMIGhMB0GA1UdDgQWBBQCuKxQmMQkAAy9KkfuD+WOmrrMbTAfBgNVHSMEGDAWgBQCuKxQmMQkAAy9KkfuD+WOmrrMbTAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwEwBQYDK2VwA0EArZng3XitiH2E1pW/NTxQvEOBXJYpYE8coQmLV4yTjfI43CWHMG6lIrwk/so67oe6Z2R4iHGjUm3Tuy50Fl8hBw==";
14
+ const TEST_KEY_DER_B64 = "MC4CAQAwBQYDK2VwBCIEICxYUWHqGoh0CBBohsaNg/NThm1n3UeWCzYuq6jS+Qi6";
15
+ class SignalQueue {
16
+ #values = [];
17
+ #waiters = [];
18
+ push() {
19
+ const waiter = this.#waiters.shift();
20
+ if (waiter === undefined)
21
+ this.#values.push(undefined);
22
+ else
23
+ waiter();
24
+ }
25
+ async shift() {
26
+ if (this.#values.length > 0) {
27
+ this.#values.shift();
28
+ return;
29
+ }
30
+ await new Promise((resolve) => this.#waiters.push(resolve));
31
+ }
32
+ }
33
+ class PeerInput {
34
+ #lines = createInterface({
35
+ input: process.stdin,
36
+ crlfDelay: Infinity,
37
+ })[Symbol.asyncIterator]();
38
+ async next() {
39
+ const item = await this.#lines.next();
40
+ if (item.done || item.value.trim() === "")
41
+ throw new Error("peer stdin ended before the next protocol message");
42
+ return JSON.parse(item.value);
43
+ }
44
+ }
45
+ class ExecutedCases {
46
+ #cases = new Set();
47
+ record(...caseIds) {
48
+ for (const caseId of caseIds)
49
+ this.#cases.add(caseId);
50
+ }
51
+ snapshot() {
52
+ return [...this.#cases];
53
+ }
54
+ }
55
+ function createHandlers(path) {
56
+ const handlers = new SessionHandlers({ maxConcurrentStreams: 16 });
57
+ const notifications = new SignalQueue();
58
+ const activeStreams = { value: 0 };
59
+ const executed = new ExecutedCases();
60
+ handlers.handleRPC(ECHO_RPC, async (payload) => {
61
+ if (!validValuePayload(payload, "ping"))
62
+ return { error: { code: 400, message: "invalid echo payload" } };
63
+ executed.record("rpc");
64
+ return { payload };
65
+ });
66
+ handlers.handleRPC(COMPLETE_RPC, async (payload) => {
67
+ if (!validValuePayload(payload, "complete"))
68
+ return { error: { code: 400, message: "invalid completion payload" } };
69
+ executed.record("rekey", "liveness");
70
+ notifications.push();
71
+ return { payload };
72
+ });
73
+ handlers.handleRPC(DATAGRAM_READY_RPC, async (payload) => {
74
+ if (!validValuePayload(payload, "datagram-ready"))
75
+ return {
76
+ error: { code: 400, message: "invalid datagram barrier payload" },
77
+ };
78
+ notifications.push();
79
+ return { payload };
80
+ });
81
+ handlers.handleNotification(NOTIFY_RPC, (payload) => {
82
+ if (!validValuePayload(payload, "notify"))
83
+ throw new Error("invalid notification payload");
84
+ executed.record("notification");
85
+ notifications.push();
86
+ });
87
+ handlers.handleStream(ECHO_KIND, async (incoming) => {
88
+ activeStreams.value++;
89
+ try {
90
+ if (incoming.metadata.values.cell !== path)
91
+ throw new Error("invalid stream metadata");
92
+ executed.record("stream-metadata");
93
+ if (decoder.decode(await readAll(incoming.stream)) !== "hello")
94
+ throw new Error("invalid stream payload");
95
+ await writeAll(incoming.stream, encoder.encode("world"));
96
+ executed.record("stream-fin");
97
+ }
98
+ finally {
99
+ activeStreams.value--;
100
+ }
101
+ });
102
+ handlers.handleStream(RESET_KIND, async (incoming) => {
103
+ if (decoder.decode(await readAll(incoming.stream)) !== "reset")
104
+ throw new Error("invalid reset stream payload");
105
+ executed.record("stream-reset");
106
+ throw new Error("intentional parity reset");
107
+ });
108
+ return { handlers, notifications, activeStreams, executed };
109
+ }
110
+ function validValuePayload(payload, expected) {
111
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload))
112
+ return false;
113
+ return payload.value === expected;
114
+ }
115
+ async function writeAll(stream, bytes) {
116
+ let offset = 0;
117
+ while (offset < bytes.length) {
118
+ const written = await stream.write(bytes.subarray(offset));
119
+ if (written < 1 || written > bytes.length - offset)
120
+ throw new Error("invalid stream write count");
121
+ offset += written;
122
+ }
123
+ }
124
+ async function readAll(stream) {
125
+ const chunks = [];
126
+ let length = 0;
127
+ while (true) {
128
+ const chunk = await stream.read();
129
+ if (chunk === null)
130
+ break;
131
+ chunks.push(chunk);
132
+ length += chunk.length;
133
+ }
134
+ const output = new Uint8Array(length);
135
+ let offset = 0;
136
+ for (const chunk of chunks) {
137
+ output.set(chunk, offset);
138
+ offset += chunk.length;
139
+ }
140
+ return output;
141
+ }
142
+ async function callBarrier(session, typeId, value, label = "peer") {
143
+ const result = await session.rpc.call(typeId, { value }, (payload) => payload);
144
+ if (!result.ok || !validValuePayload(result.payload, value))
145
+ throw new Error(`${label} RPC barrier ${typeId} failed: ${JSON.stringify(result)}`);
146
+ }
147
+ async function assertCancellableWait(session) {
148
+ const cancellation = new AbortController();
149
+ cancellation.abort();
150
+ try {
151
+ await session.waitTermination({ signal: cancellation.signal });
152
+ }
153
+ catch (error) {
154
+ if (error instanceof SessionError && error.code === "canceled")
155
+ return;
156
+ throw error;
157
+ }
158
+ throw new Error("termination wait ignored cancellation");
159
+ }
160
+ async function clientStreams(session, path, executed) {
161
+ const stream = await session.openStream(ECHO_KIND, {
162
+ metadata: createStreamMetadata({ cell: path }),
163
+ });
164
+ await writeAll(stream, encoder.encode("hello"));
165
+ await stream.closeWrite();
166
+ if (decoder.decode(await readAll(stream)) !== "world")
167
+ throw new Error("echo stream did not preserve metadata and FIN");
168
+ executed.record("stream-metadata", "stream-fin");
169
+ // Let the peer finish the echo handler before opening the reset stream. This
170
+ // barrier makes the handler lifecycle ordering explicit across runtimes.
171
+ await callBarrier(session, ECHO_RPC, "ping", "echo stream cleanup");
172
+ const reset = await session.openStream(RESET_KIND);
173
+ let resetObserved = false;
174
+ try {
175
+ await writeAll(reset, encoder.encode("reset"));
176
+ await reset.closeWrite();
177
+ await readAll(reset);
178
+ resetObserved = reset.terminalError !== undefined;
179
+ }
180
+ catch (error) {
181
+ resetObserved =
182
+ error instanceof SessionError && error.code === "stream_reset";
183
+ }
184
+ finally {
185
+ await reset.close().catch(() => undefined);
186
+ }
187
+ if (!resetObserved)
188
+ throw new Error("reset stream did not fail");
189
+ executed.record("stream-reset");
190
+ }
191
+ async function serverStreams(session, path, executed) {
192
+ const incoming = await session.acceptStream();
193
+ if (incoming.kind !== ECHO_KIND || incoming.metadata.values.cell !== path)
194
+ throw new Error("invalid echo stream metadata");
195
+ executed.record("stream-metadata");
196
+ if (decoder.decode(await readAll(incoming.stream)) !== "hello")
197
+ throw new Error("invalid echo stream payload");
198
+ await writeAll(incoming.stream, encoder.encode("world"));
199
+ await incoming.stream.closeWrite();
200
+ executed.record("stream-fin");
201
+ const reset = await session.acceptStream();
202
+ if (reset.kind !== RESET_KIND ||
203
+ decoder.decode(await readAll(reset.stream)) !== "reset")
204
+ throw new Error("invalid reset stream payload");
205
+ await reset.stream.reset();
206
+ await reset.stream.close().catch(() => undefined);
207
+ executed.record("stream-reset");
208
+ }
209
+ async function exerciseClient(session, state, path, carrier) {
210
+ await callBarrier(session, ECHO_RPC, "ping", "client");
211
+ state.executed.record("rpc");
212
+ await session.rpc.notify(NOTIFY_RPC, { value: "notify" });
213
+ await state.notifications.shift();
214
+ state.executed.record("notification");
215
+ await clientStreams(session, path, state.executed);
216
+ await assertCancellableWait(session);
217
+ state.executed.record("cancel");
218
+ await callBarrier(session, ECHO_RPC, "ping", "client cleanup");
219
+ await callBarrier(session, DATAGRAM_READY_RPC, "datagram-ready", "client datagram");
220
+ await exchangeDatagram(session, carrier, true);
221
+ if (carrier === "raw-quic")
222
+ state.executed.record("datagram");
223
+ await session.rekey();
224
+ state.executed.record("rekey");
225
+ await session.probeLiveness();
226
+ state.executed.record("liveness");
227
+ await callBarrier(session, COMPLETE_RPC, "complete", "client completion");
228
+ await session.rpc.notify(NOTIFY_RPC, { value: "notify" });
229
+ }
230
+ async function exerciseServer(session, state, path, streamsServed, carrier) {
231
+ await assertCancellableWait(session);
232
+ state.executed.record("cancel");
233
+ const streams = streamsServed
234
+ ? Promise.resolve()
235
+ : serverStreams(session, path, state.executed);
236
+ await state.notifications.shift();
237
+ await callBarrier(session, ECHO_RPC, "ping", "server");
238
+ state.executed.record("rpc");
239
+ await session.rpc.notify(NOTIFY_RPC, { value: "notify" });
240
+ await state.notifications.shift();
241
+ await exchangeDatagram(session, carrier, false);
242
+ if (carrier === "raw-quic")
243
+ state.executed.record("datagram");
244
+ await streams;
245
+ await session.waitTermination();
246
+ state.executed.record("close");
247
+ }
248
+ async function exchangeDatagram(session, carrier, initiator) {
249
+ if (carrier === "websocket")
250
+ return;
251
+ const channel = session.unreliableMessages;
252
+ if (channel === undefined)
253
+ throw new Error("raw QUIC session omitted unreliable messages");
254
+ const request = Uint8Array.of(1, 2, 3);
255
+ const response = Uint8Array.of(3, 2, 1);
256
+ if (initiator) {
257
+ const outcome = await channel.send(request, { expiresAtUnixMs: Date.now() + 2_000 });
258
+ if (outcome !== "accepted")
259
+ throw new Error(`raw QUIC datagram was ${outcome}`);
260
+ }
261
+ const received = await channel.receive();
262
+ const expected = initiator ? response : request;
263
+ if (received.length !== expected.length || !expected.every((value, index) => received[index] === value)) {
264
+ throw new Error("raw QUIC datagram payload mismatch");
265
+ }
266
+ if (!initiator) {
267
+ const outcome = await channel.send(response, { expiresAtUnixMs: Date.now() + 2_000 });
268
+ if (outcome !== "accepted")
269
+ throw new Error(`raw QUIC datagram was ${outcome}`);
270
+ }
271
+ }
272
+ function fixture() {
273
+ const certificate = pem("CERTIFICATE", TEST_CERT_DER_B64);
274
+ return {
275
+ tls: {
276
+ certificate_chain_pem: certificate,
277
+ private_key_pem: pem("PRIVATE KEY", TEST_KEY_DER_B64),
278
+ root_certificate_pem: certificate,
279
+ root_certificate_der_base64: TEST_CERT_DER_B64,
280
+ leaf_certificate_der_base64: TEST_CERT_DER_B64,
281
+ },
282
+ };
283
+ }
284
+ function pem(label, encoded) {
285
+ const lines = encoded.match(/.{1,64}/g);
286
+ if (lines === null)
287
+ throw new Error("invalid embedded TLS fixture");
288
+ return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
289
+ }
290
+ async function connectArtifact(artifactJSON, relay, handlers) {
291
+ return await connect(createArtifactLease(parseArtifact(artifactJSON), async () => undefined), {
292
+ origin: relay.origin,
293
+ tls: { ca: relay.trust_pem },
294
+ handlers,
295
+ });
296
+ }
297
+ async function runServer(tls, carrier) {
298
+ const issuedByLookup = new Map();
299
+ const state = createHandlers("direct");
300
+ const acceptor = await createAcceptor({
301
+ listeners: [directListenerOptions(carrier, tls)],
302
+ maxInboundStreams: 16,
303
+ authorize: async (request) => {
304
+ const artifact = issuedByLookup.get(request.lookupKey());
305
+ return artifact === undefined
306
+ ? { decision: "reject", reason: "invalid_credential" }
307
+ : { decision: "allow", artifact };
308
+ },
309
+ resolveHandlers: () => state.handlers,
310
+ });
311
+ try {
312
+ const address = acceptor.addresses()[0];
313
+ if (address === undefined)
314
+ throw new Error(`direct ${carrier} listener did not bind`);
315
+ const issued = new Issuer().issueDirect({
316
+ session: { channelId: "node-direct-parity", maxInboundStreams: 16 },
317
+ endpoints: createEndpointSet(endpointURL(carrier, "direct", address.port)),
318
+ rendezvousGroupId: "node-direct-parity",
319
+ listenerAudience: "server-parity",
320
+ upstreamAddress: "127.0.0.1:1",
321
+ });
322
+ const artifactJSON = decoder.decode(issued.artifactJSON());
323
+ issuedByLookup.set(issued.lookupKey(), parseArtifact(artifactJSON));
324
+ const accepting = acceptor.accept();
325
+ writeJSON({
326
+ type: "ready",
327
+ runtime: RUNTIME,
328
+ carrier,
329
+ path: "direct",
330
+ artifact_json: artifactJSON,
331
+ trust_pem: tls.tls.root_certificate_pem,
332
+ origin: ORIGIN,
333
+ });
334
+ const accepted = await accepting;
335
+ state.executed.record("admission");
336
+ const serving = accepted.serve().catch((error) => error);
337
+ await exerciseServer(accepted.session, state, "direct", true, carrier);
338
+ await serving;
339
+ await accepted.close().catch(() => undefined);
340
+ if (state.activeStreams.value !== 0)
341
+ throw new Error(`direct server cleanup left ${state.activeStreams.value} streams`);
342
+ state.executed.record("cleanup");
343
+ writeJSON(result("server-result", "direct", state.executed.snapshot(), carrier));
344
+ }
345
+ finally {
346
+ await acceptor.close();
347
+ }
348
+ }
349
+ async function runClient(input, carrier) {
350
+ const ready = await input.next();
351
+ if (ready.type !== "ready" ||
352
+ ready.carrier !== carrier ||
353
+ ready.path !== "direct" ||
354
+ ready.artifact_json === "")
355
+ throw new Error("invalid direct ready message");
356
+ const state = createHandlers("direct");
357
+ const session = await connectArtifact(ready.artifact_json, ready, state.handlers);
358
+ state.executed.record("admission");
359
+ await exerciseClient(session, state, "direct", carrier);
360
+ await session.close().catch((error) => {
361
+ if (!(error instanceof SessionError) || error.code !== "closed")
362
+ throw error;
363
+ });
364
+ state.executed.record("close");
365
+ if (state.activeStreams.value !== 0)
366
+ throw new Error(`direct client cleanup left ${state.activeStreams.value} streams`);
367
+ state.executed.record("cleanup");
368
+ writeJSON(result("client-result", "direct", state.executed.snapshot(), carrier));
369
+ }
370
+ async function runRelay(tls, input, carrier) {
371
+ const decisions = new Map();
372
+ const released = new Set();
373
+ const executed = new ExecutedCases();
374
+ const runtime = createTunnelRuntime({
375
+ listeners: [tunnelListenerOptions(carrier, tls)],
376
+ maxInboundStreams: 16,
377
+ maxPendingLegs: 16,
378
+ maxActivePairs: 8,
379
+ authorize: async (request) => decisions.get(request.lookupKey()) ?? {
380
+ decision: "reject",
381
+ reason: "invalid_credential",
382
+ },
383
+ release: (leaseId) => {
384
+ released.add(leaseId);
385
+ },
386
+ });
387
+ try {
388
+ await runtime.start();
389
+ const address = runtime.addresses()[0];
390
+ if (address === undefined)
391
+ throw new Error(`tunnel ${carrier} listener did not bind`);
392
+ writeJSON({
393
+ type: "relay-ready",
394
+ runtime: RUNTIME,
395
+ carrier,
396
+ path: "tunnel",
397
+ endpoint_url: endpointURL(carrier, "tunnel", address.port),
398
+ trust_pem: tls.tls.root_certificate_pem,
399
+ trust_roots_der: [tls.tls.root_certificate_der_base64],
400
+ server_certificate_der: tls.tls.leaf_certificate_der_base64,
401
+ origin: ORIGIN,
402
+ });
403
+ const configure = await input.next();
404
+ if (configure.type !== "configure" ||
405
+ !Array.isArray(configure.authorizations) ||
406
+ configure.authorizations.length === 0)
407
+ throw new Error("invalid tunnel relay configuration");
408
+ for (const item of configure.authorizations) {
409
+ if (item.decision !== "allow" ||
410
+ item.credentialId === "" ||
411
+ item.leaseId === "" ||
412
+ item.expectedPeerEndpointInstanceId === "" ||
413
+ item.expiresAtUnixSeconds <= Math.floor(Date.now() / 1000)) {
414
+ throw new Error("invalid secret-free tunnel authorization");
415
+ }
416
+ decisions.set(item.credentialId, {
417
+ decision: "allow",
418
+ credentialId: item.credentialId,
419
+ leaseId: item.leaseId,
420
+ expiresAtUnixSeconds: item.expiresAtUnixSeconds,
421
+ expectedPeerEndpointInstanceId: item.expectedPeerEndpointInstanceId,
422
+ allowReplacement: item.allowReplacement,
423
+ });
424
+ }
425
+ const close = await input.next();
426
+ if (close.type !== "close")
427
+ throw new Error("invalid tunnel relay close command");
428
+ executed.record("close");
429
+ }
430
+ finally {
431
+ await runtime.close();
432
+ executed.record("cancel");
433
+ }
434
+ if (released.size !== decisions.size)
435
+ throw new Error(`relay cleanup released ${released.size} of ${decisions.size} leases`);
436
+ executed.record("admission", "pairing", "opaque-forwarding");
437
+ if (carrier === "raw-quic")
438
+ executed.record("datagram-forwarding");
439
+ executed.record("cleanup");
440
+ writeJSON({
441
+ type: "relay-result",
442
+ runtime: RUNTIME,
443
+ carrier,
444
+ path: "tunnel",
445
+ cases: executed.snapshot(),
446
+ observed_plaintext: false,
447
+ released_leases: released.size,
448
+ });
449
+ }
450
+ async function runTunnelEndpointB(input, carrier) {
451
+ const envelope = await input.next();
452
+ const { topology, relay } = envelope;
453
+ validateTunnelDimensions(topology, relay, "endpoint_b", carrier);
454
+ const pair = new Issuer().issueTunnelPair({
455
+ session: { channelId: `parity-${topology.id}`, maxInboundStreams: 16 },
456
+ endpoints: createEndpointSet(relay.endpoint_url),
457
+ rendezvousGroupId: topology.id,
458
+ listenerAudience: "server-parity",
459
+ firstEndpointId: `${topology.id}-a`,
460
+ secondEndpointId: `${topology.id}-b`,
461
+ });
462
+ const firstJSON = decoder.decode(pair.first.artifactJSON());
463
+ const secondJSON = decoder.decode(pair.second.artifactJSON());
464
+ const ready = {
465
+ type: "endpoint-b-ready",
466
+ runtime: RUNTIME,
467
+ carrier,
468
+ path: "tunnel",
469
+ endpoint_a_artifact_json: firstJSON,
470
+ endpoint_b_artifact_json: secondJSON,
471
+ relay,
472
+ authorizations: [
473
+ authorizationWire(pair.first, firstJSON, "lease-endpoint-a"),
474
+ authorizationWire(pair.second, secondJSON, "lease-endpoint-b"),
475
+ ],
476
+ };
477
+ writeJSON(ready);
478
+ const command = await input.next();
479
+ if (command.type !== "connect")
480
+ throw new Error("endpoint B did not receive connect command");
481
+ const state = createHandlers("tunnel");
482
+ const session = await connectArtifact(secondJSON, relay, state.handlers);
483
+ state.executed.record("admission");
484
+ await exerciseServer(session, state, "tunnel", false, carrier);
485
+ await session.close().catch(() => undefined);
486
+ state.executed.record("close");
487
+ if (state.activeStreams.value !== 0)
488
+ throw new Error(`endpoint B cleanup left ${state.activeStreams.value} streams`);
489
+ state.executed.record("cleanup");
490
+ writeJSON(result("endpoint-b-result", "tunnel", state.executed.snapshot(), carrier));
491
+ }
492
+ async function runTunnelEndpointA(input, carrier) {
493
+ const envelope = await input.next();
494
+ validateTunnelDimensions(envelope.topology, envelope.endpoint_b.relay, "endpoint_a", carrier);
495
+ const ready = envelope.endpoint_b;
496
+ if (ready.type !== "endpoint-b-ready" ||
497
+ ready.carrier !== carrier ||
498
+ ready.path !== "tunnel" ||
499
+ ready.endpoint_a_artifact_json === "")
500
+ throw new Error("invalid endpoint B ready message");
501
+ const state = createHandlers("tunnel");
502
+ const session = await connectArtifact(ready.endpoint_a_artifact_json, ready.relay, state.handlers);
503
+ state.executed.record("admission");
504
+ await exerciseClient(session, state, "tunnel", carrier);
505
+ await session.close().catch(() => undefined);
506
+ state.executed.record("close");
507
+ if (state.activeStreams.value !== 0)
508
+ throw new Error(`endpoint A cleanup left ${state.activeStreams.value} streams`);
509
+ state.executed.record("cleanup");
510
+ writeJSON(result("endpoint-a-result", "tunnel", state.executed.snapshot(), carrier));
511
+ }
512
+ function validateTunnelDimensions(topology, relay, endpoint, carrier) {
513
+ if (topology[endpoint] !== RUNTIME ||
514
+ topology.tunnel_runtime !== relay.runtime ||
515
+ topology.ingress_carrier_a !== carrier ||
516
+ topology.ingress_carrier_b !== carrier ||
517
+ relay.type !== "relay-ready" ||
518
+ relay.carrier !== carrier ||
519
+ relay.path !== "tunnel") {
520
+ throw new Error("invalid tunnel topology dimensions");
521
+ }
522
+ }
523
+ function authorizationWire(issued, artifactJSON, leaseId) {
524
+ const artifact = JSON.parse(artifactJSON);
525
+ if (!Number.isSafeInteger(artifact.session.init_expire_at_unix_s) ||
526
+ artifact.path.expected_peer_endpoint_instance_id === "")
527
+ throw new Error("issued tunnel artifact omitted relay claims");
528
+ return {
529
+ decision: "allow",
530
+ credentialId: issued.lookupKey(),
531
+ leaseId,
532
+ expiresAtUnixSeconds: artifact.session.init_expire_at_unix_s,
533
+ expectedPeerEndpointInstanceId: artifact.path.expected_peer_endpoint_instance_id,
534
+ allowReplacement: false,
535
+ };
536
+ }
537
+ function result(type, path, cases, carrier) {
538
+ return {
539
+ type,
540
+ runtime: RUNTIME,
541
+ carrier,
542
+ path,
543
+ cases,
544
+ };
545
+ }
546
+ function writeJSON(value) {
547
+ process.stdout.write(`${JSON.stringify(value)}\n`);
548
+ }
549
+ function parseArguments(arguments_) {
550
+ const roles = new Set([
551
+ "server",
552
+ "client",
553
+ "relay",
554
+ "tunnel-endpoint-a",
555
+ "tunnel-endpoint-b",
556
+ ]);
557
+ const role = arguments_[0];
558
+ if (process.env.FLOWERSEC_SERVER_PARITY_PEER !== "1")
559
+ throw new Error("server parity peer is test-only");
560
+ if (arguments_.length !== 3 ||
561
+ !roles.has(role) ||
562
+ arguments_[1] !== "--carrier")
563
+ throw new Error("invalid server parity peer arguments");
564
+ if (arguments_[2] !== "websocket" && arguments_[2] !== "raw-quic")
565
+ throw new Error(`Node.js production carrier is unsupported by this peer: ${arguments_[2] ?? "missing"}`);
566
+ return { role, carrier: arguments_[2] };
567
+ }
568
+ function endpointURL(carrier, path, port) {
569
+ return carrier === "websocket"
570
+ ? `wss://localhost:${port}/flowersec/v2/${path}`
571
+ : `quic://127.0.0.1:${port}`;
572
+ }
573
+ function directListenerOptions(carrier, tls) {
574
+ const material = { certificate: tls.tls.certificate_chain_pem, privateKey: tls.tls.private_key_pem };
575
+ return carrier === "websocket"
576
+ ? { carrier, path: "direct", host: "127.0.0.1", port: 0, tls: material, allowedOrigins: [ORIGIN] }
577
+ : { carrier: "raw_quic", path: "direct", host: "127.0.0.1", port: 0, tls: material };
578
+ }
579
+ function tunnelListenerOptions(carrier, tls) {
580
+ const material = { certificate: tls.tls.certificate_chain_pem, privateKey: tls.tls.private_key_pem };
581
+ return carrier === "websocket"
582
+ ? { carrier, host: "127.0.0.1", port: 0, tls: material, allowedOrigins: [ORIGIN] }
583
+ : { carrier: "raw_quic", host: "127.0.0.1", port: 0, tls: material };
584
+ }
585
+ async function main() {
586
+ const { role, carrier } = parseArguments(process.argv.slice(2));
587
+ switch (role) {
588
+ case "server":
589
+ await runServer(fixture(), carrier);
590
+ break;
591
+ case "client":
592
+ await runClient(new PeerInput(), carrier);
593
+ break;
594
+ case "relay":
595
+ await runRelay(fixture(), new PeerInput(), carrier);
596
+ break;
597
+ case "tunnel-endpoint-a":
598
+ await runTunnelEndpointA(new PeerInput(), carrier);
599
+ break;
600
+ case "tunnel-endpoint-b":
601
+ await runTunnelEndpointB(new PeerInput(), carrier);
602
+ break;
603
+ }
604
+ }
605
+ await main().catch((error) => {
606
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
607
+ process.exitCode = 1;
608
+ });