@floegence/flowersec-core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +42 -0
  3. package/YAMUX_ALIGNMENT.md +127 -0
  4. package/dist/_examples/flowersec/demo/v1.facade.gen.d.ts +12 -0
  5. package/dist/_examples/flowersec/demo/v1.facade.gen.js +15 -0
  6. package/dist/_examples/flowersec/demo/v1.gen.d.ts +16 -0
  7. package/dist/_examples/flowersec/demo/v1.gen.js +86 -0
  8. package/dist/_examples/flowersec/demo/v1.rpc.gen.d.ts +11 -0
  9. package/dist/_examples/flowersec/demo/v1.rpc.gen.js +22 -0
  10. package/dist/browser/connect.d.ts +12 -0
  11. package/dist/browser/connect.js +31 -0
  12. package/dist/browser/index.d.ts +2 -0
  13. package/dist/browser/index.js +1 -0
  14. package/dist/client-connect/common.d.ts +26 -0
  15. package/dist/client-connect/common.js +167 -0
  16. package/dist/client-connect/connectCore.d.ts +42 -0
  17. package/dist/client-connect/connectCore.js +302 -0
  18. package/dist/client-connect/tunnelAttachCloseReason.d.ts +3 -0
  19. package/dist/client-connect/tunnelAttachCloseReason.js +16 -0
  20. package/dist/client.d.ts +17 -0
  21. package/dist/client.js +1 -0
  22. package/dist/direct-client/connect.d.ts +4 -0
  23. package/dist/direct-client/connect.js +67 -0
  24. package/dist/direct-client/index.d.ts +1 -0
  25. package/dist/direct-client/index.js +1 -0
  26. package/dist/e2ee/constants.d.ts +9 -0
  27. package/dist/e2ee/constants.js +18 -0
  28. package/dist/e2ee/errors.d.ts +5 -0
  29. package/dist/e2ee/errors.js +8 -0
  30. package/dist/e2ee/framing.d.ts +12 -0
  31. package/dist/e2ee/framing.js +57 -0
  32. package/dist/e2ee/handshake.d.ts +80 -0
  33. package/dist/e2ee/handshake.js +322 -0
  34. package/dist/e2ee/index.d.ts +7 -0
  35. package/dist/e2ee/index.js +7 -0
  36. package/dist/e2ee/kdf.d.ts +15 -0
  37. package/dist/e2ee/kdf.js +39 -0
  38. package/dist/e2ee/record.d.ts +11 -0
  39. package/dist/e2ee/record.js +69 -0
  40. package/dist/e2ee/secureChannel.d.ts +82 -0
  41. package/dist/e2ee/secureChannel.js +265 -0
  42. package/dist/e2ee/transcript.d.ts +23 -0
  43. package/dist/e2ee/transcript.js +31 -0
  44. package/dist/facade.d.ts +21 -0
  45. package/dist/facade.js +61 -0
  46. package/dist/gen/flowersec/controlplane/v1.gen.d.ts +36 -0
  47. package/dist/gen/flowersec/controlplane/v1.gen.js +135 -0
  48. package/dist/gen/flowersec/direct/v1.gen.d.ts +21 -0
  49. package/dist/gen/flowersec/direct/v1.gen.js +101 -0
  50. package/dist/gen/flowersec/e2ee/v1.gen.d.ts +68 -0
  51. package/dist/gen/flowersec/e2ee/v1.gen.js +194 -0
  52. package/dist/gen/flowersec/rpc/v1.gen.d.ts +30 -0
  53. package/dist/gen/flowersec/rpc/v1.gen.js +107 -0
  54. package/dist/gen/flowersec/tunnel/v1.gen.d.ts +23 -0
  55. package/dist/gen/flowersec/tunnel/v1.gen.js +104 -0
  56. package/dist/index.d.ts +19 -0
  57. package/dist/index.js +19 -0
  58. package/dist/node/connect.d.ts +9 -0
  59. package/dist/node/connect.js +13 -0
  60. package/dist/node/index.d.ts +2 -0
  61. package/dist/node/index.js +2 -0
  62. package/dist/node/wsFactory.d.ts +2 -0
  63. package/dist/node/wsFactory.js +69 -0
  64. package/dist/observability/index.d.ts +1 -0
  65. package/dist/observability/index.js +1 -0
  66. package/dist/observability/observer.d.ts +23 -0
  67. package/dist/observability/observer.js +28 -0
  68. package/dist/rpc/callError.d.ts +5 -0
  69. package/dist/rpc/callError.js +11 -0
  70. package/dist/rpc/caller.d.ts +8 -0
  71. package/dist/rpc/caller.js +1 -0
  72. package/dist/rpc/client.d.ts +22 -0
  73. package/dist/rpc/client.js +170 -0
  74. package/dist/rpc/framing.d.ts +4 -0
  75. package/dist/rpc/framing.js +24 -0
  76. package/dist/rpc/index.d.ts +6 -0
  77. package/dist/rpc/index.js +6 -0
  78. package/dist/rpc/server.d.ts +15 -0
  79. package/dist/rpc/server.js +67 -0
  80. package/dist/rpc/typed.d.ts +5 -0
  81. package/dist/rpc/typed.js +9 -0
  82. package/dist/rpc/validate.d.ts +2 -0
  83. package/dist/rpc/validate.js +27 -0
  84. package/dist/rpc-proxy/index.d.ts +1 -0
  85. package/dist/rpc-proxy/index.js +1 -0
  86. package/dist/rpc-proxy/rpcProxy.d.ts +13 -0
  87. package/dist/rpc-proxy/rpcProxy.js +59 -0
  88. package/dist/streamhello/index.d.ts +1 -0
  89. package/dist/streamhello/index.js +1 -0
  90. package/dist/streamhello/streamHello.d.ts +3 -0
  91. package/dist/streamhello/streamHello.js +13 -0
  92. package/dist/tunnel-client/connect.d.ts +7 -0
  93. package/dist/tunnel-client/connect.js +125 -0
  94. package/dist/tunnel-client/index.d.ts +1 -0
  95. package/dist/tunnel-client/index.js +1 -0
  96. package/dist/utils/base64url.d.ts +2 -0
  97. package/dist/utils/base64url.js +40 -0
  98. package/dist/utils/bin.d.ts +6 -0
  99. package/dist/utils/bin.js +55 -0
  100. package/dist/utils/errors.d.ts +26 -0
  101. package/dist/utils/errors.js +42 -0
  102. package/dist/utils/number.d.ts +2 -0
  103. package/dist/utils/number.js +9 -0
  104. package/dist/ws/index.d.ts +1 -0
  105. package/dist/ws/index.js +1 -0
  106. package/dist/ws-client/binaryTransport.d.ts +49 -0
  107. package/dist/ws-client/binaryTransport.js +301 -0
  108. package/dist/yamux/byteReader.d.ts +10 -0
  109. package/dist/yamux/byteReader.js +50 -0
  110. package/dist/yamux/constants.d.ts +10 -0
  111. package/dist/yamux/constants.js +14 -0
  112. package/dist/yamux/header.d.ts +17 -0
  113. package/dist/yamux/header.js +26 -0
  114. package/dist/yamux/index.d.ts +5 -0
  115. package/dist/yamux/index.js +5 -0
  116. package/dist/yamux/session.d.ts +44 -0
  117. package/dist/yamux/session.js +228 -0
  118. package/dist/yamux/stream.d.ts +30 -0
  119. package/dist/yamux/stream.js +222 -0
  120. package/package.json +112 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Floegence, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @floegence/flowersec-core
