@floegence/flowersec-core 0.21.1 → 0.22.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.
@@ -0,0 +1,83 @@
1
+ import { type TransportSecurityPolicy } from "../client-connect/transportSecurity.js";
2
+ import { serverHandshake, ServerHandshakeCache, type Suite } from "../e2ee/handshake.js";
3
+ import { type YamuxLimits } from "../yamux/session.js";
4
+ import type { YamuxStream } from "../yamux/stream.js";
5
+ import { type RpcRouter, type RpcServerOptions } from "../rpc/server.js";
6
+ import { type WebSocketLike, type WebSocketLimits } from "../ws-client/binaryTransport.js";
7
+ export type { Suite } from "../e2ee/handshake.js";
8
+ export type EndpointPath = "direct" | "tunnel";
9
+ export type DirectHandshakeInit = Readonly<{
10
+ channelId: string;
11
+ version: number;
12
+ suite: Suite;
13
+ clientFeatures: number;
14
+ }>;
15
+ export type DirectHandshakeCredential = Readonly<{
16
+ psk: Uint8Array | string;
17
+ initExpireAtUnixS: number;
18
+ commitAuthenticated?: () => void | Promise<void>;
19
+ }>;
20
+ export type DirectCredentialResolver = (init: DirectHandshakeInit) => DirectHandshakeCredential | Promise<DirectHandshakeCredential>;
21
+ export type EndpointOptions = Readonly<{
22
+ signal?: AbortSignal;
23
+ handshakeTimeoutMs?: number;
24
+ handshakeClockSkewMs?: number;
25
+ serverFeatures?: number;
26
+ maxHandshakePayload?: number;
27
+ maxRecordBytes?: number;
28
+ maxBufferedBytes?: number;
29
+ maxOutboundBufferedBytes?: number;
30
+ outboundRecordChunkBytes?: number;
31
+ webSocketLimits?: Partial<WebSocketLimits>;
32
+ yamuxLimits?: Partial<YamuxLimits>;
33
+ handshakeCache?: ServerHandshakeCache;
34
+ }>;
35
+ export type DirectAcceptOptions = EndpointOptions & Readonly<{
36
+ secureTransport?: boolean;
37
+ transportSecurityPolicy?: TransportSecurityPolicy;
38
+ }>;
39
+ export type TunnelEndpointOptions = EndpointOptions & Readonly<{
40
+ origin: string;
41
+ connectTimeoutMs?: number;
42
+ endpointInstanceId?: string;
43
+ wsFactory: (url: string, origin: string) => WebSocketLike;
44
+ transportSecurityPolicy?: TransportSecurityPolicy;
45
+ }>;
46
+ export type EndpointStream = Readonly<{
47
+ kind: string;
48
+ stream: YamuxStream;
49
+ }>;
50
+ export declare class Session {
51
+ private readonly secure;
52
+ private readonly mux;
53
+ readonly path: EndpointPath;
54
+ readonly endpointInstanceId: string | undefined;
55
+ private readonly streams;
56
+ private readonly waiters;
57
+ private terminalError;
58
+ private constructor();
59
+ static create(path: EndpointPath, secure: Awaited<ReturnType<typeof serverHandshake>>, options?: Readonly<{
60
+ yamuxLimits?: Partial<YamuxLimits>;
61
+ endpointInstanceId?: string;
62
+ }>): Session;
63
+ openStream(kind: string, options?: Readonly<{
64
+ signal?: AbortSignal;
65
+ }>): Promise<YamuxStream>;
66
+ acceptStream(options?: Readonly<{
67
+ signal?: AbortSignal;
68
+ }>): Promise<EndpointStream>;
69
+ serveRPC(router: RpcRouter, options?: RpcServerOptions & Readonly<{
70
+ signal?: AbortSignal;
71
+ }>): Promise<void>;
72
+ probeLiveness(timeoutMs?: 10000): Promise<number>;
73
+ close(): void;
74
+ private pushStream;
75
+ private acceptRawStream;
76
+ private fail;
77
+ }
78
+ export declare function acceptDirect(websocket: WebSocketLike, handshake: Readonly<{
79
+ channelId: string;
80
+ suite: Suite;
81
+ }> & DirectHandshakeCredential, options?: DirectAcceptOptions): Promise<Session>;
82
+ export declare function acceptDirectResolved(websocket: WebSocketLike, resolver: DirectCredentialResolver, options?: DirectAcceptOptions): Promise<Session>;
83
+ export declare function connectTunnel(grantInput: unknown, options: TunnelEndpointOptions): Promise<Session>;
@@ -0,0 +1,384 @@
1
+ import { Role as ControlRole, assertChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
2
+ import { Role as TunnelRole } from "../gen/flowersec/tunnel/v1.gen.js";
3
+ import { assertTunnelGrantContract, assertValidPSK, prepareChannelId } from "../client-connect/contract.js";
4
+ import { enforceTransportSecurity } from "../client-connect/transportSecurity.js";
5
+ import { SDK_DEFAULTS } from "../defaults.js";
6
+ import { serverHandshake, ServerHandshakeCache } from "../e2ee/handshake.js";
7
+ import { decodeHandshakeFrame } from "../e2ee/framing.js";
8
+ import { HANDSHAKE_TYPE_INIT, PROTOCOL_VERSION } from "../e2ee/constants.js";
9
+ import { readStreamHello, writeStreamHello } from "../streamhello/streamHello.js";
10
+ import { ByteReader } from "../yamux/byteReader.js";
11
+ import { YamuxSession } from "../yamux/session.js";
12
+ import { RpcServer } from "../rpc/server.js";
13
+ import { base64urlDecode, base64urlEncode } from "../utils/base64url.js";
14
+ import { AbortError, FlowersecError } from "../utils/errors.js";
15
+ import { WebSocketBinaryTransport } from "../ws-client/binaryTransport.js";
16
+ export class Session {
17
+ secure;
18
+ mux;
19
+ path;
20
+ endpointInstanceId;
21
+ streams = [];
22
+ waiters = [];
23
+ terminalError;
24
+ constructor(path, secure, mux, endpointInstanceId) {
25
+ this.secure = secure;
26
+ this.mux = mux;
27
+ this.path = path;
28
+ this.endpointInstanceId = endpointInstanceId;
29
+ }
30
+ static create(path, secure, options = {}) {
31
+ let session;
32
+ const mux = new YamuxSession({
33
+ read: () => secure.read(),
34
+ write: (bytes) => secure.write(bytes),
35
+ close: () => secure.close(),
36
+ }, {
37
+ client: false,
38
+ ...(options.yamuxLimits === undefined ? {} : { limits: options.yamuxLimits }),
39
+ onIncomingStream: (stream) => session.pushStream(stream),
40
+ onTerminal: (error) => session.fail(error),
41
+ });
42
+ session = new Session(path, secure, mux, options.endpointInstanceId);
43
+ return session;
44
+ }
45
+ async openStream(kind, options = {}) {
46
+ const stream = await this.mux.openStream(options);
47
+ try {
48
+ await writeStreamHello((bytes) => stream.write(bytes), normalizeStreamKind(kind, this.path));
49
+ return stream;
50
+ }
51
+ catch (error) {
52
+ await stream.reset(asError(error));
53
+ throw new FlowersecError({ path: this.path, stage: "rpc", code: "stream_hello_failed", message: "failed to write stream hello", cause: error });
54
+ }
55
+ }
56
+ async acceptStream(options = {}) {
57
+ const stream = await this.acceptRawStream(options.signal);
58
+ try {
59
+ const reader = new ByteReader(() => stream.read());
60
+ const hello = await readStreamHello((length) => reader.readExactly(length));
61
+ return { kind: hello.kind, stream };
62
+ }
63
+ catch (error) {
64
+ await stream.reset(asError(error));
65
+ throw new FlowersecError({ path: this.path, stage: "rpc", code: "stream_hello_failed", message: "failed to read stream hello", cause: error });
66
+ }
67
+ }
68
+ async serveRPC(router, options = {}) {
69
+ while (true) {
70
+ const accepted = await this.acceptStream(options.signal === undefined ? {} : { signal: options.signal });
71
+ if (accepted.kind !== "rpc") {
72
+ await accepted.stream.reset(new Error(`unexpected stream kind ${accepted.kind}`));
73
+ continue;
74
+ }
75
+ const reader = new ByteReader(() => accepted.stream.read());
76
+ const server = new RpcServer({
77
+ readExactly: (length) => reader.readExactly(length),
78
+ write: (bytes) => accepted.stream.write(bytes),
79
+ close: (error) => { void accepted.stream.reset(asError(error)); },
80
+ }, options, router);
81
+ await server.serve(options.signal);
82
+ return;
83
+ }
84
+ }
85
+ probeLiveness(timeoutMs = SDK_DEFAULTS.transport.handshakeTimeoutMs) {
86
+ return this.mux.probeLiveness(timeoutMs);
87
+ }
88
+ close() {
89
+ this.fail(new Error("endpoint session closed"));
90
+ this.mux.close();
91
+ this.secure.close();
92
+ }
93
+ pushStream(stream) {
94
+ const waiter = this.waiters.shift();
95
+ if (waiter == null) {
96
+ this.streams.push(stream);
97
+ return;
98
+ }
99
+ cleanupWaiter(waiter);
100
+ waiter.resolve(stream);
101
+ }
102
+ acceptRawStream(signal) {
103
+ if (signal?.aborted)
104
+ return Promise.reject(new AbortError("accept stream aborted"));
105
+ if (this.terminalError != null)
106
+ return Promise.reject(this.terminalError);
107
+ const stream = this.streams.shift();
108
+ if (stream != null)
109
+ return Promise.resolve(stream);
110
+ return new Promise((resolve, reject) => {
111
+ const waiter = { resolve, reject, ...(signal === undefined ? {} : { signal }) };
112
+ waiter.onAbort = () => {
113
+ const index = this.waiters.indexOf(waiter);
114
+ if (index >= 0)
115
+ this.waiters.splice(index, 1);
116
+ cleanupWaiter(waiter);
117
+ reject(new AbortError("accept stream aborted"));
118
+ };
119
+ signal?.addEventListener("abort", waiter.onAbort, { once: true });
120
+ this.waiters.push(waiter);
121
+ });
122
+ }
123
+ fail(error) {
124
+ if (this.terminalError != null)
125
+ return;
126
+ this.terminalError = error;
127
+ for (const waiter of this.waiters.splice(0)) {
128
+ cleanupWaiter(waiter);
129
+ waiter.reject(error);
130
+ }
131
+ }
132
+ }
133
+ export async function acceptDirect(websocket, handshake, options = {}) {
134
+ await enforceIncomingDirectTransport(options);
135
+ const transport = new WebSocketBinaryTransport(websocket, webSocketTransportOptions(options));
136
+ return await establishSession("direct", transport, handshake, options);
137
+ }
138
+ export async function acceptDirectResolved(websocket, resolver, options = {}) {
139
+ await enforceIncomingDirectTransport(options);
140
+ const transport = new WebSocketBinaryTransport(websocket, webSocketTransportOptions(options));
141
+ const first = await transport.readBinary(readOptions(options));
142
+ let init;
143
+ try {
144
+ const decoded = decodeHandshakeFrame(first, options.maxHandshakePayload ?? SDK_DEFAULTS.e2ee.maxHandshakePayloadBytes);
145
+ if (decoded.handshakeType !== HANDSHAKE_TYPE_INIT)
146
+ throw new Error("expected handshake init");
147
+ init = JSON.parse(new TextDecoder().decode(decoded.payloadJsonUtf8));
148
+ if (init.version !== PROTOCOL_VERSION || init.role !== 1 || (init.suite !== 1 && init.suite !== 2))
149
+ throw new Error("invalid handshake init");
150
+ }
151
+ catch (error) {
152
+ transport.close();
153
+ throw new FlowersecError({ path: "direct", stage: "handshake", code: "handshake_failed", message: "invalid handshake init", cause: error });
154
+ }
155
+ let credential;
156
+ try {
157
+ credential = await resolver({
158
+ channelId: prepareChannelId(init.channel_id, "direct"),
159
+ version: init.version,
160
+ suite: init.suite,
161
+ clientFeatures: init.client_features >>> 0,
162
+ });
163
+ }
164
+ catch (error) {
165
+ transport.close();
166
+ throw new FlowersecError({ path: "direct", stage: "validate", code: "resolve_failed", message: "credential resolution failed", cause: error });
167
+ }
168
+ const replay = new PrefetchedTransport(transport, first);
169
+ return await establishSession("direct", replay, {
170
+ channelId: init.channel_id,
171
+ suite: init.suite,
172
+ ...credential,
173
+ }, options);
174
+ }
175
+ export async function connectTunnel(grantInput, options) {
176
+ let grant;
177
+ try {
178
+ grant = assertChannelInitGrant(unwrapServerGrant(grantInput));
179
+ }
180
+ catch (error) {
181
+ throw new FlowersecError({ path: "tunnel", stage: "validate", code: "invalid_input", message: "invalid ChannelInitGrant", cause: error });
182
+ }
183
+ assertTunnelGrantContract(grant, ControlRole.Role_server);
184
+ const tunnelUrl = grant.tunnel_url.trim();
185
+ if (tunnelUrl === "")
186
+ throw new FlowersecError({ path: "tunnel", stage: "validate", code: "missing_tunnel_url", message: "missing tunnel_url" });
187
+ if (grant.token.trim() === "")
188
+ throw new FlowersecError({ path: "tunnel", stage: "validate", code: "missing_token", message: "missing token" });
189
+ if (grant.channel_init_expire_at_unix_s <= 0)
190
+ throw new FlowersecError({ path: "tunnel", stage: "validate", code: "missing_init_exp", message: "missing channel init expiry" });
191
+ const origin = options.origin.trim();
192
+ if (origin === "")
193
+ throw new FlowersecError({ path: "tunnel", stage: "validate", code: "missing_origin", message: "missing origin" });
194
+ await enforceTransportSecurity({ rawUrl: tunnelUrl, path: "tunnel", ...(options.transportSecurityPolicy === undefined ? {} : { policy: options.transportSecurityPolicy }) });
195
+ const endpointInstanceId = normalizeEndpointInstanceId(options.endpointInstanceId);
196
+ const websocket = options.wsFactory(tunnelUrl, origin);
197
+ const transport = new WebSocketBinaryTransport(websocket, webSocketTransportOptions(options));
198
+ try {
199
+ await waitForOpen(websocket, options.connectTimeoutMs ?? SDK_DEFAULTS.transport.connectTimeoutMs, options.signal);
200
+ const attach = {
201
+ v: 1,
202
+ channel_id: prepareChannelId(grant.channel_id, "tunnel"),
203
+ role: TunnelRole.Role_server,
204
+ token: grant.token.trim(),
205
+ endpoint_instance_id: endpointInstanceId,
206
+ };
207
+ websocket.send(JSON.stringify(attach));
208
+ return await establishSession("tunnel", transport, {
209
+ channelId: grant.channel_id,
210
+ suite: grant.default_suite,
211
+ psk: assertValidPSK(grant.e2ee_psk_b64u, "tunnel"),
212
+ initExpireAtUnixS: grant.channel_init_expire_at_unix_s,
213
+ }, options, endpointInstanceId);
214
+ }
215
+ catch (error) {
216
+ transport.close();
217
+ if (error instanceof FlowersecError)
218
+ throw error;
219
+ throw new FlowersecError({ path: "tunnel", stage: "connect", code: "dial_failed", message: "endpoint tunnel connect failed", cause: error });
220
+ }
221
+ }
222
+ async function establishSession(path, transport, handshake, options, endpointInstanceId) {
223
+ let psk;
224
+ try {
225
+ psk = normalizePSK(handshake.psk, path);
226
+ }
227
+ catch (error) {
228
+ transport.close();
229
+ throw error;
230
+ }
231
+ try {
232
+ const secure = await serverHandshake(transport, options.handshakeCache ?? new ServerHandshakeCache(), {
233
+ channelId: prepareChannelId(handshake.channelId, path),
234
+ suite: handshake.suite,
235
+ psk,
236
+ serverFeatures: options.serverFeatures ?? 0,
237
+ initExpireAtUnixS: handshake.initExpireAtUnixS,
238
+ clockSkewSeconds: Math.ceil((options.handshakeClockSkewMs ?? SDK_DEFAULTS.transport.handshakeClockSkewMs) / 1000),
239
+ maxHandshakePayload: options.maxHandshakePayload ?? SDK_DEFAULTS.e2ee.maxHandshakePayloadBytes,
240
+ maxRecordBytes: options.maxRecordBytes ?? SDK_DEFAULTS.e2ee.maxRecordBytes,
241
+ outboundRecordChunkBytes: options.outboundRecordChunkBytes ?? SDK_DEFAULTS.e2ee.outboundRecordChunkBytes,
242
+ maxBufferedBytes: options.maxBufferedBytes ?? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes,
243
+ maxOutboundBufferedBytes: options.maxOutboundBufferedBytes ?? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes,
244
+ timeoutMs: options.handshakeTimeoutMs ?? SDK_DEFAULTS.transport.handshakeTimeoutMs,
245
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
246
+ });
247
+ try {
248
+ await handshake.commitAuthenticated?.();
249
+ }
250
+ catch (error) {
251
+ secure.close();
252
+ throw new FlowersecError({ path, stage: "handshake", code: "credential_commit_failed", message: "credential commit failed", cause: error });
253
+ }
254
+ return Session.create(path, secure, {
255
+ ...(options.yamuxLimits === undefined ? {} : { yamuxLimits: options.yamuxLimits }),
256
+ ...(endpointInstanceId === undefined ? {} : { endpointInstanceId }),
257
+ });
258
+ }
259
+ catch (error) {
260
+ transport.close();
261
+ if (error instanceof FlowersecError)
262
+ throw error;
263
+ throw new FlowersecError({ path, stage: "handshake", code: "handshake_failed", message: "endpoint handshake failed", cause: error });
264
+ }
265
+ finally {
266
+ psk.fill(0);
267
+ }
268
+ }
269
+ class PrefetchedTransport {
270
+ inner;
271
+ first;
272
+ constructor(inner, first) {
273
+ this.inner = inner;
274
+ this.first = first;
275
+ }
276
+ readBinary(options) {
277
+ if (this.first != null) {
278
+ const first = this.first;
279
+ this.first = undefined;
280
+ return Promise.resolve(first);
281
+ }
282
+ return this.inner.readBinary(options);
283
+ }
284
+ writeBinary(frame, options) {
285
+ return this.inner.writeBinary(frame, options);
286
+ }
287
+ close() {
288
+ this.inner.close();
289
+ }
290
+ }
291
+ function normalizePSK(input, path) {
292
+ try {
293
+ const psk = typeof input === "string" ? base64urlDecode(input.trim()) : input.slice();
294
+ if (psk.length !== 32)
295
+ throw new Error("psk must be 32 bytes");
296
+ return psk;
297
+ }
298
+ catch (error) {
299
+ throw new FlowersecError({ path, stage: "validate", code: "invalid_psk", message: "invalid psk", cause: error });
300
+ }
301
+ }
302
+ function normalizeEndpointInstanceId(input) {
303
+ const value = input ?? randomEndpointInstanceId();
304
+ try {
305
+ const bytes = base64urlDecode(value);
306
+ if (bytes.length < 16 || bytes.length > 32)
307
+ throw new Error("endpoint instance ID must decode to 16..32 bytes");
308
+ }
309
+ catch (error) {
310
+ throw new FlowersecError({ path: "tunnel", stage: "validate", code: "invalid_endpoint_instance_id", message: "invalid endpoint instance ID", cause: error });
311
+ }
312
+ return value;
313
+ }
314
+ function randomEndpointInstanceId() {
315
+ const bytes = new Uint8Array(24);
316
+ crypto.getRandomValues(bytes);
317
+ return base64urlEncode(bytes);
318
+ }
319
+ function normalizeStreamKind(kind, path) {
320
+ const value = kind.trim();
321
+ if (value === "")
322
+ throw new FlowersecError({ path, stage: "validate", code: "missing_stream_kind", message: "missing stream kind" });
323
+ return value;
324
+ }
325
+ function unwrapServerGrant(input) {
326
+ if (typeof input !== "object" || input == null || Array.isArray(input))
327
+ return input;
328
+ const record = input;
329
+ return record["grant_server"] ?? input;
330
+ }
331
+ function webSocketTransportOptions(options) {
332
+ return options.webSocketLimits === undefined ? {} : { webSocketLimits: options.webSocketLimits };
333
+ }
334
+ function readOptions(options) {
335
+ return {
336
+ timeoutMs: options.handshakeTimeoutMs ?? SDK_DEFAULTS.transport.handshakeTimeoutMs,
337
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
338
+ };
339
+ }
340
+ async function enforceIncomingDirectTransport(options) {
341
+ const rawUrl = options.secureTransport === false ? "ws://127.0.0.1/" : "wss://127.0.0.1/";
342
+ await enforceTransportSecurity({ rawUrl, path: "direct", ...(options.transportSecurityPolicy === undefined ? {} : { policy: options.transportSecurityPolicy }) });
343
+ }
344
+ function waitForOpen(websocket, timeoutMs, signal) {
345
+ if (websocket.readyState === 1)
346
+ return Promise.resolve();
347
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
348
+ return Promise.reject(new RangeError("connectTimeoutMs must be non-negative"));
349
+ return new Promise((resolve, reject) => {
350
+ let timer;
351
+ const cleanup = () => {
352
+ if (timer != null)
353
+ clearTimeout(timer);
354
+ websocket.removeEventListener("open", onOpen);
355
+ websocket.removeEventListener("error", onError);
356
+ websocket.removeEventListener("close", onClose);
357
+ signal?.removeEventListener("abort", onAbort);
358
+ };
359
+ const finish = (error) => {
360
+ cleanup();
361
+ if (error == null)
362
+ resolve();
363
+ else
364
+ reject(error);
365
+ };
366
+ const onOpen = () => finish();
367
+ const onError = () => finish(new Error("websocket open failed"));
368
+ const onClose = () => finish(new Error("websocket closed before open"));
369
+ const onAbort = () => finish(new AbortError("connect aborted"));
370
+ websocket.addEventListener("open", onOpen);
371
+ websocket.addEventListener("error", onError);
372
+ websocket.addEventListener("close", onClose);
373
+ signal?.addEventListener("abort", onAbort, { once: true });
374
+ if (timeoutMs > 0)
375
+ timer = setTimeout(() => finish(new Error("websocket open timeout")), timeoutMs);
376
+ });
377
+ }
378
+ function cleanupWaiter(waiter) {
379
+ if (waiter.onAbort != null)
380
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
381
+ }
382
+ function asError(error) {
383
+ return error instanceof Error ? error : new Error(String(error));
384
+ }
@@ -0,0 +1,9 @@
1
+ import type { WebSocketLike } from "../ws-client/binaryTransport.js";
2
+ import { type DirectAcceptOptions, type DirectCredentialResolver, type DirectHandshakeCredential, type Session, type Suite, type TunnelEndpointOptions } from "./index.js";
3
+ export declare function adaptNodeWebSocket(websocket: unknown): WebSocketLike;
4
+ export declare function acceptDirectNode(websocket: unknown, handshake: Readonly<{
5
+ channelId: string;
6
+ suite: Suite;
7
+ }> & DirectHandshakeCredential, options?: DirectAcceptOptions): Promise<Session>;
8
+ export declare function acceptDirectResolvedNode(websocket: unknown, resolver: DirectCredentialResolver, options?: DirectAcceptOptions): Promise<Session>;
9
+ export declare function connectTunnelEndpointNode(grant: unknown, options: Omit<TunnelEndpointOptions, "wsFactory">): Promise<Session>;
@@ -0,0 +1,51 @@
1
+ import { createNodeWsFactory } from "../node/wsFactory.js";
2
+ import { acceptDirect, acceptDirectResolved, connectTunnel, } from "./index.js";
3
+ export function adaptNodeWebSocket(websocket) {
4
+ const raw = websocket;
5
+ if (raw == null || typeof raw.send !== "function" || typeof raw.close !== "function") {
6
+ throw new TypeError("a Node WebSocket is required");
7
+ }
8
+ if (typeof raw.addEventListener === "function" && typeof raw.removeEventListener === "function") {
9
+ return raw;
10
+ }
11
+ const listeners = new Map();
12
+ return {
13
+ get binaryType() { return String(raw.binaryType ?? "nodebuffer"); },
14
+ set binaryType(value) { raw.binaryType = value; },
15
+ get readyState() { return Number(raw.readyState); },
16
+ get bufferedAmount() { return Number(raw.bufferedAmount ?? 0); },
17
+ send(data) { raw.send(data); },
18
+ close(code, reason) { raw.close(code, reason); },
19
+ addEventListener(type, listener) {
20
+ const wrapped = (...args) => {
21
+ if (type === "message")
22
+ listener({ data: args[0] });
23
+ else if (type === "close")
24
+ listener({ code: args[0], reason: args[1]?.toString?.() ?? "" });
25
+ else
26
+ listener(args[0]);
27
+ };
28
+ const byListener = listeners.get(type) ?? new Map();
29
+ byListener.set(listener, wrapped);
30
+ listeners.set(type, byListener);
31
+ raw.on(type, wrapped);
32
+ },
33
+ removeEventListener(type, listener) {
34
+ const byListener = listeners.get(type);
35
+ const wrapped = byListener?.get(listener);
36
+ if (wrapped == null)
37
+ return;
38
+ byListener.delete(listener);
39
+ raw.off(type, wrapped);
40
+ },
41
+ };
42
+ }
43
+ export function acceptDirectNode(websocket, handshake, options = {}) {
44
+ return acceptDirect(adaptNodeWebSocket(websocket), handshake, options);
45
+ }
46
+ export function acceptDirectResolvedNode(websocket, resolver, options = {}) {
47
+ return acceptDirectResolved(adaptNodeWebSocket(websocket), resolver, options);
48
+ }
49
+ export function connectTunnelEndpointNode(grant, options) {
50
+ return connectTunnel(grant, { ...options, wsFactory: createNodeWsFactory() });
51
+ }
@@ -1,5 +1,6 @@
1
1
  import { readU32be, u32be } from "../utils/bin.js";
2
- export const DEFAULT_MAX_JSON_FRAME_BYTES = 1 << 20;
2
+ import { SDK_DEFAULTS } from "../defaults.js";
3
+ export const DEFAULT_MAX_JSON_FRAME_BYTES = SDK_DEFAULTS.rpc.maxJsonFrameBytes;
3
4
  const te = new TextEncoder();
4
5
  const td = new TextDecoder();
5
6
  // JsonFramingError marks malformed or oversized frames.
@@ -4,5 +4,8 @@ export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSe
4
4
  export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
5
5
  export type { DirectNodeReconnectConfig, NodeReconnectConfig, TunnelNodeReconnectConfig, } from "./reconnectConfig.js";
6
6
  export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
7
+ export * from "../endpoint/index.js";
8
+ export * from "../endpoint/node.js";
9
+ export * from "../proxy/server.js";
7
10
  export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
8
11
  export { assertConnectArtifact } from "../connect/artifact.js";
@@ -2,4 +2,7 @@ export { createNodeWsFactory } from "./wsFactory.js";
2
2
  export { AllowPlaintext, AllowPlaintextForLoopback, RequireTLS, } from "../client-connect/transportSecurity.js";
3
3
  export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
4
4
  export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
5
+ export * from "../endpoint/index.js";
6
+ export * from "../endpoint/node.js";
7
+ export * from "../proxy/server.js";
5
8
  export { assertConnectArtifact } from "../connect/artifact.js";
@@ -1,6 +1,7 @@
1
+ import { SDK_DEFAULTS } from "../defaults.js";
1
2
  export const PROXY_PROTOCOL_VERSION = 1;
2
3
  export const PROXY_KIND_HTTP1 = "flowersec-proxy/http1";
3
4
  export const PROXY_KIND_WS = "flowersec-proxy/ws";
4
- export const DEFAULT_MAX_CHUNK_BYTES = 256 * 1024;
5
- export const DEFAULT_MAX_BODY_BYTES = 64 * 1024 * 1024;
6
- export const DEFAULT_MAX_WS_FRAME_BYTES = 1024 * 1024;
5
+ export const DEFAULT_MAX_CHUNK_BYTES = SDK_DEFAULTS.proxy.maxChunkBytes;
6
+ export const DEFAULT_MAX_BODY_BYTES = SDK_DEFAULTS.proxy.maxBodyBytes;
7
+ export const DEFAULT_MAX_WS_FRAME_BYTES = SDK_DEFAULTS.proxy.maxWsFrameBytes;
@@ -7,6 +7,7 @@ const DEFAULT_REQUEST_HEADER_ALLOWLIST = new Set([
7
7
  "if-modified-since",
8
8
  "if-none-match",
9
9
  "if-unmodified-since",
10
+ "origin",
10
11
  "pragma",
11
12
  "range",
12
13
  "x-requested-with"
@@ -0,0 +1,27 @@
1
+ import type { Session } from "../endpoint/index.js";
2
+ import { RpcRouter, type RpcServerOptions } from "../rpc/server.js";
3
+ import type { YamuxStream } from "../yamux/stream.js";
4
+ import { PROXY_KIND_HTTP1, PROXY_KIND_WS } from "./constants.js";
5
+ export type ProxyServerOptions = Readonly<{
6
+ upstream: string;
7
+ upstreamOrigin?: string;
8
+ allowedUpstreamHosts?: readonly string[];
9
+ maxJsonFrameBytes?: number;
10
+ maxChunkBytes?: number;
11
+ maxBodyBytes?: number;
12
+ maxWsFrameBytes?: number;
13
+ defaultTimeoutMs?: number;
14
+ maxTimeoutMs?: number;
15
+ maxConcurrentStreams?: number;
16
+ extraRequestHeaders?: readonly string[];
17
+ extraResponseHeaders?: readonly string[];
18
+ blockedResponseHeaders?: readonly string[];
19
+ extraWsHeaders?: readonly string[];
20
+ forbiddenCookieNames?: readonly string[];
21
+ forbiddenCookieNamePrefixes?: readonly string[];
22
+ fetch?: typeof fetch;
23
+ rpcRouter?: RpcRouter;
24
+ rpcServerOptions?: RpcServerOptions;
25
+ }>;
26
+ export declare function serveProxySession(session: Session, options: ProxyServerOptions, signal?: AbortSignal): Promise<void>;
27
+ export declare function serveProxyStream(kind: typeof PROXY_KIND_HTTP1 | typeof PROXY_KIND_WS, stream: YamuxStream, options: ProxyServerOptions, signal?: AbortSignal): Promise<void>;