@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.
Files changed (103) hide show
  1. package/README.md +46 -18
  2. package/dist/browser/connectSessionV3.d.ts +13 -0
  3. package/dist/browser/connectSessionV3.js +75 -0
  4. package/dist/browser/index.d.ts +3 -2
  5. package/dist/browser/index.js +2 -1
  6. package/dist/browser/v2.d.ts +3 -0
  7. package/dist/browser/v2.js +2 -0
  8. package/dist/facade.d.ts +15 -8
  9. package/dist/facade.js +5 -4
  10. package/dist/interop/proxyServerPeer.js +1 -1
  11. package/dist/interop/serverParityPeer.js +8 -1
  12. package/dist/node/acceptor.d.ts +10 -0
  13. package/dist/node/acceptor.js +45 -2
  14. package/dist/node/acceptorV3.d.ts +57 -0
  15. package/dist/node/acceptorV3.js +227 -0
  16. package/dist/node/connectSessionV3.d.ts +21 -0
  17. package/dist/node/connectSessionV3.js +73 -0
  18. package/dist/node/index.d.ts +15 -10
  19. package/dist/node/index.js +6 -5
  20. package/dist/node/nativeTransportAddon.d.ts +39 -1
  21. package/dist/node/nativeTransportAddon.js +29 -5
  22. package/dist/node/rawQuicAdapterV3.d.ts +10 -0
  23. package/dist/node/rawQuicAdapterV3.js +76 -0
  24. package/dist/node/rawQuicServerV3.d.ts +24 -0
  25. package/dist/node/rawQuicServerV3.js +34 -0
  26. package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
  27. package/dist/node/runtimeAuthorizationV3.js +126 -0
  28. package/dist/node/tunnelRuntimeV3.d.ts +57 -0
  29. package/dist/node/tunnelRuntimeV3.js +857 -0
  30. package/dist/node/v2.d.ts +11 -0
  31. package/dist/node/v2.js +6 -0
  32. package/dist/node/webSocketServerV3.d.ts +27 -0
  33. package/dist/node/webSocketServerV3.js +219 -0
  34. package/dist/public/streamHandlers.d.ts +3 -0
  35. package/dist/public/streamHandlers.js +6 -1
  36. package/dist/v2/index.d.ts +14 -0
  37. package/dist/v2/index.js +7 -0
  38. package/dist/v2/protocol.js +23 -32
  39. package/dist/v2/session.d.ts +5 -0
  40. package/dist/v2/session.js +90 -35
  41. package/dist/v3/admissionError.d.ts +4 -0
  42. package/dist/v3/admissionError.js +8 -0
  43. package/dist/v3/artifact.d.ts +145 -0
  44. package/dist/v3/artifact.js +1162 -0
  45. package/dist/v3/artifactLease.d.ts +17 -0
  46. package/dist/v3/artifactLease.js +109 -0
  47. package/dist/v3/browserRuntime.d.ts +37 -0
  48. package/dist/v3/browserRuntime.js +197 -0
  49. package/dist/v3/browserSessionRuntime.d.ts +2 -0
  50. package/dist/v3/browserSessionRuntime.js +21 -0
  51. package/dist/v3/capability.d.ts +32 -0
  52. package/dist/v3/capability.js +345 -0
  53. package/dist/v3/carrier.d.ts +84 -0
  54. package/dist/v3/carrier.js +411 -0
  55. package/dist/v3/connectionController.d.ts +87 -0
  56. package/dist/v3/connectionController.js +805 -0
  57. package/dist/v3/contract.d.ts +65 -0
  58. package/dist/v3/contract.js +3 -0
  59. package/dist/v3/controller.d.ts +43 -0
  60. package/dist/v3/controller.js +303 -0
  61. package/dist/v3/handshake.d.ts +81 -0
  62. package/dist/v3/handshake.js +471 -0
  63. package/dist/v3/jcs.d.ts +6 -0
  64. package/dist/v3/jcs.js +69 -0
  65. package/dist/v3/jsonPreflight.d.ts +1 -0
  66. package/dist/v3/jsonPreflight.js +251 -0
  67. package/dist/v3/nodeRuntime.d.ts +40 -0
  68. package/dist/v3/nodeRuntime.js +315 -0
  69. package/dist/v3/nodeSessionRuntime.d.ts +2 -0
  70. package/dist/v3/nodeSessionRuntime.js +13 -0
  71. package/dist/v3/protocol.d.ts +99 -0
  72. package/dist/v3/protocol.js +773 -0
  73. package/dist/v3/publicApi.d.ts +12 -0
  74. package/dist/v3/publicApi.js +46 -0
  75. package/dist/v3/publicSession.d.ts +1 -0
  76. package/dist/v3/publicSession.js +281 -0
  77. package/dist/v3/retryDisposition.d.ts +16 -0
  78. package/dist/v3/retryDisposition.js +30 -0
  79. package/dist/v3/runtimeAdapters.d.ts +9 -0
  80. package/dist/v3/runtimeAdapters.js +109 -0
  81. package/dist/v3/security.d.ts +34 -0
  82. package/dist/v3/security.js +100 -0
  83. package/dist/v3/serverAdmission.d.ts +38 -0
  84. package/dist/v3/serverAdmission.js +223 -0
  85. package/dist/v3/session.d.ts +318 -0
  86. package/dist/v3/session.js +2289 -0
  87. package/dist/v3/sessionConnector.d.ts +37 -0
  88. package/dist/v3/sessionConnector.js +440 -0
  89. package/dist/v3/streamLifetimeLedger.d.ts +31 -0
  90. package/dist/v3/streamLifetimeLedger.js +131 -0
  91. package/dist/v3/streamMetadata.d.ts +1 -0
  92. package/dist/v3/streamMetadata.js +7 -0
  93. package/dist/v3/transportConstants.d.ts +56 -0
  94. package/dist/v3/transportConstants.js +59 -0
  95. package/dist/v3/unreliableMessage.d.ts +44 -0
  96. package/dist/v3/unreliableMessage.js +284 -0
  97. package/dist/v3/webSocketCarrier.d.ts +31 -0
  98. package/dist/v3/webSocketCarrier.js +370 -0
  99. package/dist/v3/webTransportCarrier.d.ts +27 -0
  100. package/dist/v3/webTransportCarrier.js +534 -0
  101. package/package.json +4 -4
  102. package/sbom/cyclonedx.json +6 -6
  103. 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";
