@floegence/flowersec-core 2.4.1 → 2.5.0
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/framing/jsonframe.js +9 -2
- package/dist/node/acceptor.d.ts +6 -9
- package/dist/node/acceptor.js +89 -135
- package/dist/node/controlplane.js +6 -2
- 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/artifactLease.js +2 -9
- package/dist/public/streamHandlers.d.ts +20 -0
- package/dist/public/streamHandlers.js +124 -0
- package/dist/rpc/server.js +11 -2
- package/dist/rpc/validate.d.ts +2 -1
- package/dist/rpc/validate.js +25 -7
- 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";
|
|
@@ -2,7 +2,7 @@ import { readU32be, u32be } from "../utils/bin.js";
|
|
|
2
2
|
import { SDK_DEFAULTS } from "../defaults.js";
|
|
3
3
|
export const DEFAULT_MAX_JSON_FRAME_BYTES = SDK_DEFAULTS.rpc.maxJsonFrameBytes;
|
|
4
4
|
const te = new TextEncoder();
|
|
5
|
-
const td = new TextDecoder();
|
|
5
|
+
const td = new TextDecoder("utf-8", { fatal: true });
|
|
6
6
|
// JsonFramingError marks malformed or oversized frames.
|
|
7
7
|
export class JsonFramingError extends Error {
|
|
8
8
|
}
|
|
@@ -29,5 +29,12 @@ export async function readJsonFrame(readExactly, maxBytes) {
|
|
|
29
29
|
if (maxBytes > 0 && n > maxBytes)
|
|
30
30
|
throw new JsonFramingError("frame too large");
|
|
31
31
|
const payload = await read(n);
|
|
32
|
-
|
|
32
|
+
let text;
|
|
33
|
+
try {
|
|
34
|
+
text = td.decode(payload);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new JsonFramingError("invalid UTF-8");
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(text);
|
|
33
40
|
}
|
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RpcRouter } from "../rpc/server.js";
|
|
2
|
+
import { assertRpcError } from "../rpc/validate.js";
|
|
2
3
|
import { acceptReceivedSessionV2, receiveSessionAdmissionV2, rejectSessionAdmissionV2, } from "../connector/sessionAcceptor.js";
|
|
3
4
|
import { nodeSessionRuntimeV2 } from "./sessionRuntime.js";
|
|
4
5
|
import { startNodeWebSocketServer, } from "./webSocketServer.js";
|
|
@@ -8,18 +9,9 @@ import { unwrapArtifact } from "../public/artifact.js";
|
|
|
8
9
|
import { SessionError, } from "../public/contract.js";
|
|
9
10
|
import { projectSessionV2 } from "../v2/publicSession.js";
|
|
10
11
|
import { runtimeAuthorizationRequestFromDecoded, } from "./controlplane.js";
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
import { HandlerRegistrationError, StreamHandlers, freezeStreamHandlers, registerStreamHandlersAtomically, serveFrozenStreamHandlers, } from "../public/streamHandlers.js";
|
|
13
|
+
export { HandlerRegistrationError } from "../public/streamHandlers.js";
|
|
13
14
|
const DEFAULT_CLEANUP_TIMEOUT_MS = 2_000;
|
|
14
|
-
const encoder = new TextEncoder();
|
|
15
|
-
export class HandlerRegistrationError extends Error {
|
|
16
|
-
code;
|
|
17
|
-
constructor(code) {
|
|
18
|
-
super(`Flowersec handler registration failed (code=${code})`);
|
|
19
|
-
this.code = code;
|
|
20
|
-
this.name = "HandlerRegistrationError";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
15
|
export class RPCHandlers {
|
|
24
16
|
constructor() {
|
|
25
17
|
rpcHandlerStates.set(this, createRPCHandlerState());
|
|
@@ -33,16 +25,9 @@ export class RPCHandlers {
|
|
|
33
25
|
}
|
|
34
26
|
export class SessionHandlers {
|
|
35
27
|
constructor(options = {}) {
|
|
36
|
-
const maximum = options.maxConcurrentStreams ?? DEFAULT_MAX_CONCURRENT_STREAMS;
|
|
37
|
-
if (!Number.isSafeInteger(maximum) ||
|
|
38
|
-
maximum < 1 ||
|
|
39
|
-
maximum > MAX_CONCURRENT_STREAMS) {
|
|
40
|
-
throw new HandlerRegistrationError("invalid_handler");
|
|
41
|
-
}
|
|
42
28
|
sessionHandlerStates.set(this, {
|
|
43
|
-
maxConcurrentStreams: maximum,
|
|
44
29
|
rpc: createRPCHandlerState(),
|
|
45
|
-
streams: new
|
|
30
|
+
streams: new StreamHandlers(options),
|
|
46
31
|
frozen: false,
|
|
47
32
|
});
|
|
48
33
|
}
|
|
@@ -56,15 +41,7 @@ export class SessionHandlers {
|
|
|
56
41
|
}
|
|
57
42
|
handleStream(kind, handler) {
|
|
58
43
|
const state = mutableSessionHandlerState(this);
|
|
59
|
-
|
|
60
|
-
encoder.encode(kind).length > 255 ||
|
|
61
|
-
kind === "flowersec.rpc.v2" ||
|
|
62
|
-
typeof handler !== "function") {
|
|
63
|
-
throw new HandlerRegistrationError("invalid_handler");
|
|
64
|
-
}
|
|
65
|
-
if (state.streams.has(kind))
|
|
66
|
-
throw new HandlerRegistrationError("already_registered");
|
|
67
|
-
state.streams.set(kind, handler);
|
|
44
|
+
state.streams.handleStream(kind, handler);
|
|
68
45
|
}
|
|
69
46
|
}
|
|
70
47
|
const rpcHandlerStates = new WeakMap();
|
|
@@ -88,6 +65,11 @@ function mutableSessionHandlerState(handlers) {
|
|
|
88
65
|
throw new HandlerRegistrationError("frozen");
|
|
89
66
|
return state;
|
|
90
67
|
}
|
|
68
|
+
/** @internal */
|
|
69
|
+
export function registerSessionStreamHandlersAtomically(handlers, entries) {
|
|
70
|
+
const state = mutableSessionHandlerState(handlers);
|
|
71
|
+
registerStreamHandlersAtomically(state.streams, entries);
|
|
72
|
+
}
|
|
91
73
|
function registerRPC(state, typeId, handler) {
|
|
92
74
|
validateRPCRegistration(typeId, handler);
|
|
93
75
|
if (state.requests.has(typeId) || state.notifications.has(typeId)) {
|
|
@@ -161,63 +143,18 @@ function freezeSessionHandlers(handlers) {
|
|
|
161
143
|
state.frozen = true;
|
|
162
144
|
state.snapshot = Object.freeze({
|
|
163
145
|
rpc: freezeRPCHandlerState(state.rpc),
|
|
164
|
-
|
|
165
|
-
streams: new Map(state.streams),
|
|
146
|
+
streams: freezeStreamHandlers(state.streams),
|
|
166
147
|
});
|
|
167
148
|
return state.snapshot;
|
|
168
149
|
}
|
|
169
|
-
/** @internal */
|
|
170
|
-
export function registerSessionStreamsAtomically(handlers, entries) {
|
|
171
|
-
const state = mutableSessionHandlerState(handlers);
|
|
172
|
-
const pending = new Set();
|
|
173
|
-
for (const [kind, handler] of entries) {
|
|
174
|
-
if (kind.length < 1 ||
|
|
175
|
-
encoder.encode(kind).length > 255 ||
|
|
176
|
-
kind === "flowersec.rpc.v2" ||
|
|
177
|
-
typeof handler !== "function")
|
|
178
|
-
throw new HandlerRegistrationError("invalid_handler");
|
|
179
|
-
if (state.streams.has(kind) || pending.has(kind))
|
|
180
|
-
throw new HandlerRegistrationError("already_registered");
|
|
181
|
-
pending.add(kind);
|
|
182
|
-
}
|
|
183
|
-
for (const [kind, handler] of entries)
|
|
184
|
-
state.streams.set(kind, handler);
|
|
185
|
-
}
|
|
186
150
|
export class AcceptedSession {
|
|
187
151
|
constructor() { }
|
|
188
152
|
get session() {
|
|
189
153
|
return acceptedSessionState(this).session;
|
|
190
154
|
}
|
|
191
155
|
async serve(options = {}) {
|
|
192
|
-
const active = new Set();
|
|
193
156
|
const state = acceptedSessionState(this);
|
|
194
|
-
|
|
195
|
-
while (true) {
|
|
196
|
-
if (options.signal?.aborted)
|
|
197
|
-
throw new SessionError("canceled");
|
|
198
|
-
const incoming = await this.session.acceptStream(options);
|
|
199
|
-
const handler = state.handlers.streams.get(incoming.kind);
|
|
200
|
-
if (handler === undefined ||
|
|
201
|
-
active.size >= state.handlers.maxConcurrentStreams) {
|
|
202
|
-
await incoming.stream.reset();
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
const task = (async () => {
|
|
206
|
-
try {
|
|
207
|
-
await handler(incoming, options);
|
|
208
|
-
await incoming.stream.closeWrite();
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
await incoming.stream.reset().catch(() => undefined);
|
|
212
|
-
}
|
|
213
|
-
})().finally(() => active.delete(task));
|
|
214
|
-
active.add(task);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
finally {
|
|
218
|
-
await this.close().catch(() => undefined);
|
|
219
|
-
await Promise.allSettled(active);
|
|
220
|
-
}
|
|
157
|
+
await serveFrozenStreamHandlers(state.handlers.streams, this.session, options, async () => await this.close());
|
|
221
158
|
}
|
|
222
159
|
async close() {
|
|
223
160
|
const state = acceptedSessionState(this);
|
|
@@ -264,11 +201,27 @@ export class Acceptor {
|
|
|
264
201
|
}
|
|
265
202
|
async close() {
|
|
266
203
|
const state = acceptorState(this);
|
|
267
|
-
state.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
await
|
|
271
|
-
|
|
204
|
+
if (state.lifecycle.completion === undefined) {
|
|
205
|
+
state.lifecycle.completion = closeAcceptor(state);
|
|
206
|
+
}
|
|
207
|
+
await withCleanupTimeout(state.lifecycle.completion, state.cleanupTimeoutMs);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const MAX_CONCURRENT_ADMISSIONS = 64;
|
|
211
|
+
const MAX_PENDING_ACCEPTED_SESSIONS = 64;
|
|
212
|
+
async function closeAcceptor(state) {
|
|
213
|
+
state.abort.abort();
|
|
214
|
+
const queued = state.accepted.close();
|
|
215
|
+
const cleanup = await Promise.allSettled([
|
|
216
|
+
...state.listeners.map(async (listener) => await listener.close()),
|
|
217
|
+
...queued.map(async (accepted) => await accepted.close()),
|
|
218
|
+
]);
|
|
219
|
+
while (state.tasks.size > 0) {
|
|
220
|
+
await Promise.allSettled([...state.tasks]);
|
|
221
|
+
}
|
|
222
|
+
if (cleanup.some((result) => result.status === "rejected") ||
|
|
223
|
+
state.lifecycle.cleanupFailed) {
|
|
224
|
+
throw new SessionError("operation_failed");
|
|
272
225
|
}
|
|
273
226
|
}
|
|
274
227
|
async function releaseLease(state, leaseId) {
|
|
@@ -280,7 +233,7 @@ async function authorizeCarrier(state, carrier) {
|
|
|
280
233
|
const received = await receiveSessionAdmissionV2(carrier, state.abort.signal);
|
|
281
234
|
const decoded = received.decoded;
|
|
282
235
|
const request = runtimeAuthorizationRequestFromDecoded(decoded);
|
|
283
|
-
const decision = await
|
|
236
|
+
const decision = await state.options.authorize(request, { signal: state.abort.signal });
|
|
284
237
|
if (decision.decision !== "allow") {
|
|
285
238
|
return await rejectSessionAdmissionV2(received, {
|
|
286
239
|
accepted: false,
|
|
@@ -294,9 +247,7 @@ async function authorizeCarrier(state, carrier) {
|
|
|
294
247
|
try {
|
|
295
248
|
const registry = state.options.resolveHandlers === undefined
|
|
296
249
|
? new SessionHandlers()
|
|
297
|
-
: await
|
|
298
|
-
signal: state.abort.signal,
|
|
299
|
-
})), state.abort.signal);
|
|
250
|
+
: await state.options.resolveHandlers(request, { signal: state.abort.signal });
|
|
300
251
|
const handlers = freezeSessionHandlers(registry);
|
|
301
252
|
const leg = {
|
|
302
253
|
received,
|
|
@@ -326,28 +277,50 @@ async function processCarrier(state, carrier) {
|
|
|
326
277
|
const leg = await authorizeCarrier(state, carrier);
|
|
327
278
|
if (leg === undefined)
|
|
328
279
|
return;
|
|
280
|
+
let releaseOwnedByAcceptedSession = false;
|
|
329
281
|
try {
|
|
330
282
|
if (leg.received.decoded.request.pathKind !== "direct" ||
|
|
331
283
|
leg.artifact.path.kind !== "direct") {
|
|
332
284
|
await leg.received.carrier.close().catch(() => undefined);
|
|
333
285
|
throw new SessionError("operation_failed");
|
|
334
286
|
}
|
|
335
|
-
|
|
287
|
+
const accepted = await establishDirect(leg, state.abort.signal, leg.leaseId === undefined ? undefined : () => releaseLease(state, leg.leaseId));
|
|
288
|
+
releaseOwnedByAcceptedSession = true;
|
|
289
|
+
if (state.accepted.push(accepted) === "rejected") {
|
|
290
|
+
try {
|
|
291
|
+
await accepted.close();
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
state.lifecycle.cleanupFailed = true;
|
|
295
|
+
throw new SessionError("operation_failed");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
336
298
|
}
|
|
337
299
|
catch (error) {
|
|
338
|
-
|
|
300
|
+
if (!releaseOwnedByAcceptedSession) {
|
|
301
|
+
await releaseLease(state, leg.leaseId).catch(() => undefined);
|
|
302
|
+
}
|
|
339
303
|
throw error;
|
|
340
304
|
}
|
|
341
305
|
}
|
|
342
306
|
async function runAcceptLoop(state) {
|
|
343
307
|
while (!state.abort.signal.aborted) {
|
|
344
308
|
try {
|
|
309
|
+
while (state.admissions.size >= MAX_CONCURRENT_ADMISSIONS) {
|
|
310
|
+
await Promise.race(state.admissions);
|
|
311
|
+
if (state.abort.signal.aborted)
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
345
314
|
const carrier = await state.accept({ signal: state.abort.signal });
|
|
346
315
|
const task = processCarrier(state, carrier)
|
|
347
316
|
.catch(async () => {
|
|
348
317
|
await carrier.close().catch(() => undefined);
|
|
349
318
|
})
|
|
350
|
-
.finally(() =>
|
|
319
|
+
.finally(() => {
|
|
320
|
+
state.admissions.delete(task);
|
|
321
|
+
state.tasks.delete(task);
|
|
322
|
+
});
|
|
323
|
+
state.admissions.add(task);
|
|
351
324
|
state.tasks.add(task);
|
|
352
325
|
}
|
|
353
326
|
catch (error) {
|
|
@@ -362,16 +335,19 @@ class AcceptedQueue {
|
|
|
362
335
|
waiters = new Set();
|
|
363
336
|
failure;
|
|
364
337
|
push(value) {
|
|
365
|
-
if (this.failure !== undefined)
|
|
366
|
-
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
338
|
+
if (this.failure !== undefined)
|
|
339
|
+
return "rejected";
|
|
369
340
|
const waiter = this.waiters.values().next().value;
|
|
370
|
-
if (waiter === undefined)
|
|
341
|
+
if (waiter === undefined) {
|
|
342
|
+
if (this.values.length >= MAX_PENDING_ACCEPTED_SESSIONS)
|
|
343
|
+
return "rejected";
|
|
371
344
|
this.values.push(value);
|
|
345
|
+
return "queued";
|
|
346
|
+
}
|
|
372
347
|
else {
|
|
373
348
|
this.waiters.delete(waiter);
|
|
374
349
|
waiter.resolve(value);
|
|
350
|
+
return "delivered";
|
|
375
351
|
}
|
|
376
352
|
}
|
|
377
353
|
async shift(signal) {
|
|
@@ -412,6 +388,7 @@ class AcceptedQueue {
|
|
|
412
388
|
}
|
|
413
389
|
close() {
|
|
414
390
|
this.fail(new SessionError("closed"));
|
|
391
|
+
return this.values.splice(0);
|
|
415
392
|
}
|
|
416
393
|
}
|
|
417
394
|
const acceptorStates = new WeakMap();
|
|
@@ -482,6 +459,7 @@ export async function createAcceptor(options) {
|
|
|
482
459
|
const accepted = new AcceptedQueue();
|
|
483
460
|
const abort = new AbortController();
|
|
484
461
|
const tasks = new Set();
|
|
462
|
+
const admissions = new Set();
|
|
485
463
|
let started = false;
|
|
486
464
|
const state = {
|
|
487
465
|
listeners,
|
|
@@ -490,7 +468,9 @@ export async function createAcceptor(options) {
|
|
|
490
468
|
accepted,
|
|
491
469
|
abort,
|
|
492
470
|
tasks,
|
|
471
|
+
admissions,
|
|
493
472
|
cleanupTimeoutMs,
|
|
473
|
+
lifecycle: { cleanupFailed: false },
|
|
494
474
|
start() {
|
|
495
475
|
if (started)
|
|
496
476
|
return;
|
|
@@ -502,52 +482,26 @@ export async function createAcceptor(options) {
|
|
|
502
482
|
acceptorStates.set(acceptor, state);
|
|
503
483
|
return Object.freeze(acceptor);
|
|
504
484
|
}
|
|
505
|
-
async function
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return;
|
|
520
|
-
settled = true;
|
|
521
|
-
cleanup();
|
|
522
|
-
reject(signal.reason instanceof Error ? signal.reason : new SessionError("closed"));
|
|
523
|
-
};
|
|
524
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
525
|
-
void promise.then((value) => {
|
|
526
|
-
if (settled)
|
|
527
|
-
return;
|
|
528
|
-
settled = true;
|
|
529
|
-
cleanup();
|
|
530
|
-
resolve(value);
|
|
531
|
-
}, (error) => {
|
|
532
|
-
if (settled)
|
|
533
|
-
return;
|
|
534
|
-
settled = true;
|
|
535
|
-
cleanup();
|
|
536
|
-
reject(error);
|
|
537
|
-
});
|
|
538
|
-
});
|
|
485
|
+
async function withCleanupTimeout(completion, timeoutMs) {
|
|
486
|
+
let timer;
|
|
487
|
+
try {
|
|
488
|
+
await Promise.race([
|
|
489
|
+
completion,
|
|
490
|
+
new Promise((_, reject) => {
|
|
491
|
+
timer = setTimeout(() => reject(new SessionError("timeout")), timeoutMs);
|
|
492
|
+
}),
|
|
493
|
+
]);
|
|
494
|
+
}
|
|
495
|
+
finally {
|
|
496
|
+
if (timer !== undefined)
|
|
497
|
+
clearTimeout(timer);
|
|
498
|
+
}
|
|
539
499
|
}
|
|
540
500
|
function validRPCError(error) {
|
|
541
|
-
|
|
542
|
-
error
|
|
543
|
-
error.code > 0xffff_ffff) {
|
|
544
|
-
return { code: 500, message: "handler failed" };
|
|
501
|
+
try {
|
|
502
|
+
return assertRpcError(error);
|
|
545
503
|
}
|
|
546
|
-
|
|
547
|
-
encoder.encode(error.message).length > 1024) {
|
|
504
|
+
catch {
|
|
548
505
|
return { code: 500, message: "handler failed" };
|
|
549
506
|
}
|
|
550
|
-
return error.message === undefined
|
|
551
|
-
? { code: error.code }
|
|
552
|
-
: { code: error.code, message: error.message };
|
|
553
507
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes as nodeRandomBytes } from "node:crypto";
|
|
1
|
+
import { randomBytes as nodeRandomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { buildFSB2RequestV2, canonicalizeCandidatesV2, computeSessionContractHashV2, decodeArtifactV2JSON, decodeFSB2RequestV2, encodeArtifactV2JSON, encodeFSB2RequestV2, } from "../v2/artifact.js";
|
|
3
3
|
import { wrapArtifact } from "../public/artifact.js";
|
|
4
4
|
import { base64urlDecode, base64urlEncode } from "../utils/base64url.js";
|
|
@@ -413,7 +413,11 @@ function validTCPAddress(value) {
|
|
|
413
413
|
return port >= 1 && port <= 65_535;
|
|
414
414
|
}
|
|
415
415
|
function validObservedText(value) { return value.length > 0 && value.length <= 512 && !/[\u0000-\u001f\u007f]/u.test(value); }
|
|
416
|
-
function bytesEqual(a, b) {
|
|
416
|
+
function bytesEqual(a, b) {
|
|
417
|
+
if (a.length !== b.length)
|
|
418
|
+
return false;
|
|
419
|
+
return timingSafeEqual(a, b);
|
|
420
|
+
}
|
|
417
421
|
function strictObject(bytes) {
|
|
418
422
|
let value;
|
|
419
423
|
try {
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,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);
|
|
@@ -36,13 +36,6 @@ export async function commitArtifactLeaseSpend(lease, signal) {
|
|
|
36
36
|
const leaseState = artifactLeaseStates.get(lease);
|
|
37
37
|
if (leaseState === undefined || leaseState.state !== "idle")
|
|
38
38
|
throw new ArtifactLeaseError();
|
|
39
|
-
leaseState.state = "
|
|
40
|
-
|
|
41
|
-
await leaseState.commitSpend(signal);
|
|
42
|
-
leaseState.state = "consumed";
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
leaseState.state = "idle";
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
39
|
+
leaseState.state = "consumed";
|
|
40
|
+
await leaseState.commitSpend(signal);
|
|
48
41
|
}
|
|
@@ -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/rpc/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
2
|
-
import { assertRpcEnvelope } from "./validate.js";
|
|
2
|
+
import { assertRpcEnvelope, assertRpcError } from "./validate.js";
|
|
3
3
|
import { SDK_DEFAULTS } from "../defaults.js";
|
|
4
4
|
const DEFAULT_RPC_SERVER_OPTIONS = Object.freeze({
|
|
5
5
|
maxConcurrentRequests: SDK_DEFAULTS.rpc.maxConcurrentRequests,
|
|
@@ -217,12 +217,21 @@ export class RpcServer {
|
|
|
217
217
|
waiters.shift()?.();
|
|
218
218
|
}
|
|
219
219
|
async writeResponse(request, out) {
|
|
220
|
+
let error;
|
|
221
|
+
if (out.error != null) {
|
|
222
|
+
try {
|
|
223
|
+
error = assertRpcError(out.error);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
error = { code: 500, message: "internal error" };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
220
229
|
const resp = {
|
|
221
230
|
type_id: request.type_id,
|
|
222
231
|
request_id: 0,
|
|
223
232
|
response_to: request.request_id,
|
|
224
233
|
payload: out.payload,
|
|
225
|
-
...(
|
|
234
|
+
...(error != null ? { error } : {}),
|
|
226
235
|
};
|
|
227
236
|
await this.writeEnvelope(resp);
|
|
228
237
|
}
|
package/dist/rpc/validate.d.ts
CHANGED
package/dist/rpc/validate.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isSafeU32Number, isSafeU64Number } from "../utils/number.js";
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const strictDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
2
4
|
// assertRpcEnvelope validates numeric fields that are u32/u64 in the IDL.
|
|
3
5
|
//
|
|
4
6
|
// The wire format is JSON, so JS numbers are used. For u64 we enforce the safe integer range
|
|
@@ -15,13 +17,29 @@ export function assertRpcEnvelope(v) {
|
|
|
15
17
|
throw new Error("bad rpc envelope: response_to");
|
|
16
18
|
// payload: unknown (JSON)
|
|
17
19
|
if (o.error != null) {
|
|
18
|
-
|
|
19
|
-
throw new Error("bad rpc envelope: error");
|
|
20
|
-
if (!isSafeU32Number(o.error.code))
|
|
21
|
-
throw new Error("bad rpc envelope: error.code");
|
|
22
|
-
const msg = o.error.message;
|
|
23
|
-
if (msg !== undefined && typeof msg !== "string")
|
|
24
|
-
throw new Error("bad rpc envelope: error.message");
|
|
20
|
+
assertRpcError(o.error);
|
|
25
21
|
}
|
|
26
22
|
return o;
|
|
27
23
|
}
|
|
24
|
+
export function assertRpcError(value) {
|
|
25
|
+
if (typeof value !== "object" || value == null)
|
|
26
|
+
throw new Error("bad rpc envelope: error");
|
|
27
|
+
const error = value;
|
|
28
|
+
if (Object.keys(error).some((key) => key !== "code" && key !== "message")) {
|
|
29
|
+
throw new Error("bad rpc envelope: error shape");
|
|
30
|
+
}
|
|
31
|
+
if (!isSafeU32Number(error.code) || error.code === 0) {
|
|
32
|
+
throw new Error("bad rpc envelope: error.code");
|
|
33
|
+
}
|
|
34
|
+
const message = error.message;
|
|
35
|
+
if (message !== undefined && typeof message !== "string") {
|
|
36
|
+
throw new Error("bad rpc envelope: error.message");
|
|
37
|
+
}
|
|
38
|
+
if (typeof message === "string") {
|
|
39
|
+
const encoded = encoder.encode(message);
|
|
40
|
+
if (encoded.byteLength > 1_024 || strictDecoder.decode(encoded) !== message) {
|
|
41
|
+
throw new Error("bad rpc envelope: error.message");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return message === undefined ? { code: error.code } : { code: error.code, message };
|
|
45
|
+
}
|
package/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.0",
|
|
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.0"
|
|
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": "9838dbf4ecd425679be6ce39274dd80eb33c1ab9"
|
|
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:aa95f71f-a09a-5de4-8522-e341d9e45e95",
|
|
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.0",
|
|
11
|
+
"purl": "pkg:npm/%40floegence/flowersec-core@2.5.0",
|
|
12
|
+
"bom-ref": "pkg:npm/%40floegence/flowersec-core@2.5.0"
|
|
13
13
|
},
|
|
14
14
|
"properties": [
|
|
15
15
|
{
|
|
16
16
|
"name": "flowersec:source-inventory-sha256",
|
|
17
|
-
"value": "
|
|
17
|
+
"value": "6e49aafbd7bd94f6524b61df3a4eed908c4df609841a553c15d155ebee6095ad"
|
|
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.0",
|
|
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/6e49aafbd7bd94f6524b61df3a4eed908c4df609841a553c15d155ebee6095ad",
|
|
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-83d1827278ec8ca210fa",
|
|
17
|
+
"versionInfo": "2.5.0",
|
|
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: 6e49aafbd7bd94f6524b61df3a4eed908c4df609841a553c15d155ebee6095ad",
|
|
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.0"
|
|
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-83d1827278ec8ca210fa"
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
|
-
"spdxElementId": "SPDXRef-Package-
|
|
142
|
+
"spdxElementId": "SPDXRef-Package-83d1827278ec8ca210fa",
|
|
143
143
|
"relationshipType": "DEPENDS_ON",
|
|
144
144
|
"relatedSpdxElement": "SPDXRef-Package-5ce913a03239b02770fb"
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
|
-
"spdxElementId": "SPDXRef-Package-
|
|
147
|
+
"spdxElementId": "SPDXRef-Package-83d1827278ec8ca210fa",
|
|
148
148
|
"relationshipType": "DEPENDS_ON",
|
|
149
149
|
"relatedSpdxElement": "SPDXRef-Package-01009adf60db02c13634"
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
|
-
"spdxElementId": "SPDXRef-Package-
|
|
152
|
+
"spdxElementId": "SPDXRef-Package-83d1827278ec8ca210fa",
|
|
153
153
|
"relationshipType": "DEPENDS_ON",
|
|
154
154
|
"relatedSpdxElement": "SPDXRef-Package-8815b117c8a1d5f7eeb0"
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
|
-
"spdxElementId": "SPDXRef-Package-
|
|
157
|
+
"spdxElementId": "SPDXRef-Package-83d1827278ec8ca210fa",
|
|
158
158
|
"relationshipType": "DEPENDS_ON",
|
|
159
159
|
"relatedSpdxElement": "SPDXRef-Package-f8b45a289df643042b94"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
|
-
"spdxElementId": "SPDXRef-Package-
|
|
162
|
+
"spdxElementId": "SPDXRef-Package-83d1827278ec8ca210fa",
|
|
163
163
|
"relationshipType": "DEPENDS_ON",
|
|
164
164
|
"relatedSpdxElement": "SPDXRef-Package-b779412f685822663496"
|
|
165
165
|
},
|