@floegence/flowersec-core 2.4.2 → 2.5.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 +15 -1
- package/dist/facade.d.ts +2 -0
- package/dist/facade.js +1 -0
- package/dist/node/acceptor.d.ts +6 -9
- package/dist/node/acceptor.js +11 -75
- package/dist/node/index.d.ts +2 -2
- package/dist/node/proxyServer.d.ts +3 -1
- package/dist/node/proxyServer.js +13 -5
- package/dist/public/streamHandlers.d.ts +20 -0
- package/dist/public/streamHandlers.js +124 -0
- package/dist/v2/protocol.js +11 -6
- package/package.json +3 -3
- package/sbom/cyclonedx.json +6 -6
- package/sbom/spdx.json +11 -11
package/README.md
CHANGED
|
@@ -65,6 +65,20 @@ const session = await controller.waitForSession();
|
|
|
65
65
|
The immutable callback definition applies to every generation, while each
|
|
66
66
|
Session gets a fresh router. Terminated Session work is never replayed.
|
|
67
67
|
|
|
68
|
+
### Application streams on any Session
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { StreamHandlers } from "@floegence/flowersec-core";
|
|
72
|
+
|
|
73
|
+
const streamHandlers = new StreamHandlers({ maxConcurrentStreams: 32 });
|
|
74
|
+
streamHandlers.handleStream("files/read", async (incoming) => serveFile(incoming));
|
|
75
|
+
await streamHandlers.serve(session);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The portable root, browser, and Node entrypoints share this dispatcher. The
|
|
79
|
+
sealed registrar used by Node `ProxyServer.register(...)` is exported only from
|
|
80
|
+
the Node entrypoint.
|
|
81
|
+
|
|
68
82
|
For the complete durable `ArtifactLease` spend workflow, see the
|
|
69
83
|
[TypeScript cookbook](../examples/ts/README.md). Node raw-QUIC-only artifacts
|
|
70
84
|
may omit `origin`; providing an absolute HTTP(S) origin enables WebSocket
|
|
@@ -98,7 +112,7 @@ The Browser and Node `connect(...)` operations are one-shot and never reconnect.
|
|
|
98
112
|
|
|
99
113
|
The controller has one scheduler and one in-flight attempt. Its states are `idle`, `connecting`, `connected`, `waiting`, `failed`, and `closed`; immutable snapshots expose `ConnectionSnapshot.retryDisposition` while the corresponding retry decision applies and clear it before a new attempt, after connection, and on close. Call `start()` once, observe snapshots with `subscribe(...)`, await an established session with `waitForSession(...)`, and use `retryNow()` only to wake a `waiting` controller. `close()` cancels acquisition, connection, and waiting before closing the current session.
|
|
100
114
|
|
|
101
|
-
Node `SessionHandlers` accept application stream kinds
|
|
115
|
+
`StreamHandlers` and Node `SessionHandlers` accept application stream kinds containing 1 through 128 canonical UTF-8 bytes, reject leading or trailing Unicode whitespace, controls, and unassigned scalars, and reserve `flowersec.rpc.v2` for Flowersec RPC. Successful handlers half-close their stream. A rejected handler Promise resets only that stream; the accept loop and unrelated streams continue.
|
|
102
116
|
|
|
103
117
|
Reliable streams apply bounded per-stream receive backpressure instead of buffering application data without limit. A slow consumer pauses carrier progress until reads release capacity; records retain carrier order, so a rekey behind backpressured DATA completes after the consumer resumes. `closeWrite()` sends the graceful FIN and keeps reads available. `reset()` and `close()` abort both directions. If a write is canceled or fails after its wire commit may have started, only that stream becomes terminal and cannot be reused.
|
|
104
118
|
|
package/dist/facade.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export type { ByteStream, IncomingStream, JsonObject, JsonPrimitive, JsonValue,
|
|
|
2
2
|
export { SessionError, UnreliableMessageError } from "./public/contract.js";
|
|
3
3
|
export { createStreamMetadata, StreamMetadataError } from "./public/streamMetadata.js";
|
|
4
4
|
export type { StreamMetadata } from "./public/streamMetadata.js";
|
|
5
|
+
export { HandlerRegistrationError, StreamHandlers, } from "./public/streamHandlers.js";
|
|
6
|
+
export type { StreamHandler, StreamHandlerOptions, } from "./public/streamHandlers.js";
|
|
5
7
|
export { ArtifactLeaseError, createArtifactLease, } from "./public/artifactLease.js";
|
|
6
8
|
export type { ArtifactLease } from "./public/artifactLease.js";
|
|
7
9
|
export { Artifact, ArtifactError, parseArtifact } from "./public/artifact.js";
|
package/dist/facade.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { SessionError, UnreliableMessageError } from "./public/contract.js";
|
|
2
2
|
export { createStreamMetadata, StreamMetadataError } from "./public/streamMetadata.js";
|
|
3
|
+
export { HandlerRegistrationError, StreamHandlers, } from "./public/streamHandlers.js";
|
|
3
4
|
export { ArtifactLeaseError, createArtifactLease, } from "./public/artifactLease.js";
|
|
4
5
|
export { Artifact, ArtifactError, parseArtifact } from "./public/artifact.js";
|
|
5
6
|
export { ConnectError } from "./public/connectError.js";
|
package/dist/node/acceptor.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type JsonValue, type OperationOptions, type Session } from "../public/contract.js";
|
|
2
2
|
import { type DirectAuthorizationDecision, type RuntimeAuthorizationRequest } from "./controlplane.js";
|
|
3
|
+
import { type StreamHandler, type StreamHandlerOptions } from "../public/streamHandlers.js";
|
|
3
4
|
export type { RuntimeAuthorizationRequest } from "./controlplane.js";
|
|
5
|
+
export { HandlerRegistrationError } from "../public/streamHandlers.js";
|
|
6
|
+
export type { StreamHandler } from "../public/streamHandlers.js";
|
|
4
7
|
export type AuthorizationDecision = DirectAuthorizationDecision;
|
|
5
8
|
export type RPCHandlerResult = Readonly<{
|
|
6
9
|
payload: JsonValue;
|
|
@@ -16,20 +19,14 @@ export type RPCHandler = (payload: JsonValue, request: Readonly<{
|
|
|
16
19
|
export type NotificationHandler = (payload: JsonValue, request: Readonly<{
|
|
17
20
|
typeId: number;
|
|
18
21
|
}>) => Promise<void> | void;
|
|
19
|
-
export type
|
|
20
|
-
export type SessionHandlerOptions = Readonly<{
|
|
21
|
-
maxConcurrentStreams?: number;
|
|
22
|
-
}>;
|
|
23
|
-
export declare class HandlerRegistrationError extends Error {
|
|
24
|
-
readonly code: "invalid_handler" | "already_registered" | "frozen";
|
|
25
|
-
constructor(code: "invalid_handler" | "already_registered" | "frozen");
|
|
26
|
-
}
|
|
22
|
+
export type SessionHandlerOptions = StreamHandlerOptions;
|
|
27
23
|
export declare class RPCHandlers {
|
|
28
24
|
constructor();
|
|
29
25
|
handleRPC(typeId: number, handler: RPCHandler): void;
|
|
30
26
|
handleNotification(typeId: number, handler: NotificationHandler): void;
|
|
31
27
|
}
|
|
32
28
|
export declare class SessionHandlers {
|
|
29
|
+
private readonly streamHandlerRegistrarBrand;
|
|
33
30
|
constructor(options?: SessionHandlerOptions);
|
|
34
31
|
handleRPC(typeId: number, handler: RPCHandler): void;
|
|
35
32
|
handleNotification(typeId: number, handler: NotificationHandler): void;
|
package/dist/node/acceptor.js
CHANGED
|
@@ -9,18 +9,9 @@ import { unwrapArtifact } from "../public/artifact.js";
|
|
|
9
9
|
import { SessionError, } from "../public/contract.js";
|
|
10
10
|
import { projectSessionV2 } from "../v2/publicSession.js";
|
|
11
11
|
import { runtimeAuthorizationRequestFromDecoded, } from "./controlplane.js";
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
import { HandlerRegistrationError, StreamHandlers, freezeStreamHandlers, registerStreamHandlersAtomically, serveFrozenStreamHandlers, } from "../public/streamHandlers.js";
|
|
13
|
+
export { HandlerRegistrationError } from "../public/streamHandlers.js";
|
|
14
14
|
const DEFAULT_CLEANUP_TIMEOUT_MS = 2_000;
|
|
15
|
-
const encoder = new TextEncoder();
|
|
16
|
-
export class HandlerRegistrationError extends Error {
|
|
17
|
-
code;
|
|
18
|
-
constructor(code) {
|
|
19
|
-
super(`Flowersec handler registration failed (code=${code})`);
|
|
20
|
-
this.code = code;
|
|
21
|
-
this.name = "HandlerRegistrationError";
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
15
|
export class RPCHandlers {
|
|
25
16
|
constructor() {
|
|
26
17
|
rpcHandlerStates.set(this, createRPCHandlerState());
|
|
@@ -34,16 +25,9 @@ export class RPCHandlers {
|
|
|
34
25
|
}
|
|
35
26
|
export class SessionHandlers {
|
|
36
27
|
constructor(options = {}) {
|
|
37
|
-
const maximum = options.maxConcurrentStreams ?? DEFAULT_MAX_CONCURRENT_STREAMS;
|
|
38
|
-
if (!Number.isSafeInteger(maximum) ||
|
|
39
|
-
maximum < 1 ||
|
|
40
|
-
maximum > MAX_CONCURRENT_STREAMS) {
|
|
41
|
-
throw new HandlerRegistrationError("invalid_handler");
|
|
42
|
-
}
|
|
43
28
|
sessionHandlerStates.set(this, {
|
|
44
|
-
maxConcurrentStreams: maximum,
|
|
45
29
|
rpc: createRPCHandlerState(),
|
|
46
|
-
streams: new
|
|
30
|
+
streams: new StreamHandlers(options),
|
|
47
31
|
frozen: false,
|
|
48
32
|
});
|
|
49
33
|
}
|
|
@@ -57,15 +41,7 @@ export class SessionHandlers {
|
|
|
57
41
|
}
|
|
58
42
|
handleStream(kind, handler) {
|
|
59
43
|
const state = mutableSessionHandlerState(this);
|
|
60
|
-
|
|
61
|
-
encoder.encode(kind).length > 255 ||
|
|
62
|
-
kind === "flowersec.rpc.v2" ||
|
|
63
|
-
typeof handler !== "function") {
|
|
64
|
-
throw new HandlerRegistrationError("invalid_handler");
|
|
65
|
-
}
|
|
66
|
-
if (state.streams.has(kind))
|
|
67
|
-
throw new HandlerRegistrationError("already_registered");
|
|
68
|
-
state.streams.set(kind, handler);
|
|
44
|
+
state.streams.handleStream(kind, handler);
|
|
69
45
|
}
|
|
70
46
|
}
|
|
71
47
|
const rpcHandlerStates = new WeakMap();
|
|
@@ -89,6 +65,11 @@ function mutableSessionHandlerState(handlers) {
|
|
|
89
65
|
throw new HandlerRegistrationError("frozen");
|
|
90
66
|
return state;
|
|
91
67
|
}
|
|
68
|
+
/** @internal */
|
|
69
|
+
export function registerSessionStreamHandlersAtomically(handlers, entries) {
|
|
70
|
+
const state = mutableSessionHandlerState(handlers);
|
|
71
|
+
registerStreamHandlersAtomically(state.streams, entries);
|
|
72
|
+
}
|
|
92
73
|
function registerRPC(state, typeId, handler) {
|
|
93
74
|
validateRPCRegistration(typeId, handler);
|
|
94
75
|
if (state.requests.has(typeId) || state.notifications.has(typeId)) {
|
|
@@ -162,63 +143,18 @@ function freezeSessionHandlers(handlers) {
|
|
|
162
143
|
state.frozen = true;
|
|
163
144
|
state.snapshot = Object.freeze({
|
|
164
145
|
rpc: freezeRPCHandlerState(state.rpc),
|
|
165
|
-
|
|
166
|
-
streams: new Map(state.streams),
|
|
146
|
+
streams: freezeStreamHandlers(state.streams),
|
|
167
147
|
});
|
|
168
148
|
return state.snapshot;
|
|
169
149
|
}
|
|
170
|
-
/** @internal */
|
|
171
|
-
export function registerSessionStreamsAtomically(handlers, entries) {
|
|
172
|
-
const state = mutableSessionHandlerState(handlers);
|
|
173
|
-
const pending = new Set();
|
|
174
|
-
for (const [kind, handler] of entries) {
|
|
175
|
-
if (kind.length < 1 ||
|
|
176
|
-
encoder.encode(kind).length > 255 ||
|
|
177
|
-
kind === "flowersec.rpc.v2" ||
|
|
178
|
-
typeof handler !== "function")
|
|
179
|
-
throw new HandlerRegistrationError("invalid_handler");
|
|
180
|
-
if (state.streams.has(kind) || pending.has(kind))
|
|
181
|
-
throw new HandlerRegistrationError("already_registered");
|
|
182
|
-
pending.add(kind);
|
|
183
|
-
}
|
|
184
|
-
for (const [kind, handler] of entries)
|
|
185
|
-
state.streams.set(kind, handler);
|
|
186
|
-
}
|
|
187
150
|
export class AcceptedSession {
|
|
188
151
|
constructor() { }
|
|
189
152
|
get session() {
|
|
190
153
|
return acceptedSessionState(this).session;
|
|
191
154
|
}
|
|
192
155
|
async serve(options = {}) {
|
|
193
|
-
const active = new Set();
|
|
194
156
|
const state = acceptedSessionState(this);
|
|
195
|
-
|
|
196
|
-
while (true) {
|
|
197
|
-
if (options.signal?.aborted)
|
|
198
|
-
throw new SessionError("canceled");
|
|
199
|
-
const incoming = await this.session.acceptStream(options);
|
|
200
|
-
const handler = state.handlers.streams.get(incoming.kind);
|
|
201
|
-
if (handler === undefined ||
|
|
202
|
-
active.size >= state.handlers.maxConcurrentStreams) {
|
|
203
|
-
await incoming.stream.reset();
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
const task = (async () => {
|
|
207
|
-
try {
|
|
208
|
-
await handler(incoming, options);
|
|
209
|
-
await incoming.stream.closeWrite();
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
await incoming.stream.reset().catch(() => undefined);
|
|
213
|
-
}
|
|
214
|
-
})().finally(() => active.delete(task));
|
|
215
|
-
active.add(task);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
await this.close().catch(() => undefined);
|
|
220
|
-
await Promise.allSettled(active);
|
|
221
|
-
}
|
|
157
|
+
await serveFrozenStreamHandlers(state.handlers.streams, this.session, options, async () => await this.close());
|
|
222
158
|
}
|
|
223
159
|
async close() {
|
|
224
160
|
const state = acceptedSessionState(this);
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export { connect, createConnectionController } from "./connectSession.js";
|
|
2
2
|
export { AcceptedSession, Acceptor, HandlerRegistrationError, RPCHandlers, SessionHandlers, createAcceptor, } from "./acceptor.js";
|
|
3
|
-
export type { AcceptorListener, AcceptorOptions, AuthorizationDecision, RPCHandler, RPCHandlerResult, NotificationHandler, SessionHandlerOptions,
|
|
3
|
+
export type { AcceptorListener, AcceptorOptions, AuthorizationDecision, RPCHandler, RPCHandlerResult, NotificationHandler, SessionHandlerOptions, } from "./acceptor.js";
|
|
4
4
|
export { TunnelRuntime, createTunnelRuntime } from "./tunnelRuntime.js";
|
|
5
5
|
export type { TunnelAuthorizationDecision, TunnelRuntimeListener, TunnelRuntimeOptions, } from "./tunnelRuntime.js";
|
|
6
6
|
export type { ConnectionControllerOptions, SessionOptions, SessionTLSOptions, } from "./connectSession.js";
|
|
7
7
|
export { AuthorizationRecord, AuthorizationResponse, TunnelAuthorizationResponse, ControlPlaneError, EndpointSet, IssuedArtifact, Issuer, RuntimeAuthorizationRequest, authorizeRuntime, authorizeTunnelRuntime, createEndpointSet, parseAuthorizationRecord, parseRuntimeAuthorizationRequest, rejectRuntime, rejectTunnelRuntime, } from "./controlplane.js";
|
|
8
8
|
export type { ArtifactMetadata, ControlPlaneErrorCode, DirectIssueOptions, IssuedTunnelPair, Scope, SessionOptions as ControlPlaneSessionOptions, TunnelIssueOptions, } from "./controlplane.js";
|
|
9
9
|
export { ProxyServer, ProxyServerError } from "./proxyServer.js";
|
|
10
|
-
export type { ProxyServerOptions } from "./proxyServer.js";
|
|
10
|
+
export type { ProxyServerOptions, StreamHandlerRegistrar } from "./proxyServer.js";
|
|
11
11
|
export * from "../facade.js";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { StreamHandlers } from "../public/streamHandlers.js";
|
|
1
2
|
import { SessionHandlers } from "./acceptor.js";
|
|
3
|
+
export type StreamHandlerRegistrar = StreamHandlers | SessionHandlers;
|
|
2
4
|
export type ProxyServerOptions = Readonly<{
|
|
3
5
|
upstream: string;
|
|
4
6
|
upstreamOrigin: string;
|
|
@@ -26,6 +28,6 @@ export declare class ProxyServerError extends Error {
|
|
|
26
28
|
export declare class ProxyServer {
|
|
27
29
|
#private;
|
|
28
30
|
constructor(options: ProxyServerOptions);
|
|
29
|
-
register(handlers:
|
|
31
|
+
register(handlers: StreamHandlerRegistrar): void;
|
|
30
32
|
close(): Promise<void>;
|
|
31
33
|
}
|
package/dist/node/proxyServer.js
CHANGED
|
@@ -2,7 +2,8 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { SessionError } from "../public/contract.js";
|
|
3
3
|
import { writeJsonFrame } from "../framing/jsonframe.js";
|
|
4
4
|
import { ProxyByteReader, writeAll } from "../proxy/stream.js";
|
|
5
|
-
import {
|
|
5
|
+
import { StreamHandlers, registerStreamHandlersAtomically, } from "../public/streamHandlers.js";
|
|
6
|
+
import { SessionHandlers, registerSessionStreamHandlersAtomically, } from "./acceptor.js";
|
|
6
7
|
const HTTP_KIND = "flowersec-proxy/http1";
|
|
7
8
|
const WS_KIND = "flowersec-proxy/ws";
|
|
8
9
|
const WIRE_VERSION = 1;
|
|
@@ -38,13 +39,20 @@ export class ProxyServer {
|
|
|
38
39
|
register(handlers) {
|
|
39
40
|
if (this.#closed)
|
|
40
41
|
throw new ProxyServerError("closed");
|
|
41
|
-
if (!(handlers instanceof SessionHandlers))
|
|
42
|
-
throw new ProxyServerError("handler_registration");
|
|
43
42
|
try {
|
|
44
|
-
|
|
43
|
+
const registrations = [
|
|
45
44
|
[HTTP_KIND, this.#httpHandler()],
|
|
46
45
|
[WS_KIND, this.#webSocketHandler()],
|
|
47
|
-
]
|
|
46
|
+
];
|
|
47
|
+
if (handlers instanceof StreamHandlers) {
|
|
48
|
+
registerStreamHandlersAtomically(handlers, registrations);
|
|
49
|
+
}
|
|
50
|
+
else if (handlers instanceof SessionHandlers) {
|
|
51
|
+
registerSessionStreamHandlersAtomically(handlers, registrations);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new TypeError("invalid Flowersec stream handler registrar");
|
|
55
|
+
}
|
|
48
56
|
}
|
|
49
57
|
catch (error) {
|
|
50
58
|
this.#report(error);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type IncomingStream, type OperationOptions, type Session } from "./contract.js";
|
|
2
|
+
export type StreamHandler = (incoming: IncomingStream, options: OperationOptions) => Promise<void>;
|
|
3
|
+
export type StreamHandlerOptions = Readonly<{
|
|
4
|
+
maxConcurrentStreams?: number;
|
|
5
|
+
}>;
|
|
6
|
+
export declare class HandlerRegistrationError extends Error {
|
|
7
|
+
readonly code: "invalid_handler" | "already_registered" | "frozen";
|
|
8
|
+
constructor(code: "invalid_handler" | "already_registered" | "frozen");
|
|
9
|
+
}
|
|
10
|
+
export type FrozenStreamHandlers = Readonly<{
|
|
11
|
+
maxConcurrentStreams: number;
|
|
12
|
+
streams: ReadonlyMap<string, StreamHandler>;
|
|
13
|
+
}>;
|
|
14
|
+
/** Carrier-neutral application-stream handlers for any established Session. */
|
|
15
|
+
export declare class StreamHandlers {
|
|
16
|
+
private readonly streamHandlerRegistrarBrand;
|
|
17
|
+
constructor(options?: StreamHandlerOptions);
|
|
18
|
+
handleStream(kind: string, handler: StreamHandler): void;
|
|
19
|
+
serve(session: Session, options?: OperationOptions): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { SessionError, } from "./contract.js";
|
|
2
|
+
import { validApplicationStreamKind } from "../v2/protocol.js";
|
|
3
|
+
const DEFAULT_MAX_CONCURRENT_STREAMS = 64;
|
|
4
|
+
const MAX_CONCURRENT_STREAMS = 128;
|
|
5
|
+
export class HandlerRegistrationError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code) {
|
|
8
|
+
super(`Flowersec handler registration failed (code=${code})`);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = "HandlerRegistrationError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const streamHandlerStates = new WeakMap();
|
|
14
|
+
/** Carrier-neutral application-stream handlers for any established Session. */
|
|
15
|
+
export class StreamHandlers {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
const maximum = options.maxConcurrentStreams ?? DEFAULT_MAX_CONCURRENT_STREAMS;
|
|
18
|
+
if (!Number.isSafeInteger(maximum) ||
|
|
19
|
+
maximum < 1 ||
|
|
20
|
+
maximum > MAX_CONCURRENT_STREAMS) {
|
|
21
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
22
|
+
}
|
|
23
|
+
streamHandlerStates.set(this, {
|
|
24
|
+
maxConcurrentStreams: maximum,
|
|
25
|
+
streams: new Map(),
|
|
26
|
+
frozen: false,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
handleStream(kind, handler) {
|
|
30
|
+
registerStreamHandlersAtomically(this, [[kind, handler]]);
|
|
31
|
+
}
|
|
32
|
+
async serve(session, options = {}) {
|
|
33
|
+
if (session === null || typeof session !== "object") {
|
|
34
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
35
|
+
}
|
|
36
|
+
await serveFrozenStreamHandlers(freezeStreamHandlers(this), session, options, async () => await session.close());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function mutableStreamHandlerState(handlers) {
|
|
40
|
+
const state = streamHandlerStates.get(handlers);
|
|
41
|
+
if (state === undefined)
|
|
42
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
43
|
+
if (state.frozen)
|
|
44
|
+
throw new HandlerRegistrationError("frozen");
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
function registerIntoState(state, entries) {
|
|
48
|
+
if (entries.length === 0)
|
|
49
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
50
|
+
const pending = new Set();
|
|
51
|
+
for (const [kind, handler] of entries) {
|
|
52
|
+
if (!validApplicationStreamKind(kind) ||
|
|
53
|
+
kind === "flowersec.rpc.v2" ||
|
|
54
|
+
typeof handler !== "function") {
|
|
55
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
56
|
+
}
|
|
57
|
+
if (state.streams.has(kind) || pending.has(kind)) {
|
|
58
|
+
throw new HandlerRegistrationError("already_registered");
|
|
59
|
+
}
|
|
60
|
+
pending.add(kind);
|
|
61
|
+
}
|
|
62
|
+
for (const [kind, handler] of entries)
|
|
63
|
+
state.streams.set(kind, handler);
|
|
64
|
+
}
|
|
65
|
+
/** @internal */
|
|
66
|
+
export function registerStreamHandlersAtomically(handlers, entries) {
|
|
67
|
+
registerIntoState(mutableStreamHandlerState(handlers), entries);
|
|
68
|
+
}
|
|
69
|
+
/** @internal */
|
|
70
|
+
export function freezeStreamHandlers(handlers) {
|
|
71
|
+
const state = streamHandlerStates.get(handlers);
|
|
72
|
+
if (state === undefined)
|
|
73
|
+
throw new HandlerRegistrationError("invalid_handler");
|
|
74
|
+
if (state.snapshot !== undefined)
|
|
75
|
+
return state.snapshot;
|
|
76
|
+
state.frozen = true;
|
|
77
|
+
state.snapshot = Object.freeze({
|
|
78
|
+
maxConcurrentStreams: state.maxConcurrentStreams,
|
|
79
|
+
streams: new Map(state.streams),
|
|
80
|
+
});
|
|
81
|
+
return state.snapshot;
|
|
82
|
+
}
|
|
83
|
+
/** @internal */
|
|
84
|
+
export async function serveFrozenStreamHandlers(snapshot, session, options, close) {
|
|
85
|
+
const active = new Set();
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const abortFromCaller = () => controller.abort(options.signal?.reason);
|
|
88
|
+
if (options.signal?.aborted === true)
|
|
89
|
+
abortFromCaller();
|
|
90
|
+
else
|
|
91
|
+
options.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
92
|
+
const handlerOptions = Object.freeze({
|
|
93
|
+
signal: controller.signal,
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
while (true) {
|
|
97
|
+
if (controller.signal.aborted)
|
|
98
|
+
throw new SessionError("canceled");
|
|
99
|
+
const incoming = await session.acceptStream(handlerOptions);
|
|
100
|
+
const handler = snapshot.streams.get(incoming.kind);
|
|
101
|
+
if (handler === undefined ||
|
|
102
|
+
active.size >= snapshot.maxConcurrentStreams) {
|
|
103
|
+
await incoming.stream.reset();
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const task = (async () => {
|
|
107
|
+
try {
|
|
108
|
+
await handler(incoming, handlerOptions);
|
|
109
|
+
await incoming.stream.closeWrite();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
await incoming.stream.reset().catch(() => undefined);
|
|
113
|
+
}
|
|
114
|
+
})().finally(() => active.delete(task));
|
|
115
|
+
active.add(task);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
options.signal?.removeEventListener("abort", abortFromCaller);
|
|
120
|
+
controller.abort(new SessionError("closed"));
|
|
121
|
+
await close().catch(() => undefined);
|
|
122
|
+
await Promise.allSettled(active);
|
|
123
|
+
}
|
|
124
|
+
}
|
package/dist/v2/protocol.js
CHANGED
|
@@ -567,16 +567,21 @@ function bytesEqual(left, right) {
|
|
|
567
567
|
}
|
|
568
568
|
function validateOpenKind(value) {
|
|
569
569
|
const encoded = encoder.encode(value);
|
|
570
|
-
if (!
|
|
570
|
+
if (!validApplicationStreamKind(value)) {
|
|
571
571
|
throw new ProtocolV2Error("invalid OPEN kind");
|
|
572
572
|
}
|
|
573
|
-
const scalars = Array.from(value);
|
|
574
|
-
if (isUnicodeWhitespace(scalars[0].codePointAt(0)) ||
|
|
575
|
-
isUnicodeWhitespace(scalars.at(-1).codePointAt(0))) {
|
|
576
|
-
throw new ProtocolV2Error("OPEN kind has leading or trailing Unicode whitespace");
|
|
577
|
-
}
|
|
578
573
|
return encoded;
|
|
579
574
|
}
|
|
575
|
+
/** @internal */
|
|
576
|
+
export function validApplicationStreamKind(value) {
|
|
577
|
+
if (typeof value !== "string" ||
|
|
578
|
+
!validOpenUnicodeString(value, MAX_OPEN_KIND_BYTES, false)) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
const scalars = Array.from(value);
|
|
582
|
+
return !(isUnicodeWhitespace(scalars[0].codePointAt(0)) ||
|
|
583
|
+
isUnicodeWhitespace(scalars.at(-1).codePointAt(0)));
|
|
584
|
+
}
|
|
580
585
|
function canonicalMetadata(raw, allowEmpty) {
|
|
581
586
|
if (raw.length === 0 && allowEmpty)
|
|
582
587
|
return encoder.encode("{}");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/flowersec-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
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.
|
|
80
|
+
"@floegence/flowersec-node-native": "2.5.1"
|
|
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": "8debd2888634e8e7c88fd848ff5b08d22aa6d1f2"
|
|
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:7870295b-822d-59a5-8999-b6c86bdedcd2",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
7
|
"component": {
|
|
8
8
|
"type": "library",
|
|
9
9
|
"name": "@floegence/flowersec-core",
|
|
10
|
-
"version": "2.
|
|
11
|
-
"purl": "pkg:npm/%40floegence/flowersec-core@2.
|
|
12
|
-
"bom-ref": "pkg:npm/%40floegence/flowersec-core@2.
|
|
10
|
+
"version": "2.5.1",
|
|
11
|
+
"purl": "pkg:npm/%40floegence/flowersec-core@2.5.1",
|
|
12
|
+
"bom-ref": "pkg:npm/%40floegence/flowersec-core@2.5.1"
|
|
13
13
|
},
|
|
14
14
|
"properties": [
|
|
15
15
|
{
|
|
16
16
|
"name": "flowersec:source-inventory-sha256",
|
|
17
|
-
"value": "
|
|
17
|
+
"value": "aba1d1c470bb7af7da0d63211ac38f323b17e6e6f91337c0951508521d23b2b0"
|
|
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.
|
|
193
|
+
"ref": "pkg:npm/%40floegence/flowersec-core@2.5.1",
|
|
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/aba1d1c470bb7af7da0d63211ac38f323b17e6e6f91337c0951508521d23b2b0",
|
|
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.
|
|
16
|
+
"SPDXID": "SPDXRef-Package-8e2bb9ca6799b3b43dc6",
|
|
17
|
+
"versionInfo": "2.5.1",
|
|
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: aba1d1c470bb7af7da0d63211ac38f323b17e6e6f91337c0951508521d23b2b0",
|
|
24
24
|
"externalRefs": [
|
|
25
25
|
{
|
|
26
26
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
27
27
|
"referenceType": "purl",
|
|
28
|
-
"referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.
|
|
28
|
+
"referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.5.1"
|
|
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-8e2bb9ca6799b3b43dc6"
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
|
-
"spdxElementId": "SPDXRef-Package-
|
|
142
|
+
"spdxElementId": "SPDXRef-Package-8e2bb9ca6799b3b43dc6",
|
|
143
143
|
"relationshipType": "DEPENDS_ON",
|
|
144
144
|
"relatedSpdxElement": "SPDXRef-Package-5ce913a03239b02770fb"
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
|
-
"spdxElementId": "SPDXRef-Package-
|
|
147
|
+
"spdxElementId": "SPDXRef-Package-8e2bb9ca6799b3b43dc6",
|
|
148
148
|
"relationshipType": "DEPENDS_ON",
|
|
149
149
|
"relatedSpdxElement": "SPDXRef-Package-01009adf60db02c13634"
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
|
-
"spdxElementId": "SPDXRef-Package-
|
|
152
|
+
"spdxElementId": "SPDXRef-Package-8e2bb9ca6799b3b43dc6",
|
|
153
153
|
"relationshipType": "DEPENDS_ON",
|
|
154
154
|
"relatedSpdxElement": "SPDXRef-Package-8815b117c8a1d5f7eeb0"
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
|
-
"spdxElementId": "SPDXRef-Package-
|
|
157
|
+
"spdxElementId": "SPDXRef-Package-8e2bb9ca6799b3b43dc6",
|
|
158
158
|
"relationshipType": "DEPENDS_ON",
|
|
159
159
|
"relatedSpdxElement": "SPDXRef-Package-f8b45a289df643042b94"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
|
-
"spdxElementId": "SPDXRef-Package-
|
|
162
|
+
"spdxElementId": "SPDXRef-Package-8e2bb9ca6799b3b43dc6",
|
|
163
163
|
"relationshipType": "DEPENDS_ON",
|
|
164
164
|
"relatedSpdxElement": "SPDXRef-Package-b779412f685822663496"
|
|
165
165
|
},
|