@@ -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
- kind === "flowersec.rpc.v2" ||
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";
@@ -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";
@@ -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
- validateMetadataValue(value, 1, state);
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 validateMetadataValue(value, depth, state) {
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 || typeof value === "boolean")
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
- if (value.length > MAX_OPEN_METADATA_ARRAY)
647
+ const length = value.length;
648
+ if (length > MAX_OPEN_METADATA_ARRAY)
647
649
  throw new ProtocolV2Error("OPEN metadata array is too large");
648
- for (const item of value)
649
- validateMetadataValue(item, depth + 1, state);
650
- return;
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
- validateMetadataValue(value[key], depth + 1, state);
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('"', '\\"')}"`;
@@ -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;
@@ -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.sendControl(InnerTypeV2.StreamReset, idReason(stream.id, 6));
316
- this.commitLocalReset(stream.id);
317
- if (stream.hasRemoteFIN())
318
- controlConfirmed = await this.confirmControlDelivery();
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.sendControl(InnerTypeV2.StreamReset, idReason(id, 3));
571
- this.peerLedger.localResetCommitted(id);
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
- const inner = encodeInnerRecordV2(type, payload);
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.sendControl(InnerTypeV2.Pong, record.payload);
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 this.sendGoAway(1);
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.sendControl(InnerTypeV2.SessionKeyUpdateACK, payload), signal);
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.sendControl(InnerTypeV2.StreamReset, idReason(id, 6));
901
- this.outboundLedger.localResetCommitted(id);
902
- this.notifyOutboundFrontierChanged();
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)
@@ -0,0 +1,4 @@
1
+ export declare class AdmissionSessionV3Error extends Error {
2
+ readonly reason: string;
3
+ constructor(reason: string, message: string);
4
+ }
@@ -0,0 +1,8 @@
1
+ export class AdmissionSessionV3Error extends Error {
2
+ reason;
3
+ constructor(reason, message) {
4
+ super(message);
5
+ this.reason = reason;
6
+ this.name = "AdmissionSessionV3Error";
7
+ }
8
+ }