@floegence/floe-webapp-protocol 0.1.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,50 @@
1
+ import { type JSX } from 'solid-js';
2
+ import type { ChannelInitGrant, Client, ClientObserverLike, DirectConnectInfo } from '@floegence/flowersec-core';
3
+ import { type ControlplaneConfig } from './controlplane';
4
+ /**
5
+ * Connection status
6
+ */
7
+ export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
8
+ interface ProtocolContextValue {
9
+ status: () => ConnectionStatus;
10
+ error: () => Error | null;
11
+ client: () => Client | null;
12
+ connect: (config: ConnectConfig) => Promise<void>;
13
+ disconnect: () => void;
14
+ }
15
+ /**
16
+ * Configuration for connecting to a flowersec server
17
+ */
18
+ export interface AutoReconnectConfig {
19
+ /**
20
+ * Enable auto reconnect on failure / unexpected disconnect.
21
+ * Default: false.
22
+ */
23
+ enabled?: boolean;
24
+ /** Maximum total attempts (including the first). Default: 5. */
25
+ maxAttempts?: number;
26
+ /** Base delay for the first retry. Default: 500ms. */
27
+ initialDelayMs?: number;
28
+ /** Max delay cap. Default: 10s. */
29
+ maxDelayMs?: number;
30
+ /** Exponential backoff factor. Default: 1.8. */
31
+ factor?: number;
32
+ /** Random jitter ratio in [-ratio, +ratio]. Default: 0.2. */
33
+ jitterRatio?: number;
34
+ }
35
+ export interface ConnectConfig {
36
+ mode?: 'tunnel' | 'direct';
37
+ observer?: ClientObserverLike;
38
+ keepaliveIntervalMs?: number;
39
+ connectTimeoutMs?: number;
40
+ handshakeTimeoutMs?: number;
41
+ autoReconnect?: AutoReconnectConfig;
42
+ controlplane?: ControlplaneConfig;
43
+ grant?: ChannelInitGrant;
44
+ directInfo?: DirectConnectInfo;
45
+ }
46
+ export declare function ProtocolProvider(props: {
47
+ children: JSX.Element;
48
+ }): JSX.Element;
49
+ export declare function useProtocol(): ProtocolContextValue;
50
+ export {};
@@ -0,0 +1,13 @@
1
+ import { type ChannelInitGrant } from '@floegence/flowersec-core';
2
+ export interface ControlplaneConfig {
3
+ baseUrl: string;
4
+ endpointId: string;
5
+ }
6
+ /**
7
+ * 向控制面请求隧道连接凭证(grant)。
8
+ *
9
+ * 说明:
10
+ * - `.design.md` 约定的接口为 POST `${baseUrl}/v1/channel/init`,body: { endpoint_id }
11
+ * - 返回体结构按设计约定:`{ grant_client: ChannelInitGrant }`
12
+ */
13
+ export declare function requestChannelGrant(config: ControlplaneConfig): Promise<ChannelInitGrant>;
@@ -0,0 +1,4 @@
1
+ export { ProtocolProvider, useProtocol, type ConnectionStatus, type ConnectConfig, type AutoReconnectConfig } from './client';
2
+ export { useRpc, RpcError, ProtocolNotConnectedError } from './rpc';
3
+ export { requestChannelGrant, type ControlplaneConfig } from './controlplane';
4
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import { ProtocolProvider as e, useProtocol as t } from "./index2.js";
2
+ import { ProtocolNotConnectedError as c, RpcError as s, useRpc as d } from "./index3.js";
3
+ import { requestChannelGrant as f } from "./index4.js";
4
+ import { ReservedTypeIds as m, TypeIds as x } from "./index5.js";
5
+ export {
6
+ c as ProtocolNotConnectedError,
7
+ e as ProtocolProvider,
8
+ m as ReservedTypeIds,
9
+ s as RpcError,
10
+ x as TypeIds,
11
+ f as requestChannelGrant,
12
+ t as useProtocol,
13
+ d as useRpc
14
+ };
package/dist/index2.js ADDED
@@ -0,0 +1,189 @@
1
+ import { createContext as T, onCleanup as E, createComponent as P, useContext as k } from "solid-js";
2
+ import { createStore as A } from "solid-js/store";
3
+ import { requestChannelGrant as W } from "./index4.js";
4
+ const R = T();
5
+ function y(a) {
6
+ return a != null && a.enabled ? {
7
+ enabled: !0,
8
+ maxAttempts: Math.max(1, a.maxAttempts ?? 5),
9
+ initialDelayMs: Math.max(0, a.initialDelayMs ?? 500),
10
+ maxDelayMs: Math.max(0, a.maxDelayMs ?? 1e4),
11
+ factor: Math.max(1, a.factor ?? 1.8),
12
+ jitterRatio: Math.max(0, a.jitterRatio ?? 0.2)
13
+ } : {
14
+ enabled: !1,
15
+ maxAttempts: 1,
16
+ initialDelayMs: 500,
17
+ maxDelayMs: 1e4,
18
+ factor: 1.8,
19
+ jitterRatio: 0.2
20
+ };
21
+ }
22
+ function j(a, c) {
23
+ const i = Math.min(c.maxDelayMs, c.initialDelayMs * Math.pow(c.factor, a)), l = c.jitterRatio <= 0 ? 0 : i * c.jitterRatio * (Math.random() * 2 - 1);
24
+ return Math.max(0, Math.round(i + l));
25
+ }
26
+ function H(a) {
27
+ const [c, i] = A({
28
+ status: "disconnected",
29
+ error: null,
30
+ client: null
31
+ });
32
+ let l = 0, u = null, d = null, m = null;
33
+ const h = () => {
34
+ d && (clearTimeout(d), d = null), m == null || m(), m = null;
35
+ }, b = (r) => new Promise((e) => {
36
+ m = e, d = setTimeout(() => {
37
+ d = null, m = null, e();
38
+ }, r);
39
+ }), M = () => {
40
+ h(), u = null, l += 1, c.client && c.client.close(), i({
41
+ status: "disconnected",
42
+ error: null,
43
+ client: null
44
+ });
45
+ }, w = (r, e, t) => {
46
+ if (r !== l || u !== e || c.status !== "connected") return;
47
+ if (!y(e.autoReconnect).enabled) {
48
+ i({
49
+ status: "error",
50
+ error: t,
51
+ client: null
52
+ });
53
+ return;
54
+ }
55
+ h(), l += 1;
56
+ const n = l;
57
+ i({
58
+ status: "connecting",
59
+ error: t,
60
+ client: null
61
+ }), x(n, e).catch(() => {
62
+ });
63
+ }, C = (r, e) => {
64
+ const t = e.observer;
65
+ return {
66
+ onConnect: (...o) => {
67
+ var n;
68
+ return (n = t == null ? void 0 : t.onConnect) == null ? void 0 : n.call(t, ...o);
69
+ },
70
+ onAttach: (...o) => {
71
+ var n;
72
+ return (n = t == null ? void 0 : t.onAttach) == null ? void 0 : n.call(t, ...o);
73
+ },
74
+ onHandshake: (...o) => {
75
+ var n;
76
+ return (n = t == null ? void 0 : t.onHandshake) == null ? void 0 : n.call(t, ...o);
77
+ },
78
+ onWsClose: (o, n) => {
79
+ var s;
80
+ (s = t == null ? void 0 : t.onWsClose) == null || s.call(t, o, n), o === "peer_or_error" && w(r, e, new Error(`WebSocket closed (${n ?? "unknown"})`));
81
+ },
82
+ onWsError: (o) => {
83
+ var n;
84
+ (n = t == null ? void 0 : t.onWsError) == null || n.call(t, o), w(r, e, new Error(`WebSocket error: ${o}`));
85
+ },
86
+ onRpcCall: (...o) => {
87
+ var n;
88
+ return (n = t == null ? void 0 : t.onRpcCall) == null ? void 0 : n.call(t, ...o);
89
+ },
90
+ onRpcNotify: (...o) => {
91
+ var n;
92
+ return (n = t == null ? void 0 : t.onRpcNotify) == null ? void 0 : n.call(t, ...o);
93
+ }
94
+ };
95
+ }, f = async (r, e) => {
96
+ const {
97
+ connectTunnelBrowser: t,
98
+ connectDirectBrowser: o
99
+ } = await import("@floegence/flowersec-core/browser"), n = {
100
+ observer: C(r, e),
101
+ keepaliveIntervalMs: e.keepaliveIntervalMs ?? 15e3,
102
+ connectTimeoutMs: e.connectTimeoutMs ?? 1e4,
103
+ handshakeTimeoutMs: e.handshakeTimeoutMs ?? 1e4
104
+ };
105
+ if ((e.mode ?? "tunnel") === "tunnel") {
106
+ const p = e.grant ?? (e.controlplane ? await W(e.controlplane) : null);
107
+ if (!p)
108
+ throw new Error("Tunnel mode requires `grant` or `controlplane` config");
109
+ return t(p, n);
110
+ }
111
+ if (!e.directInfo)
112
+ throw new Error("Direct mode requires `directInfo`");
113
+ return o(e.directInfo, n);
114
+ }, x = async (r, e) => {
115
+ const t = y(e.autoReconnect);
116
+ let o = 0;
117
+ for (; ; ) {
118
+ if (r !== l || u !== e) return;
119
+ o += 1;
120
+ try {
121
+ const n = await f(r, e);
122
+ if (r !== l) {
123
+ n.close();
124
+ return;
125
+ }
126
+ if (u !== e) {
127
+ n.close();
128
+ return;
129
+ }
130
+ i({
131
+ status: "connected",
132
+ client: n,
133
+ error: null
134
+ });
135
+ return;
136
+ } catch (n) {
137
+ const s = n instanceof Error ? n : new Error(String(n));
138
+ if (r !== l || u !== e) return;
139
+ if (!(t.enabled && o < t.maxAttempts))
140
+ throw i({
141
+ status: "error",
142
+ error: s,
143
+ client: null
144
+ }), s;
145
+ i({
146
+ status: "connecting",
147
+ error: s,
148
+ client: null
149
+ });
150
+ const D = j(o - 1, t);
151
+ await b(D);
152
+ }
153
+ }
154
+ }, v = {
155
+ status: () => c.status,
156
+ error: () => c.error,
157
+ client: () => c.client,
158
+ connect: async (r) => {
159
+ h(), l += 1;
160
+ const e = l;
161
+ u = r, c.client && c.client.close(), i({
162
+ status: "connecting",
163
+ error: null,
164
+ client: null
165
+ }), await x(e, r);
166
+ },
167
+ disconnect: () => {
168
+ M();
169
+ }
170
+ };
171
+ return E(() => {
172
+ M();
173
+ }), P(R.Provider, {
174
+ value: v,
175
+ get children() {
176
+ return a.children;
177
+ }
178
+ });
179
+ }
180
+ function N() {
181
+ const a = k(R);
182
+ if (!a)
183
+ throw new Error("useProtocol must be used within a ProtocolProvider");
184
+ return a;
185
+ }
186
+ export {
187
+ H as ProtocolProvider,
188
+ N as useProtocol
189
+ };
package/dist/index3.js ADDED
@@ -0,0 +1,56 @@
1
+ var i = Object.defineProperty;
2
+ var p = (o, e, r) => e in o ? i(o, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : o[e] = r;
3
+ var s = (o, e, r) => p(o, typeof e != "symbol" ? e + "" : e, r);
4
+ import { useProtocol as E } from "./index2.js";
5
+ import { TypeIds as c } from "./index5.js";
6
+ class u extends Error {
7
+ constructor() {
8
+ super("Not connected"), this.name = "ProtocolNotConnectedError";
9
+ }
10
+ }
11
+ class a extends Error {
12
+ constructor(r) {
13
+ super(r.message ?? `RPC error: ${r.code}`, { cause: r.cause });
14
+ s(this, "typeId");
15
+ s(this, "code");
16
+ this.name = "RpcError", this.typeId = r.typeId, this.code = r.code;
17
+ }
18
+ }
19
+ function w() {
20
+ const o = E(), e = async (r, d) => {
21
+ const n = o.client();
22
+ if (!n)
23
+ throw new u();
24
+ let t;
25
+ try {
26
+ t = await n.rpc.call(r, d);
27
+ } catch (l) {
28
+ throw new a({ typeId: r, code: -1, message: "RPC transport error", cause: l });
29
+ }
30
+ if (t.error)
31
+ throw new a({
32
+ typeId: r,
33
+ code: t.error.code,
34
+ message: t.error.message ?? `RPC error: ${t.error.code}`,
35
+ cause: t.error
36
+ });
37
+ return t.payload;
38
+ };
39
+ return {
40
+ // File system operations
41
+ fs: {
42
+ list: (r) => e(c.FS_LIST, r),
43
+ readFile: (r) => e(c.FS_READ_FILE, r),
44
+ writeFile: (r) => e(c.FS_WRITE_FILE, r),
45
+ delete: (r) => e(c.FS_DELETE, r),
46
+ getHome: () => e(c.FS_GET_HOME, {})
47
+ },
48
+ // Raw call for custom operations
49
+ call: e
50
+ };
51
+ }
52
+ export {
53
+ u as ProtocolNotConnectedError,
54
+ a as RpcError,
55
+ w as useRpc
56
+ };
package/dist/index4.js ADDED
@@ -0,0 +1,17 @@
1
+ import { assertChannelInitGrant as r } from "@floegence/flowersec-core";
2
+ async function a(e) {
3
+ const t = await fetch(`${e.baseUrl}/v1/channel/init`, {
4
+ method: "POST",
5
+ headers: { "Content-Type": "application/json" },
6
+ body: JSON.stringify({ endpoint_id: e.endpointId })
7
+ });
8
+ if (!t.ok)
9
+ throw new Error(`Failed to get channel grant: ${t.status}`);
10
+ const n = await t.json();
11
+ if (!(n != null && n.grant_client))
12
+ throw new Error("Invalid controlplane response: missing `grant_client`");
13
+ return r(n.grant_client);
14
+ }
15
+ export {
16
+ a as requestChannelGrant
17
+ };
package/dist/index5.js ADDED
@@ -0,0 +1,28 @@
1
+ const E = {
2
+ // File system operations
3
+ FS_LIST: 1001,
4
+ FS_READ_FILE: 1002,
5
+ FS_WRITE_FILE: 1003,
6
+ FS_DELETE: 1006,
7
+ FS_GET_HOME: 1010
8
+ }, S = {
9
+ // File system operations
10
+ FS_CREATE_FILE: 1004,
11
+ FS_CREATE_DIR: 1005,
12
+ FS_RENAME: 1007,
13
+ FS_MOVE: 1008,
14
+ FS_COPY: 1009,
15
+ // Terminal operations
16
+ TERMINAL_SESSION_CREATE: 2001,
17
+ TERMINAL_SESSION_LIST: 2002,
18
+ TERMINAL_SESSION_ATTACH: 2003,
19
+ TERMINAL_DATA: 2004,
20
+ // Bidirectional notification
21
+ // System operations
22
+ SYSTEM_INFO: 3001,
23
+ SYSTEM_EXEC: 3002
24
+ };
25
+ export {
26
+ S as ReservedTypeIds,
27
+ E as TypeIds
28
+ };
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { type ListRequest, type ListResponse, type ReadFileRequest, type ReadFileResponse, type WriteFileRequest, type WriteFileResponse, type DeleteRequest, type DeleteResponse } from './types';
2
+ /**
3
+ * RPC wrapper for type-safe remote calls
4
+ */
5
+ export declare class ProtocolNotConnectedError extends Error {
6
+ constructor();
7
+ }
8
+ export declare class RpcError extends Error {
9
+ readonly typeId: number;
10
+ readonly code: number;
11
+ constructor(args: {
12
+ typeId: number;
13
+ code: number;
14
+ message?: string;
15
+ cause?: unknown;
16
+ });
17
+ }
18
+ export declare function useRpc(): {
19
+ fs: {
20
+ list: (req: ListRequest) => Promise<ListResponse>;
21
+ readFile: (req: ReadFileRequest) => Promise<ReadFileResponse>;
22
+ writeFile: (req: WriteFileRequest) => Promise<WriteFileResponse>;
23
+ delete: (req: DeleteRequest) => Promise<DeleteResponse>;
24
+ getHome: () => Promise<{
25
+ path: string;
26
+ }>;
27
+ };
28
+ call: <Req, Res>(typeId: number, request: Req) => Promise<Res>;
29
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Common type definitions for the protocol layer
3
+ */
4
+ /**
5
+ * Type IDs that are currently implemented by `useRpc()`.
6
+ *
7
+ * Keep this list aligned with `packages/protocol/src/rpc.ts` to avoid misleading downstream users.
8
+ */
9
+ export declare const TypeIds: {
10
+ readonly FS_LIST: 1001;
11
+ readonly FS_READ_FILE: 1002;
12
+ readonly FS_WRITE_FILE: 1003;
13
+ readonly FS_DELETE: 1006;
14
+ readonly FS_GET_HOME: 1010;
15
+ };
16
+ /**
17
+ * Reserved Type IDs for future protocol domains.
18
+ *
19
+ * These are part of the protocol contract, but are not yet exposed via `useRpc()`.
20
+ */
21
+ export declare const ReservedTypeIds: {
22
+ readonly FS_CREATE_FILE: 1004;
23
+ readonly FS_CREATE_DIR: 1005;
24
+ readonly FS_RENAME: 1007;
25
+ readonly FS_MOVE: 1008;
26
+ readonly FS_COPY: 1009;
27
+ readonly TERMINAL_SESSION_CREATE: 2001;
28
+ readonly TERMINAL_SESSION_LIST: 2002;
29
+ readonly TERMINAL_SESSION_ATTACH: 2003;
30
+ readonly TERMINAL_DATA: 2004;
31
+ readonly SYSTEM_INFO: 3001;
32
+ readonly SYSTEM_EXEC: 3002;
33
+ };
34
+ export interface FileInfo {
35
+ name: string;
36
+ path: string;
37
+ isDirectory: boolean;
38
+ size: number;
39
+ modifiedAt: number;
40
+ createdAt: number;
41
+ permissions?: string;
42
+ }
43
+ export interface ListRequest {
44
+ path: string;
45
+ showHidden?: boolean;
46
+ }
47
+ export interface ListResponse {
48
+ entries: FileInfo[];
49
+ }
50
+ export interface ReadFileRequest {
51
+ path: string;
52
+ encoding?: 'utf8' | 'base64';
53
+ }
54
+ export interface ReadFileResponse {
55
+ content: string;
56
+ encoding: string;
57
+ }
58
+ export interface WriteFileRequest {
59
+ path: string;
60
+ content: string;
61
+ encoding?: 'utf8' | 'base64';
62
+ createDirs?: boolean;
63
+ }
64
+ export interface WriteFileResponse {
65
+ success: boolean;
66
+ }
67
+ export interface DeleteRequest {
68
+ path: string;
69
+ recursive?: boolean;
70
+ }
71
+ export interface DeleteResponse {
72
+ success: boolean;
73
+ }
74
+ export interface TerminalSession {
75
+ id: string;
76
+ name?: string;
77
+ cols: number;
78
+ rows: number;
79
+ pid?: number;
80
+ }
81
+ export interface CreateTerminalRequest {
82
+ name?: string;
83
+ cols: number;
84
+ rows: number;
85
+ cwd?: string;
86
+ env?: Record<string, string>;
87
+ }
88
+ export interface CreateTerminalResponse {
89
+ session: TerminalSession;
90
+ }
91
+ export interface TerminalDataPayload {
92
+ sessionId: string;
93
+ data: Uint8Array;
94
+ }
95
+ export interface TerminalResizePayload {
96
+ sessionId: string;
97
+ cols: number;
98
+ rows: number;
99
+ }
@@ -0,0 +1 @@
1
+ export * from './common';
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@floegence/floe-webapp-protocol",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "dev": "vite build --watch",
19
+ "build": "pnpm clean && vite build && tsc -p tsconfig.build.json",
20
+ "typecheck": "tsc --noEmit",
21
+ "clean": "rm -rf dist *.tsbuildinfo"
22
+ },
23
+ "peerDependencies": {
24
+ "solid-js": "^1.8.0"
25
+ },
26
+ "dependencies": {
27
+ "@floegence/flowersec-core": "^0.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "solid-js": "^1.9.3",
31
+ "typescript": "^5.7.2",
32
+ "vite": "^6.0.7",
33
+ "vite-plugin-solid": "^2.11.0"
34
+ }
35
+ }