2
+
3
+ Flowersec core TypeScript library for building an end-to-end encrypted, multiplexed connection over WebSocket (browser-friendly).
4
+
5
+ Status: experimental; not audited.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @floegence/flowersec-core
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Browser (recommended):
16
+
17
+ ```ts
18
+ import { connectBrowser } from "@floegence/flowersec-core/browser";
19
+
20
+ const grant = await fetch("/api/flowersec/channel/init", { method: "POST" }).then((r) => r.json());
21
+ const client = await connectBrowser(grant);
22
+ await client.ping();
23
+ client.close();
24
+ ```
25
+
26
+ Node.js (recommended):
27
+
28
+ ```ts
29
+ import { connectNode } from "@floegence/flowersec-core/node";
30
+
31
+ const grant = await fetch("https://your-app.example/api/flowersec/channel/init", { method: "POST" }).then((r) => r.json());
32
+ const client = await connectNode(grant, { origin: "https://your-app.example" });
33
+ await client.ping();
34
+ client.close();
35
+ ```
36
+
37
+ ## Docs
38
+
39
+ - Frontend quickstart: `docs/FRONTEND_QUICKSTART.md`
40
+ - Integration guide: `docs/INTEGRATION_GUIDE.md`
41
+ - API surface contract: `docs/API_SURFACE.md`
42
+ - Error model: `docs/ERROR_MODEL.md`
@@ -0,0 +1,127 @@
1
+ # TS Yamux Alignment Status
2
+
3
+ This repository contains a TypeScript implementation of the Yamux protocol in `flowersec-ts/src/yamux/`.
4
+ It is designed to be interoperable with HashiCorp's Yamux (protocol version `0`) for the subset of
5
+ features required by Flowersec: multiplexing multiple logical byte streams over a single encrypted
6
+ transport.
7
+
8
+ ## What “aligned” means here
9
+
10
+ “Aligned” in this document means:
11
+
12
+ - **Wire compatibility**: our frames can be understood by `github.com/hashicorp/yamux` and vice versa.
13
+ - **Behavioral compatibility (subset)**: stream open/accept, data transfer, and basic flow control work
14
+ correctly when interoperating with HashiCorp Yamux.
15
+ - **Not a full spec clone**: some Yamux behaviors are intentionally simplified or currently unimplemented.
16
+
17
+ ## Implemented protocol surface (TS)
18
+
19
+ Source: `flowersec-ts/src/yamux/constants.ts`, `flowersec-ts/src/yamux/header.ts`, `flowersec-ts/src/yamux/session.ts`, `flowersec-ts/src/yamux/stream.ts`.
20
+
21
+ ### Header & framing
22
+
23
+ - 12-byte header (`HEADER_LEN=12`) with:
24
+ - `version: u8`
25
+ - `type: u8`
26
+ - `flags: u16` (big-endian)
27
+ - `stream_id: u32` (big-endian)
28
+ - `length: u32` (big-endian)
29
+ - Supported `version`: `0` (`YAMUX_VERSION=0`).
30
+ - Unknown `type` or invalid `version` causes session close (strict).
31
+
32
+ ### Frame types & flags
33
+
34
+ - Types: `DATA(0)`, `WINDOW_UPDATE(1)`, `PING(2)`, `GO_AWAY(3)` (`flowersec-ts/src/yamux/constants.ts`).
35
+ - Flags: `SYN(1)`, `ACK(2)`, `FIN(4)`, `RST(8)`.
36
+
37
+ ### Stream IDs
38
+
39
+ - Client opens **odd** stream IDs, server opens **even** stream IDs:
40
+ - `opts.client=true` starts at `1`, step `+2`
41
+ - `opts.client=false` starts at `2`, step `+2`
42
+ - (`flowersec-ts/src/yamux/session.ts`)
43
+
44
+ ### Stream open handshake
45
+
46
+ - Opening a stream starts by sending a `WINDOW_UPDATE` with `SYN` (length may be `0`).
47
+ - Peer creates the stream when it receives `SYN` on either `DATA` or `WINDOW_UPDATE`.
48
+ - The peer responds with a `WINDOW_UPDATE` carrying `ACK`, moving both sides into “established”.
49
+
50
+ ### Flow control (per-stream windows)
51
+
52
+ - Default per-stream max window: `256 KiB` (`DEFAULT_MAX_STREAM_WINDOW=256*1024`).
53
+ - Each stream maintains:
54
+ - `recvWindow`: decremented by received `DATA` length; receiving beyond window triggers `RST`.
55
+ - `sendWindow`: incremented by `WINDOW_UPDATE` deltas; writing waits if `sendWindow<=0`.
56
+ - `WINDOW_UPDATE` generation strategy:
57
+ - Always send for SYN/ACK transitions.
58
+ - Otherwise, send when replenishment delta reaches at least half the max window.
59
+
60
+ ### FIN/RST semantics
61
+
62
+ - Local close sends a `WINDOW_UPDATE` with `FIN`.
63
+ - Receiving `FIN` transitions the stream to a closed-ish state and unblocks pending reads.
64
+ - `RST` is sent as `WINDOW_UPDATE` with `RST` (and length `0`) and terminates the stream.
65
+ - If a frame references an unknown stream **without** `SYN`, TS sends `RST`.
66
+
67
+ ### PING / GO_AWAY behavior
68
+
69
+ - PING:
70
+ - If `SYN` is set, TS replies with `ACK` and the same opaque `length`.
71
+ - TS does not currently initiate pings or track RTT.
72
+ - GO_AWAY:
73
+ - Any `GO_AWAY` immediately closes the session (no reason code handling).
74
+
75
+ ## Interop evidence (what is actually tested today)
76
+
77
+ The TS Yamux implementation is exercised in real interop scenarios:
78
+
79
+ - **TS client ↔ Go server (minimal Yamux over TCP)**:
80
+ - Test: `flowersec-ts/src/e2e/yamux_interop.test.ts` (minimal tcp mode)
81
+ - Go harness: `flowersec-go/internal/cmd/flowersec-yamux-harness/main.go`
82
+ - Covers: window update race, RST handling, concurrent open/close, session close.
83
+ - Notes: opt-in via `YAMUX_INTEROP=1` (runs Go harnesses).
84
+ - Notes: sizes scale via `YAMUX_INTEROP_SCALE` (e.g. `2` => 20 streams / 1 MiB per stream).
85
+ - Notes: client-initiated RST scenarios run when `YAMUX_INTEROP_CLIENT_RST=1`.
86
+ - Notes: window-update and concurrent-open/close stress runs when `YAMUX_INTEROP_STRESS=1`.
87
+ - **TS client ↔ Go server (full chain: E2EE + tunnel + Yamux)**:
88
+ - Test: `flowersec-ts/src/e2e/yamux_interop.test.ts` (full chain mode)
89
+ - Go harness: `flowersec-go/internal/cmd/flowersec-e2e-harness/main.go` with `-scenario`
90
+ - Notes: reduced stream counts/payload sizes to keep end-to-end runtime bounded.
91
+ - **Layered close/reset probes (memory + WS + full chain)**:
92
+ - Test: `flowersec-ts/src/e2e/yamux_interop_layers.test.ts`
93
+ - Covers: session-close wakeup, FIN/RST delivery across SecureChannel + WebSocketBinaryTransport,
94
+ and a full-chain FIN/RST probe on `rst_mid_write_go`.
95
+ - Notes: gated by `YAMUX_INTEROP=1`; the full-chain probe additionally requires
96
+ `YAMUX_INTEROP_DEBUG=1`.
97
+ - **TS client ↔ Go server (RPC happy path)**:
98
+ - Test: `flowersec-ts/src/e2e/go_integration.test.ts`
99
+ - Go harness: `flowersec-go/internal/cmd/flowersec-e2e-harness/main.go`
100
+ - Covers: single-stream RPC framing and basic window updates.
101
+
102
+ These confirm wire-level interop and the correctness of the happy-path subset under real IO.
103
+
104
+ ## Current gaps (not yet proven or intentionally simplified)
105
+
106
+ These items are either untested or simplified compared to HashiCorp Yamux behavior:
107
+
108
+ - **Multi-stream concurrency**: covered by interop tests, but fairness/priority under heavy contention
109
+ is not proven.
110
+ - **Large payload / fragmentation**: covered up to ~1 MiB per stream; larger payloads and pathological
111
+ fragmentation are still untested.
112
+ - **Stress & fuzz robustness**: no fuzz tests for invalid headers/flags/lengths and adversarial streams.
113
+ - **Session-level behaviors**: GO_AWAY reason handling, keepalive/heartbeats, and more nuanced shutdown.
114
+ - **Strictness differences**: unknown frame types trigger session close, which may be stricter than some
115
+ implementations.
116
+ - **Server-initiated streams**: Go-initiated streams are not covered by interop tests yet.
117
+ - **Bidirectional window-update stress**: current window update tests run TS -> Go only.
118
+
119
+ ## Practical alignment summary
120
+
121
+ - **Aligned (high confidence)**:
122
+ - Version 0 framing, stream ID parity, open handshake via SYN/ACK, basic DATA transfer, basic window
123
+ updates, and interop with HashiCorp Yamux server for the Flowersec RPC use-case.
124
+ - **Aligned but under-tested**:
125
+ - FIN/RST edges, ping handling, and window boundary conditions.
126
+ - **Not aligned / out of scope today**:
127
+ - Full Yamux feature parity (advanced GO_AWAY semantics, keepalive, exhaustive error handling).
@@ -0,0 +1,12 @@
1
+ import type { Client } from "../../../client.js";
2
+ import type { DirectConnectOptions } from "../../../direct-client/connect.js";
3
+ import type { TunnelConnectOptions } from "../../../tunnel-client/connect.js";
4
+ import type { ChannelInitGrant } from "../../../gen/flowersec/controlplane/v1.gen.js";
5
+ import type { DirectConnectInfo } from "../../../gen/flowersec/direct/v1.gen.js";
6
+ import { createDemoClient } from "./v1.rpc.gen.js";
7
+ export type DemoSession = Client & Readonly<{
8
+ demo: ReturnType<typeof createDemoClient>;
9
+ }>;
10
+ export declare function createDemoSession(client: Client): DemoSession;
11
+ export declare function connectDemoTunnel(grant: ChannelInitGrant, opts: TunnelConnectOptions): Promise<DemoSession>;
12
+ export declare function connectDemoDirect(info: DirectConnectInfo, opts: DirectConnectOptions): Promise<DemoSession>;
@@ -0,0 +1,15 @@
1
+ // Code generated by idlgen. DO NOT EDIT.
2
+ import { connectDirect } from "../../../direct-client/connect.js";
3
+ import { connectTunnel } from "../../../tunnel-client/connect.js";
4
+ import { createDemoClient, } from "./v1.rpc.gen.js";
5
+ export function createDemoSession(client) {
6
+ return { ...client, demo: createDemoClient(client.rpc) };
7
+ }
8
+ export async function connectDemoTunnel(grant, opts) {
9
+ const client = await connectTunnel(grant, opts);
10
+ return createDemoSession(client);
11
+ }
12
+ export async function connectDemoDirect(info, opts) {
13
+ const client = await connectDirect(info, opts);
14
+ return createDemoSession(client);
15
+ }
@@ -0,0 +1,16 @@
1
+ /** Demo hello notification. */
2
+ export interface HelloNotify {
3
+ /** Hello message. */
4
+ hello: string;
5
+ }
6
+ /** Demo ping request. */
7
+ export interface PingRequest {
8
+ }
9
+ /** Demo ping response. */
10
+ export interface PingResponse {
11
+ /** Whether the request succeeded. */
12
+ ok: boolean;
13
+ }
14
+ export declare function assertHelloNotify(v: unknown): HelloNotify;
15
+ export declare function assertPingRequest(v: unknown): PingRequest;
16
+ export declare function assertPingResponse(v: unknown): PingResponse;
@@ -0,0 +1,86 @@
1
+ // Code generated by idlgen. DO NOT EDIT.
2
+ function isRecord(v) {
3
+ return typeof v === "object" && v != null && !Array.isArray(v);
4
+ }
5
+ function assertString(name, v) {
6
+ if (typeof v !== "string")
7
+ throw new Error(`bad ${name}`);
8
+ return v;
9
+ }
10
+ function assertBoolean(name, v) {
11
+ if (typeof v !== "boolean")
12
+ throw new Error(`bad ${name}`);
13
+ return v;
14
+ }
15
+ function assertSafeInt(name, v) {
16
+ if (typeof v !== "number" || !Number.isSafeInteger(v))
17
+ throw new Error(`bad ${name}`);
18
+ return v;
19
+ }
20
+ function assertU32(name, v) {
21
+ const n = assertSafeInt(name, v);
22
+ if (n < 0 || n > 0xffffffff)
23
+ throw new Error(`bad ${name}`);
24
+ return n;
25
+ }
26
+ function assertU16(name, v) {
27
+ const n = assertU32(name, v);
28
+ if (n > 0xffff)
29
+ throw new Error(`bad ${name}`);
30
+ return n;
31
+ }
32
+ function assertU8(name, v) {
33
+ const n = assertU32(name, v);
34
+ if (n > 0xff)
35
+ throw new Error(`bad ${name}`);
36
+ return n;
37
+ }
38
+ function assertU64(name, v) {
39
+ const n = assertSafeInt(name, v);
40
+ if (n < 0)
41
+ throw new Error(`bad ${name}`);
42
+ return n;
43
+ }
44
+ function assertI32(name, v) {
45
+ const n = assertSafeInt(name, v);
46
+ if (n < -2147483648 || n > 2147483647)
47
+ throw new Error(`bad ${name}`);
48
+ return n;
49
+ }
50
+ function assertI64(name, v) {
51
+ return assertSafeInt(name, v);
52
+ }
53
+ function assertStringMap(name, v) {
54
+ if (!isRecord(v))
55
+ throw new Error(`bad ${name}`);
56
+ for (const [k, vv] of Object.entries(v)) {
57
+ void k;
58
+ if (typeof vv !== "string")
59
+ throw new Error(`bad ${name}`);
60
+ }
61
+ return v;
62
+ }
63
+ export function assertHelloNotify(v) {
64
+ if (!isRecord(v))
65
+ throw new Error("bad HelloNotify");
66
+ const o = v;
67
+ if (o["hello"] === undefined)
68
+ throw new Error("bad HelloNotify.hello");
69
+ assertString("HelloNotify.hello", o["hello"]);
70
+ return o;
71
+ }
72
+ export function assertPingRequest(v) {
73
+ if (!isRecord(v))
74
+ throw new Error("bad PingRequest");
75
+ const o = v;
76
+ return o;
77
+ }
78
+ export function assertPingResponse(v) {
79
+ if (!isRecord(v))
80
+ throw new Error("bad PingResponse");
81
+ const o = v;
82
+ if (o["ok"] === undefined)
83
+ throw new Error("bad PingResponse.ok");
84
+ assertBoolean("PingResponse.ok", o["ok"]);
85
+ return o;
86
+ }
@@ -0,0 +1,11 @@
1
+ import type { RpcCaller } from "../../../rpc/caller.js";
2
+ import { HelloNotify, PingRequest, PingResponse } from "./v1.gen.js";
3
+ /** Demo service used by examples and integration tests. */
4
+ export declare function createDemoClient(rpc: RpcCaller): {
5
+ /** Hello notification. */
6
+ onHello: (handler: (v: HelloNotify) => void) => () => void;
7
+ /** Ping request/response. */
8
+ ping: (req: PingRequest, opts?: Readonly<{
9
+ signal?: AbortSignal;
10
+ }>) => Promise<PingResponse>;
11
+ };
@@ -0,0 +1,22 @@
1
+ // Code generated by idlgen. DO NOT EDIT.
2
+ import { RpcCallError } from "../../../rpc/callError.js";
3
+ import { assertHelloNotify, assertPingRequest, assertPingResponse, } from "./v1.gen.js";
4
+ /** Demo service used by examples and integration tests. */
5
+ export function createDemoClient(rpc) {
6
+ return {
7
+ /** Hello notification. */
8
+ onHello: (handler) => {
9
+ return rpc.onNotify(2, (payload) => {
10
+ handler(assertHelloNotify(payload));
11
+ });
12
+ },
13
+ /** Ping request/response. */
14
+ ping: async (req, opts = {}) => {
15
+ const checkedReq = assertPingRequest(req);
16
+ const resp = await rpc.call(1, checkedReq, opts.signal);
17
+ if (resp.error != null)
18
+ throw new RpcCallError(resp.error.code, resp.error.message, 1);
19
+ return assertPingResponse(resp.payload);
20
+ },
21
+ };
22
+ }
@@ -0,0 +1,12 @@
1
+ import type { Client } from "../client.js";
2
+ import type { DirectConnectOptions } from "../direct-client/connect.js";
3
+ import type { TunnelConnectOptions } from "../tunnel-client/connect.js";
4
+ import { type ConnectOptions } from "../facade.js";
5
+ import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
6
+ import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
7
+ export type TunnelConnectBrowserOptions = Omit<TunnelConnectOptions, "origin" | "wsFactory">;
8
+ export type DirectConnectBrowserOptions = Omit<DirectConnectOptions, "origin" | "wsFactory">;
9
+ export type ConnectBrowserOptions = Omit<ConnectOptions, "origin" | "wsFactory">;
10
+ export declare function connectBrowser(input: unknown, opts?: ConnectBrowserOptions): Promise<Client>;
11
+ export declare function connectTunnelBrowser(grant: ChannelInitGrant, opts?: TunnelConnectBrowserOptions): Promise<Client>;
12
+ export declare function connectDirectBrowser(info: DirectConnectInfo, opts?: DirectConnectBrowserOptions): Promise<Client>;
@@ -0,0 +1,31 @@
1
+ import { connectDirect } from "../direct-client/connect.js";
2
+ import { connectTunnel } from "../tunnel-client/connect.js";
3
+ import { connect } from "../facade.js";
4
+ import { FlowersecError } from "../utils/errors.js";
5
+ function getBrowserOrigin() {
6
+ if (typeof window === "undefined")
7
+ return "";
8
+ const o = window?.location?.origin;
9
+ return typeof o === "string" ? o : "";
10
+ }
11
+ export async function connectBrowser(input, opts = {}) {
12
+ const origin = getBrowserOrigin();
13
+ if (origin === "") {
14
+ throw new FlowersecError({ stage: "validate", code: "missing_origin", path: "auto", message: "missing browser origin" });
15
+ }
16
+ return await connect(input, { ...opts, origin });
17
+ }
18
+ export async function connectTunnelBrowser(grant, opts = {}) {
19
+ const origin = getBrowserOrigin();
20
+ if (origin === "") {
21
+ throw new FlowersecError({ stage: "validate", code: "missing_origin", path: "tunnel", message: "missing browser origin" });
22
+ }
23
+ return await connectTunnel(grant, { ...opts, origin });
24
+ }
25
+ export async function connectDirectBrowser(info, opts = {}) {
26
+ const origin = getBrowserOrigin();
27
+ if (origin === "") {
28
+ throw new FlowersecError({ stage: "validate", code: "missing_origin", path: "direct", message: "missing browser origin" });
29
+ }
30
+ return await connectDirect(info, { ...opts, origin });
31
+ }
@@ -0,0 +1,2 @@
1
+ export type { ConnectBrowserOptions, DirectConnectBrowserOptions, TunnelConnectBrowserOptions } from "./connect.js";
2
+ export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
@@ -0,0 +1 @@
1
+ export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
@@ -0,0 +1,26 @@
1
+ import type { WebSocketLike } from "../ws-client/binaryTransport.js";
2
+ export declare class OriginMismatchError extends Error {
3
+ readonly expected: string;
4
+ readonly got: string;
5
+ constructor(expected: string, got: string);
6
+ }
7
+ export declare class WsFactoryRequiredError extends Error {
8
+ constructor();
9
+ }
10
+ export declare function createWebSocket(url: string, origin: string, wsFactory: ((url: string, origin: string) => WebSocketLike) | undefined): WebSocketLike;
11
+ export declare function classifyConnectError(err: unknown): "websocket_error" | "websocket_closed" | "timeout" | "canceled";
12
+ export declare function classifyHandshakeError(err: unknown): "auth_tag_mismatch" | "handshake_failed" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "timeout" | "canceled";
13
+ export declare function withAbortAndTimeout<T>(p: Promise<T>, opts: Readonly<{
14
+ signal?: AbortSignal;
15
+ timeoutMs?: number;
16
+ onCancel?: () => void;
17
+ }>): Promise<T>;
18
+ export declare function waitOpen(ws: WebSocketLike, opts?: Readonly<{
19
+ signal?: AbortSignal;
20
+ timeoutMs?: number;
21
+ }>): Promise<void>;
22
+ export declare function randomBytes(n: number): Uint8Array;
23
+ export declare function ioReadOpts(signal: AbortSignal | undefined, timeoutMs: number | undefined): {
24
+ signal?: AbortSignal;
25
+ timeoutMs?: number;
26
+ };
@@ -0,0 +1,167 @@
1
+ import { AbortError, TimeoutError, isAbortError, isTimeoutError, throwIfAborted } from "../utils/errors.js";
2
+ import { E2EEHandshakeError } from "../e2ee/errors.js";
3
+ class WebSocketClosedError extends Error {
4
+ constructor() {
5
+ super("websocket closed");
6
+ this.name = "WebSocketClosedError";
7
+ }
8
+ }
9
+ class WebSocketOpenError extends Error {
10
+ constructor() {
11
+ super("websocket error");
12
+ this.name = "WebSocketOpenError";
13
+ }
14
+ }
15
+ export class OriginMismatchError extends Error {
16
+ expected;
17
+ got;
18
+ constructor(expected, got) {
19
+ super(`origin mismatch: expected ${expected}, got ${got}`);
20
+ this.name = "OriginMismatchError";
21
+ this.expected = expected;
22
+ this.got = got;
23
+ }
24
+ }
25
+ export class WsFactoryRequiredError extends Error {
26
+ constructor() {
27
+ super("wsFactory is required outside the browser to set the Origin header (use connectTunnelNode/connectDirectNode or createNodeWsFactory from @floegence/flowersec-core/node)");
28
+ this.name = "WsFactoryRequiredError";
29
+ }
30
+ }
31
+ function defaultWebSocketFactory(url) {
32
+ return new WebSocket(url);
33
+ }
34
+ function isBrowserEnv() {
35
+ return typeof window !== "undefined" && typeof window.location !== "undefined" && typeof window.location.origin === "string";
36
+ }
37
+ export function createWebSocket(url, origin, wsFactory) {
38
+ if (wsFactory != null)
39
+ return wsFactory(url, origin);
40
+ if (isBrowserEnv()) {
41
+ if (window.location.origin !== origin)
42
+ throw new OriginMismatchError(origin, window.location.origin);
43
+ return defaultWebSocketFactory(url);
44
+ }
45
+ throw new WsFactoryRequiredError();
46
+ }
47
+ export function classifyConnectError(err) {
48
+ if (isTimeoutError(err))
49
+ return "timeout";
50
+ if (isAbortError(err))
51
+ return "canceled";
52
+ if (err instanceof WebSocketClosedError)
53
+ return "websocket_closed";
54
+ return "websocket_error";
55
+ }
56
+ export function classifyHandshakeError(err) {
57
+ if (isTimeoutError(err))
58
+ return "timeout";
59
+ if (isAbortError(err))
60
+ return "canceled";
61
+ if (err instanceof E2EEHandshakeError)
62
+ return err.code;
63
+ return "handshake_failed";
64
+ }
65
+ export async function withAbortAndTimeout(p, opts) {
66
+ if (opts.signal?.aborted) {
67
+ opts.onCancel?.();
68
+ throw new AbortError("canceled");
69
+ }
70
+ const timeoutMs = Math.max(0, opts.timeoutMs ?? 0);
71
+ if (timeoutMs <= 0 && opts.signal == null)
72
+ return await p;
73
+ return await new Promise((resolve, reject) => {
74
+ let settled = false;
75
+ const cleanup = () => {
76
+ settled = true;
77
+ if (timer != null)
78
+ clearTimeout(timer);
79
+ opts.signal?.removeEventListener("abort", onAbort);
80
+ };
81
+ const finish = (fn) => {
82
+ if (settled)
83
+ return;
84
+ cleanup();
85
+ fn();
86
+ };
87
+ const onAbort = () => {
88
+ opts.onCancel?.();
89
+ finish(() => reject(new AbortError("canceled")));
90
+ };
91
+ const timer = timeoutMs > 0
92
+ ? setTimeout(() => {
93
+ opts.onCancel?.();
94
+ finish(() => reject(new TimeoutError("timeout")));
95
+ }, timeoutMs)
96
+ : undefined;
97
+ opts.signal?.addEventListener("abort", onAbort);
98
+ p.then((v) => finish(() => resolve(v)), (e) => finish(() => reject(e)));
99
+ });
100
+ }
101
+ // waitOpen resolves when the websocket opens or rejects on error/close.
102
+ export function waitOpen(ws, opts = {}) {
103
+ return new Promise((resolve, reject) => {
104
+ if (opts.signal?.aborted) {
105
+ reject(new AbortError("connect aborted"));
106
+ return;
107
+ }
108
+ const onOpen = () => {
109
+ cleanup();
110
+ resolve();
111
+ };
112
+ const onErr = () => {
113
+ cleanup();
114
+ reject(new WebSocketOpenError());
115
+ };
116
+ const onClose = () => {
117
+ cleanup();
118
+ reject(new WebSocketClosedError());
119
+ };
120
+ const onAbort = () => {
121
+ cleanup();
122
+ try {
123
+ ws.close();
124
+ }
125
+ catch {
126
+ // ignore
127
+ }
128
+ reject(new AbortError("connect aborted"));
129
+ };
130
+ const timeoutMs = Math.max(0, opts.timeoutMs ?? 0);
131
+ const timer = timeoutMs > 0
132
+ ? setTimeout(() => {
133
+ cleanup();
134
+ try {
135
+ ws.close();
136
+ }
137
+ catch {
138
+ // ignore
139
+ }
140
+ reject(new TimeoutError("connect timeout"));
141
+ }, timeoutMs)
142
+ : undefined;
143
+ const cleanup = () => {
144
+ if (timer != null)
145
+ clearTimeout(timer);
146
+ ws.removeEventListener("open", onOpen);
147
+ ws.removeEventListener("error", onErr);
148
+ ws.removeEventListener("close", onClose);
149
+ opts.signal?.removeEventListener("abort", onAbort);
150
+ };
151
+ ws.addEventListener("open", onOpen);
152
+ ws.addEventListener("error", onErr);
153
+ ws.addEventListener("close", onClose);
154
+ opts.signal?.addEventListener("abort", onAbort);
155
+ });
156
+ }
157
+ // randomBytes uses the Web Crypto API for nonces and IDs.
158
+ export function randomBytes(n) {
159
+ const out = new Uint8Array(n);
160
+ crypto.getRandomValues(out);
161
+ return out;
162
+ }
163
+ export function ioReadOpts(signal, timeoutMs) {
164
+ throwIfAborted(signal, "operation aborted");
165
+ const ms = timeoutMs == null ? undefined : Math.max(0, timeoutMs);
166
+ return signal != null ? { signal, ...(ms !== undefined ? { timeoutMs: ms } : {}) } : ms !== undefined ? { timeoutMs: ms } : {};
167
+ }
@@ -0,0 +1,42 @@
1
+ import { type ClientObserverLike } from "../observability/observer.js";
2
+ import { type WebSocketLike } from "../ws-client/binaryTransport.js";
3
+ import type { ClientInternal } from "../client.js";
4
+ export type ConnectOptionsBase = Readonly<{
5
+ /** Explicit Origin value (required). In browsers this must match window.location.origin. */
6
+ origin: string;
7
+ /** Optional AbortSignal to cancel connect/handshake. */
8
+ signal?: AbortSignal;
9
+ /** Optional websocket connect timeout in milliseconds (0 disables). */
10
+ connectTimeoutMs?: number;
11
+ /** Optional total E2EE handshake timeout in milliseconds (0 disables). */
12
+ handshakeTimeoutMs?: number;
13
+ /** Feature bitset advertised during the E2EE handshake. */
14
+ clientFeatures?: number;
15
+ /** Maximum allowed bytes for handshake payloads (0 uses default). */
16
+ maxHandshakePayload?: number;
17
+ /** Maximum encrypted record size on the wire (0 uses default). */
18
+ maxRecordBytes?: number;
19
+ /** Maximum buffered plaintext bytes in the secure channel (0 uses default). */
20
+ maxBufferedBytes?: number;
21
+ /** Maximum queued websocket bytes before backpressure (0 uses default). */
22
+ maxWsQueuedBytes?: number;
23
+ /** Optional factory for creating the WebSocket instance. */
24
+ wsFactory?: (url: string, origin: string) => WebSocketLike;
25
+ /** Optional observer for client metrics. */
26
+ observer?: ClientObserverLike;
27
+ /** Encrypted keepalive ping interval in milliseconds (0 disables). */
28
+ keepaliveIntervalMs?: number;
29
+ }>;
30
+ export type ConnectCoreArgs = Readonly<{
31
+ path: "tunnel" | "direct";
32
+ wsUrl: string;
33
+ channelId: string;
34
+ e2eePskB64u: string;
35
+ defaultSuite: number;
36
+ opts: ConnectOptionsBase;
37
+ attach?: Readonly<{
38
+ attachJson: string;
39
+ endpointInstanceId: string;
40
+ }>;
41
+ }>;
42
+ export declare function connectCore(args: ConnectCoreArgs): Promise<ClientInternal>;