@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
@@ -0,0 +1,13 @@
1
+ import { connectDirect } from "../direct-client/connect.js";
2
+ import { connectTunnel } from "../tunnel-client/connect.js";
3
+ import { connect } from "../facade.js";
4
+ import { createNodeWsFactory } from "./wsFactory.js";
5
+ export async function connectNode(input, opts) {
6
+ return await connect(input, { ...opts, wsFactory: opts.wsFactory ?? createNodeWsFactory() });
7
+ }
8
+ export async function connectTunnelNode(grant, opts) {
9
+ return await connectTunnel(grant, { ...opts, wsFactory: opts.wsFactory ?? createNodeWsFactory() });
10
+ }
11
+ export async function connectDirectNode(info, opts) {
12
+ return await connectDirect(info, { ...opts, wsFactory: opts.wsFactory ?? createNodeWsFactory() });
13
+ }
@@ -0,0 +1,2 @@
1
+ export { createNodeWsFactory } from "./wsFactory.js";
2
+ export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
@@ -0,0 +1,2 @@
1
+ export { createNodeWsFactory } from "./wsFactory.js";
2
+ export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
@@ -0,0 +1,2 @@
1
+ import type { WebSocketLike } from "../ws-client/binaryTransport.js";
2
+ export declare function createNodeWsFactory(): (url: string, origin: string) => WebSocketLike;
@@ -0,0 +1,69 @@
1
+ import { createRequire } from "node:module";
2
+ // createNodeWsFactory returns a wsFactory compatible with connectTunnel/connectDirect in Node.js.
3
+ //
4
+ // It uses the "ws" package to set the Origin header explicitly (browsers set Origin automatically).
5
+ export function createNodeWsFactory() {
6
+ const require = createRequire(import.meta.url);
7
+ const wsMod = require("ws");
8
+ const WebSocketCtor = wsMod?.WebSocket ?? wsMod;
9
+ return (url, origin) => {
10
+ const raw = new WebSocketCtor(url, { headers: { Origin: origin } });
11
+ // Map (type -> user listener -> wrapped listener) so removeEventListener works.
12
+ const listeners = new Map();
13
+ const addEventListener = (type, listener) => {
14
+ const wrapped = (...args) => {
15
+ if (type === "message") {
16
+ listener({ data: args[0] });
17
+ return;
18
+ }
19
+ if (type === "close") {
20
+ const code = typeof args[0] === "number" ? args[0] : undefined;
21
+ const reason = typeof args[1] === "string"
22
+ ? args[1]
23
+ : args[1] != null && typeof args[1].toString === "function"
24
+ ? args[1].toString()
25
+ : undefined;
26
+ listener({ code, reason });
27
+ return;
28
+ }
29
+ listener(args[0]);
30
+ };
31
+ let m = listeners.get(type);
32
+ if (m == null) {
33
+ m = new Map();
34
+ listeners.set(type, m);
35
+ }
36
+ m.set(listener, wrapped);
37
+ raw.on(type, wrapped);
38
+ };
39
+ const removeEventListener = (type, listener) => {
40
+ const m = listeners.get(type);
41
+ const wrapped = m?.get(listener);
42
+ if (wrapped == null)
43
+ return;
44
+ m.delete(listener);
45
+ raw.off(type, wrapped);
46
+ if (m.size === 0)
47
+ listeners.delete(type);
48
+ };
49
+ return {
50
+ get binaryType() {
51
+ return raw.binaryType;
52
+ },
53
+ set binaryType(v) {
54
+ raw.binaryType = v;
55
+ },
56
+ get readyState() {
57
+ return raw.readyState;
58
+ },
59
+ send(data) {
60
+ raw.send(data);
61
+ },
62
+ close(code, reason) {
63
+ raw.close(code, reason);
64
+ },
65
+ addEventListener,
66
+ removeEventListener,
67
+ };
68
+ };
69
+ }
@@ -0,0 +1 @@
1
+ export * from "./observer.js";
@@ -0,0 +1 @@
1
+ export * from "./observer.js";
@@ -0,0 +1,23 @@
1
+ import type { ClientPath } from "../client.js";
2
+ export type ConnectResult = "ok" | "fail";
3
+ export type ConnectReason = "websocket_error" | "websocket_closed" | "timeout" | "canceled";
4
+ export type AttachResult = "ok" | "fail";
5
+ export type AttachReason = "send_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "role_mismatch" | "token_replay" | "replace_rate_limited" | "attach_failed";
6
+ export type HandshakeResult = "ok" | "fail";
7
+ export type HandshakeReason = "auth_tag_mismatch" | "handshake_failed" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "timeout" | "canceled";
8
+ export type WsCloseKind = "local" | "peer_or_error";
9
+ export type WsErrorReason = "error" | "recv_buffer_exceeded" | "unexpected_text_frame" | "unexpected_message_type";
10
+ export type RpcCallResult = "ok" | "rpc_error" | "handler_not_found" | "transport_error" | "canceled";
11
+ export type ClientObserver = {
12
+ onConnect(path: ClientPath, result: ConnectResult, reason: ConnectReason | undefined, elapsedSeconds: number): void;
13
+ onAttach(result: AttachResult, reason: AttachReason | undefined): void;
14
+ onHandshake(path: ClientPath, result: HandshakeResult, reason: HandshakeReason | undefined, elapsedSeconds: number): void;
15
+ onWsClose(kind: WsCloseKind, code?: number): void;
16
+ onWsError(reason: WsErrorReason): void;
17
+ onRpcCall(result: RpcCallResult, elapsedSeconds: number): void;
18
+ onRpcNotify(): void;
19
+ };
20
+ export type ClientObserverLike = Partial<ClientObserver>;
21
+ export declare const NoopObserver: ClientObserver;
22
+ export declare function normalizeObserver(observer?: ClientObserverLike): ClientObserver;
23
+ export declare function nowSeconds(): number;
@@ -0,0 +1,28 @@
1
+ export const NoopObserver = {
2
+ onConnect: () => { },
3
+ onAttach: () => { },
4
+ onHandshake: () => { },
5
+ onWsClose: () => { },
6
+ onWsError: () => { },
7
+ onRpcCall: () => { },
8
+ onRpcNotify: () => { }
9
+ };
10
+ export function normalizeObserver(observer) {
11
+ if (observer == null)
12
+ return NoopObserver;
13
+ return {
14
+ onConnect: observer.onConnect ?? NoopObserver.onConnect,
15
+ onAttach: observer.onAttach ?? NoopObserver.onAttach,
16
+ onHandshake: observer.onHandshake ?? NoopObserver.onHandshake,
17
+ onWsClose: observer.onWsClose ?? NoopObserver.onWsClose,
18
+ onWsError: observer.onWsError ?? NoopObserver.onWsError,
19
+ onRpcCall: observer.onRpcCall ?? NoopObserver.onRpcCall,
20
+ onRpcNotify: observer.onRpcNotify ?? NoopObserver.onRpcNotify
21
+ };
22
+ }
23
+ export function nowSeconds() {
24
+ if (typeof performance !== "undefined" && typeof performance.now === "function") {
25
+ return performance.now() / 1000;
26
+ }
27
+ return Date.now() / 1000;
28
+ }
@@ -0,0 +1,5 @@
1
+ export declare class RpcCallError extends Error {
2
+ readonly code: number;
3
+ readonly typeId: number;
4
+ constructor(code: number, message?: string, typeId?: number);
5
+ }
@@ -0,0 +1,11 @@
1
+ // RpcCallError represents an RPC-layer error returned in the wire envelope.
2
+ export class RpcCallError extends Error {
3
+ code;
4
+ typeId;
5
+ constructor(code, message, typeId) {
6
+ super(message ?? "rpc error");
7
+ this.name = "RpcCallError";
8
+ this.code = code >>> 0;
9
+ this.typeId = (typeId ?? 0) >>> 0;
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ import type { RpcError } from "../gen/flowersec/rpc/v1.gen.js";
2
+ export type RpcCaller = {
3
+ call(typeId: number, payload: unknown, signal?: AbortSignal): Promise<{
4
+ payload: unknown;
5
+ error?: RpcError;
6
+ }>;
7
+ onNotify(typeId: number, handler: (payload: unknown) => void): () => void;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { RpcError } from "../gen/flowersec/rpc/v1.gen.js";
2
+ import { type ClientObserverLike } from "../observability/observer.js";
3
+ export declare class RpcClient {
4
+ private readonly readExactly;
5
+ private readonly write;
6
+ private nextId;
7
+ private readonly pending;
8
+ private readonly notifyHandlers;
9
+ private closed;
10
+ private readonly observer;
11
+ constructor(readExactly: (n: number) => Promise<Uint8Array>, write: (b: Uint8Array) => Promise<void>, opts?: Readonly<{
12
+ observer?: ClientObserverLike;
13
+ }>);
14
+ call(typeId: number, payload: unknown, signal?: AbortSignal): Promise<{
15
+ payload: unknown;
16
+ error?: RpcError;
17
+ }>;
18
+ close(): void;
19
+ onNotify(typeId: number, handler: (payload: unknown) => void): () => void;
20
+ notify(typeId: number, payload: unknown): Promise<void>;
21
+ private readLoop;
22
+ }
@@ -0,0 +1,170 @@
1
+ import { normalizeObserver, nowSeconds } from "../observability/observer.js";
2
+ import { readJsonFrame, writeJsonFrame } from "./framing.js";
3
+ import { assertRpcEnvelope } from "./validate.js";
4
+ // Guard against precision loss when encoding request IDs as numbers.
5
+ const MAX_SAFE_REQUEST_ID = BigInt(Number.MAX_SAFE_INTEGER);
6
+ // RpcClient sends request/response envelopes and dispatches notifications.
7
+ export class RpcClient {
8
+ readExactly;
9
+ write;
10
+ // Next request ID (bigint to avoid JS number precision loss).
11
+ nextId = 1n;
12
+ // Pending requests keyed by request ID.
13
+ pending = new Map();
14
+ // Notification handlers keyed by type ID.
15
+ notifyHandlers = new Map();
16
+ // Closed state to stop the read loop and reject calls.
17
+ closed = false;
18
+ // Observer for RPC events.
19
+ observer;
20
+ constructor(readExactly, write, opts = {}) {
21
+ this.readExactly = readExactly;
22
+ this.write = write;
23
+ this.observer = normalizeObserver(opts.observer);
24
+ void this.readLoop();
25
+ }
26
+ // call sends a request and awaits a response or abort.
27
+ async call(typeId, payload, signal) {
28
+ if (this.closed)
29
+ throw new Error("rpc client closed");
30
+ if (this.nextId > MAX_SAFE_REQUEST_ID)
31
+ throw new Error("request id overflow");
32
+ const start = nowSeconds();
33
+ const record = (result) => {
34
+ this.observer.onRpcCall(result, nowSeconds() - start);
35
+ };
36
+ const requestId = this.nextId;
37
+ this.nextId += 1n;
38
+ const env = {
39
+ type_id: typeId >>> 0,
40
+ request_id: Number(requestId),
41
+ response_to: 0,
42
+ payload
43
+ };
44
+ const p = new Promise((resolve, reject) => {
45
+ this.pending.set(requestId, { resolve, reject });
46
+ });
47
+ try {
48
+ await writeJsonFrame(this.write, env);
49
+ }
50
+ catch (e) {
51
+ this.pending.delete(requestId);
52
+ record("transport_error");
53
+ throw e;
54
+ }
55
+ if (signal?.aborted) {
56
+ this.pending.delete(requestId);
57
+ record("canceled");
58
+ throw signal.reason ?? new Error("aborted");
59
+ }
60
+ let resp;
61
+ try {
62
+ resp = await raceAbort(p, signal);
63
+ }
64
+ catch (e) {
65
+ this.pending.delete(requestId);
66
+ record(signal?.aborted ? "canceled" : "transport_error");
67
+ throw e;
68
+ }
69
+ const result = rpcResultFromError(resp.error);
70
+ record(result);
71
+ if (resp.error == null)
72
+ return { payload: resp.payload };
73
+ return { payload: resp.payload, error: resp.error };
74
+ }
75
+ // close rejects all pending calls and stops the read loop.
76
+ close() {
77
+ this.closed = true;
78
+ for (const [, p] of this.pending)
79
+ p.reject(new Error("rpc closed"));
80
+ this.pending.clear();
81
+ this.notifyHandlers.clear();
82
+ }
83
+ // onNotify registers a handler for incoming notifications.
84
+ onNotify(typeId, handler) {
85
+ const tid = typeId >>> 0;
86
+ const set = this.notifyHandlers.get(tid) ?? new Set();
87
+ set.add(handler);
88
+ this.notifyHandlers.set(tid, set);
89
+ return () => {
90
+ const s = this.notifyHandlers.get(tid);
91
+ s?.delete(handler);
92
+ if (s != null && s.size === 0)
93
+ this.notifyHandlers.delete(tid);
94
+ };
95
+ }
96
+ // notify sends a one-way notification to the peer.
97
+ async notify(typeId, payload) {
98
+ if (this.closed)
99
+ throw new Error("rpc client closed");
100
+ const env = {
101
+ type_id: typeId >>> 0,
102
+ request_id: 0,
103
+ response_to: 0,
104
+ payload
105
+ };
106
+ await writeJsonFrame(this.write, env);
107
+ }
108
+ async readLoop() {
109
+ try {
110
+ while (!this.closed) {
111
+ const v = assertRpcEnvelope(await readJsonFrame(this.readExactly, 1 << 20));
112
+ if (v.response_to === 0) {
113
+ // Notification: response_to=0 and request_id=0.
114
+ if (v.request_id === 0) {
115
+ this.observer.onRpcNotify();
116
+ const set = this.notifyHandlers.get(v.type_id >>> 0);
117
+ if (set != null) {
118
+ for (const h of set) {
119
+ try {
120
+ h(v.payload);
121
+ }
122
+ catch {
123
+ // User handlers should not be able to take down the transport read loop.
124
+ }
125
+ }
126
+ }
127
+ }
128
+ continue;
129
+ }
130
+ const key = BigInt(v.response_to);
131
+ const p = this.pending.get(key);
132
+ if (p != null) {
133
+ this.pending.delete(key);
134
+ p.resolve(v);
135
+ }
136
+ }
137
+ }
138
+ catch (e) {
139
+ this.closed = true;
140
+ for (const [, p] of this.pending)
141
+ p.reject(e);
142
+ this.pending.clear();
143
+ }
144
+ }
145
+ }
146
+ // raceAbort resolves p unless the signal aborts first.
147
+ async function raceAbort(p, signal) {
148
+ if (signal == null)
149
+ return p;
150
+ if (signal.aborted)
151
+ throw signal.reason ?? new Error("aborted");
152
+ return await new Promise((resolve, reject) => {
153
+ const onAbort = () => reject(signal.reason ?? new Error("aborted"));
154
+ signal.addEventListener("abort", onAbort, { once: true });
155
+ void p.then((v) => {
156
+ signal.removeEventListener("abort", onAbort);
157
+ resolve(v);
158
+ }, (e) => {
159
+ signal.removeEventListener("abort", onAbort);
160
+ reject(e);
161
+ });
162
+ });
163
+ }
164
+ function rpcResultFromError(err) {
165
+ if (err == null)
166
+ return "ok";
167
+ if (err.code === 404)
168
+ return "handler_not_found";
169
+ return "rpc_error";
170
+ }
@@ -0,0 +1,4 @@
1
+ export declare class RpcFramingError extends Error {
2
+ }
3
+ export declare function writeJsonFrame(write: (b: Uint8Array) => Promise<void>, v: unknown): Promise<void>;
4
+ export declare function readJsonFrame(readExactly: (n: number) => Promise<Uint8Array>, maxBytes: number): Promise<unknown>;
@@ -0,0 +1,24 @@
1
+ import { readU32be, u32be } from "../utils/bin.js";
2
+ const te = new TextEncoder();
3
+ const td = new TextDecoder();
4
+ // RpcFramingError marks malformed or oversized frames.
5
+ export class RpcFramingError extends Error {
6
+ }
7
+ // writeJsonFrame encodes a JSON payload with a 4-byte length prefix.
8
+ export async function writeJsonFrame(write, v) {
9
+ const json = te.encode(JSON.stringify(v));
10
+ const hdr = u32be(json.length);
11
+ const out = new Uint8Array(4 + json.length);
12
+ out.set(hdr, 0);
13
+ out.set(json, 4);
14
+ await write(out);
15
+ }
16
+ // readJsonFrame reads and parses a length-prefixed JSON payload.
17
+ export async function readJsonFrame(readExactly, maxBytes) {
18
+ const hdr = await readExactly(4);
19
+ const n = readU32be(hdr, 0);
20
+ if (maxBytes > 0 && n > maxBytes)
21
+ throw new RpcFramingError("frame too large");
22
+ const payload = await readExactly(n);
23
+ return JSON.parse(td.decode(payload));
24
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./framing.js";
2
+ export * from "./caller.js";
3
+ export * from "./client.js";
4
+ export * from "./server.js";
5
+ export * from "./callError.js";
6
+ export * from "./typed.js";
@@ -0,0 +1,6 @@
1
+ export * from "./framing.js";
2
+ export * from "./caller.js";
3
+ export * from "./client.js";
4
+ export * from "./server.js";
5
+ export * from "./callError.js";
6
+ export * from "./typed.js";
@@ -0,0 +1,15 @@
1
+ import type { RpcError } from "../gen/flowersec/rpc/v1.gen.js";
2
+ export type RpcHandler = (payload: unknown) => Promise<{
3
+ payload: unknown;
4
+ error?: RpcError;
5
+ }>;
6
+ export declare class RpcServer {
7
+ private readonly readExactly;
8
+ private readonly write;
9
+ private readonly handlers;
10
+ private closed;
11
+ constructor(readExactly: (n: number) => Promise<Uint8Array>, write: (b: Uint8Array) => Promise<void>);
12
+ register(typeId: number, h: RpcHandler): void;
13
+ serve(signal?: AbortSignal): Promise<void>;
14
+ close(): void;
15
+ }
@@ -0,0 +1,67 @@
1
+ import { readJsonFrame, writeJsonFrame } from "./framing.js";
2
+ import { assertRpcEnvelope } from "./validate.js";
3
+ // RpcServer dispatches request envelopes to registered handlers.
4
+ export class RpcServer {
5
+ readExactly;
6
+ write;
7
+ // Registered handlers keyed by type ID.
8
+ handlers = new Map();
9
+ // Closed flag to stop the serve loop.
10
+ closed = false;
11
+ constructor(readExactly, write) {
12
+ this.readExactly = readExactly;
13
+ this.write = write;
14
+ }
15
+ // register binds a handler to a type ID.
16
+ register(typeId, h) {
17
+ this.handlers.set(typeId >>> 0, h);
18
+ }
19
+ // serve handles request/response frames until closed or aborted.
20
+ async serve(signal) {
21
+ while (!this.closed) {
22
+ if (signal?.aborted)
23
+ throw signal.reason ?? new Error("aborted");
24
+ const v = assertRpcEnvelope(await readJsonFrame(this.readExactly, 1 << 20));
25
+ if (v.response_to !== 0)
26
+ continue;
27
+ if (v.request_id === 0) {
28
+ const h = this.handlers.get(v.type_id >>> 0);
29
+ if (h != null) {
30
+ try {
31
+ await h(v.payload);
32
+ }
33
+ catch {
34
+ // Keep the serve loop alive on notification handler errors.
35
+ }
36
+ }
37
+ continue;
38
+ }
39
+ const h = this.handlers.get(v.type_id >>> 0);
40
+ let out;
41
+ if (h == null) {
42
+ out = { payload: null, error: { code: 404, message: "handler not found" } };
43
+ }
44
+ else {
45
+ try {
46
+ out = await h(v.payload);
47
+ }
48
+ catch {
49
+ // Keep the serve loop alive on request handler errors.
50
+ out = { payload: null, error: { code: 500, message: "internal error" } };
51
+ }
52
+ }
53
+ const resp = {
54
+ type_id: v.type_id,
55
+ request_id: 0,
56
+ response_to: v.request_id,
57
+ payload: out.payload,
58
+ ...(out.error != null ? { error: out.error } : {})
59
+ };
60
+ await writeJsonFrame(this.write, resp);
61
+ }
62
+ }
63
+ // close stops the serve loop.
64
+ close() {
65
+ this.closed = true;
66
+ }
67
+ }
@@ -0,0 +1,5 @@
1
+ import type { RpcCaller } from "./caller.js";
2
+ export declare function callTyped<TResp>(rpc: RpcCaller, typeId: number, req: unknown, opts?: Readonly<{
3
+ signal?: AbortSignal;
4
+ assert?: (v: unknown) => TResp;
5
+ }>): Promise<TResp>;
@@ -0,0 +1,9 @@
1
+ import { RpcCallError } from "./callError.js";
2
+ export async function callTyped(rpc, typeId, req, opts = {}) {
3
+ const resp = await rpc.call(typeId, req, opts.signal);
4
+ if (resp.error != null)
5
+ throw new RpcCallError(resp.error.code, resp.error.message, typeId);
6
+ if (opts.assert != null)
7
+ return opts.assert(resp.payload);
8
+ return resp.payload;
9
+ }
@@ -0,0 +1,2 @@
1
+ import type { RpcEnvelope } from "../gen/flowersec/rpc/v1.gen.js";
2
+ export declare function assertRpcEnvelope(v: unknown): RpcEnvelope;
@@ -0,0 +1,27 @@
1
+ import { isSafeU32Number, isSafeU64Number } from "../utils/number.js";
2
+ // assertRpcEnvelope validates numeric fields that are u32/u64 in the IDL.
3
+ //
4
+ // The wire format is JSON, so JS numbers are used. For u64 we enforce the safe integer range
5
+ // to avoid silent precision loss on request/response correlation.
6
+ export function assertRpcEnvelope(v) {
7
+ if (typeof v !== "object" || v == null)
8
+ throw new Error("bad rpc envelope");
9
+ const o = v;
10
+ if (!isSafeU32Number(o.type_id))
11
+ throw new Error("bad rpc envelope: type_id");
12
+ if (!isSafeU64Number(o.request_id))
13
+ throw new Error("bad rpc envelope: request_id");
14
+ if (!isSafeU64Number(o.response_to))
15
+ throw new Error("bad rpc envelope: response_to");
16
+ // payload: unknown (JSON)
17
+ if (o.error != null) {
18
+ if (typeof o.error !== "object" || o.error == null)
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");
25
+ }
26
+ return o;
27
+ }
@@ -0,0 +1 @@
1
+ export * from "./rpcProxy.js";
@@ -0,0 +1 @@
1
+ export * from "./rpcProxy.js";
@@ -0,0 +1,13 @@
1
+ import type { RpcClient } from "../rpc/client.js";
2
+ export declare class RpcProxy {
3
+ private client;
4
+ private readonly notifyHandlers;
5
+ attach(client: RpcClient): void;
6
+ detach(): void;
7
+ onNotify(typeId: number, handler: (payload: unknown) => void): () => void;
8
+ call(typeId: number, payload: unknown, signal?: AbortSignal): Promise<{
9
+ payload: unknown;
10
+ error?: import("../gen/flowersec/rpc/v1.gen.js").RpcError;
11
+ }>;
12
+ notify(typeId: number, payload: unknown): Promise<void>;
13
+ }
@@ -0,0 +1,59 @@
1
+ // RpcProxy allows handlers to survive client reattachment.
2
+ export class RpcProxy {
3
+ // Active client connection (null when detached).
4
+ client = null;
5
+ // Persisted notification handlers across reattachments.
6
+ notifyHandlers = new Map();
7
+ // attach wires existing notification handlers to a new client.
8
+ attach(client) {
9
+ this.detach();
10
+ this.client = client;
11
+ for (const [typeId, handlers] of this.notifyHandlers) {
12
+ for (const [h, state] of handlers) {
13
+ state.unsub = client.onNotify(typeId, h);
14
+ }
15
+ }
16
+ }
17
+ // detach unwires handlers from the current client.
18
+ detach() {
19
+ for (const [, handlers] of this.notifyHandlers) {
20
+ for (const [, state] of handlers) {
21
+ state.unsub?.();
22
+ delete state.unsub;
23
+ }
24
+ }
25
+ this.client = null;
26
+ }
27
+ // onNotify registers handlers that will be rebound on reattach.
28
+ onNotify(typeId, handler) {
29
+ const tid = typeId >>> 0;
30
+ const handlers = this.notifyHandlers.get(tid) ?? new Map();
31
+ if (!this.notifyHandlers.has(tid))
32
+ this.notifyHandlers.set(tid, handlers);
33
+ if (!handlers.has(handler)) {
34
+ const state = {};
35
+ if (this.client != null)
36
+ state.unsub = this.client.onNotify(tid, handler);
37
+ handlers.set(handler, state);
38
+ }
39
+ return () => {
40
+ const state = handlers.get(handler);
41
+ state?.unsub?.();
42
+ handlers.delete(handler);
43
+ if (handlers.size === 0)
44
+ this.notifyHandlers.delete(tid);
45
+ };
46
+ }
47
+ // call forwards the RPC call to the attached client.
48
+ async call(typeId, payload, signal) {
49
+ if (this.client == null)
50
+ throw new Error("rpc proxy is not attached");
51
+ return await this.client.call(typeId, payload, signal);
52
+ }
53
+ // notify forwards a one-way notification to the attached client.
54
+ async notify(typeId, payload) {
55
+ if (this.client == null)
56
+ throw new Error("rpc proxy is not attached");
57
+ await this.client.notify(typeId, payload);
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export * from "./streamHello.js";
@@ -0,0 +1 @@
1
+ export * from "./streamHello.js";
@@ -0,0 +1,3 @@
1
+ import type { StreamHello } from "../gen/flowersec/rpc/v1.gen.js";
2
+ export declare function writeStreamHello(write: (b: Uint8Array) => Promise<void>, kind: string): Promise<void>;
3
+ export declare function readStreamHello(readExactly: (n: number) => Promise<Uint8Array>): Promise<StreamHello>;