@floegence/flowersec-core 0.14.1 → 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.
@@ -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[];
@@ -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
- runtime.dispose();
300
+ try {
301
+ runtime.dispose();
302
+ }
303
+ catch (error) {
304
+ firstError = error;
305
+ }
287
306
  for (const plugin of plugins) {
288
- await plugin.onDisposed?.();
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
  }
@@ -2,4 +2,5 @@ export * from "./caller.js";
2
2
  export * from "./client.js";
3
3
  export * from "./server.js";
4
4
  export * from "./callError.js";
5
+ export * from "./rpcProxy.js";
5
6
  export * from "./typed.js";
package/dist/rpc/index.js CHANGED
@@ -2,4 +2,5 @@ export * from "./caller.js";
2
2
  export * from "./client.js";
3
3
  export * from "./server.js";
4
4
  export * from "./callError.js";
5
+ export * from "./rpcProxy.js";
5
6
  export * from "./typed.js";
@@ -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
- 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
- }
1
+ export { RpcProxy, RpcProxyDetachedError } from "../rpc/rpcProxy.js";
@@ -1,59 +1 @@
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
- }
1
+ export { RpcProxy, RpcProxyDetachedError } from "../rpc/rpcProxy.js";
@@ -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>;
@@ -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.14.1",
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": {