@floegence/flowersec-core 0.14.0 → 0.15.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/dist/client-connect/connectCore.js +18 -5
- package/dist/proxy/bootstrap.d.ts +1 -0
- package/dist/proxy/bootstrap.js +1 -0
- package/dist/proxy/integration.d.ts +1 -0
- package/dist/proxy/integration.js +36 -2
- package/dist/rpc/index.d.ts +1 -0
- package/dist/rpc/index.js +1 -0
- package/dist/rpc/rpcProxy.d.ts +16 -0
- package/dist/rpc/rpcProxy.js +58 -0
- package/dist/rpc-proxy/rpcProxy.d.ts +1 -13
- package/dist/rpc-proxy/rpcProxy.js +1 -59
- package/dist/streamio/index.d.ts +17 -0
- package/dist/streamio/index.js +32 -0
- package/package.json +5 -2
|
@@ -14,6 +14,19 @@ export async function connectCore(args) {
|
|
|
14
14
|
const observer = normalizeObserver(args.opts.observer);
|
|
15
15
|
const signal = args.opts.signal;
|
|
16
16
|
const connectStart = nowSeconds();
|
|
17
|
+
let attachState = args.path === "tunnel" ? "not_started" : "accepted";
|
|
18
|
+
const reportAttachSuccess = () => {
|
|
19
|
+
if (args.path !== "tunnel" || attachState !== "sent")
|
|
20
|
+
return;
|
|
21
|
+
observer.onAttach("ok", undefined);
|
|
22
|
+
attachState = "accepted";
|
|
23
|
+
};
|
|
24
|
+
const reportAttachFailure = (reason) => {
|
|
25
|
+
if (args.path !== "tunnel" || attachState !== "sent")
|
|
26
|
+
return;
|
|
27
|
+
observer.onAttach("fail", reason);
|
|
28
|
+
attachState = "failed";
|
|
29
|
+
};
|
|
17
30
|
const origin = typeof args.opts.origin === "string" ? args.opts.origin.trim() : "";
|
|
18
31
|
if (origin === "") {
|
|
19
32
|
throw new FlowersecError({ path: args.path, stage: "validate", code: "missing_origin", message: "missing origin" });
|
|
@@ -131,9 +144,11 @@ export async function connectCore(args) {
|
|
|
131
144
|
if (args.path === "tunnel") {
|
|
132
145
|
try {
|
|
133
146
|
ws.send(args.attach.attachJson);
|
|
147
|
+
attachState = "sent";
|
|
134
148
|
}
|
|
135
149
|
catch (err) {
|
|
136
150
|
observer.onAttach("fail", "send_failed");
|
|
151
|
+
attachState = "failed";
|
|
137
152
|
try {
|
|
138
153
|
transport.close();
|
|
139
154
|
}
|
|
@@ -161,9 +176,7 @@ export async function connectCore(args) {
|
|
|
161
176
|
...(signal !== undefined ? { signal } : {}),
|
|
162
177
|
onCancel: () => transport.close(),
|
|
163
178
|
});
|
|
164
|
-
|
|
165
|
-
observer.onAttach("ok", undefined);
|
|
166
|
-
}
|
|
179
|
+
reportAttachSuccess();
|
|
167
180
|
observer.onHandshake(args.path, "ok", undefined, nowSeconds() - handshakeStart);
|
|
168
181
|
}
|
|
169
182
|
catch (err) {
|
|
@@ -173,12 +186,12 @@ export async function connectCore(args) {
|
|
|
173
186
|
if (args.path === "tunnel" && err instanceof WsCloseError) {
|
|
174
187
|
const reason = err.reason;
|
|
175
188
|
if (isTunnelAttachCloseReason(reason)) {
|
|
176
|
-
|
|
189
|
+
reportAttachFailure(reason);
|
|
177
190
|
throw new FlowersecError({ path: args.path, stage: "attach", code: reason, message: "tunnel rejected attach", cause: err });
|
|
178
191
|
}
|
|
179
192
|
}
|
|
180
193
|
if (args.path === "tunnel") {
|
|
181
|
-
|
|
194
|
+
reportAttachFailure(handshakeCode === "timeout" ? "timeout" : handshakeCode === "canceled" ? "canceled" : "attach_failed");
|
|
182
195
|
}
|
|
183
196
|
observer.onHandshake(args.path, "fail", handshakeCode, handshakeElapsedSeconds);
|
|
184
197
|
throw new FlowersecError({
|
|
@@ -7,6 +7,7 @@ import type { ProxyRuntime } from "./runtime.js";
|
|
|
7
7
|
export type ConnectTunnelProxyBrowserOptions = Readonly<{
|
|
8
8
|
connect?: TunnelConnectBrowserOptions;
|
|
9
9
|
profile?: ProxyProfileName | Partial<ProxyProfile>;
|
|
10
|
+
runtimeGlobalKey?: string;
|
|
10
11
|
runtime?: RegisterProxyIntegrationOptions["runtime"];
|
|
11
12
|
serviceWorker: ProxyIntegrationServiceWorkerOptions;
|
|
12
13
|
plugins?: readonly ProxyIntegrationPlugin[];
|
package/dist/proxy/bootstrap.js
CHANGED
|
@@ -6,6 +6,7 @@ export async function connectTunnelProxyBrowser(grant, opts) {
|
|
|
6
6
|
client,
|
|
7
7
|
serviceWorker: opts.serviceWorker,
|
|
8
8
|
...(opts.profile === undefined ? {} : { profile: opts.profile }),
|
|
9
|
+
...(opts.runtimeGlobalKey === undefined ? {} : { runtimeGlobalKey: opts.runtimeGlobalKey }),
|
|
9
10
|
...(opts.runtime === undefined ? {} : { runtime: opts.runtime }),
|
|
10
11
|
...(opts.plugins === undefined ? {} : { plugins: opts.plugins }),
|
|
11
12
|
};
|
|
@@ -27,6 +27,7 @@ export type ProxyIntegrationServiceWorkerOptions = Readonly<{
|
|
|
27
27
|
export type RegisterProxyIntegrationOptions = Readonly<{
|
|
28
28
|
client: Client;
|
|
29
29
|
profile?: ProxyProfileName | Partial<ProxyProfile>;
|
|
30
|
+
runtimeGlobalKey?: string;
|
|
30
31
|
runtime?: Readonly<{
|
|
31
32
|
maxJsonFrameBytes?: number;
|
|
32
33
|
maxChunkBytes?: number;
|
|
@@ -172,6 +172,18 @@ function buildRuntimeOptions(profile, runtime) {
|
|
|
172
172
|
timeoutMs: runtime?.timeoutMs ?? profile.timeoutMs,
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
|
+
function bindRuntimeGlobal(runtime, key) {
|
|
176
|
+
const runtimeGlobalKey = String(key ?? "").trim();
|
|
177
|
+
if (runtimeGlobalKey === "")
|
|
178
|
+
return null;
|
|
179
|
+
const target = globalThis;
|
|
180
|
+
target[runtimeGlobalKey] = runtime;
|
|
181
|
+
return () => {
|
|
182
|
+
if (target[runtimeGlobalKey] === runtime) {
|
|
183
|
+
delete target[runtimeGlobalKey];
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
175
187
|
export function createProxyIntegrationServiceWorkerScript(opts = {}) {
|
|
176
188
|
const plugins = opts.plugins ?? [];
|
|
177
189
|
let scriptOpts = opts.baseOptions ?? {};
|
|
@@ -212,6 +224,7 @@ export async function registerProxyIntegration(input) {
|
|
|
212
224
|
const profile = resolveProxyProfile(opts.profile);
|
|
213
225
|
const runtimeOpts = buildRuntimeOptions(profile, opts.runtime);
|
|
214
226
|
const runtime = createProxyRuntime({ ...runtimeOpts, client: opts.client });
|
|
227
|
+
const releaseRuntimeGlobal = bindRuntimeGlobal(runtime, opts.runtimeGlobalKey);
|
|
215
228
|
const swCfg = opts.serviceWorker;
|
|
216
229
|
const repairQueryKey = String(swCfg.repair?.queryKey ?? "_flowersec_sw_repair").trim() || "_flowersec_sw_repair";
|
|
217
230
|
const maxRepairAttempts = Math.max(0, Math.floor(swCfg.repair?.maxAttempts ?? 2));
|
|
@@ -280,13 +293,34 @@ export async function registerProxyIntegration(input) {
|
|
|
280
293
|
return {
|
|
281
294
|
runtime,
|
|
282
295
|
dispose: async () => {
|
|
296
|
+
let firstError = null;
|
|
283
297
|
if (monitorHandler && sw) {
|
|
284
298
|
sw.removeEventListener("controllerchange", monitorHandler);
|
|
285
299
|
}
|
|
286
|
-
|
|
300
|
+
try {
|
|
301
|
+
runtime.dispose();
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
firstError = error;
|
|
305
|
+
}
|
|
287
306
|
for (const plugin of plugins) {
|
|
288
|
-
|
|
307
|
+
try {
|
|
308
|
+
await plugin.onDisposed?.();
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
if (firstError == null)
|
|
312
|
+
firstError = error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
releaseRuntimeGlobal?.();
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
if (firstError == null)
|
|
320
|
+
firstError = error;
|
|
289
321
|
}
|
|
322
|
+
if (firstError != null)
|
|
323
|
+
throw firstError;
|
|
290
324
|
},
|
|
291
325
|
};
|
|
292
326
|
}
|
package/dist/rpc/index.d.ts
CHANGED
package/dist/rpc/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RpcClient } from "./client.js";
|
|
2
|
+
export declare class RpcProxyDetachedError extends Error {
|
|
3
|
+
constructor();
|
|
4
|
+
}
|
|
5
|
+
export declare class RpcProxy {
|
|
6
|
+
private client;
|
|
7
|
+
private readonly notifyHandlers;
|
|
8
|
+
attach(client: RpcClient): void;
|
|
9
|
+
detach(): void;
|
|
10
|
+
onNotify(typeId: number, handler: (payload: unknown) => void): () => void;
|
|
11
|
+
call(typeId: number, payload: unknown, signal?: AbortSignal): Promise<{
|
|
12
|
+
payload: unknown;
|
|
13
|
+
error?: import("../gen/flowersec/rpc/v1.gen.js").RpcError;
|
|
14
|
+
}>;
|
|
15
|
+
notify(typeId: number, payload: unknown): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export class RpcProxyDetachedError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("rpc proxy is not attached");
|
|
4
|
+
this.name = "RpcProxyDetachedError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
// RpcProxy keeps notify subscriptions stable across client reattachment.
|
|
8
|
+
export class RpcProxy {
|
|
9
|
+
client = null;
|
|
10
|
+
notifyHandlers = new Map();
|
|
11
|
+
attach(client) {
|
|
12
|
+
this.detach();
|
|
13
|
+
this.client = client;
|
|
14
|
+
for (const [typeId, handlers] of this.notifyHandlers) {
|
|
15
|
+
for (const [handler, state] of handlers) {
|
|
16
|
+
state.unsub = client.onNotify(typeId, handler);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
detach() {
|
|
21
|
+
for (const [, handlers] of this.notifyHandlers) {
|
|
22
|
+
for (const [, state] of handlers) {
|
|
23
|
+
state.unsub?.();
|
|
24
|
+
delete state.unsub;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
this.client = null;
|
|
28
|
+
}
|
|
29
|
+
onNotify(typeId, handler) {
|
|
30
|
+
const tid = typeId >>> 0;
|
|
31
|
+
const handlers = this.notifyHandlers.get(tid) ?? new Map();
|
|
32
|
+
if (!this.notifyHandlers.has(tid))
|
|
33
|
+
this.notifyHandlers.set(tid, handlers);
|
|
34
|
+
if (!handlers.has(handler)) {
|
|
35
|
+
const state = {};
|
|
36
|
+
if (this.client != null)
|
|
37
|
+
state.unsub = this.client.onNotify(tid, handler);
|
|
38
|
+
handlers.set(handler, state);
|
|
39
|
+
}
|
|
40
|
+
return () => {
|
|
41
|
+
const state = handlers.get(handler);
|
|
42
|
+
state?.unsub?.();
|
|
43
|
+
handlers.delete(handler);
|
|
44
|
+
if (handlers.size === 0)
|
|
45
|
+
this.notifyHandlers.delete(tid);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async call(typeId, payload, signal) {
|
|
49
|
+
if (this.client == null)
|
|
50
|
+
throw new RpcProxyDetachedError();
|
|
51
|
+
return await this.client.call(typeId, payload, signal);
|
|
52
|
+
}
|
|
53
|
+
async notify(typeId, payload) {
|
|
54
|
+
if (this.client == null)
|
|
55
|
+
throw new RpcProxyDetachedError();
|
|
56
|
+
await this.client.notify(typeId, payload);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,13 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export { RpcProxy, RpcProxyDetachedError } from "../rpc/rpcProxy.js";
|
|
@@ -1,59 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
export { RpcProxy, RpcProxyDetachedError } from "../rpc/rpcProxy.js";
|
package/dist/streamio/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Client } from "../client.js";
|
|
1
2
|
import { ByteReader } from "../yamux/byteReader.js";
|
|
2
3
|
import type { YamuxStream } from "../yamux/stream.js";
|
|
3
4
|
export declare function readMaybe(stream: YamuxStream): Promise<Uint8Array | null>;
|
|
@@ -13,3 +14,19 @@ export type ReadNBytesOptions = Readonly<{
|
|
|
13
14
|
onProgress?: (read: number) => void;
|
|
14
15
|
}>;
|
|
15
16
|
export declare function readNBytes(reader: ByteReader, n: number, opts?: ReadNBytesOptions): Promise<Uint8Array>;
|
|
17
|
+
export type JsonFrameChannel = Readonly<{
|
|
18
|
+
stream: YamuxStream;
|
|
19
|
+
reader: ByteReader;
|
|
20
|
+
writeFrame: (value: unknown) => Promise<void>;
|
|
21
|
+
readFrame: <T = unknown>(opts?: Readonly<{
|
|
22
|
+
maxBytes?: number;
|
|
23
|
+
assert?: (value: unknown) => T;
|
|
24
|
+
}>) => Promise<T>;
|
|
25
|
+
close: () => Promise<void>;
|
|
26
|
+
}>;
|
|
27
|
+
export type JsonFrameChannelOptions = Readonly<{
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
maxJsonFrameBytes?: number;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function createJsonFrameChannel(stream: YamuxStream, opts?: JsonFrameChannelOptions): JsonFrameChannel;
|
|
32
|
+
export declare function openJsonFrameChannel(client: Pick<Client, "openStream">, kind: string, opts?: JsonFrameChannelOptions): Promise<JsonFrameChannel>;
|
package/dist/streamio/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
|
|
1
2
|
import { AbortError, throwIfAborted } from "../utils/errors.js";
|
|
2
3
|
import { ByteReader } from "../yamux/byteReader.js";
|
|
3
4
|
function abortReasonToError(signal) {
|
|
@@ -56,3 +57,34 @@ export async function readNBytes(reader, n, opts = {}) {
|
|
|
56
57
|
}
|
|
57
58
|
return out;
|
|
58
59
|
}
|
|
60
|
+
function normalizeMaxJsonFrameBytes(maxBytes) {
|
|
61
|
+
const value = maxBytes ?? DEFAULT_MAX_JSON_FRAME_BYTES;
|
|
62
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
63
|
+
throw new Error("maxJsonFrameBytes must be a non-negative finite number");
|
|
64
|
+
}
|
|
65
|
+
return Math.floor(value);
|
|
66
|
+
}
|
|
67
|
+
export function createJsonFrameChannel(stream, opts = {}) {
|
|
68
|
+
const reader = createByteReader(stream, opts.signal == null ? {} : { signal: opts.signal });
|
|
69
|
+
const defaultMaxBytes = normalizeMaxJsonFrameBytes(opts.maxJsonFrameBytes);
|
|
70
|
+
return {
|
|
71
|
+
stream,
|
|
72
|
+
reader,
|
|
73
|
+
writeFrame: async (value) => {
|
|
74
|
+
await writeJsonFrame(stream, value);
|
|
75
|
+
},
|
|
76
|
+
readFrame: async (readOpts = {}) => {
|
|
77
|
+
const raw = await readJsonFrame(reader, normalizeMaxJsonFrameBytes(readOpts.maxBytes ?? defaultMaxBytes));
|
|
78
|
+
if (readOpts.assert != null)
|
|
79
|
+
return readOpts.assert(raw);
|
|
80
|
+
return raw;
|
|
81
|
+
},
|
|
82
|
+
close: async () => {
|
|
83
|
+
await stream.close();
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export async function openJsonFrameChannel(client, kind, opts = {}) {
|
|
88
|
+
const stream = await client.openStream(kind, opts.signal == null ? {} : { signal: opts.signal });
|
|
89
|
+
return createJsonFrameChannel(stream, opts);
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floegence/flowersec-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -108,7 +108,10 @@
|
|
|
108
108
|
"build": "tsc -p tsconfig.build.json",
|
|
109
109
|
"bench": "vitest bench --run",
|
|
110
110
|
"test": "vitest run",
|
|
111
|
-
"lint": "eslint ."
|
|
111
|
+
"lint": "eslint .",
|
|
112
|
+
"verify:package": "node ./scripts/verify-package-exports.mjs",
|
|
113
|
+
"prepack": "npm run build",
|
|
114
|
+
"prepublishOnly": "npm run build && npm run verify:package"
|
|
112
115
|
},
|
|
113
116
|
"dependencies": {
|
|
114
117
|
"@noble/ciphers": "^0.6.0",
|