@floegence/flowersec-core 2.5.4 → 3.0.1
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 +46 -18
- package/dist/browser/connectSessionV3.d.ts +13 -0
- package/dist/browser/connectSessionV3.js +75 -0
- package/dist/browser/index.d.ts +3 -2
- package/dist/browser/index.js +2 -1
- package/dist/browser/v2.d.ts +3 -0
- package/dist/browser/v2.js +2 -0
- package/dist/facade.d.ts +15 -8
- package/dist/facade.js +5 -4
- package/dist/interop/proxyServerPeer.js +1 -1
- package/dist/interop/serverParityPeer.js +8 -1
- package/dist/node/acceptor.d.ts +10 -0
- package/dist/node/acceptor.js +45 -2
- package/dist/node/acceptorV3.d.ts +57 -0
- package/dist/node/acceptorV3.js +227 -0
- package/dist/node/connectSessionV3.d.ts +21 -0
- package/dist/node/connectSessionV3.js +73 -0
- package/dist/node/index.d.ts +15 -10
- package/dist/node/index.js +6 -5
- package/dist/node/nativeTransportAddon.d.ts +39 -1
- package/dist/node/nativeTransportAddon.js +29 -5
- package/dist/node/rawQuicAdapterV3.d.ts +10 -0
- package/dist/node/rawQuicAdapterV3.js +76 -0
- package/dist/node/rawQuicServerV3.d.ts +24 -0
- package/dist/node/rawQuicServerV3.js +34 -0
- package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
- package/dist/node/runtimeAuthorizationV3.js +126 -0
- package/dist/node/tunnelRuntimeV3.d.ts +57 -0
- package/dist/node/tunnelRuntimeV3.js +857 -0
- package/dist/node/v2.d.ts +11 -0
- package/dist/node/v2.js +6 -0
- package/dist/node/webSocketServerV3.d.ts +27 -0
- package/dist/node/webSocketServerV3.js +219 -0
- package/dist/public/streamHandlers.d.ts +3 -0
- package/dist/public/streamHandlers.js +6 -1
- package/dist/v2/index.d.ts +14 -0
- package/dist/v2/index.js +7 -0
- package/dist/v2/protocol.js +23 -32
- package/dist/v2/session.d.ts +5 -0
- package/dist/v2/session.js +90 -35
- package/dist/v3/admissionError.d.ts +4 -0
- package/dist/v3/admissionError.js +8 -0
- package/dist/v3/artifact.d.ts +145 -0
- package/dist/v3/artifact.js +1162 -0
- package/dist/v3/artifactLease.d.ts +17 -0
- package/dist/v3/artifactLease.js +109 -0
- package/dist/v3/browserRuntime.d.ts +37 -0
- package/dist/v3/browserRuntime.js +197 -0
- package/dist/v3/browserSessionRuntime.d.ts +2 -0
- package/dist/v3/browserSessionRuntime.js +21 -0
- package/dist/v3/capability.d.ts +32 -0
- package/dist/v3/capability.js +345 -0
- package/dist/v3/carrier.d.ts +84 -0
- package/dist/v3/carrier.js +411 -0
- package/dist/v3/connectionController.d.ts +87 -0
- package/dist/v3/connectionController.js +805 -0
- package/dist/v3/contract.d.ts +65 -0
- package/dist/v3/contract.js +3 -0
- package/dist/v3/controller.d.ts +43 -0
- package/dist/v3/controller.js +303 -0
- package/dist/v3/handshake.d.ts +81 -0
- package/dist/v3/handshake.js +471 -0
- package/dist/v3/jcs.d.ts +6 -0
- package/dist/v3/jcs.js +69 -0
- package/dist/v3/jsonPreflight.d.ts +1 -0
- package/dist/v3/jsonPreflight.js +251 -0
- package/dist/v3/nodeRuntime.d.ts +40 -0
- package/dist/v3/nodeRuntime.js +315 -0
- package/dist/v3/nodeSessionRuntime.d.ts +2 -0
- package/dist/v3/nodeSessionRuntime.js +13 -0
- package/dist/v3/protocol.d.ts +99 -0
- package/dist/v3/protocol.js +773 -0
- package/dist/v3/publicApi.d.ts +12 -0
- package/dist/v3/publicApi.js +46 -0
- package/dist/v3/publicSession.d.ts +1 -0
- package/dist/v3/publicSession.js +281 -0
- package/dist/v3/retryDisposition.d.ts +16 -0
- package/dist/v3/retryDisposition.js +30 -0
- package/dist/v3/runtimeAdapters.d.ts +9 -0
- package/dist/v3/runtimeAdapters.js +109 -0
- package/dist/v3/security.d.ts +34 -0
- package/dist/v3/security.js +100 -0
- package/dist/v3/serverAdmission.d.ts +38 -0
- package/dist/v3/serverAdmission.js +223 -0
- package/dist/v3/session.d.ts +318 -0
- package/dist/v3/session.js +2289 -0
- package/dist/v3/sessionConnector.d.ts +37 -0
- package/dist/v3/sessionConnector.js +440 -0
- package/dist/v3/streamLifetimeLedger.d.ts +31 -0
- package/dist/v3/streamLifetimeLedger.js +131 -0
- package/dist/v3/streamMetadata.d.ts +1 -0
- package/dist/v3/streamMetadata.js +7 -0
- package/dist/v3/transportConstants.d.ts +56 -0
- package/dist/v3/transportConstants.js +59 -0
- package/dist/v3/unreliableMessage.d.ts +44 -0
- package/dist/v3/unreliableMessage.js +284 -0
- package/dist/v3/webSocketCarrier.d.ts +31 -0
- package/dist/v3/webSocketCarrier.js +370 -0
- package/dist/v3/webTransportCarrier.d.ts +27 -0
- package/dist/v3/webTransportCarrier.js +534 -0
- package/package.json +4 -4
- package/sbom/cyclonedx.json +6 -6
- package/sbom/spdx.json +11 -11
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "../v2/index.js";
|
|
2
|
+
export { connect, createConnectionController } from "./connectSession.js";
|
|
3
|
+
export type { ConnectionControllerOptions, SessionOptions, SessionTLSOptions, } from "./connectSession.js";
|
|
4
|
+
export { AcceptedSession, Acceptor, HandlerRegistrationError, RPCHandlers, SessionHandlers, createAcceptor, } from "./acceptor.js";
|
|
5
|
+
export type { AcceptorListener, AcceptorOptions, AuthorizationDecision, RPCHandler, RPCHandlerResult, NotificationHandler, SessionHandlerOptions, } from "./acceptor.js";
|
|
6
|
+
export { TunnelRuntime, createTunnelRuntime } from "./tunnelRuntime.js";
|
|
7
|
+
export type { TunnelAuthorizationDecision, TunnelRuntimeListener, TunnelRuntimeOptions, } from "./tunnelRuntime.js";
|
|
8
|
+
export { AuthorizationRecord, AuthorizationResponse, TunnelAuthorizationResponse, ControlPlaneError, EndpointSet, IssuedArtifact, Issuer, RuntimeAuthorizationRequest, authorizeRuntime, authorizeTunnelRuntime, createEndpointSet, parseAuthorizationRecord, parseRuntimeAuthorizationRequest, rejectRuntime, rejectTunnelRuntime, } from "./controlplane.js";
|
|
9
|
+
export type { ArtifactMetadata, ControlPlaneErrorCode, DirectIssueOptions, IssuedTunnelPair, Scope, SessionOptions as ControlPlaneSessionOptions, TunnelIssueOptions, } from "./controlplane.js";
|
|
10
|
+
export { ProxyServer, ProxyServerError } from "./proxyServer.js";
|
|
11
|
+
export type { ProxyServerOptions, StreamHandlerRegistrar } from "./proxyServer.js";
|
package/dist/node/v2.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "../v2/index.js";
|
|
2
|
+
export { connect, createConnectionController } from "./connectSession.js";
|
|
3
|
+
export { AcceptedSession, Acceptor, HandlerRegistrationError, RPCHandlers, SessionHandlers, createAcceptor, } from "./acceptor.js";
|
|
4
|
+
export { TunnelRuntime, createTunnelRuntime } from "./tunnelRuntime.js";
|
|
5
|
+
export { AuthorizationRecord, AuthorizationResponse, TunnelAuthorizationResponse, ControlPlaneError, EndpointSet, IssuedArtifact, Issuer, RuntimeAuthorizationRequest, authorizeRuntime, authorizeTunnelRuntime, createEndpointSet, parseAuthorizationRecord, parseRuntimeAuthorizationRequest, rejectRuntime, rejectTunnelRuntime, } from "./controlplane.js";
|
|
6
|
+
export { ProxyServer, ProxyServerError } from "./proxyServer.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CarrierSessionV3 } from "../v3/carrier.js";
|
|
2
|
+
import type { PathKind } from "../v3/contract.js";
|
|
3
|
+
export type NodeWebSocketListenerOptionsV3 = Readonly<{
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
path: PathKind;
|
|
7
|
+
tls: Readonly<{
|
|
8
|
+
certificate: string;
|
|
9
|
+
privateKey: string;
|
|
10
|
+
}>;
|
|
11
|
+
allowedOrigins: readonly string[];
|
|
12
|
+
inboundBidirectionalStreamCapacity: number;
|
|
13
|
+
maxPendingSessions?: number;
|
|
14
|
+
pendingSessionTimeoutMs?: number;
|
|
15
|
+
cleanupTimeoutMs?: number;
|
|
16
|
+
}>;
|
|
17
|
+
export type NodeWebSocketListenerV3 = Readonly<{
|
|
18
|
+
address(): Readonly<{
|
|
19
|
+
host: string;
|
|
20
|
+
port: number;
|
|
21
|
+
}>;
|
|
22
|
+
accept(options?: Readonly<{
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}>): Promise<CarrierSessionV3>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function startNodeWebSocketListenerV3(options: NodeWebSocketListenerOptionsV3): Promise<NodeWebSocketListenerV3>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createServer as createHTTPSServer } from "node:https";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { constants } from "node:crypto";
|
|
4
|
+
import { createServerWebSocketCarrierSessionV3 } from "../v3/webSocketCarrier.js";
|
|
5
|
+
import { WebSocketBinaryTransport } from "../ws-client/binaryTransport.js";
|
|
6
|
+
import { defaultWsMaxPayload } from "./wsDefaults.js";
|
|
7
|
+
import { FLOWERSEC_V3_PATHS, websocketSubprotocolForPathV3, } from "../v3/transportConstants.js";
|
|
8
|
+
export async function startNodeWebSocketListenerV3(options) {
|
|
9
|
+
validateOptions(options);
|
|
10
|
+
const maxPendingSessions = options.maxPendingSessions ?? 1_024;
|
|
11
|
+
const pendingSessionTimeoutMs = options.pendingSessionTimeoutMs ?? 10_000;
|
|
12
|
+
const cleanupTimeoutMs = options.cleanupTimeoutMs ?? 2_000;
|
|
13
|
+
const protocol = websocketSubprotocolForPathV3(options.path);
|
|
14
|
+
const endpoint = FLOWERSEC_V3_PATHS.websocket[options.path];
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const wsModule = require("ws");
|
|
17
|
+
const WebSocketServer = wsModule.WebSocketServer;
|
|
18
|
+
const server = createHTTPSServer({
|
|
19
|
+
key: options.tls.privateKey,
|
|
20
|
+
cert: options.tls.certificate,
|
|
21
|
+
minVersion: "TLSv1.3",
|
|
22
|
+
maxVersion: "TLSv1.3",
|
|
23
|
+
secureOptions: constants.SSL_OP_NO_TICKET,
|
|
24
|
+
});
|
|
25
|
+
const sessions = new SessionQueueV3(maxPendingSessions, pendingSessionTimeoutMs);
|
|
26
|
+
const sockets = new Set();
|
|
27
|
+
const networkSockets = new Set();
|
|
28
|
+
const wss = new WebSocketServer({
|
|
29
|
+
noServer: true,
|
|
30
|
+
perMessageDeflate: false,
|
|
31
|
+
maxPayload: defaultWsMaxPayload({}),
|
|
32
|
+
handleProtocols(protocols) { return protocols.size === 1 && protocols.has(protocol) ? protocol : false; },
|
|
33
|
+
});
|
|
34
|
+
server.on("connection", (socket) => {
|
|
35
|
+
networkSockets.add(socket);
|
|
36
|
+
socket.once("close", () => networkSockets.delete(socket));
|
|
37
|
+
});
|
|
38
|
+
server.on("upgrade", (request, socket, head) => {
|
|
39
|
+
const origin = request.headers.origin;
|
|
40
|
+
const requested = request.headers["sec-websocket-protocol"];
|
|
41
|
+
if (request.url !== endpoint || typeof origin !== "string" ||
|
|
42
|
+
!options.allowedOrigins.includes(origin) || requested !== protocol) {
|
|
43
|
+
socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
|
|
44
|
+
socket.destroy();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
wss.handleUpgrade(request, socket, head, (webSocket) => wss.emit("connection", webSocket));
|
|
48
|
+
});
|
|
49
|
+
wss.on("connection", (socket) => {
|
|
50
|
+
sockets.add(socket);
|
|
51
|
+
const session = createServerWebSocketCarrierSessionV3(new WebSocketBinaryTransport(socket), {
|
|
52
|
+
path: options.path,
|
|
53
|
+
inboundBidirectionalStreamCapacity: options.inboundBidirectionalStreamCapacity,
|
|
54
|
+
});
|
|
55
|
+
socket.once("close", () => {
|
|
56
|
+
sockets.delete(socket);
|
|
57
|
+
sessions.remove(session);
|
|
58
|
+
});
|
|
59
|
+
sessions.push(session);
|
|
60
|
+
});
|
|
61
|
+
await new Promise((resolve, reject) => {
|
|
62
|
+
server.once("error", reject);
|
|
63
|
+
server.listen(options.port, options.host, resolve);
|
|
64
|
+
});
|
|
65
|
+
let closePromise;
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
address() {
|
|
68
|
+
const address = server.address();
|
|
69
|
+
if (address === null || typeof address === "string")
|
|
70
|
+
throw new Error("Node WebSocket v3 listener is not listening");
|
|
71
|
+
return Object.freeze({ host: address.address, port: address.port });
|
|
72
|
+
},
|
|
73
|
+
async accept(acceptOptions = {}) { return await sessions.shift(acceptOptions.signal); },
|
|
74
|
+
async close() {
|
|
75
|
+
closePromise ??= closeListener();
|
|
76
|
+
await closePromise;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
async function closeListener() {
|
|
80
|
+
sessions.close();
|
|
81
|
+
for (const socket of sockets)
|
|
82
|
+
socket.close();
|
|
83
|
+
const graceful = Promise.all([
|
|
84
|
+
new Promise((resolve, reject) => {
|
|
85
|
+
wss.close((error) => error === undefined ? resolve() : reject(error));
|
|
86
|
+
}),
|
|
87
|
+
new Promise((resolve, reject) => {
|
|
88
|
+
server.close((error) => error === undefined ? resolve() : reject(error));
|
|
89
|
+
}),
|
|
90
|
+
]);
|
|
91
|
+
let timer;
|
|
92
|
+
let timedOut;
|
|
93
|
+
try {
|
|
94
|
+
timedOut = await Promise.race([
|
|
95
|
+
graceful.then(() => false),
|
|
96
|
+
new Promise((resolve) => {
|
|
97
|
+
timer = setTimeout(() => resolve(true), cleanupTimeoutMs);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
}),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
if (timer !== undefined)
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
if (!timedOut)
|
|
107
|
+
return;
|
|
108
|
+
for (const socket of sockets)
|
|
109
|
+
socket.terminate();
|
|
110
|
+
for (const socket of networkSockets)
|
|
111
|
+
socket.destroy();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function validateOptions(options) {
|
|
115
|
+
if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65_535 ||
|
|
116
|
+
options.allowedOrigins.length === 0 || new Set(options.allowedOrigins).size !== options.allowedOrigins.length ||
|
|
117
|
+
options.allowedOrigins.some((origin) => !validOrigin(origin)) ||
|
|
118
|
+
!Number.isInteger(options.inboundBidirectionalStreamCapacity) ||
|
|
119
|
+
options.inboundBidirectionalStreamCapacity < 3 || options.inboundBidirectionalStreamCapacity > 130 ||
|
|
120
|
+
(options.maxPendingSessions !== undefined && (!Number.isSafeInteger(options.maxPendingSessions) || options.maxPendingSessions < 1 || options.maxPendingSessions > 1_024)) ||
|
|
121
|
+
(options.pendingSessionTimeoutMs !== undefined && (!Number.isSafeInteger(options.pendingSessionTimeoutMs) || options.pendingSessionTimeoutMs < 1 || options.pendingSessionTimeoutMs > 600_000)) ||
|
|
122
|
+
(options.cleanupTimeoutMs !== undefined && (!Number.isSafeInteger(options.cleanupTimeoutMs) || options.cleanupTimeoutMs < 1 || options.cleanupTimeoutMs > 600_000)) ||
|
|
123
|
+
options.tls.certificate.length === 0 || options.tls.privateKey.length === 0) {
|
|
124
|
+
throw new TypeError("invalid Node WebSocket v3 listener options");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function validOrigin(raw) {
|
|
128
|
+
try {
|
|
129
|
+
const value = new URL(raw);
|
|
130
|
+
return (value.protocol === "https:" || value.protocol === "http:") && value.origin === raw &&
|
|
131
|
+
value.username === "" && value.password === "";
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
class SessionQueueV3 {
|
|
138
|
+
maximum;
|
|
139
|
+
timeoutMs;
|
|
140
|
+
values = [];
|
|
141
|
+
waiters = new Set();
|
|
142
|
+
closed = false;
|
|
143
|
+
constructor(maximum, timeoutMs) {
|
|
144
|
+
this.maximum = maximum;
|
|
145
|
+
this.timeoutMs = timeoutMs;
|
|
146
|
+
}
|
|
147
|
+
push(value) {
|
|
148
|
+
if (this.closed) {
|
|
149
|
+
value.abort({ code: 1, reason: "WebSocket v3 listener closed" });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const waiter = this.waiters.values().next().value;
|
|
153
|
+
if (waiter !== undefined) {
|
|
154
|
+
this.waiters.delete(waiter);
|
|
155
|
+
waiter.resolve(value);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (this.values.length >= this.maximum) {
|
|
159
|
+
value.abort({ code: 1, reason: "WebSocket v3 pending capacity exceeded" });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const entry = {
|
|
163
|
+
value,
|
|
164
|
+
timer: setTimeout(() => {
|
|
165
|
+
if (this.remove(value)) {
|
|
166
|
+
value.abort({ code: 1, reason: "WebSocket v3 pending admission timed out" });
|
|
167
|
+
}
|
|
168
|
+
}, this.timeoutMs),
|
|
169
|
+
};
|
|
170
|
+
this.values.push(entry);
|
|
171
|
+
}
|
|
172
|
+
remove(value) {
|
|
173
|
+
const index = this.values.findIndex((entry) => entry.value === value);
|
|
174
|
+
if (index < 0)
|
|
175
|
+
return false;
|
|
176
|
+
const [entry] = this.values.splice(index, 1);
|
|
177
|
+
if (entry !== undefined)
|
|
178
|
+
clearTimeout(entry.timer);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
async shift(signal) {
|
|
182
|
+
if (signal?.aborted === true)
|
|
183
|
+
throw signal.reason ?? new Error("accept canceled");
|
|
184
|
+
const entry = this.values.shift();
|
|
185
|
+
if (entry !== undefined) {
|
|
186
|
+
clearTimeout(entry.timer);
|
|
187
|
+
return entry.value;
|
|
188
|
+
}
|
|
189
|
+
if (this.closed)
|
|
190
|
+
throw new Error("WebSocket v3 listener is closed");
|
|
191
|
+
return await new Promise((resolve, reject) => {
|
|
192
|
+
const waiter = {
|
|
193
|
+
resolve: (session) => { cleanup(); resolve(session); },
|
|
194
|
+
reject: (error) => { cleanup(); reject(error); },
|
|
195
|
+
};
|
|
196
|
+
const abort = () => {
|
|
197
|
+
this.waiters.delete(waiter);
|
|
198
|
+
reject(signal?.reason ?? new Error("accept canceled"));
|
|
199
|
+
};
|
|
200
|
+
const cleanup = () => signal?.removeEventListener("abort", abort);
|
|
201
|
+
this.waiters.add(waiter);
|
|
202
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
close() {
|
|
206
|
+
if (this.closed)
|
|
207
|
+
return;
|
|
208
|
+
this.closed = true;
|
|
209
|
+
const error = new Error("WebSocket v3 listener is closed");
|
|
210
|
+
for (const waiter of this.waiters)
|
|
211
|
+
waiter.reject(error);
|
|
212
|
+
this.waiters.clear();
|
|
213
|
+
for (const entry of this.values) {
|
|
214
|
+
clearTimeout(entry.timer);
|
|
215
|
+
entry.value.abort({ code: 1, reason: "WebSocket v3 listener closed" });
|
|
216
|
+
}
|
|
217
|
+
this.values.length = 0;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -18,3 +18,6 @@ export declare class StreamHandlers {
|
|
|
18
18
|
handleStream(kind: string, handler: StreamHandler): void;
|
|
19
19
|
serve(session: Session, options?: OperationOptions): Promise<void>;
|
|
20
20
|
}
|
|
21
|
+
/** Explicit-compatibility application-stream handlers. */
|
|
22
|
+
export declare class LegacyStreamHandlers extends StreamHandlers {
|
|
23
|
+
}
|
|
@@ -11,6 +11,7 @@ export class HandlerRegistrationError extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
const streamHandlerStates = new WeakMap();
|
|
14
|
+
const RESERVED_RPC_KINDS = new Set(["flowersec.rpc.v2", "flowersec.rpc.v3"]);
|
|
14
15
|
/** Carrier-neutral application-stream handlers for any established Session. */
|
|
15
16
|
export class StreamHandlers {
|
|
16
17
|
constructor(options = {}) {
|
|
@@ -22,6 +23,7 @@ export class StreamHandlers {
|
|
|
22
23
|
}
|
|
23
24
|
streamHandlerStates.set(this, {
|
|
24
25
|
maxConcurrentStreams: maximum,
|
|
26
|
+
reservedRPCKinds: RESERVED_RPC_KINDS,
|
|
25
27
|
streams: new Map(),
|
|
26
28
|
frozen: false,
|
|
27
29
|
});
|
|
@@ -36,6 +38,9 @@ export class StreamHandlers {
|
|
|
36
38
|
await serveFrozenStreamHandlers(freezeStreamHandlers(this), session, options, async () => await session.close());
|
|
37
39
|
}
|
|
38
40
|
}
|
|
41
|
+
/** Explicit-compatibility application-stream handlers. */
|
|
42
|
+
export class LegacyStreamHandlers extends StreamHandlers {
|
|
43
|
+
}
|
|
39
44
|
function mutableStreamHandlerState(handlers) {
|
|
40
45
|
const state = streamHandlerStates.get(handlers);
|
|
41
46
|
if (state === undefined)
|
|
@@ -50,7 +55,7 @@ function registerIntoState(state, entries) {
|
|
|
50
55
|
const pending = new Set();
|
|
51
56
|
for (const [kind, handler] of entries) {
|
|
52
57
|
if (!validApplicationStreamKind(kind) ||
|
|
53
|
-
|
|
58
|
+
state.reservedRPCKinds.has(kind) ||
|
|
54
59
|
typeof handler !== "function") {
|
|
55
60
|
throw new HandlerRegistrationError("invalid_handler");
|
|
56
61
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type { ByteStream, IncomingStream, JsonObject, JsonPrimitive, JsonValue, OperationOptions, RpcPeer, RpcResult, SessionErrorCode, StreamOpenOptions, UnreliableMessageChannel, UnreliableMessageSendOptions, UnreliableMessageSendResult, SessionTermination, Session, } from "../public/contract.js";
|
|
2
|
+
export { SessionError, UnreliableMessageError } from "../public/contract.js";
|
|
3
|
+
export { createStreamMetadata, StreamMetadataError } from "../public/streamMetadata.js";
|
|
4
|
+
export type { StreamMetadata } from "../public/streamMetadata.js";
|
|
5
|
+
export { HandlerRegistrationError, LegacyStreamHandlers as StreamHandlers, } from "../public/streamHandlers.js";
|
|
6
|
+
export type { StreamHandler, StreamHandlerOptions, } from "../public/streamHandlers.js";
|
|
7
|
+
export { ArtifactLeaseError, createArtifactLease, } from "../public/artifactLease.js";
|
|
8
|
+
export type { ArtifactLease } from "../public/artifactLease.js";
|
|
9
|
+
export { Artifact, ArtifactError, parseArtifact } from "../public/artifact.js";
|
|
10
|
+
export type { ArtifactErrorCode } from "../public/artifact.js";
|
|
11
|
+
export type { ConnectErrorCode } from "../public/connectError.js";
|
|
12
|
+
export { ConnectError } from "../public/connectError.js";
|
|
13
|
+
export { ConnectionControllerError } from "../connectionController.js";
|
|
14
|
+
export type { ArtifactSource, ArtifactSourceResult, ConnectionController, ConnectionControllerFailure, ConnectionControllerOptions, ConnectionSnapshot, ConnectionState, RetryDisposition, } from "../connectionController.js";
|
package/dist/v2/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { SessionError, UnreliableMessageError } from "../public/contract.js";
|
|
2
|
+
export { createStreamMetadata, StreamMetadataError } from "../public/streamMetadata.js";
|
|
3
|
+
export { HandlerRegistrationError, LegacyStreamHandlers as StreamHandlers, } from "../public/streamHandlers.js";
|
|
4
|
+
export { ArtifactLeaseError, createArtifactLease, } from "../public/artifactLease.js";
|
|
5
|
+
export { Artifact, ArtifactError, parseArtifact } from "../public/artifact.js";
|
|
6
|
+
export { ConnectError } from "../public/connectError.js";
|
|
7
|
+
export { ConnectionControllerError } from "../connectionController.js";
|
package/dist/v2/protocol.js
CHANGED
|
@@ -616,68 +616,59 @@ export function canonicalStreamMetadataJSONV2Internal(value) {
|
|
|
616
616
|
if (!isJSONObject(value))
|
|
617
617
|
throw new ProtocolV2Error("OPEN metadata root must be an object");
|
|
618
618
|
const state = { nodes: -1 };
|
|
619
|
-
|
|
620
|
-
const canonical = canonicalJSONString(value);
|
|
619
|
+
const canonical = canonicalValidatedMetadataJSONString(value, 1, state);
|
|
621
620
|
if (encoder.encode(canonical).length > MAX_OPEN_METADATA_BYTES) {
|
|
622
621
|
throw new ProtocolV2Error("OPEN metadata is too large");
|
|
623
622
|
}
|
|
624
623
|
return canonical;
|
|
625
624
|
}
|
|
626
|
-
function
|
|
625
|
+
function canonicalValidatedMetadataJSONString(value, depth, state) {
|
|
627
626
|
if (depth > MAX_OPEN_METADATA_DEPTH)
|
|
628
627
|
throw new ProtocolV2Error("OPEN metadata exceeds depth limit");
|
|
629
628
|
state.nodes += 1;
|
|
630
629
|
if (state.nodes > MAX_OPEN_METADATA_NODES)
|
|
631
630
|
throw new ProtocolV2Error("OPEN metadata exceeds node limit");
|
|
632
|
-
if (value === null
|
|
633
|
-
return;
|
|
631
|
+
if (value === null)
|
|
632
|
+
return "null";
|
|
633
|
+
if (typeof value === "boolean")
|
|
634
|
+
return value ? "true" : "false";
|
|
634
635
|
if (typeof value === "number") {
|
|
635
|
-
if (!Number.isSafeInteger(value))
|
|
636
|
+
if (!Number.isSafeInteger(value) || Object.is(value, -0))
|
|
636
637
|
throw new ProtocolV2Error("OPEN metadata number is not an I-JSON safe integer");
|
|
637
|
-
return;
|
|
638
|
+
return value.toString(10);
|
|
638
639
|
}
|
|
639
640
|
if (typeof value === "string") {
|
|
640
641
|
if (!validOpenUnicodeString(value, MAX_OPEN_METADATA_STRING_BYTES, true)) {
|
|
641
642
|
throw new ProtocolV2Error("invalid OPEN metadata string");
|
|
642
643
|
}
|
|
643
|
-
return;
|
|
644
|
+
return quoteCanonicalJSONString(value);
|
|
644
645
|
}
|
|
645
646
|
if (Array.isArray(value)) {
|
|
646
|
-
|
|
647
|
+
const length = value.length;
|
|
648
|
+
if (length > MAX_OPEN_METADATA_ARRAY)
|
|
647
649
|
throw new ProtocolV2Error("OPEN metadata array is too large");
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
650
|
+
const items = new Array(length);
|
|
651
|
+
for (let index = 0; index < length; index++) {
|
|
652
|
+
const item = value[index];
|
|
653
|
+
items[index] = canonicalValidatedMetadataJSONString(item, depth + 1, state);
|
|
654
|
+
}
|
|
655
|
+
return `[${items.join(",")}]`;
|
|
651
656
|
}
|
|
652
657
|
if (!isJSONObject(value))
|
|
653
658
|
throw new ProtocolV2Error("unsupported OPEN metadata value");
|
|
654
|
-
const keys = Object.keys(value);
|
|
659
|
+
const keys = Object.keys(value).sort();
|
|
655
660
|
if (keys.length > MAX_OPEN_METADATA_KEYS)
|
|
656
661
|
throw new ProtocolV2Error("OPEN metadata object is too large");
|
|
662
|
+
const members = new Array(keys.length);
|
|
663
|
+
let index = 0;
|
|
657
664
|
for (const key of keys) {
|
|
658
665
|
if (!validOpenUnicodeString(key, MAX_OPEN_METADATA_KEY_BYTES, false)) {
|
|
659
666
|
throw new ProtocolV2Error("invalid OPEN metadata key");
|
|
660
667
|
}
|
|
661
|
-
|
|
668
|
+
const item = value[key];
|
|
669
|
+
members[index++] = `${quoteCanonicalJSONString(key)}:${canonicalValidatedMetadataJSONString(item, depth + 1, state)}`;
|
|
662
670
|
}
|
|
663
|
-
}
|
|
664
|
-
function canonicalJSONString(value) {
|
|
665
|
-
if (value === null)
|
|
666
|
-
return "null";
|
|
667
|
-
if (typeof value === "boolean")
|
|
668
|
-
return value ? "true" : "false";
|
|
669
|
-
if (typeof value === "number")
|
|
670
|
-
return value.toString(10);
|
|
671
|
-
if (typeof value === "string")
|
|
672
|
-
return quoteCanonicalJSONString(value);
|
|
673
|
-
if (Array.isArray(value))
|
|
674
|
-
return `[${value.map(canonicalJSONString).join(",")}]`;
|
|
675
|
-
if (!isJSONObject(value))
|
|
676
|
-
throw new ProtocolV2Error("unsupported OPEN metadata value");
|
|
677
|
-
return `{${Object.keys(value)
|
|
678
|
-
.sort()
|
|
679
|
-
.map((key) => `${quoteCanonicalJSONString(key)}:${canonicalJSONString(value[key])}`)
|
|
680
|
-
.join(",")}}`;
|
|
671
|
+
return `{${members.join(",")}}`;
|
|
681
672
|
}
|
|
682
673
|
function quoteCanonicalJSONString(value) {
|
|
683
674
|
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
package/dist/v2/session.d.ts
CHANGED
|
@@ -91,6 +91,7 @@ export declare class SessionV2 implements SessionV2Contract {
|
|
|
91
91
|
private controlReceiveEpoch;
|
|
92
92
|
private controlReceiveSequence;
|
|
93
93
|
private controlWriteTail;
|
|
94
|
+
private controlTerminalSealed;
|
|
94
95
|
private nextLogicalID;
|
|
95
96
|
private receivedGoAway;
|
|
96
97
|
private receivedGoAwayLastAccepted;
|
|
@@ -164,6 +165,10 @@ export declare class SessionV2 implements SessionV2Contract {
|
|
|
164
165
|
private acceptCarrierStreamAfterFSS2;
|
|
165
166
|
private resetInboundBeforeDelivery;
|
|
166
167
|
private sendControl;
|
|
168
|
+
private sendControlResponse;
|
|
169
|
+
private sendControlCleanup;
|
|
170
|
+
private sendTerminalControl;
|
|
171
|
+
private writeControl;
|
|
167
172
|
private readControl;
|
|
168
173
|
private controlLoop;
|
|
169
174
|
private closeOnce;
|
package/dist/v2/session.js
CHANGED
|
@@ -96,6 +96,7 @@ export class SessionV2 {
|
|
|
96
96
|
controlReceiveEpoch = 0;
|
|
97
97
|
controlReceiveSequence = 0n;
|
|
98
98
|
controlWriteTail = Promise.resolve();
|
|
99
|
+
controlTerminalSealed = false;
|
|
99
100
|
nextLogicalID;
|
|
100
101
|
receivedGoAway = false;
|
|
101
102
|
receivedGoAwayLastAccepted = 0n;
|
|
@@ -312,10 +313,11 @@ export class SessionV2 {
|
|
|
312
313
|
this.releaseStream(stream);
|
|
313
314
|
let controlConfirmed = false;
|
|
314
315
|
try {
|
|
315
|
-
await this.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
if (await this.sendControlCleanup(InnerTypeV2.StreamReset, idReason(stream.id, 6))) {
|
|
317
|
+
this.commitLocalReset(stream.id);
|
|
318
|
+
if (stream.hasRemoteFIN())
|
|
319
|
+
controlConfirmed = await this.confirmControlDelivery();
|
|
320
|
+
}
|
|
319
321
|
}
|
|
320
322
|
catch (cause) {
|
|
321
323
|
this.fail(asError(cause));
|
|
@@ -343,6 +345,8 @@ export class SessionV2 {
|
|
|
343
345
|
this.cleanupEpochRoots();
|
|
344
346
|
}
|
|
345
347
|
async confirmControlDelivery() {
|
|
348
|
+
if (this.lifecycle !== "open" || this.controlTerminalSealed)
|
|
349
|
+
return false;
|
|
346
350
|
const nonce = this.nextPing++;
|
|
347
351
|
const pending = deferred();
|
|
348
352
|
this.pings.set(nonce, pending);
|
|
@@ -567,33 +571,87 @@ export class SessionV2 {
|
|
|
567
571
|
async resetInboundBeforeDelivery(id, carrierStream) {
|
|
568
572
|
await carrierStream.reset().catch(() => undefined);
|
|
569
573
|
try {
|
|
570
|
-
await this.
|
|
571
|
-
|
|
574
|
+
if (await this.sendControlCleanup(InnerTypeV2.StreamReset, idReason(id, 3))) {
|
|
575
|
+
this.peerLedger.localResetCommitted(id);
|
|
576
|
+
}
|
|
572
577
|
}
|
|
573
578
|
catch (error) {
|
|
574
579
|
this.fail(asError(error));
|
|
575
580
|
}
|
|
576
581
|
}
|
|
577
582
|
async sendControl(type, payload, signal) {
|
|
583
|
+
if (this.controlTerminalSealed) {
|
|
584
|
+
throw this.terminalError ?? new SessionV2Error("closed", "Flowersec v2 control stream is sealed");
|
|
585
|
+
}
|
|
578
586
|
const task = this.controlWriteTail.then(async () => {
|
|
579
587
|
if (this.lifecycle === "closed")
|
|
580
588
|
throw this.terminalError ?? new SessionV2Error("closed", "Flowersec v2 session closed");
|
|
581
|
-
|
|
582
|
-
const roots = this.rootForSend(this.controlSendEpoch);
|
|
583
|
-
const material = deriveControlMaterial(roots.controlRoot, this.h3, this.sendDirection, this.controlSendEpoch);
|
|
584
|
-
const header = {
|
|
585
|
-
epoch: this.controlSendEpoch,
|
|
586
|
-
sequence: this.controlSendSequence,
|
|
587
|
-
ciphertextLength: inner.length + 16,
|
|
588
|
-
};
|
|
589
|
-
const ciphertext = sealRecord(this.config.suite, material, this.h3, 0n, this.sendDirection, header, inner);
|
|
590
|
-
this.controlSendSequence += 1n;
|
|
591
|
-
await writeAll(this.control, concat(encodeRecordHeader(header), ciphertext), signal);
|
|
592
|
-
this.markAuthenticatedActivity();
|
|
589
|
+
await this.writeControl(type, payload, signal);
|
|
593
590
|
});
|
|
594
591
|
this.controlWriteTail = task.catch(() => undefined);
|
|
595
592
|
await task;
|
|
596
593
|
}
|
|
594
|
+
async sendControlResponse(type, payload) {
|
|
595
|
+
if (this.lifecycle !== "open" || this.controlTerminalSealed)
|
|
596
|
+
return false;
|
|
597
|
+
const task = this.controlWriteTail.then(async () => {
|
|
598
|
+
if (this.lifecycle !== "open" || this.controlTerminalSealed)
|
|
599
|
+
return false;
|
|
600
|
+
await this.writeControl(type, payload);
|
|
601
|
+
return true;
|
|
602
|
+
});
|
|
603
|
+
this.controlWriteTail = task.then(() => undefined, () => undefined);
|
|
604
|
+
return await task;
|
|
605
|
+
}
|
|
606
|
+
async sendControlCleanup(type, payload) {
|
|
607
|
+
if (this.controlTerminalSealed || this.lifecycle === "closed")
|
|
608
|
+
return false;
|
|
609
|
+
const task = this.controlWriteTail.then(async () => {
|
|
610
|
+
if (this.lifecycle === "closed")
|
|
611
|
+
return false;
|
|
612
|
+
await this.writeControl(type, payload);
|
|
613
|
+
return true;
|
|
614
|
+
});
|
|
615
|
+
this.controlWriteTail = task.then(() => undefined, () => undefined);
|
|
616
|
+
return await task;
|
|
617
|
+
}
|
|
618
|
+
sendTerminalControl(reason) {
|
|
619
|
+
if (this.controlTerminalSealed)
|
|
620
|
+
return this.controlWriteTail;
|
|
621
|
+
this.controlTerminalSealed = true;
|
|
622
|
+
const task = this.controlWriteTail.then(async () => {
|
|
623
|
+
if (this.lifecycle === "closed")
|
|
624
|
+
throw this.terminalError ?? new SessionV2Error("closed", "Flowersec v2 session closed");
|
|
625
|
+
if (!this.sentGoAway) {
|
|
626
|
+
this.sentGoAway = true;
|
|
627
|
+
this.sentGoAwayLastAccepted = this.peerLedger.frontier;
|
|
628
|
+
this.sentGoAwayReason = reason;
|
|
629
|
+
}
|
|
630
|
+
await this.writeControl(InnerTypeV2.GoAway, idReason(this.sentGoAwayLastAccepted, this.sentGoAwayReason));
|
|
631
|
+
if (!this.sentSessionClose) {
|
|
632
|
+
this.sentSessionClose = true;
|
|
633
|
+
await this.writeControl(InnerTypeV2.SessionClose, Uint8Array.of(0, reason));
|
|
634
|
+
this.sessionCloseCommitted = true;
|
|
635
|
+
}
|
|
636
|
+
await this.control.closeWrite();
|
|
637
|
+
});
|
|
638
|
+
this.controlWriteTail = task.catch(() => undefined);
|
|
639
|
+
return task;
|
|
640
|
+
}
|
|
641
|
+
async writeControl(type, payload, signal) {
|
|
642
|
+
const inner = encodeInnerRecordV2(type, payload);
|
|
643
|
+
const roots = this.rootForSend(this.controlSendEpoch);
|
|
644
|
+
const material = deriveControlMaterial(roots.controlRoot, this.h3, this.sendDirection, this.controlSendEpoch);
|
|
645
|
+
const header = {
|
|
646
|
+
epoch: this.controlSendEpoch,
|
|
647
|
+
sequence: this.controlSendSequence,
|
|
648
|
+
ciphertextLength: inner.length + 16,
|
|
649
|
+
};
|
|
650
|
+
const ciphertext = sealRecord(this.config.suite, material, this.h3, 0n, this.sendDirection, header, inner);
|
|
651
|
+
this.controlSendSequence += 1n;
|
|
652
|
+
await writeAll(this.control, concat(encodeRecordHeader(header), ciphertext), signal);
|
|
653
|
+
this.markAuthenticatedActivity();
|
|
654
|
+
}
|
|
597
655
|
async readControl() {
|
|
598
656
|
const header = decodeRecordHeader(await this.controlReader.readExactly(24));
|
|
599
657
|
const cutover = header.epoch === this.controlReceiveEpoch + 1 &&
|
|
@@ -622,7 +680,7 @@ export class SessionV2 {
|
|
|
622
680
|
const record = await this.readControl();
|
|
623
681
|
switch (record.type) {
|
|
624
682
|
case InnerTypeV2.Ping:
|
|
625
|
-
await this.
|
|
683
|
+
await this.sendControlResponse(InnerTypeV2.Pong, record.payload);
|
|
626
684
|
break;
|
|
627
685
|
case InnerTypeV2.Pong:
|
|
628
686
|
this.pings.get(readU64(record.payload))?.resolve();
|
|
@@ -685,20 +743,10 @@ export class SessionV2 {
|
|
|
685
743
|
if (this.lifecycle === "closed")
|
|
686
744
|
return;
|
|
687
745
|
const closed = new SessionV2Error("closed", "Flowersec v2 session closed");
|
|
746
|
+
const terminal = this.sendTerminalControl(1);
|
|
688
747
|
const work = (async () => {
|
|
689
748
|
try {
|
|
690
|
-
await
|
|
691
|
-
}
|
|
692
|
-
catch {
|
|
693
|
-
// A prior GOAWAY boundary must not suppress the close response.
|
|
694
|
-
}
|
|
695
|
-
try {
|
|
696
|
-
if (!this.sentSessionClose) {
|
|
697
|
-
this.sentSessionClose = true;
|
|
698
|
-
await this.sendControl(InnerTypeV2.SessionClose, Uint8Array.of(0, 1));
|
|
699
|
-
this.sessionCloseCommitted = true;
|
|
700
|
-
await this.control.closeWrite();
|
|
701
|
-
}
|
|
749
|
+
await terminal;
|
|
702
750
|
}
|
|
703
751
|
catch {
|
|
704
752
|
// Closing is best-effort once the bounded shutdown has started.
|
|
@@ -853,7 +901,12 @@ export class SessionV2 {
|
|
|
853
901
|
}
|
|
854
902
|
const streams = [...this.streams.values()];
|
|
855
903
|
await Promise.all(streams.map(async (stream) => await stream.waitReceiveRekey(transition, nextEpoch, signal)));
|
|
856
|
-
await raceAbort(this.
|
|
904
|
+
const acknowledged = await raceAbort(this.sendControlResponse(InnerTypeV2.SessionKeyUpdateACK, payload), signal);
|
|
905
|
+
if (!acknowledged) {
|
|
906
|
+
this.pendingReceiveEpoch = undefined;
|
|
907
|
+
this.cleanupEpochRoots();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
857
910
|
throwIfAborted(signal);
|
|
858
911
|
this.receiveEpoch = nextEpoch;
|
|
859
912
|
this.receiveTransition = transition;
|
|
@@ -897,9 +950,10 @@ export class SessionV2 {
|
|
|
897
950
|
}
|
|
898
951
|
}
|
|
899
952
|
async commitOutboundReset(id) {
|
|
900
|
-
await this.
|
|
901
|
-
|
|
902
|
-
|
|
953
|
+
if (await this.sendControlCleanup(InnerTypeV2.StreamReset, idReason(id, 6))) {
|
|
954
|
+
this.outboundLedger.localResetCommitted(id);
|
|
955
|
+
this.notifyOutboundFrontierChanged();
|
|
956
|
+
}
|
|
903
957
|
}
|
|
904
958
|
commitLocalReset(id) {
|
|
905
959
|
if (this.isLocalLogicalID(id)) {
|
|
@@ -1026,6 +1080,7 @@ export class SessionV2 {
|
|
|
1026
1080
|
fail(error, abortCarrier = true) {
|
|
1027
1081
|
if (this.lifecycle === "closed")
|
|
1028
1082
|
return;
|
|
1083
|
+
this.controlTerminalSealed = true;
|
|
1029
1084
|
this.beginClosing();
|
|
1030
1085
|
const normalPeerCarrierClose = this.sessionCloseCommitted && error instanceof CarrierError && error.code === "closed";
|
|
1031
1086
|
if (normalPeerCarrierClose)
|