@floegence/flowersec-core 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -3
- package/dist/framing/jsonframe.js +9 -2
- package/dist/interop/serverParityPeer.js +43 -38
- package/dist/node/acceptor.d.ts +10 -1
- package/dist/node/acceptor.js +184 -111
- package/dist/node/connectSession.d.ts +5 -4
- package/dist/node/connectSession.js +42 -21
- package/dist/node/controlplane.js +6 -2
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/public/artifactLease.js +2 -9
- package/dist/rpc/server.js +11 -2
- package/dist/rpc/validate.d.ts +2 -1
- package/dist/rpc/validate.js +25 -7
- package/package.json +3 -3
- package/sbom/cyclonedx.json +6 -6
- package/sbom/spdx.json +11 -11
package/README.md
CHANGED
|
@@ -33,6 +33,65 @@ Retry ownership belongs to `ConnectionController`; applications do not classify
|
|
|
33
33
|
|
|
34
34
|
When connector options omit a connection timeout, browser and Node.js connectors use the shared ten-second default.
|
|
35
35
|
|
|
36
|
+
### One-shot Node client
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { RPCHandlers, connect } from "@floegence/flowersec-core/node";
|
|
40
|
+
|
|
41
|
+
const rpcHandlers = new RPCHandlers();
|
|
42
|
+
rpcHandlers.handleRPC(7, async (payload) => ({ payload }));
|
|
43
|
+
rpcHandlers.handleNotification(8, (payload) => onNotice(payload));
|
|
44
|
+
const session = await connect(lease, {
|
|
45
|
+
origin: "https://app.example",
|
|
46
|
+
rpcHandlers,
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Long-lived Node client
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { RPCHandlers, createConnectionController } from "@floegence/flowersec-core/node";
|
|
54
|
+
|
|
55
|
+
const rpcHandlers = new RPCHandlers();
|
|
56
|
+
rpcHandlers.handleRPC(7, async (payload) => ({ payload }));
|
|
57
|
+
const controller = createConnectionController(source, {
|
|
58
|
+
origin: "https://app.example",
|
|
59
|
+
rpcHandlers,
|
|
60
|
+
});
|
|
61
|
+
controller.start();
|
|
62
|
+
const session = await controller.waitForSession();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The immutable callback definition applies to every generation, while each
|
|
66
|
+
Session gets a fresh router. Terminated Session work is never replayed.
|
|
67
|
+
|
|
68
|
+
For the complete durable `ArtifactLease` spend workflow, see the
|
|
69
|
+
[TypeScript cookbook](../examples/ts/README.md). Node raw-QUIC-only artifacts
|
|
70
|
+
may omit `origin`; providing an absolute HTTP(S) origin enables WebSocket
|
|
71
|
+
candidates. Secure raw QUIC still requires an explicit `tls.ca` trust root.
|
|
72
|
+
|
|
73
|
+
### Accepted Node server Session
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { SessionHandlers, createAcceptor } from "@floegence/flowersec-core/node";
|
|
77
|
+
|
|
78
|
+
const handlers = new SessionHandlers({ maxConcurrentStreams: 32 });
|
|
79
|
+
handlers.handleRPC(7, async (payload) => ({ payload }));
|
|
80
|
+
handlers.handleNotification(8, (payload) => onNotice(payload));
|
|
81
|
+
handlers.handleStream("files/read", async (incoming) => serveFile(incoming));
|
|
82
|
+
const acceptor = await createAcceptor({
|
|
83
|
+
listeners,
|
|
84
|
+
maxInboundStreams: 32,
|
|
85
|
+
authorize,
|
|
86
|
+
resolveHandlers: () => handlers,
|
|
87
|
+
});
|
|
88
|
+
const accepted = await acceptor.accept();
|
|
89
|
+
await accepted.serve();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`RPCHandlers` is available only from the Node entrypoint and cannot register
|
|
93
|
+
application streams. `SessionHandlers` is accepted-server-only.
|
|
94
|
+
|
|
36
95
|
## Connection Lifecycle
|
|
37
96
|
|
|
38
97
|
The Browser and Node `connect(...)` operations are one-shot and never reconnect. Long-lived applications can create the runtime-specific `ConnectionController` with a refreshable `ArtifactSource`. Every attempt must return a fresh `ArtifactLease`; a one-time artifact or lease is not a controller source.
|
|
@@ -91,9 +150,10 @@ Cold-connection diagnostics require every independent carrier to meet the declar
|
|
|
91
150
|
|
|
92
151
|
Node.js applications receive the same `Session` contract from `connect(...)`.
|
|
93
152
|
The Node connector supports WSS, restricted plaintext loopback WebSocket
|
|
94
|
-
direct connections, and raw QUIC through the optional native package.
|
|
95
|
-
|
|
96
|
-
supplied through `tls.ca`, and
|
|
153
|
+
direct connections, and raw QUIC through the optional native package. WebSocket
|
|
154
|
+
candidates require an absolute HTTP(S) `origin`; raw-QUIC-only artifacts may
|
|
155
|
+
omit it. Custom certificate authorities can be supplied through `tls.ca`, and
|
|
156
|
+
secure raw QUIC requires an explicit trust root.
|
|
97
157
|
|
|
98
158
|
The connectors choose an eligible connection path from the invitation. They do
|
|
99
159
|
not expose transport selectors, candidate lists, or native carrier objects to application code.
|
|
@@ -2,7 +2,7 @@ import { readU32be, u32be } from "../utils/bin.js";
|
|
|
2
2
|
import { SDK_DEFAULTS } from "../defaults.js";
|
|
3
3
|
export const DEFAULT_MAX_JSON_FRAME_BYTES = SDK_DEFAULTS.rpc.maxJsonFrameBytes;
|
|
4
4
|
const te = new TextEncoder();
|
|
5
|
-
const td = new TextDecoder();
|
|
5
|
+
const td = new TextDecoder("utf-8", { fatal: true });
|
|
6
6
|
// JsonFramingError marks malformed or oversized frames.
|
|
7
7
|
export class JsonFramingError extends Error {
|
|
8
8
|
}
|
|
@@ -29,5 +29,12 @@ export async function readJsonFrame(readExactly, maxBytes) {
|
|
|
29
29
|
if (maxBytes > 0 && n > maxBytes)
|
|
30
30
|
throw new JsonFramingError("frame too large");
|
|
31
31
|
const payload = await read(n);
|
|
32
|
-
|
|
32
|
+
let text;
|
|
33
|
+
try {
|
|
34
|
+
text = td.decode(payload);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new JsonFramingError("invalid UTF-8");
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(text);
|
|
33
40
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { createPrivateKey, createPublicKey, X509Certificate } from "node:crypto";
|
|
3
|
-
import { createAcceptor, createArtifactLease, createEndpointSet, createStreamMetadata, createTunnelRuntime, connect, Issuer, parseArtifact, SessionError, SessionHandlers, } from "../node/index.js";
|
|
3
|
+
import { createAcceptor, createArtifactLease, createEndpointSet, createStreamMetadata, createTunnelRuntime, connect, Issuer, parseArtifact, RPCHandlers, SessionError, SessionHandlers, } from "../node/index.js";
|
|
4
4
|
const RUNTIME = "node-typescript";
|
|
5
5
|
const ORIGIN = process.env.FLOWERSEC_PARITY_ORIGIN ?? "https://client.example";
|
|
6
6
|
const ECHO_RPC = 7001;
|
|
@@ -56,38 +56,43 @@ class ExecutedCases {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
function createHandlers(path) {
|
|
59
|
-
const
|
|
59
|
+
const rpcHandlers = new RPCHandlers();
|
|
60
|
+
const sessionHandlers = new SessionHandlers({ maxConcurrentStreams: 16 });
|
|
60
61
|
const notifications = new SignalQueue();
|
|
61
62
|
const activeStreams = { value: 0 };
|
|
62
63
|
const executed = new ExecutedCases();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
64
|
+
const registerRPC = (handlers) => {
|
|
65
|
+
handlers.handleRPC(ECHO_RPC, async (payload) => {
|
|
66
|
+
if (!validValuePayload(payload, "ping"))
|
|
67
|
+
return { error: { code: 400, message: "invalid echo payload" } };
|
|
68
|
+
executed.record("rpc");
|
|
69
|
+
return { payload };
|
|
70
|
+
});
|
|
71
|
+
handlers.handleRPC(COMPLETE_RPC, async (payload) => {
|
|
72
|
+
if (!validValuePayload(payload, "complete"))
|
|
73
|
+
return { error: { code: 400, message: "invalid completion payload" } };
|
|
74
|
+
executed.record("rekey", "liveness");
|
|
75
|
+
notifications.push();
|
|
76
|
+
return { payload };
|
|
77
|
+
});
|
|
78
|
+
handlers.handleRPC(DATAGRAM_READY_RPC, async (payload) => {
|
|
79
|
+
if (!validValuePayload(payload, "datagram-ready"))
|
|
80
|
+
return {
|
|
81
|
+
error: { code: 400, message: "invalid datagram barrier payload" },
|
|
82
|
+
};
|
|
83
|
+
notifications.push();
|
|
84
|
+
return { payload };
|
|
85
|
+
});
|
|
86
|
+
handlers.handleNotification(NOTIFY_RPC, (payload) => {
|
|
87
|
+
if (!validValuePayload(payload, "notify"))
|
|
88
|
+
throw new Error("invalid notification payload");
|
|
89
|
+
executed.record("notification");
|
|
90
|
+
notifications.push();
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
registerRPC(rpcHandlers);
|
|
94
|
+
registerRPC(sessionHandlers);
|
|
95
|
+
sessionHandlers.handleStream(ECHO_KIND, async (incoming) => {
|
|
91
96
|
activeStreams.value++;
|
|
92
97
|
try {
|
|
93
98
|
if (incoming.metadata.values.cell !== path)
|
|
@@ -102,13 +107,13 @@ function createHandlers(path) {
|
|
|
102
107
|
activeStreams.value--;
|
|
103
108
|
}
|
|
104
109
|
});
|
|
105
|
-
|
|
110
|
+
sessionHandlers.handleStream(RESET_KIND, async (incoming) => {
|
|
106
111
|
if (decoder.decode(await readAll(incoming.stream)) !== "reset")
|
|
107
112
|
throw new Error("invalid reset stream payload");
|
|
108
113
|
executed.record("stream-reset");
|
|
109
114
|
throw new Error("intentional parity reset");
|
|
110
115
|
});
|
|
111
|
-
return {
|
|
116
|
+
return { rpcHandlers, sessionHandlers, notifications, activeStreams, executed };
|
|
112
117
|
}
|
|
113
118
|
function validValuePayload(payload, expected) {
|
|
114
119
|
if (payload === null || typeof payload !== "object" || Array.isArray(payload))
|
|
@@ -315,11 +320,11 @@ function pem(label, encoded) {
|
|
|
315
320
|
throw new Error("invalid embedded TLS fixture");
|
|
316
321
|
return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
|
|
317
322
|
}
|
|
318
|
-
async function connectArtifact(artifactJSON, relay,
|
|
323
|
+
async function connectArtifact(artifactJSON, relay, rpcHandlers) {
|
|
319
324
|
return await connect(createArtifactLease(parseArtifact(artifactJSON), async () => undefined), {
|
|
320
325
|
origin: relay.origin,
|
|
321
326
|
tls: { ca: relay.trust_pem },
|
|
322
|
-
|
|
327
|
+
rpcHandlers,
|
|
323
328
|
});
|
|
324
329
|
}
|
|
325
330
|
async function runServer(tls, carrier) {
|
|
@@ -334,7 +339,7 @@ async function runServer(tls, carrier) {
|
|
|
334
339
|
? { decision: "reject", reason: "invalid_credential" }
|
|
335
340
|
: { decision: "allow", artifact };
|
|
336
341
|
},
|
|
337
|
-
resolveHandlers: () => state.
|
|
342
|
+
resolveHandlers: () => state.sessionHandlers,
|
|
338
343
|
});
|
|
339
344
|
try {
|
|
340
345
|
const address = acceptor.addresses()[0];
|
|
@@ -387,7 +392,7 @@ async function runClient(input, carrier) {
|
|
|
387
392
|
ready.artifact_json === "")
|
|
388
393
|
throw new Error("invalid direct ready message");
|
|
389
394
|
const state = createHandlers("direct");
|
|
390
|
-
const session = await connectArtifact(ready.artifact_json, ready, state.
|
|
395
|
+
const session = await connectArtifact(ready.artifact_json, ready, state.rpcHandlers);
|
|
391
396
|
state.executed.record("admission");
|
|
392
397
|
await exerciseClient(session, state, "direct", carrier);
|
|
393
398
|
await session.close().catch((error) => {
|
|
@@ -512,7 +517,7 @@ async function runTunnelEndpointB(input, carrier) {
|
|
|
512
517
|
if (command.type !== "connect")
|
|
513
518
|
throw new Error("endpoint B did not receive connect command");
|
|
514
519
|
const state = createHandlers("tunnel");
|
|
515
|
-
const session = await connectArtifact(secondJSON, relay, state.
|
|
520
|
+
const session = await connectArtifact(secondJSON, relay, state.rpcHandlers);
|
|
516
521
|
state.executed.record("admission");
|
|
517
522
|
if (process.env.FLOWERSEC_PARITY_CLIENT_PROFILE !== undefined) {
|
|
518
523
|
await externalServer(session, state);
|
|
@@ -541,7 +546,7 @@ async function runTunnelEndpointA(input, carrier) {
|
|
|
541
546
|
ready.endpoint_a_artifact_json === "")
|
|
542
547
|
throw new Error("invalid endpoint B ready message");
|
|
543
548
|
const state = createHandlers("tunnel");
|
|
544
|
-
const session = await connectArtifact(ready.endpoint_a_artifact_json, ready.relay, state.
|
|
549
|
+
const session = await connectArtifact(ready.endpoint_a_artifact_json, ready.relay, state.rpcHandlers);
|
|
545
550
|
state.executed.record("admission");
|
|
546
551
|
await exerciseClient(session, state, "tunnel", carrier);
|
|
547
552
|
await session.close().catch(() => undefined);
|
package/dist/node/acceptor.d.ts
CHANGED
|
@@ -20,16 +20,25 @@ export type StreamHandler = (incoming: IncomingStream, options: OperationOptions
|
|
|
20
20
|
export type SessionHandlerOptions = Readonly<{
|
|
21
21
|
maxConcurrentStreams?: number;
|
|
22
22
|
}>;
|
|
23
|
-
export declare class
|
|
23
|
+
export declare class HandlerRegistrationError extends Error {
|
|
24
24
|
readonly code: "invalid_handler" | "already_registered" | "frozen";
|
|
25
25
|
constructor(code: "invalid_handler" | "already_registered" | "frozen");
|
|
26
26
|
}
|
|
27
|
+
export declare class RPCHandlers {
|
|
28
|
+
constructor();
|
|
29
|
+
handleRPC(typeId: number, handler: RPCHandler): void;
|
|
30
|
+
handleNotification(typeId: number, handler: NotificationHandler): void;
|
|
31
|
+
}
|
|
27
32
|
export declare class SessionHandlers {
|
|
28
33
|
constructor(options?: SessionHandlerOptions);
|
|
29
34
|
handleRPC(typeId: number, handler: RPCHandler): void;
|
|
30
35
|
handleNotification(typeId: number, handler: NotificationHandler): void;
|
|
31
36
|
handleStream(kind: string, handler: StreamHandler): void;
|
|
32
37
|
}
|
|
38
|
+
export type FrozenRPCHandlers = Readonly<{
|
|
39
|
+
requests: ReadonlyMap<number, RPCHandler>;
|
|
40
|
+
notifications: ReadonlyMap<number, NotificationHandler>;
|
|
41
|
+
}>;
|
|
33
42
|
export type AcceptorListener = Readonly<{
|
|
34
43
|
carrier: "websocket";
|
|
35
44
|
path: "direct";
|
package/dist/node/acceptor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RpcRouter } from "../rpc/server.js";
|
|
2
|
+
import { assertRpcError } from "../rpc/validate.js";
|
|
2
3
|
import { acceptReceivedSessionV2, receiveSessionAdmissionV2, rejectSessionAdmissionV2, } from "../connector/sessionAcceptor.js";
|
|
3
4
|
import { nodeSessionRuntimeV2 } from "./sessionRuntime.js";
|
|
4
5
|
import { startNodeWebSocketServer, } from "./webSocketServer.js";
|
|
@@ -12,12 +13,23 @@ const DEFAULT_MAX_CONCURRENT_STREAMS = 64;
|
|
|
12
13
|
const MAX_CONCURRENT_STREAMS = 128;
|
|
13
14
|
const DEFAULT_CLEANUP_TIMEOUT_MS = 2_000;
|
|
14
15
|
const encoder = new TextEncoder();
|
|
15
|
-
export class
|
|
16
|
+
export class HandlerRegistrationError extends Error {
|
|
16
17
|
code;
|
|
17
18
|
constructor(code) {
|
|
18
|
-
super(`Flowersec
|
|
19
|
+
super(`Flowersec handler registration failed (code=${code})`);
|
|
19
20
|
this.code = code;
|
|
20
|
-
this.name = "
|
|
21
|
+
this.name = "HandlerRegistrationError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class RPCHandlers {
|
|
25
|
+
constructor() {
|
|
26
|
+
rpcHandlerStates.set(this, createRPCHandlerState());
|
|
27
|
+
}
|
|
28
|
+
handleRPC(typeId, handler) {
|
|
29
|
+
registerRPC(mutableRPCHandlerState(this), typeId, handler);
|
|
30
|
+
}
|
|
31
|
+
handleNotification(typeId, handler) {
|
|
32
|
+
registerNotification(mutableRPCHandlerState(this), typeId, handler);
|
|
21
33
|
}
|
|
22
34
|
}
|
|
23
35
|
export class SessionHandlers {
|
|
@@ -26,68 +38,107 @@ export class SessionHandlers {
|
|
|
26
38
|
if (!Number.isSafeInteger(maximum) ||
|
|
27
39
|
maximum < 1 ||
|
|
28
40
|
maximum > MAX_CONCURRENT_STREAMS) {
|
|
29
|
-
throw new
|
|
41
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
30
42
|
}
|
|
31
43
|
sessionHandlerStates.set(this, {
|
|
32
44
|
maxConcurrentStreams: maximum,
|
|
33
|
-
rpc:
|
|
34
|
-
notifications: new Map(),
|
|
45
|
+
rpc: createRPCHandlerState(),
|
|
35
46
|
streams: new Map(),
|
|
36
47
|
frozen: false,
|
|
37
48
|
});
|
|
38
49
|
}
|
|
39
50
|
handleRPC(typeId, handler) {
|
|
40
|
-
const state =
|
|
41
|
-
|
|
42
|
-
typeId < 1 ||
|
|
43
|
-
typeId > 0xffff_ffff ||
|
|
44
|
-
typeof handler !== "function") {
|
|
45
|
-
throw new SessionHandlersError("invalid_handler");
|
|
46
|
-
}
|
|
47
|
-
if (state.rpc.has(typeId))
|
|
48
|
-
throw new SessionHandlersError("already_registered");
|
|
49
|
-
if (state.notifications.has(typeId))
|
|
50
|
-
throw new SessionHandlersError("already_registered");
|
|
51
|
-
state.rpc.set(typeId, handler);
|
|
51
|
+
const state = mutableSessionHandlerState(this);
|
|
52
|
+
registerRPC(state.rpc, typeId, handler);
|
|
52
53
|
}
|
|
53
54
|
handleNotification(typeId, handler) {
|
|
54
|
-
const state =
|
|
55
|
-
|
|
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);
|
|
55
|
+
const state = mutableSessionHandlerState(this);
|
|
56
|
+
registerNotification(state.rpc, typeId, handler);
|
|
64
57
|
}
|
|
65
58
|
handleStream(kind, handler) {
|
|
66
|
-
const state =
|
|
59
|
+
const state = mutableSessionHandlerState(this);
|
|
67
60
|
if (kind.length < 1 ||
|
|
68
61
|
encoder.encode(kind).length > 255 ||
|
|
69
62
|
kind === "flowersec.rpc.v2" ||
|
|
70
63
|
typeof handler !== "function") {
|
|
71
|
-
throw new
|
|
64
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
72
65
|
}
|
|
73
66
|
if (state.streams.has(kind))
|
|
74
|
-
throw new
|
|
67
|
+
throw new HandlerRegistrationError("already_registered");
|
|
75
68
|
state.streams.set(kind, handler);
|
|
76
69
|
}
|
|
77
70
|
}
|
|
71
|
+
const rpcHandlerStates = new WeakMap();
|
|
78
72
|
const sessionHandlerStates = new WeakMap();
|
|
79
|
-
function
|
|
73
|
+
function createRPCHandlerState() {
|
|
74
|
+
return { requests: new Map(), notifications: new Map(), frozen: false };
|
|
75
|
+
}
|
|
76
|
+
function mutableRPCHandlerState(handlers) {
|
|
77
|
+
const state = rpcHandlerStates.get(handlers);
|
|
78
|
+
if (state === undefined)
|
|
79
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
80
|
+
if (state.frozen)
|
|
81
|
+
throw new HandlerRegistrationError("frozen");
|
|
82
|
+
return state;
|
|
83
|
+
}
|
|
84
|
+
function mutableSessionHandlerState(handlers) {
|
|
80
85
|
const state = sessionHandlerStates.get(handlers);
|
|
81
86
|
if (state === undefined)
|
|
82
|
-
throw new
|
|
87
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
83
88
|
if (state.frozen)
|
|
84
|
-
throw new
|
|
89
|
+
throw new HandlerRegistrationError("frozen");
|
|
85
90
|
return state;
|
|
86
91
|
}
|
|
87
|
-
function
|
|
88
|
-
|
|
92
|
+
function registerRPC(state, typeId, handler) {
|
|
93
|
+
validateRPCRegistration(typeId, handler);
|
|
94
|
+
if (state.requests.has(typeId) || state.notifications.has(typeId)) {
|
|
95
|
+
throw new HandlerRegistrationError("already_registered");
|
|
96
|
+
}
|
|
97
|
+
state.requests.set(typeId, handler);
|
|
98
|
+
}
|
|
99
|
+
function registerNotification(state, typeId, handler) {
|
|
100
|
+
validateRPCRegistration(typeId, handler);
|
|
101
|
+
if (state.requests.has(typeId) || state.notifications.has(typeId)) {
|
|
102
|
+
throw new HandlerRegistrationError("already_registered");
|
|
103
|
+
}
|
|
104
|
+
state.notifications.set(typeId, handler);
|
|
105
|
+
}
|
|
106
|
+
function validateRPCRegistration(typeId, handler) {
|
|
107
|
+
if (!Number.isSafeInteger(typeId)
|
|
108
|
+
|| typeId < 1
|
|
109
|
+
|| typeId > 0xffff_ffff
|
|
110
|
+
|| typeof handler !== "function") {
|
|
111
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** @internal */
|
|
115
|
+
export function freezeRPCHandlers(handlers) {
|
|
116
|
+
const state = rpcHandlerStates.get(handlers);
|
|
117
|
+
if (state === undefined)
|
|
118
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
119
|
+
if (state.snapshot !== undefined)
|
|
120
|
+
return state.snapshot;
|
|
121
|
+
state.frozen = true;
|
|
122
|
+
state.snapshot = Object.freeze({
|
|
123
|
+
requests: new Map(state.requests),
|
|
124
|
+
notifications: new Map(state.notifications),
|
|
125
|
+
});
|
|
126
|
+
return state.snapshot;
|
|
127
|
+
}
|
|
128
|
+
function freezeRPCHandlerState(state) {
|
|
129
|
+
if (state.snapshot !== undefined)
|
|
130
|
+
return state.snapshot;
|
|
89
131
|
state.frozen = true;
|
|
90
|
-
|
|
132
|
+
state.snapshot = Object.freeze({
|
|
133
|
+
requests: new Map(state.requests),
|
|
134
|
+
notifications: new Map(state.notifications),
|
|
135
|
+
});
|
|
136
|
+
return state.snapshot;
|
|
137
|
+
}
|
|
138
|
+
/** @internal */
|
|
139
|
+
export function createRPCRouter(snapshot) {
|
|
140
|
+
const router = new RpcRouter();
|
|
141
|
+
for (const [typeId, handler] of snapshot.requests) {
|
|
91
142
|
router.register(typeId, async (payload) => {
|
|
92
143
|
const result = await handler(payload, Object.freeze({ typeId }));
|
|
93
144
|
if ("error" in result)
|
|
@@ -95,34 +146,39 @@ function freezeHandlers(handlers, router) {
|
|
|
95
146
|
return { payload: result.payload };
|
|
96
147
|
});
|
|
97
148
|
}
|
|
98
|
-
for (const [typeId, handler] of
|
|
149
|
+
for (const [typeId, handler] of snapshot.notifications) {
|
|
99
150
|
router.onNotify(typeId, (payload) => {
|
|
100
151
|
void Promise.resolve(handler(payload, Object.freeze({ typeId }))).catch(() => undefined);
|
|
101
152
|
});
|
|
102
153
|
}
|
|
103
|
-
return
|
|
154
|
+
return router;
|
|
155
|
+
}
|
|
156
|
+
function freezeSessionHandlers(handlers) {
|
|
157
|
+
const state = sessionHandlerStates.get(handlers);
|
|
158
|
+
if (state === undefined)
|
|
159
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
160
|
+
if (state.snapshot !== undefined)
|
|
161
|
+
return state.snapshot;
|
|
162
|
+
state.frozen = true;
|
|
163
|
+
state.snapshot = Object.freeze({
|
|
164
|
+
rpc: freezeRPCHandlerState(state.rpc),
|
|
104
165
|
maxConcurrentStreams: state.maxConcurrentStreams,
|
|
105
166
|
streams: new Map(state.streams),
|
|
106
167
|
});
|
|
107
|
-
|
|
108
|
-
/** @internal */
|
|
109
|
-
export function freezeSessionHandlersForConnector(handlers) {
|
|
110
|
-
const router = new RpcRouter();
|
|
111
|
-
freezeHandlers(handlers, router);
|
|
112
|
-
return router;
|
|
168
|
+
return state.snapshot;
|
|
113
169
|
}
|
|
114
170
|
/** @internal */
|
|
115
171
|
export function registerSessionStreamsAtomically(handlers, entries) {
|
|
116
|
-
const state =
|
|
172
|
+
const state = mutableSessionHandlerState(handlers);
|
|
117
173
|
const pending = new Set();
|
|
118
174
|
for (const [kind, handler] of entries) {
|
|
119
175
|
if (kind.length < 1 ||
|
|
120
176
|
encoder.encode(kind).length > 255 ||
|
|
121
177
|
kind === "flowersec.rpc.v2" ||
|
|
122
178
|
typeof handler !== "function")
|
|
123
|
-
throw new
|
|
179
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
124
180
|
if (state.streams.has(kind) || pending.has(kind))
|
|
125
|
-
throw new
|
|
181
|
+
throw new HandlerRegistrationError("already_registered");
|
|
126
182
|
pending.add(kind);
|
|
127
183
|
}
|
|
128
184
|
for (const [kind, handler] of entries)
|
|
@@ -209,11 +265,27 @@ export class Acceptor {
|
|
|
209
265
|
}
|
|
210
266
|
async close() {
|
|
211
267
|
const state = acceptorState(this);
|
|
212
|
-
state.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await
|
|
216
|
-
|
|
268
|
+
if (state.lifecycle.completion === undefined) {
|
|
269
|
+
state.lifecycle.completion = closeAcceptor(state);
|
|
270
|
+
}
|
|
271
|
+
await withCleanupTimeout(state.lifecycle.completion, state.cleanupTimeoutMs);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const MAX_CONCURRENT_ADMISSIONS = 64;
|
|
275
|
+
const MAX_PENDING_ACCEPTED_SESSIONS = 64;
|
|
276
|
+
async function closeAcceptor(state) {
|
|
277
|
+
state.abort.abort();
|
|
278
|
+
const queued = state.accepted.close();
|
|
279
|
+
const cleanup = await Promise.allSettled([
|
|
280
|
+
...state.listeners.map(async (listener) => await listener.close()),
|
|
281
|
+
...queued.map(async (accepted) => await accepted.close()),
|
|
282
|
+
]);
|
|
283
|
+
while (state.tasks.size > 0) {
|
|
284
|
+
await Promise.allSettled([...state.tasks]);
|
|
285
|
+
}
|
|
286
|
+
if (cleanup.some((result) => result.status === "rejected") ||
|
|
287
|
+
state.lifecycle.cleanupFailed) {
|
|
288
|
+
throw new SessionError("operation_failed");
|
|
217
289
|
}
|
|
218
290
|
}
|
|
219
291
|
async function releaseLease(state, leaseId) {
|
|
@@ -225,7 +297,7 @@ async function authorizeCarrier(state, carrier) {
|
|
|
225
297
|
const received = await receiveSessionAdmissionV2(carrier, state.abort.signal);
|
|
226
298
|
const decoded = received.decoded;
|
|
227
299
|
const request = runtimeAuthorizationRequestFromDecoded(decoded);
|
|
228
|
-
const decision = await
|
|
300
|
+
const decision = await state.options.authorize(request, { signal: state.abort.signal });
|
|
229
301
|
if (decision.decision !== "allow") {
|
|
230
302
|
return await rejectSessionAdmissionV2(received, {
|
|
231
303
|
accepted: false,
|
|
@@ -237,17 +309,15 @@ async function authorizeCarrier(state, carrier) {
|
|
|
237
309
|
? decision.leaseId
|
|
238
310
|
: undefined;
|
|
239
311
|
try {
|
|
240
|
-
const router = new RpcRouter();
|
|
241
312
|
const registry = state.options.resolveHandlers === undefined
|
|
242
313
|
? new SessionHandlers()
|
|
243
|
-
: await
|
|
244
|
-
|
|
245
|
-
})), state.abort.signal);
|
|
314
|
+
: await state.options.resolveHandlers(request, { signal: state.abort.signal });
|
|
315
|
+
const handlers = freezeSessionHandlers(registry);
|
|
246
316
|
const leg = {
|
|
247
317
|
received,
|
|
248
318
|
artifact: unwrapArtifact(decision.artifact),
|
|
249
|
-
handlers
|
|
250
|
-
router,
|
|
319
|
+
handlers,
|
|
320
|
+
router: createRPCRouter(handlers.rpc),
|
|
251
321
|
};
|
|
252
322
|
if (leaseId !== undefined)
|
|
253
323
|
leg.leaseId = leaseId;
|
|
@@ -271,28 +341,50 @@ async function processCarrier(state, carrier) {
|
|
|
271
341
|
const leg = await authorizeCarrier(state, carrier);
|
|
272
342
|
if (leg === undefined)
|
|
273
343
|
return;
|
|
344
|
+
let releaseOwnedByAcceptedSession = false;
|
|
274
345
|
try {
|
|
275
346
|
if (leg.received.decoded.request.pathKind !== "direct" ||
|
|
276
347
|
leg.artifact.path.kind !== "direct") {
|
|
277
348
|
await leg.received.carrier.close().catch(() => undefined);
|
|
278
349
|
throw new SessionError("operation_failed");
|
|
279
350
|
}
|
|
280
|
-
|
|
351
|
+
const accepted = await establishDirect(leg, state.abort.signal, leg.leaseId === undefined ? undefined : () => releaseLease(state, leg.leaseId));
|
|
352
|
+
releaseOwnedByAcceptedSession = true;
|
|
353
|
+
if (state.accepted.push(accepted) === "rejected") {
|
|
354
|
+
try {
|
|
355
|
+
await accepted.close();
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
state.lifecycle.cleanupFailed = true;
|
|
359
|
+
throw new SessionError("operation_failed");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
281
362
|
}
|
|
282
363
|
catch (error) {
|
|
283
|
-
|
|
364
|
+
if (!releaseOwnedByAcceptedSession) {
|
|
365
|
+
await releaseLease(state, leg.leaseId).catch(() => undefined);
|
|
366
|
+
}
|
|
284
367
|
throw error;
|
|
285
368
|
}
|
|
286
369
|
}
|
|
287
370
|
async function runAcceptLoop(state) {
|
|
288
371
|
while (!state.abort.signal.aborted) {
|
|
289
372
|
try {
|
|
373
|
+
while (state.admissions.size >= MAX_CONCURRENT_ADMISSIONS) {
|
|
374
|
+
await Promise.race(state.admissions);
|
|
375
|
+
if (state.abort.signal.aborted)
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
290
378
|
const carrier = await state.accept({ signal: state.abort.signal });
|
|
291
379
|
const task = processCarrier(state, carrier)
|
|
292
380
|
.catch(async () => {
|
|
293
381
|
await carrier.close().catch(() => undefined);
|
|
294
382
|
})
|
|
295
|
-
.finally(() =>
|
|
383
|
+
.finally(() => {
|
|
384
|
+
state.admissions.delete(task);
|
|
385
|
+
state.tasks.delete(task);
|
|
386
|
+
});
|
|
387
|
+
state.admissions.add(task);
|
|
296
388
|
state.tasks.add(task);
|
|
297
389
|
}
|
|
298
390
|
catch (error) {
|
|
@@ -307,16 +399,19 @@ class AcceptedQueue {
|
|
|
307
399
|
waiters = new Set();
|
|
308
400
|
failure;
|
|
309
401
|
push(value) {
|
|
310
|
-
if (this.failure !== undefined)
|
|
311
|
-
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
402
|
+
if (this.failure !== undefined)
|
|
403
|
+
return "rejected";
|
|
314
404
|
const waiter = this.waiters.values().next().value;
|
|
315
|
-
if (waiter === undefined)
|
|
405
|
+
if (waiter === undefined) {
|
|
406
|
+
if (this.values.length >= MAX_PENDING_ACCEPTED_SESSIONS)
|
|
407
|
+
return "rejected";
|
|
316
408
|
this.values.push(value);
|
|
409
|
+
return "queued";
|
|
410
|
+
}
|
|
317
411
|
else {
|
|
318
412
|
this.waiters.delete(waiter);
|
|
319
413
|
waiter.resolve(value);
|
|
414
|
+
return "delivered";
|
|
320
415
|
}
|
|
321
416
|
}
|
|
322
417
|
async shift(signal) {
|
|
@@ -357,6 +452,7 @@ class AcceptedQueue {
|
|
|
357
452
|
}
|
|
358
453
|
close() {
|
|
359
454
|
this.fail(new SessionError("closed"));
|
|
455
|
+
return this.values.splice(0);
|
|
360
456
|
}
|
|
361
457
|
}
|
|
362
458
|
const acceptorStates = new WeakMap();
|
|
@@ -427,6 +523,7 @@ export async function createAcceptor(options) {
|
|
|
427
523
|
const accepted = new AcceptedQueue();
|
|
428
524
|
const abort = new AbortController();
|
|
429
525
|
const tasks = new Set();
|
|
526
|
+
const admissions = new Set();
|
|
430
527
|
let started = false;
|
|
431
528
|
const state = {
|
|
432
529
|
listeners,
|
|
@@ -435,7 +532,9 @@ export async function createAcceptor(options) {
|
|
|
435
532
|
accepted,
|
|
436
533
|
abort,
|
|
437
534
|
tasks,
|
|
535
|
+
admissions,
|
|
438
536
|
cleanupTimeoutMs,
|
|
537
|
+
lifecycle: { cleanupFailed: false },
|
|
439
538
|
start() {
|
|
440
539
|
if (started)
|
|
441
540
|
return;
|
|
@@ -447,52 +546,26 @@ export async function createAcceptor(options) {
|
|
|
447
546
|
acceptorStates.set(acceptor, state);
|
|
448
547
|
return Object.freeze(acceptor);
|
|
449
548
|
}
|
|
450
|
-
async function
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
});
|
|
549
|
+
async function withCleanupTimeout(completion, timeoutMs) {
|
|
550
|
+
let timer;
|
|
551
|
+
try {
|
|
552
|
+
await Promise.race([
|
|
553
|
+
completion,
|
|
554
|
+
new Promise((_, reject) => {
|
|
555
|
+
timer = setTimeout(() => reject(new SessionError("timeout")), timeoutMs);
|
|
556
|
+
}),
|
|
557
|
+
]);
|
|
558
|
+
}
|
|
559
|
+
finally {
|
|
560
|
+
if (timer !== undefined)
|
|
561
|
+
clearTimeout(timer);
|
|
562
|
+
}
|
|
484
563
|
}
|
|
485
564
|
function validRPCError(error) {
|
|
486
|
-
|
|
487
|
-
error
|
|
488
|
-
error.code > 0xffff_ffff) {
|
|
489
|
-
return { code: 500, message: "handler failed" };
|
|
565
|
+
try {
|
|
566
|
+
return assertRpcError(error);
|
|
490
567
|
}
|
|
491
|
-
|
|
492
|
-
encoder.encode(error.message).length > 1024) {
|
|
568
|
+
catch {
|
|
493
569
|
return { code: 500, message: "handler failed" };
|
|
494
570
|
}
|
|
495
|
-
return error.message === undefined
|
|
496
|
-
? { code: error.code }
|
|
497
|
-
: { code: error.code, message: error.message };
|
|
498
571
|
}
|
|
@@ -1,22 +1,23 @@
|
|
|
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
|
|
4
|
+
import { type RPCHandlers } from "./acceptor.js";
|
|
5
5
|
export type SessionTLSOptions = Readonly<{
|
|
6
6
|
ca?: string | Uint8Array;
|
|
7
7
|
}>;
|
|
8
8
|
export type SessionOptions = Readonly<{
|
|
9
|
-
origin
|
|
9
|
+
origin?: string;
|
|
10
10
|
signal?: AbortSignal;
|
|
11
11
|
connectTimeoutMs?: number;
|
|
12
12
|
tls?: SessionTLSOptions;
|
|
13
|
-
|
|
13
|
+
rpcHandlers?: RPCHandlers;
|
|
14
14
|
}>;
|
|
15
15
|
export type ConnectionControllerOptions = Readonly<{
|
|
16
|
-
origin
|
|
16
|
+
origin?: string;
|
|
17
17
|
connectTimeoutMs?: number;
|
|
18
18
|
tls?: SessionTLSOptions;
|
|
19
19
|
maximumAttempts?: number;
|
|
20
|
+
rpcHandlers?: RPCHandlers;
|
|
20
21
|
}>;
|
|
21
22
|
export declare function createConnectionController(source: ArtifactSource, options: ConnectionControllerOptions): ConnectionController;
|
|
22
23
|
export declare function connect(lease: ArtifactLease, options: SessionOptions): Promise<Session>;
|
|
@@ -7,33 +7,24 @@ import { projectSessionV2 } from "../v2/publicSession.js";
|
|
|
7
7
|
import { ConnectError } from "../public/connectError.js";
|
|
8
8
|
import { nodeSessionRuntimeV2 } from "./sessionRuntime.js";
|
|
9
9
|
import { createConnectionControllerV2, } from "../connectionController.js";
|
|
10
|
-
import {
|
|
10
|
+
import { createRPCRouter, freezeRPCHandlers, } from "./acceptor.js";
|
|
11
11
|
import { createNativeRawQuicDriver, NativeTransportUnavailableError, tryLoadNativeTransportAddon, } from "./nativeTransportAddon.js";
|
|
12
|
-
import { createNodeRawQuicClientV2 } from "./rawQuicAdapter.js";
|
|
12
|
+
import { createNodeRawQuicClientV2, normalizeCertificateChain } from "./rawQuicAdapter.js";
|
|
13
13
|
export function createConnectionController(source, options) {
|
|
14
|
+
const normalized = normalizeNodeOptions(options);
|
|
14
15
|
const controllerOptions = options.maximumAttempts === undefined
|
|
15
16
|
? {}
|
|
16
17
|
: { maximumAttempts: options.maximumAttempts };
|
|
17
|
-
return createConnectionControllerV2(source, async (lease, signal) => await
|
|
18
|
-
origin: options.origin,
|
|
19
|
-
signal,
|
|
20
|
-
...(options.connectTimeoutMs === undefined ? {} : { connectTimeoutMs: options.connectTimeoutMs }),
|
|
21
|
-
...(options.tls === undefined ? {} : { tls: options.tls }),
|
|
22
|
-
}), controllerOptions);
|
|
18
|
+
return createConnectionControllerV2(source, async (lease, signal) => await connectWithRPCSnapshot(lease, normalized, signal), controllerOptions);
|
|
23
19
|
}
|
|
24
20
|
export async function connect(lease, options) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
throw new ConnectError("invalid_options");
|
|
33
|
-
}
|
|
34
|
-
const rpcRouter = options.handlers === undefined
|
|
21
|
+
const normalized = normalizeNodeOptions(options);
|
|
22
|
+
return await connectWithRPCSnapshot(lease, normalized, options.signal);
|
|
23
|
+
}
|
|
24
|
+
async function connectWithRPCSnapshot(lease, options, signal) {
|
|
25
|
+
const rpcRouter = options.rpcSnapshot === undefined
|
|
35
26
|
? undefined
|
|
36
|
-
:
|
|
27
|
+
: createRPCRouter(options.rpcSnapshot);
|
|
37
28
|
// WebSocket sessions do not require the optional native raw QUIC addon. Load
|
|
38
29
|
// it only when a raw QUIC candidate is actually selected by the connector.
|
|
39
30
|
const nativeAddon = tryLoadNativeTransportAddon();
|
|
@@ -45,8 +36,14 @@ export async function connect(lease, options) {
|
|
|
45
36
|
}
|
|
46
37
|
return await createNodeRawQuicClientV2(createNativeRawQuicDriver(nativeAddon), candidate, artifact, { ca: options.tls.ca }, signal, options.connectTimeoutMs);
|
|
47
38
|
});
|
|
39
|
+
const origin = options.origin;
|
|
40
|
+
let websocketFactory;
|
|
41
|
+
if (origin !== undefined) {
|
|
42
|
+
const wsFactory = createNodeWsFactory(options.tls);
|
|
43
|
+
websocketFactory = createWebSocketCandidateFactoryV2((url, subprotocol) => wsFactory(url, origin, subprotocol));
|
|
44
|
+
}
|
|
48
45
|
const connector = new SessionConnectorV2(lease, composeCandidateAttemptFactoryV2({
|
|
49
|
-
|
|
46
|
+
...(websocketFactory === undefined ? {} : { websocket: websocketFactory }),
|
|
50
47
|
raw_quic: rawQuicFactory,
|
|
51
48
|
}), {
|
|
52
49
|
capability: detectNodeRuntimeCapabilityV2(nativeAddon !== undefined),
|
|
@@ -54,10 +51,34 @@ export async function connect(lease, options) {
|
|
|
54
51
|
...(rpcRouter === undefined ? {} : { rpcRouter }),
|
|
55
52
|
...(options.connectTimeoutMs === undefined ? {} : { connectTimeoutMs: options.connectTimeoutMs }),
|
|
56
53
|
});
|
|
57
|
-
const result = await connector.connect(
|
|
54
|
+
const result = await connector.connect(signal === undefined ? {} : { signal });
|
|
58
55
|
return projectSessionV2(result.session);
|
|
59
56
|
}
|
|
57
|
+
function normalizeNodeOptions(options) {
|
|
58
|
+
try {
|
|
59
|
+
const origin = normalizeOrigin(options.origin);
|
|
60
|
+
if (options.connectTimeoutMs !== undefined &&
|
|
61
|
+
(!Number.isSafeInteger(options.connectTimeoutMs) || options.connectTimeoutMs < 1)) {
|
|
62
|
+
throw new RangeError("connectTimeoutMs must be a positive safe integer");
|
|
63
|
+
}
|
|
64
|
+
if (origin !== undefined)
|
|
65
|
+
createNodeWsFactory(options.tls);
|
|
66
|
+
if (options.tls?.ca !== undefined)
|
|
67
|
+
normalizeCertificateChain(options.tls.ca);
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
...(origin === undefined ? {} : { origin }),
|
|
70
|
+
...(options.connectTimeoutMs === undefined ? {} : { connectTimeoutMs: options.connectTimeoutMs }),
|
|
71
|
+
...(options.tls === undefined ? {} : { tls: options.tls }),
|
|
72
|
+
...(options.rpcHandlers === undefined ? {} : { rpcSnapshot: freezeRPCHandlers(options.rpcHandlers) }),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new ConnectError("invalid_options");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
60
79
|
function normalizeOrigin(input) {
|
|
80
|
+
if (input === undefined)
|
|
81
|
+
return undefined;
|
|
61
82
|
let parsed;
|
|
62
83
|
try {
|
|
63
84
|
parsed = new URL(input);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes as nodeRandomBytes } from "node:crypto";
|
|
1
|
+
import { randomBytes as nodeRandomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { buildFSB2RequestV2, canonicalizeCandidatesV2, computeSessionContractHashV2, decodeArtifactV2JSON, decodeFSB2RequestV2, encodeArtifactV2JSON, encodeFSB2RequestV2, } from "../v2/artifact.js";
|
|
3
3
|
import { wrapArtifact } from "../public/artifact.js";
|
|
4
4
|
import { base64urlDecode, base64urlEncode } from "../utils/base64url.js";
|
|
@@ -413,7 +413,11 @@ function validTCPAddress(value) {
|
|
|
413
413
|
return port >= 1 && port <= 65_535;
|
|
414
414
|
}
|
|
415
415
|
function validObservedText(value) { return value.length > 0 && value.length <= 512 && !/[\u0000-\u001f\u007f]/u.test(value); }
|
|
416
|
-
function bytesEqual(a, b) {
|
|
416
|
+
function bytesEqual(a, b) {
|
|
417
|
+
if (a.length !== b.length)
|
|
418
|
+
return false;
|
|
419
|
+
return timingSafeEqual(a, b);
|
|
420
|
+
}
|
|
417
421
|
function strictObject(bytes) {
|
|
418
422
|
let value;
|
|
419
423
|
try {
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { connect, createConnectionController } from "./connectSession.js";
|
|
2
|
-
export { AcceptedSession, Acceptor,
|
|
2
|
+
export { AcceptedSession, Acceptor, HandlerRegistrationError, RPCHandlers, SessionHandlers, createAcceptor, } from "./acceptor.js";
|
|
3
3
|
export type { AcceptorListener, AcceptorOptions, AuthorizationDecision, RPCHandler, RPCHandlerResult, NotificationHandler, SessionHandlerOptions, StreamHandler, } from "./acceptor.js";
|
|
4
4
|
export { TunnelRuntime, createTunnelRuntime } from "./tunnelRuntime.js";
|
|
5
5
|
export type { TunnelAuthorizationDecision, TunnelRuntimeListener, TunnelRuntimeOptions, } from "./tunnelRuntime.js";
|
package/dist/node/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { connect, createConnectionController } from "./connectSession.js";
|
|
2
|
-
export { AcceptedSession, Acceptor,
|
|
2
|
+
export { AcceptedSession, Acceptor, HandlerRegistrationError, RPCHandlers, SessionHandlers, createAcceptor, } from "./acceptor.js";
|
|
3
3
|
export { TunnelRuntime, createTunnelRuntime } from "./tunnelRuntime.js";
|
|
4
4
|
export { AuthorizationRecord, AuthorizationResponse, TunnelAuthorizationResponse, ControlPlaneError, EndpointSet, IssuedArtifact, Issuer, RuntimeAuthorizationRequest, authorizeRuntime, authorizeTunnelRuntime, createEndpointSet, parseAuthorizationRecord, parseRuntimeAuthorizationRequest, rejectRuntime, rejectTunnelRuntime, } from "./controlplane.js";
|
|
5
5
|
export { ProxyServer, ProxyServerError } from "./proxyServer.js";
|
|
@@ -36,13 +36,6 @@ export async function commitArtifactLeaseSpend(lease, signal) {
|
|
|
36
36
|
const leaseState = artifactLeaseStates.get(lease);
|
|
37
37
|
if (leaseState === undefined || leaseState.state !== "idle")
|
|
38
38
|
throw new ArtifactLeaseError();
|
|
39
|
-
leaseState.state = "
|
|
40
|
-
|
|
41
|
-
await leaseState.commitSpend(signal);
|
|
42
|
-
leaseState.state = "consumed";
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
leaseState.state = "idle";
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
39
|
+
leaseState.state = "consumed";
|
|
40
|
+
await leaseState.commitSpend(signal);
|
|
48
41
|
}
|
package/dist/rpc/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
2
|
-
import { assertRpcEnvelope } from "./validate.js";
|
|
2
|
+
import { assertRpcEnvelope, assertRpcError } from "./validate.js";
|
|
3
3
|
import { SDK_DEFAULTS } from "../defaults.js";
|
|
4
4
|
const DEFAULT_RPC_SERVER_OPTIONS = Object.freeze({
|
|
5
5
|
maxConcurrentRequests: SDK_DEFAULTS.rpc.maxConcurrentRequests,
|
|
@@ -217,12 +217,21 @@ export class RpcServer {
|
|
|
217
217
|
waiters.shift()?.();
|
|
218
218
|
}
|
|
219
219
|
async writeResponse(request, out) {
|
|
220
|
+
let error;
|
|
221
|
+
if (out.error != null) {
|
|
222
|
+
try {
|
|
223
|
+
error = assertRpcError(out.error);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
error = { code: 500, message: "internal error" };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
220
229
|
const resp = {
|
|
221
230
|
type_id: request.type_id,
|
|
222
231
|
request_id: 0,
|
|
223
232
|
response_to: request.request_id,
|
|
224
233
|
payload: out.payload,
|
|
225
|
-
...(
|
|
234
|
+
...(error != null ? { error } : {}),
|
|
226
235
|
};
|
|
227
236
|
await this.writeEnvelope(resp);
|
|
228
237
|
}
|
package/dist/rpc/validate.d.ts
CHANGED
package/dist/rpc/validate.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isSafeU32Number, isSafeU64Number } from "../utils/number.js";
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const strictDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
2
4
|
// assertRpcEnvelope validates numeric fields that are u32/u64 in the IDL.
|
|
3
5
|
//
|
|
4
6
|
// The wire format is JSON, so JS numbers are used. For u64 we enforce the safe integer range
|
|
@@ -15,13 +17,29 @@ export function assertRpcEnvelope(v) {
|
|
|
15
17
|
throw new Error("bad rpc envelope: response_to");
|
|
16
18
|
// payload: unknown (JSON)
|
|
17
19
|
if (o.error != null) {
|
|
18
|
-
|
|
19
|
-
throw new Error("bad rpc envelope: error");
|
|
20
|
-
if (!isSafeU32Number(o.error.code))
|
|
21
|
-
throw new Error("bad rpc envelope: error.code");
|
|
22
|
-
const msg = o.error.message;
|
|
23
|
-
if (msg !== undefined && typeof msg !== "string")
|
|
24
|
-
throw new Error("bad rpc envelope: error.message");
|
|
20
|
+
assertRpcError(o.error);
|
|
25
21
|
}
|
|
26
22
|
return o;
|
|
27
23
|
}
|
|
24
|
+
export function assertRpcError(value) {
|
|
25
|
+
if (typeof value !== "object" || value == null)
|
|
26
|
+
throw new Error("bad rpc envelope: error");
|
|
27
|
+
const error = value;
|
|
28
|
+
if (Object.keys(error).some((key) => key !== "code" && key !== "message")) {
|
|
29
|
+
throw new Error("bad rpc envelope: error shape");
|
|
30
|
+
}
|
|
31
|
+
if (!isSafeU32Number(error.code) || error.code === 0) {
|
|
32
|
+
throw new Error("bad rpc envelope: error.code");
|
|
33
|
+
}
|
|
34
|
+
const message = error.message;
|
|
35
|
+
if (message !== undefined && typeof message !== "string") {
|
|
36
|
+
throw new Error("bad rpc envelope: error.message");
|
|
37
|
+
}
|
|
38
|
+
if (typeof message === "string") {
|
|
39
|
+
const encoded = encoder.encode(message);
|
|
40
|
+
if (encoded.byteLength > 1_024 || strictDecoder.decode(encoded) !== message) {
|
|
41
|
+
throw new Error("bad rpc envelope: error.message");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return message === undefined ? { code: error.code } : { code: error.code, message };
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/flowersec-core",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "Flowersec core TypeScript library for carrier-neutral encrypted sessions and multiplexed streams.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"ws": "^8.21.2"
|
|
78
78
|
},
|
|
79
79
|
"optionalDependencies": {
|
|
80
|
-
"@floegence/flowersec-node-native": "2.4.
|
|
80
|
+
"@floegence/flowersec-node-native": "2.4.2"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@playwright/test": "1.62.1",
|
|
@@ -95,5 +95,5 @@
|
|
|
95
95
|
"vite": "^8.2.1",
|
|
96
96
|
"vitest": "4.1.10"
|
|
97
97
|
},
|
|
98
|
-
"flowersecSourceCommit": "
|
|
98
|
+
"flowersecSourceCommit": "1393acdb891a6c935a435bee9068f65b68268795"
|
|
99
99
|
}
|
package/sbom/cyclonedx.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:a7cb7385-1123-5452-8424-bbcb7904bbbb",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
7
|
"component": {
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "@floegence/flowersec-core",
|
|
10
|
-
"version": "2.4.
|
|
11
|
-
"purl": "pkg:npm/%40floegence/flowersec-core@2.4.
|
|
12
|
-
"bom-ref": "pkg:npm/%40floegence/flowersec-core@2.4.
|
|
10
|
+
"version": "2.4.2",
|
|
11
|
+
"purl": "pkg:npm/%40floegence/flowersec-core@2.4.2",
|
|
12
|
+
"bom-ref": "pkg:npm/%40floegence/flowersec-core@2.4.2"
|
|
13
13
|
},
|
|
14
14
|
"properties": [
|
|
15
15
|
{
|
|
16
16
|
"name": "flowersec:source-inventory-sha256",
|
|
17
|
-
"value": "
|
|
17
|
+
"value": "59d233df0e12f2c371ab542c83edf97a2968a75c7d17c057e453b97f530a90e4"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
},
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
],
|
|
191
191
|
"dependencies": [
|
|
192
192
|
{
|
|
193
|
-
"ref": "pkg:npm/%40floegence/flowersec-core@2.4.
|
|
193
|
+
"ref": "pkg:npm/%40floegence/flowersec-core@2.4.2",
|
|
194
194
|
"dependsOn": [
|
|
195
195
|
"pkg:npm/%40noble/ciphers@2.3.0",
|
|
196
196
|
"pkg:npm/%40noble/curves@2.3.0",
|
package/sbom/spdx.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
5
|
"name": "flowersec-ts",
|
|
6
|
-
"documentNamespace": "https://github.com/floegence/flowersec/sbom/flowersec-ts/
|
|
6
|
+
"documentNamespace": "https://github.com/floegence/flowersec/sbom/flowersec-ts/59d233df0e12f2c371ab542c83edf97a2968a75c7d17c057e453b97f530a90e4",
|
|
7
7
|
"creationInfo": {
|
|
8
8
|
"created": "1970-01-01T00:00:00Z",
|
|
9
9
|
"creators": [
|
|
@@ -13,19 +13,19 @@
|
|
|
13
13
|
"packages": [
|
|
14
14
|
{
|
|
15
15
|
"name": "@floegence/flowersec-core",
|
|
16
|
-
"SPDXID": "SPDXRef-Package-
|
|
17
|
-
"versionInfo": "2.4.
|
|
16
|
+
"SPDXID": "SPDXRef-Package-9eec732cc29715c69e1e",
|
|
17
|
+
"versionInfo": "2.4.2",
|
|
18
18
|
"downloadLocation": "NOASSERTION",
|
|
19
19
|
"filesAnalyzed": false,
|
|
20
20
|
"licenseConcluded": "NOASSERTION",
|
|
21
21
|
"licenseDeclared": "NOASSERTION",
|
|
22
22
|
"copyrightText": "NOASSERTION",
|
|
23
|
-
"comment": "Flowersec source inventory SHA-256:
|
|
23
|
+
"comment": "Flowersec source inventory SHA-256: 59d233df0e12f2c371ab542c83edf97a2968a75c7d17c057e453b97f530a90e4",
|
|
24
24
|
"externalRefs": [
|
|
25
25
|
{
|
|
26
26
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
27
27
|
"referenceType": "purl",
|
|
28
|
-
"referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.4.
|
|
28
|
+
"referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.4.2"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -136,30 +136,30 @@
|
|
|
136
136
|
{
|
|
137
137
|
"spdxElementId": "SPDXRef-DOCUMENT",
|
|
138
138
|
"relationshipType": "DESCRIBES",
|
|
139
|
-
"relatedSpdxElement": "SPDXRef-Package-
|
|
139
|
+
"relatedSpdxElement": "SPDXRef-Package-9eec732cc29715c69e1e"
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
|
-
"spdxElementId": "SPDXRef-Package-
|
|
142
|
+
"spdxElementId": "SPDXRef-Package-9eec732cc29715c69e1e",
|
|
143
143
|
"relationshipType": "DEPENDS_ON",
|
|
144
144
|
"relatedSpdxElement": "SPDXRef-Package-5ce913a03239b02770fb"
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
|
-
"spdxElementId": "SPDXRef-Package-
|
|
147
|
+
"spdxElementId": "SPDXRef-Package-9eec732cc29715c69e1e",
|
|
148
148
|
"relationshipType": "DEPENDS_ON",
|
|
149
149
|
"relatedSpdxElement": "SPDXRef-Package-01009adf60db02c13634"
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
|
-
"spdxElementId": "SPDXRef-Package-
|
|
152
|
+
"spdxElementId": "SPDXRef-Package-9eec732cc29715c69e1e",
|
|
153
153
|
"relationshipType": "DEPENDS_ON",
|
|
154
154
|
"relatedSpdxElement": "SPDXRef-Package-8815b117c8a1d5f7eeb0"
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
|
-
"spdxElementId": "SPDXRef-Package-
|
|
157
|
+
"spdxElementId": "SPDXRef-Package-9eec732cc29715c69e1e",
|
|
158
158
|
"relationshipType": "DEPENDS_ON",
|
|
159
159
|
"relatedSpdxElement": "SPDXRef-Package-f8b45a289df643042b94"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
|
-
"spdxElementId": "SPDXRef-Package-
|
|
162
|
+
"spdxElementId": "SPDXRef-Package-9eec732cc29715c69e1e",
|
|
163
163
|
"relationshipType": "DEPENDS_ON",
|
|
164
164
|
"relatedSpdxElement": "SPDXRef-Package-b779412f685822663496"
|
|
165
165
|
},
|