@aigne/afs-ui 1.11.0-beta.12
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/LICENSE.md +26 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/aup-protocol.cjs +235 -0
- package/dist/aup-protocol.d.cts +78 -0
- package/dist/aup-protocol.d.cts.map +1 -0
- package/dist/aup-protocol.d.mts +78 -0
- package/dist/aup-protocol.d.mts.map +1 -0
- package/dist/aup-protocol.mjs +235 -0
- package/dist/aup-protocol.mjs.map +1 -0
- package/dist/aup-registry.cjs +2489 -0
- package/dist/aup-registry.mjs +2487 -0
- package/dist/aup-registry.mjs.map +1 -0
- package/dist/aup-spec.cjs +1467 -0
- package/dist/aup-spec.mjs +1466 -0
- package/dist/aup-spec.mjs.map +1 -0
- package/dist/aup-types.cjs +165 -0
- package/dist/aup-types.d.cts +157 -0
- package/dist/aup-types.d.cts.map +1 -0
- package/dist/aup-types.d.mts +157 -0
- package/dist/aup-types.d.mts.map +1 -0
- package/dist/aup-types.mjs +157 -0
- package/dist/aup-types.mjs.map +1 -0
- package/dist/backend.cjs +14 -0
- package/dist/backend.d.cts +104 -0
- package/dist/backend.d.cts.map +1 -0
- package/dist/backend.d.mts +104 -0
- package/dist/backend.d.mts.map +1 -0
- package/dist/backend.mjs +13 -0
- package/dist/backend.mjs.map +1 -0
- package/dist/degradation.cjs +85 -0
- package/dist/degradation.d.cts +17 -0
- package/dist/degradation.d.cts.map +1 -0
- package/dist/degradation.d.mts +17 -0
- package/dist/degradation.d.mts.map +1 -0
- package/dist/degradation.mjs +84 -0
- package/dist/degradation.mjs.map +1 -0
- package/dist/index.cjs +36 -0
- package/dist/index.d.cts +12 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.mjs +13 -0
- package/dist/runtime.cjs +117 -0
- package/dist/runtime.d.cts +59 -0
- package/dist/runtime.d.cts.map +1 -0
- package/dist/runtime.d.mts +59 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +118 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/session.cjs +159 -0
- package/dist/session.d.cts +80 -0
- package/dist/session.d.cts.map +1 -0
- package/dist/session.d.mts +80 -0
- package/dist/session.d.mts.map +1 -0
- package/dist/session.mjs +159 -0
- package/dist/session.mjs.map +1 -0
- package/dist/snapshot.cjs +162 -0
- package/dist/snapshot.mjs +163 -0
- package/dist/snapshot.mjs.map +1 -0
- package/dist/term-page.cjs +264 -0
- package/dist/term-page.mjs +264 -0
- package/dist/term-page.mjs.map +1 -0
- package/dist/term.cjs +295 -0
- package/dist/term.d.cts +84 -0
- package/dist/term.d.cts.map +1 -0
- package/dist/term.d.mts +84 -0
- package/dist/term.d.mts.map +1 -0
- package/dist/term.mjs +296 -0
- package/dist/term.mjs.map +1 -0
- package/dist/tty.cjs +136 -0
- package/dist/tty.d.cts +53 -0
- package/dist/tty.d.cts.map +1 -0
- package/dist/tty.d.mts +53 -0
- package/dist/tty.d.mts.map +1 -0
- package/dist/tty.mjs +135 -0
- package/dist/tty.mjs.map +1 -0
- package/dist/ui-provider.cjs +4615 -0
- package/dist/ui-provider.d.cts +307 -0
- package/dist/ui-provider.d.cts.map +1 -0
- package/dist/ui-provider.d.mts +307 -0
- package/dist/ui-provider.d.mts.map +1 -0
- package/dist/ui-provider.mjs +4616 -0
- package/dist/ui-provider.mjs.map +1 -0
- package/dist/web-page/core.cjs +1388 -0
- package/dist/web-page/core.mjs +1387 -0
- package/dist/web-page/core.mjs.map +1 -0
- package/dist/web-page/css.cjs +1699 -0
- package/dist/web-page/css.mjs +1698 -0
- package/dist/web-page/css.mjs.map +1 -0
- package/dist/web-page/icons.cjs +248 -0
- package/dist/web-page/icons.mjs +248 -0
- package/dist/web-page/icons.mjs.map +1 -0
- package/dist/web-page/overlay-themes.cjs +514 -0
- package/dist/web-page/overlay-themes.mjs +513 -0
- package/dist/web-page/overlay-themes.mjs.map +1 -0
- package/dist/web-page/renderers/action.cjs +72 -0
- package/dist/web-page/renderers/action.mjs +72 -0
- package/dist/web-page/renderers/action.mjs.map +1 -0
- package/dist/web-page/renderers/broadcast.cjs +160 -0
- package/dist/web-page/renderers/broadcast.mjs +160 -0
- package/dist/web-page/renderers/broadcast.mjs.map +1 -0
- package/dist/web-page/renderers/calendar.cjs +137 -0
- package/dist/web-page/renderers/calendar.mjs +137 -0
- package/dist/web-page/renderers/calendar.mjs.map +1 -0
- package/dist/web-page/renderers/canvas.cjs +173 -0
- package/dist/web-page/renderers/canvas.mjs +173 -0
- package/dist/web-page/renderers/canvas.mjs.map +1 -0
- package/dist/web-page/renderers/cdn-loader.cjs +25 -0
- package/dist/web-page/renderers/cdn-loader.mjs +25 -0
- package/dist/web-page/renderers/cdn-loader.mjs.map +1 -0
- package/dist/web-page/renderers/chart.cjs +101 -0
- package/dist/web-page/renderers/chart.mjs +101 -0
- package/dist/web-page/renderers/chart.mjs.map +1 -0
- package/dist/web-page/renderers/deck.cjs +390 -0
- package/dist/web-page/renderers/deck.mjs +390 -0
- package/dist/web-page/renderers/deck.mjs.map +1 -0
- package/dist/web-page/renderers/device.cjs +1015 -0
- package/dist/web-page/renderers/device.mjs +1015 -0
- package/dist/web-page/renderers/device.mjs.map +1 -0
- package/dist/web-page/renderers/editor.cjs +127 -0
- package/dist/web-page/renderers/editor.mjs +127 -0
- package/dist/web-page/renderers/editor.mjs.map +1 -0
- package/dist/web-page/renderers/finance-chart.cjs +178 -0
- package/dist/web-page/renderers/finance-chart.mjs +178 -0
- package/dist/web-page/renderers/finance-chart.mjs.map +1 -0
- package/dist/web-page/renderers/frame.cjs +274 -0
- package/dist/web-page/renderers/frame.mjs +274 -0
- package/dist/web-page/renderers/frame.mjs.map +1 -0
- package/dist/web-page/renderers/globe.cjs +119 -0
- package/dist/web-page/renderers/globe.mjs +119 -0
- package/dist/web-page/renderers/globe.mjs.map +1 -0
- package/dist/web-page/renderers/input.cjs +137 -0
- package/dist/web-page/renderers/input.mjs +137 -0
- package/dist/web-page/renderers/input.mjs.map +1 -0
- package/dist/web-page/renderers/list.cjs +1243 -0
- package/dist/web-page/renderers/list.mjs +1243 -0
- package/dist/web-page/renderers/list.mjs.map +1 -0
- package/dist/web-page/renderers/map.cjs +126 -0
- package/dist/web-page/renderers/map.mjs +126 -0
- package/dist/web-page/renderers/map.mjs.map +1 -0
- package/dist/web-page/renderers/media.cjs +106 -0
- package/dist/web-page/renderers/media.mjs +106 -0
- package/dist/web-page/renderers/media.mjs.map +1 -0
- package/dist/web-page/renderers/moonphase.cjs +105 -0
- package/dist/web-page/renderers/moonphase.mjs +105 -0
- package/dist/web-page/renderers/moonphase.mjs.map +1 -0
- package/dist/web-page/renderers/natal-chart.cjs +222 -0
- package/dist/web-page/renderers/natal-chart.mjs +222 -0
- package/dist/web-page/renderers/natal-chart.mjs.map +1 -0
- package/dist/web-page/renderers/overlay.cjs +531 -0
- package/dist/web-page/renderers/overlay.mjs +531 -0
- package/dist/web-page/renderers/overlay.mjs.map +1 -0
- package/dist/web-page/renderers/table.cjs +74 -0
- package/dist/web-page/renderers/table.mjs +74 -0
- package/dist/web-page/renderers/table.mjs.map +1 -0
- package/dist/web-page/renderers/terminal.cjs +30 -0
- package/dist/web-page/renderers/terminal.mjs +30 -0
- package/dist/web-page/renderers/terminal.mjs.map +1 -0
- package/dist/web-page/renderers/text.cjs +109 -0
- package/dist/web-page/renderers/text.mjs +109 -0
- package/dist/web-page/renderers/text.mjs.map +1 -0
- package/dist/web-page/renderers/ticker.cjs +133 -0
- package/dist/web-page/renderers/ticker.mjs +133 -0
- package/dist/web-page/renderers/ticker.mjs.map +1 -0
- package/dist/web-page/renderers/time.cjs +69 -0
- package/dist/web-page/renderers/time.mjs +69 -0
- package/dist/web-page/renderers/time.mjs.map +1 -0
- package/dist/web-page/renderers/unknown.cjs +20 -0
- package/dist/web-page/renderers/unknown.mjs +20 -0
- package/dist/web-page/renderers/unknown.mjs.map +1 -0
- package/dist/web-page/renderers/view.cjs +161 -0
- package/dist/web-page/renderers/view.mjs +161 -0
- package/dist/web-page/renderers/view.mjs.map +1 -0
- package/dist/web-page/renderers/wm.cjs +669 -0
- package/dist/web-page/renderers/wm.mjs +669 -0
- package/dist/web-page/renderers/wm.mjs.map +1 -0
- package/dist/web-page/skeleton.cjs +103 -0
- package/dist/web-page/skeleton.mjs +103 -0
- package/dist/web-page/skeleton.mjs.map +1 -0
- package/dist/web-page.cjs +114 -0
- package/dist/web-page.d.cts +19 -0
- package/dist/web-page.d.cts.map +1 -0
- package/dist/web-page.d.mts +19 -0
- package/dist/web-page.d.mts.map +1 -0
- package/dist/web-page.mjs +115 -0
- package/dist/web-page.mjs.map +1 -0
- package/dist/web.cjs +827 -0
- package/dist/web.d.cts +144 -0
- package/dist/web.d.cts.map +1 -0
- package/dist/web.d.mts +144 -0
- package/dist/web.d.mts.map +1 -0
- package/dist/web.mjs +828 -0
- package/dist/web.mjs.map +1 -0
- package/dist/wm-state.cjs +172 -0
- package/dist/wm-state.mjs +171 -0
- package/dist/wm-state.mjs.map +1 -0
- package/package.json +59 -0
package/dist/web.cjs
ADDED
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
const require_tty = require('./tty.cjs');
|
|
2
|
+
const require_web_page = require('./web-page.cjs');
|
|
3
|
+
let ufo = require("ufo");
|
|
4
|
+
let node_http = require("node:http");
|
|
5
|
+
let ws = require("ws");
|
|
6
|
+
|
|
7
|
+
//#region src/web.ts
|
|
8
|
+
/**
|
|
9
|
+
* WebBackend — HTTP + WebSocket based browser UI.
|
|
10
|
+
*
|
|
11
|
+
* Serves a web page and communicates with it via WebSocket.
|
|
12
|
+
* For agent code, this behaves identically to TTYBackend — pure text I/O.
|
|
13
|
+
*
|
|
14
|
+
* For testing, accepts an inputSource/stdout to bypass WebSocket entirely.
|
|
15
|
+
*/
|
|
16
|
+
var WebBackend = class WebBackend {
|
|
17
|
+
type = "web";
|
|
18
|
+
supportedFormats = [
|
|
19
|
+
"text",
|
|
20
|
+
"html",
|
|
21
|
+
"markdown",
|
|
22
|
+
"component"
|
|
23
|
+
];
|
|
24
|
+
capabilities = [
|
|
25
|
+
"text",
|
|
26
|
+
"html",
|
|
27
|
+
"markdown",
|
|
28
|
+
"component"
|
|
29
|
+
];
|
|
30
|
+
static KNOWN_COMPONENTS = new Set([
|
|
31
|
+
"code-block",
|
|
32
|
+
"table",
|
|
33
|
+
"image"
|
|
34
|
+
]);
|
|
35
|
+
port;
|
|
36
|
+
host;
|
|
37
|
+
server = null;
|
|
38
|
+
wss = null;
|
|
39
|
+
clients = /* @__PURE__ */ new Set();
|
|
40
|
+
/** Track all TCP sockets so we can force-destroy on close */
|
|
41
|
+
sockets = /* @__PURE__ */ new Set();
|
|
42
|
+
/** Input queue — fed by WebSocket messages or test inputSource */
|
|
43
|
+
inputSource;
|
|
44
|
+
/** Output handler — sends to WebSocket clients or test stdout */
|
|
45
|
+
outputHandler;
|
|
46
|
+
/** Queue for messages sent before any client connects */
|
|
47
|
+
pendingMessages = [];
|
|
48
|
+
/** Pending prompt resolve — only one prompt at a time */
|
|
49
|
+
promptResolve = null;
|
|
50
|
+
/** Pending prompt message — re-sent on client reconnect */
|
|
51
|
+
pendingPromptMessage = null;
|
|
52
|
+
testMode;
|
|
53
|
+
_url = null;
|
|
54
|
+
/** Per-client session tracking */
|
|
55
|
+
sessionForClient = /* @__PURE__ */ new Map();
|
|
56
|
+
/** Per-client session token tracking */
|
|
57
|
+
sessionTokenForClient = /* @__PURE__ */ new Map();
|
|
58
|
+
createSessionCallback = null;
|
|
59
|
+
/** AUP event handler callback */
|
|
60
|
+
aupEventHandler = null;
|
|
61
|
+
/** Page resolver callback — returns page content for HTTP serving */
|
|
62
|
+
pageResolver = null;
|
|
63
|
+
/** Snapshot resolver callback — returns snapshot HTML for a sharing slug */
|
|
64
|
+
snapshotResolver = null;
|
|
65
|
+
/** AFS root instance — injected via setAFS() when provider is mounted */
|
|
66
|
+
afs = null;
|
|
67
|
+
/** Per-client AFS event subscriptions: ws → subId → unsubscribe */
|
|
68
|
+
clientSubscriptions = /* @__PURE__ */ new Map();
|
|
69
|
+
/** Live channel subscribers: channelId → set of viewer WebSockets */
|
|
70
|
+
channelSubscribers = /* @__PURE__ */ new Map();
|
|
71
|
+
/** Reverse map: ws → channelId (only for channel viewers) */
|
|
72
|
+
channelForClient = /* @__PURE__ */ new Map();
|
|
73
|
+
/** Callback invoked when a viewer joins a channel — provider sends snapshot */
|
|
74
|
+
channelJoinHandler = null;
|
|
75
|
+
/** Callback invoked when a client joins/reconnects a session — provider sends snapshot if stale */
|
|
76
|
+
sessionJoinHandler = null;
|
|
77
|
+
constructor(options = {}) {
|
|
78
|
+
this.port = options.port ?? 0;
|
|
79
|
+
this.host = options.host ?? "localhost";
|
|
80
|
+
if (options.inputSource) {
|
|
81
|
+
this.testMode = true;
|
|
82
|
+
this.inputSource = options.inputSource;
|
|
83
|
+
this.outputHandler = options.stdout ? (data) => {
|
|
84
|
+
options.stdout.write(data);
|
|
85
|
+
} : () => {};
|
|
86
|
+
} else {
|
|
87
|
+
this.testMode = false;
|
|
88
|
+
this.inputSource = require_tty.createMockInputSource();
|
|
89
|
+
this.outputHandler = (data) => this.broadcast(JSON.stringify({
|
|
90
|
+
type: "write",
|
|
91
|
+
content: data
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** URL of the running server, or null if not started. */
|
|
96
|
+
get url() {
|
|
97
|
+
return this._url;
|
|
98
|
+
}
|
|
99
|
+
/** Register a factory that creates or reattaches a session for each new WebSocket client. */
|
|
100
|
+
setSessionFactory(fn) {
|
|
101
|
+
this.createSessionCallback = fn;
|
|
102
|
+
}
|
|
103
|
+
/** Broadcast a raw JSON-serializable message to all connected clients. */
|
|
104
|
+
broadcastRaw(msg) {
|
|
105
|
+
if (this.testMode) return;
|
|
106
|
+
this.broadcast(JSON.stringify(msg));
|
|
107
|
+
}
|
|
108
|
+
/** Send a raw JSON message to the client that owns a specific session. */
|
|
109
|
+
sendToSession(sessionId, msg) {
|
|
110
|
+
if (this.testMode) return;
|
|
111
|
+
const data = JSON.stringify(msg);
|
|
112
|
+
for (const [ws$1, sid] of this.sessionForClient) if (sid === sessionId && ws$1.readyState === 1) {
|
|
113
|
+
ws$1.send(data);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Send a raw JSON message to all viewers of a live channel. */
|
|
118
|
+
sendToLiveChannel(channelId, msg) {
|
|
119
|
+
if (this.testMode) return;
|
|
120
|
+
const viewers = this.channelSubscribers.get(channelId);
|
|
121
|
+
if (!viewers) return;
|
|
122
|
+
const data = JSON.stringify(msg);
|
|
123
|
+
for (const ws$1 of viewers) if (ws$1.readyState === 1) ws$1.send(data);
|
|
124
|
+
}
|
|
125
|
+
/** Register a handler called when a viewer joins a channel (for snapshot delivery). */
|
|
126
|
+
setChannelJoinHandler(fn) {
|
|
127
|
+
this.channelJoinHandler = fn;
|
|
128
|
+
}
|
|
129
|
+
/** Register a handler called when a client joins/reconnects a session (for snapshot replay). */
|
|
130
|
+
setSessionJoinHandler(fn) {
|
|
131
|
+
this.sessionJoinHandler = fn;
|
|
132
|
+
}
|
|
133
|
+
/** Return the set of active channel IDs. */
|
|
134
|
+
getActiveChannelIds() {
|
|
135
|
+
return [...this.channelSubscribers.keys()];
|
|
136
|
+
}
|
|
137
|
+
/** Register a handler for AUP events from clients. */
|
|
138
|
+
setAupEventHandler(fn) {
|
|
139
|
+
this.aupEventHandler = fn;
|
|
140
|
+
}
|
|
141
|
+
/** Inject the AFS root for browser AFS proxy operations. */
|
|
142
|
+
setAFS(afs) {
|
|
143
|
+
this.afs = afs;
|
|
144
|
+
}
|
|
145
|
+
/** Register a resolver for serving pages via HTTP (/p/:id). */
|
|
146
|
+
setPageResolver(fn) {
|
|
147
|
+
this.pageResolver = fn;
|
|
148
|
+
}
|
|
149
|
+
/** Set the snapshot resolver for web sharing. */
|
|
150
|
+
setSnapshotResolver(fn) {
|
|
151
|
+
this.snapshotResolver = fn;
|
|
152
|
+
}
|
|
153
|
+
/** Start the HTTP + WebSocket server. */
|
|
154
|
+
async listen() {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const server = (0, node_http.createServer)((req, res) => {
|
|
157
|
+
const requestUrl = req.url ?? "";
|
|
158
|
+
const pathname = requestUrl.split("?")[0] ?? "";
|
|
159
|
+
if (pathname === "/" || pathname === "/index.html" || pathname.startsWith("/live/")) {
|
|
160
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
161
|
+
res.end(require_web_page.WEB_CLIENT_HTML);
|
|
162
|
+
} else if (requestUrl.startsWith("/p/")) this.handlePageRequest(requestUrl, res);
|
|
163
|
+
else if (pathname.startsWith("/s/") && this.snapshotResolver) this.handleSnapshotRequest(pathname, res);
|
|
164
|
+
else {
|
|
165
|
+
res.writeHead(404);
|
|
166
|
+
res.end("Not Found");
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
server.on("connection", (socket) => {
|
|
170
|
+
this.sockets.add(socket);
|
|
171
|
+
socket.on("close", () => this.sockets.delete(socket));
|
|
172
|
+
});
|
|
173
|
+
server.on("error", (err) => {
|
|
174
|
+
server.close();
|
|
175
|
+
reject(err);
|
|
176
|
+
});
|
|
177
|
+
const bindHost = this.host === "localhost" ? "127.0.0.1" : this.host;
|
|
178
|
+
server.listen({
|
|
179
|
+
port: this.port,
|
|
180
|
+
host: bindHost,
|
|
181
|
+
exclusive: true
|
|
182
|
+
}, () => {
|
|
183
|
+
this.server = server;
|
|
184
|
+
const addr = server.address();
|
|
185
|
+
if (typeof addr === "object" && addr) this.port = addr.port;
|
|
186
|
+
this._url = `http://127.0.0.1:${this.port}`;
|
|
187
|
+
this.wss = new ws.WebSocketServer({ server });
|
|
188
|
+
this.wss.on("connection", (ws$1, req) => this.onConnection(ws$1, req));
|
|
189
|
+
resolve({
|
|
190
|
+
port: this.port,
|
|
191
|
+
host: this.host
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/** Shut down the server and disconnect all clients. */
|
|
197
|
+
async close() {
|
|
198
|
+
for (const ws$1 of this.clients) ws$1.terminate();
|
|
199
|
+
this.clients.clear();
|
|
200
|
+
this.sessionForClient.clear();
|
|
201
|
+
this.sessionTokenForClient.clear();
|
|
202
|
+
this.channelSubscribers.clear();
|
|
203
|
+
this.channelForClient.clear();
|
|
204
|
+
if (this.wss) {
|
|
205
|
+
this.wss.close();
|
|
206
|
+
this.wss = null;
|
|
207
|
+
}
|
|
208
|
+
if (this.server) {
|
|
209
|
+
this.server.close();
|
|
210
|
+
for (const socket of this.sockets) socket.destroy();
|
|
211
|
+
this.sockets.clear();
|
|
212
|
+
this.server = null;
|
|
213
|
+
}
|
|
214
|
+
this._url = null;
|
|
215
|
+
}
|
|
216
|
+
async write(content, options) {
|
|
217
|
+
const format = options?.format ?? "text";
|
|
218
|
+
if (!this.supportedFormats.includes(format)) throw new Error(`Web backend does not support format: ${format}`);
|
|
219
|
+
if (format === "component") {
|
|
220
|
+
if (!options?.component) throw new Error("format 'component' requires a component type");
|
|
221
|
+
if (!WebBackend.KNOWN_COMPONENTS.has(options.component)) throw new Error(`Unknown component type: ${options.component}`);
|
|
222
|
+
}
|
|
223
|
+
const sanitizedContent = format === "html" ? sanitizeHtml(content) : content;
|
|
224
|
+
if (this.testMode) {
|
|
225
|
+
this.outputHandler(sanitizedContent);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const msg = {
|
|
229
|
+
type: "write",
|
|
230
|
+
content: sanitizedContent
|
|
231
|
+
};
|
|
232
|
+
if (format !== "text") msg.format = format;
|
|
233
|
+
if (options?.component) msg.component = options.component;
|
|
234
|
+
if (options?.componentProps) msg.componentProps = options.componentProps;
|
|
235
|
+
const payload = JSON.stringify(msg);
|
|
236
|
+
if (this.clients.size === 0) this.pendingMessages.push(payload);
|
|
237
|
+
else this.broadcast(payload);
|
|
238
|
+
}
|
|
239
|
+
async read(options) {
|
|
240
|
+
const timeout = options?.timeout ?? 0;
|
|
241
|
+
if (timeout > 0) return withTimeout(this.inputSource.readLine(), timeout);
|
|
242
|
+
return this.inputSource.readLine();
|
|
243
|
+
}
|
|
244
|
+
async prompt(options) {
|
|
245
|
+
if (this.testMode) return this.ttyStylePrompt(options);
|
|
246
|
+
const msg = JSON.stringify({
|
|
247
|
+
type: "prompt",
|
|
248
|
+
message: options.message,
|
|
249
|
+
promptType: options.type,
|
|
250
|
+
options: options.options
|
|
251
|
+
});
|
|
252
|
+
this.pendingPromptMessage = msg;
|
|
253
|
+
if (this.clients.size > 0) this.broadcast(msg);
|
|
254
|
+
else this.pendingMessages.push(msg);
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
this.promptResolve = resolve;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async notify(message) {
|
|
260
|
+
if (this.testMode) {
|
|
261
|
+
this.outputHandler(`${message}\n`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.broadcast(JSON.stringify({
|
|
265
|
+
type: "notify",
|
|
266
|
+
message
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
async navigate(pageId, content, format, layout) {
|
|
270
|
+
const sanitizedContent = format === "html" ? sanitizeHtml(content) : content;
|
|
271
|
+
if (this.testMode) {
|
|
272
|
+
this.outputHandler(sanitizedContent);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const msg = {
|
|
276
|
+
type: "navigate",
|
|
277
|
+
pageId,
|
|
278
|
+
content: sanitizedContent,
|
|
279
|
+
format: format ?? "html"
|
|
280
|
+
};
|
|
281
|
+
if (layout) msg.layout = layout;
|
|
282
|
+
this.broadcast(JSON.stringify(msg));
|
|
283
|
+
}
|
|
284
|
+
async clear() {
|
|
285
|
+
if (this.testMode) {
|
|
286
|
+
this.outputHandler("\x1B[2J\x1B[H");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.broadcast(JSON.stringify({ type: "clear" }));
|
|
290
|
+
}
|
|
291
|
+
hasPendingInput() {
|
|
292
|
+
return this.inputSource.hasPending();
|
|
293
|
+
}
|
|
294
|
+
getViewport() {
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
async dispose() {
|
|
298
|
+
await this.close();
|
|
299
|
+
}
|
|
300
|
+
onConnection(ws$1, req) {
|
|
301
|
+
const origin = req.headers.origin;
|
|
302
|
+
if (typeof origin === "string" && origin && !this.isAllowedWsOrigin(origin)) {
|
|
303
|
+
ws$1.close(1008, "Invalid origin");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.clients.add(ws$1);
|
|
307
|
+
let initialized = false;
|
|
308
|
+
ws$1.on("message", (data) => {
|
|
309
|
+
try {
|
|
310
|
+
const msg = JSON.parse(data.toString());
|
|
311
|
+
if (!initialized) {
|
|
312
|
+
initialized = true;
|
|
313
|
+
this.onHandshake(ws$1, msg);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.onMessage(msg, ws$1);
|
|
317
|
+
} catch {}
|
|
318
|
+
});
|
|
319
|
+
ws$1.on("close", () => {
|
|
320
|
+
this.clients.delete(ws$1);
|
|
321
|
+
this.sessionForClient.delete(ws$1);
|
|
322
|
+
this.sessionTokenForClient.delete(ws$1);
|
|
323
|
+
const channelId = this.channelForClient.get(ws$1);
|
|
324
|
+
if (channelId) {
|
|
325
|
+
this.channelForClient.delete(ws$1);
|
|
326
|
+
const viewers = this.channelSubscribers.get(channelId);
|
|
327
|
+
if (viewers) {
|
|
328
|
+
viewers.delete(ws$1);
|
|
329
|
+
if (viewers.size === 0) this.channelSubscribers.delete(channelId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const subs = this.clientSubscriptions.get(ws$1);
|
|
333
|
+
if (subs) {
|
|
334
|
+
for (const unsub of subs.values()) unsub();
|
|
335
|
+
this.clientSubscriptions.delete(ws$1);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/** Handle the first WS message as a typed handshake. */
|
|
340
|
+
onHandshake(ws$1, msg) {
|
|
341
|
+
if (msg.type === "join_channel") {
|
|
342
|
+
const channelId = String(msg.channelId ?? "");
|
|
343
|
+
if (!channelId) {
|
|
344
|
+
ws$1.close(4e3, "channelId required");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
this.channelForClient.set(ws$1, channelId);
|
|
348
|
+
if (!this.channelSubscribers.has(channelId)) this.channelSubscribers.set(channelId, /* @__PURE__ */ new Set());
|
|
349
|
+
this.channelSubscribers.get(channelId).add(ws$1);
|
|
350
|
+
ws$1.send(JSON.stringify({
|
|
351
|
+
type: "channel",
|
|
352
|
+
channelId
|
|
353
|
+
}));
|
|
354
|
+
if (this.channelJoinHandler) this.channelJoinHandler(channelId, (m) => {
|
|
355
|
+
if (ws$1.readyState === 1) ws$1.send(JSON.stringify(m));
|
|
356
|
+
});
|
|
357
|
+
} else {
|
|
358
|
+
if (this.createSessionCallback) {
|
|
359
|
+
const requestedSid = msg.sessionId ? String(msg.sessionId) : void 0;
|
|
360
|
+
const requestedSessionToken = msg.sessionToken ? String(msg.sessionToken) : void 0;
|
|
361
|
+
const caps = msg.caps && typeof msg.caps === "object" && !Array.isArray(msg.caps) ? msg.caps : void 0;
|
|
362
|
+
const created = this.createSessionCallback(this.type, requestedSid, requestedSessionToken, caps);
|
|
363
|
+
const sessionId = created.sessionId;
|
|
364
|
+
this.sessionForClient.set(ws$1, sessionId);
|
|
365
|
+
if (created.sessionToken) this.sessionTokenForClient.set(ws$1, created.sessionToken);
|
|
366
|
+
ws$1.send(JSON.stringify({
|
|
367
|
+
type: "session",
|
|
368
|
+
sessionId,
|
|
369
|
+
sessionToken: created.sessionToken ?? null
|
|
370
|
+
}));
|
|
371
|
+
if (this.sessionJoinHandler) {
|
|
372
|
+
const clientVersion = typeof msg.treeVersion === "number" ? msg.treeVersion : 0;
|
|
373
|
+
this.sessionJoinHandler(sessionId, clientVersion, (m) => {
|
|
374
|
+
if (ws$1.readyState === 1) ws$1.send(JSON.stringify(m));
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
for (const m of this.pendingMessages) ws$1.send(m);
|
|
379
|
+
this.pendingMessages = [];
|
|
380
|
+
if (this.pendingPromptMessage && this.promptResolve) ws$1.send(this.pendingPromptMessage);
|
|
381
|
+
if (msg.type && msg.type !== "join_session") this.onMessage(msg, ws$1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
onMessage(msg, ws$1) {
|
|
385
|
+
switch (msg.type) {
|
|
386
|
+
case "input": {
|
|
387
|
+
const content = String(msg.content ?? "");
|
|
388
|
+
this.inputSource.push?.(content);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case "prompt_response":
|
|
392
|
+
if (this.promptResolve) {
|
|
393
|
+
const resolve = this.promptResolve;
|
|
394
|
+
this.promptResolve = null;
|
|
395
|
+
this.pendingPromptMessage = null;
|
|
396
|
+
resolve(msg.value);
|
|
397
|
+
}
|
|
398
|
+
break;
|
|
399
|
+
case "aup_event":
|
|
400
|
+
if (this.aupEventHandler) {
|
|
401
|
+
const sessionId = this.sessionForClient.get(ws$1);
|
|
402
|
+
const channelId = this.channelForClient.get(ws$1);
|
|
403
|
+
const nodeId = String(msg.nodeId ?? "");
|
|
404
|
+
const event = String(msg.event ?? "");
|
|
405
|
+
const data = msg.data != null ? msg.data : void 0;
|
|
406
|
+
this.aupEventHandler({
|
|
407
|
+
nodeId,
|
|
408
|
+
event,
|
|
409
|
+
data
|
|
410
|
+
}, sessionId, channelId).then((result) => {
|
|
411
|
+
if (ws$1.readyState === 1) ws$1.send(JSON.stringify({
|
|
412
|
+
type: "aup_event_result",
|
|
413
|
+
nodeId,
|
|
414
|
+
event,
|
|
415
|
+
result
|
|
416
|
+
}));
|
|
417
|
+
}).catch((err) => {
|
|
418
|
+
if (ws$1.readyState === 1) ws$1.send(JSON.stringify({
|
|
419
|
+
type: "aup_event_result",
|
|
420
|
+
nodeId,
|
|
421
|
+
event,
|
|
422
|
+
error: err.message
|
|
423
|
+
}));
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
case "navigate_request": {
|
|
428
|
+
const pageId = String(msg.pageId ?? "");
|
|
429
|
+
const afsNav = this.afs;
|
|
430
|
+
if (pageId && afsNav) {
|
|
431
|
+
const pagePath = (0, ufo.joinURL)("/ui/web/pages", pageId);
|
|
432
|
+
afsNav.read?.(pagePath)?.then((result) => {
|
|
433
|
+
if (result.data && ws$1.readyState === 1) {
|
|
434
|
+
const entry = result.data;
|
|
435
|
+
const content = String(entry.content?.content ?? entry.content ?? "");
|
|
436
|
+
ws$1.send(JSON.stringify({
|
|
437
|
+
type: "navigate",
|
|
438
|
+
pageId,
|
|
439
|
+
content,
|
|
440
|
+
format: entry.content?.format ?? "html"
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
})?.catch(() => {});
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
case "afs_read":
|
|
448
|
+
case "afs_list":
|
|
449
|
+
case "afs_write":
|
|
450
|
+
case "afs_exec":
|
|
451
|
+
case "afs_stat":
|
|
452
|
+
case "afs_subscribe":
|
|
453
|
+
case "afs_unsubscribe":
|
|
454
|
+
this.handleAfsMessage(msg, ws$1);
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/** Handle AFS proxy messages from browser clients. */
|
|
459
|
+
handleAfsMessage(msg, ws$1) {
|
|
460
|
+
const reqId = msg.reqId;
|
|
461
|
+
if (!reqId) return;
|
|
462
|
+
const sendResult = (data) => {
|
|
463
|
+
if (ws$1.readyState === 1) ws$1.send(JSON.stringify({
|
|
464
|
+
type: "afs_result",
|
|
465
|
+
reqId,
|
|
466
|
+
data
|
|
467
|
+
}));
|
|
468
|
+
};
|
|
469
|
+
const sendError = (error) => {
|
|
470
|
+
if (ws$1.readyState === 1) ws$1.send(JSON.stringify({
|
|
471
|
+
type: "afs_error",
|
|
472
|
+
reqId,
|
|
473
|
+
error
|
|
474
|
+
}));
|
|
475
|
+
};
|
|
476
|
+
if (!this.afs) {
|
|
477
|
+
sendError("AFS not available");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const afs = this.afs;
|
|
481
|
+
const path = String(msg.path ?? "/");
|
|
482
|
+
switch (msg.type) {
|
|
483
|
+
case "afs_read":
|
|
484
|
+
if (!afs.read) {
|
|
485
|
+
sendError("read not supported");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
afs.read(path).then((r) => sendResult(r.data), (e) => sendError(e.message));
|
|
489
|
+
break;
|
|
490
|
+
case "afs_list": {
|
|
491
|
+
const listOptions = msg.options || {};
|
|
492
|
+
afs.list(path, listOptions).then((r) => sendResult({
|
|
493
|
+
data: r.data,
|
|
494
|
+
total: r.total
|
|
495
|
+
}), (e) => sendError(e.message));
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
case "afs_stat":
|
|
499
|
+
if (!afs.stat) {
|
|
500
|
+
sendError("stat not supported");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
afs.stat(path).then((r) => sendResult(r.data), (e) => sendError(e.message));
|
|
504
|
+
break;
|
|
505
|
+
case "afs_write": {
|
|
506
|
+
if (!afs.write) {
|
|
507
|
+
sendError("write not supported");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const payload = {};
|
|
511
|
+
if (msg.content !== void 0) payload.content = msg.content;
|
|
512
|
+
if (msg.meta !== void 0) payload.meta = msg.meta;
|
|
513
|
+
afs.write(path, payload).then((r) => sendResult(r.data), (e) => sendError(e.message));
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case "afs_exec": {
|
|
517
|
+
if (!afs.exec) {
|
|
518
|
+
sendError("exec not supported");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const args = msg.args ?? {};
|
|
522
|
+
afs.exec(path, args, {}).then((r) => sendResult(r.data), (e) => sendError(e.message));
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
case "afs_subscribe": {
|
|
526
|
+
const subId = String(msg.subId ?? "");
|
|
527
|
+
const filter = msg.filter ?? {};
|
|
528
|
+
if (!subId) {
|
|
529
|
+
sendError("subId is required for subscribe");
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (!afs.subscribe) {
|
|
533
|
+
sendError("subscribe not supported");
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
const unsub = afs.subscribe(filter, (event) => {
|
|
538
|
+
if (ws$1.readyState === 1) ws$1.send(JSON.stringify({
|
|
539
|
+
type: "afs_event",
|
|
540
|
+
subId,
|
|
541
|
+
event
|
|
542
|
+
}));
|
|
543
|
+
});
|
|
544
|
+
if (!this.clientSubscriptions.has(ws$1)) this.clientSubscriptions.set(ws$1, /* @__PURE__ */ new Map());
|
|
545
|
+
this.clientSubscriptions.get(ws$1).set(subId, unsub);
|
|
546
|
+
sendResult(null);
|
|
547
|
+
} catch (e) {
|
|
548
|
+
sendError(e instanceof Error ? e.message : String(e));
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
case "afs_unsubscribe": {
|
|
553
|
+
const subId = String(msg.subId ?? "");
|
|
554
|
+
const subs = this.clientSubscriptions.get(ws$1);
|
|
555
|
+
if (subs) {
|
|
556
|
+
const unsub = subs.get(subId);
|
|
557
|
+
if (unsub) {
|
|
558
|
+
unsub();
|
|
559
|
+
subs.delete(subId);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
sendResult(null);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/** Serve a page as standalone HTML via /p/:id[?sid=xxx][&st=xxx][&bridge=1] */
|
|
568
|
+
handlePageRequest(requestUrl, res) {
|
|
569
|
+
if (!this.pageResolver) {
|
|
570
|
+
res.writeHead(404);
|
|
571
|
+
res.end("Not Found");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const url = new URL(requestUrl, "http://localhost");
|
|
575
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
576
|
+
let pageId = "";
|
|
577
|
+
try {
|
|
578
|
+
pageId = decodeURIComponent(pathParts[1] ?? "");
|
|
579
|
+
} catch {
|
|
580
|
+
res.writeHead(400);
|
|
581
|
+
res.end("Bad Request: invalid page ID encoding");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const sessionId = url.searchParams.get("sid") ?? void 0;
|
|
585
|
+
const sessionToken = url.searchParams.get("st") ?? void 0;
|
|
586
|
+
const wantBridge = url.searchParams.get("bridge") === "1";
|
|
587
|
+
if (!pageId) {
|
|
588
|
+
res.writeHead(400);
|
|
589
|
+
res.end("Bad Request: missing page ID");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
this.pageResolver(pageId, sessionId, sessionToken).then((result) => {
|
|
593
|
+
if (!result) {
|
|
594
|
+
res.writeHead(404);
|
|
595
|
+
res.end("Page not found");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
let html;
|
|
599
|
+
if (result.format === "html") html = result.content;
|
|
600
|
+
else html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"></head><body><pre>${escapeHtmlForPage(result.content)}</pre></body></html>`;
|
|
601
|
+
html = injectPageBaseCSS(html);
|
|
602
|
+
if (wantBridge) {
|
|
603
|
+
const serverOrigin = `http://127.0.0.1:${this.port}`;
|
|
604
|
+
html = injectBridgeScript(html, serverOrigin);
|
|
605
|
+
}
|
|
606
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
607
|
+
res.end(html);
|
|
608
|
+
}).catch(() => {
|
|
609
|
+
res.writeHead(500);
|
|
610
|
+
res.end("Internal Server Error");
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
/** Handle HTTP GET /s/:slug — serve snapshot HTML for a sharing entry */
|
|
614
|
+
handleSnapshotRequest(pathname, res) {
|
|
615
|
+
if (!this.snapshotResolver) {
|
|
616
|
+
res.writeHead(404);
|
|
617
|
+
res.end("Not Found");
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const slug = pathname.slice(3);
|
|
621
|
+
if (!slug) {
|
|
622
|
+
res.writeHead(404);
|
|
623
|
+
res.end("Not Found");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const html = this.snapshotResolver(slug);
|
|
627
|
+
if (!html) {
|
|
628
|
+
res.writeHead(404);
|
|
629
|
+
res.end("Not Found");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
res.writeHead(200, {
|
|
633
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
634
|
+
"Cache-Control": "public, max-age=300"
|
|
635
|
+
});
|
|
636
|
+
res.end(html);
|
|
637
|
+
}
|
|
638
|
+
broadcast(data) {
|
|
639
|
+
for (const ws$1 of this.clients) if (ws$1.readyState === 1) ws$1.send(data);
|
|
640
|
+
}
|
|
641
|
+
isLoopbackHost(hostname) {
|
|
642
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
643
|
+
}
|
|
644
|
+
isAllowedWsOrigin(origin) {
|
|
645
|
+
try {
|
|
646
|
+
const u = new URL(origin);
|
|
647
|
+
return this.isLoopbackHost(u.hostname);
|
|
648
|
+
} catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/** TTY-style prompt for test mode (reuses TTYBackend logic) */
|
|
653
|
+
async ttyStylePrompt(options) {
|
|
654
|
+
const { message, type } = options;
|
|
655
|
+
switch (type) {
|
|
656
|
+
case "text":
|
|
657
|
+
case "password":
|
|
658
|
+
this.outputHandler(`${message} `);
|
|
659
|
+
return (await this.read()).trim();
|
|
660
|
+
case "confirm":
|
|
661
|
+
this.outputHandler(`${message} (y/n) `);
|
|
662
|
+
return (await this.read()).trim().toLowerCase().startsWith("y");
|
|
663
|
+
case "select": {
|
|
664
|
+
if (!options.options || options.options.length === 0) throw new Error("select prompt requires options");
|
|
665
|
+
this.outputHandler(`${message}\n`);
|
|
666
|
+
for (let i = 0; i < options.options.length; i++) this.outputHandler(` ${i + 1}. ${options.options[i]}\n`);
|
|
667
|
+
this.outputHandler("Choice: ");
|
|
668
|
+
const input = await this.read();
|
|
669
|
+
const idx = Number.parseInt(input.trim(), 10) - 1;
|
|
670
|
+
if (idx >= 0 && idx < options.options.length) return options.options[idx];
|
|
671
|
+
return options.options[0];
|
|
672
|
+
}
|
|
673
|
+
case "multiselect":
|
|
674
|
+
if (!options.options || options.options.length === 0) throw new Error("multiselect prompt requires options");
|
|
675
|
+
this.outputHandler(`${message}\n`);
|
|
676
|
+
for (let i = 0; i < options.options.length; i++) this.outputHandler(` ${i + 1}. ${options.options[i]}\n`);
|
|
677
|
+
this.outputHandler("Choices (comma-separated): ");
|
|
678
|
+
return (await this.read()).split(",").map((s) => Number.parseInt(s.trim(), 10) - 1).filter((i) => i >= 0 && i < options.options.length).map((i) => options.options[i]);
|
|
679
|
+
default: throw new Error(`Unknown prompt type: ${type}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
/**
|
|
684
|
+
* Strip dangerous HTML: <script> tags, on* event handlers, javascript: URLs.
|
|
685
|
+
*/
|
|
686
|
+
function sanitizeHtml(html) {
|
|
687
|
+
return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<script\b[^>]*\/?>/gi, "").replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "").replace(/(href|src|action)\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, "$1=\"\"");
|
|
688
|
+
}
|
|
689
|
+
/** Escape HTML for embedding in a minimal page shell. */
|
|
690
|
+
function escapeHtmlForPage(str) {
|
|
691
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
692
|
+
}
|
|
693
|
+
/** AUP Bridge script (~2KB) — injected into iframe pages when bridge=1. */
|
|
694
|
+
function buildBridgeScript(serverOrigin) {
|
|
695
|
+
return `<script>
|
|
696
|
+
(function(){
|
|
697
|
+
var _msgId = 0;
|
|
698
|
+
var _parentOrigin = '${serverOrigin.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}';
|
|
699
|
+
|
|
700
|
+
function _isTrustedParentMessage(e) {
|
|
701
|
+
if (e.source !== parent) return false;
|
|
702
|
+
if (e.origin !== _parentOrigin) return false;
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function _bridgeRequest(type, params) {
|
|
707
|
+
return new Promise(function(resolve, reject) {
|
|
708
|
+
var id = 'b' + (++_msgId);
|
|
709
|
+
function handler(e) {
|
|
710
|
+
if (!_isTrustedParentMessage(e)) return;
|
|
711
|
+
if (e.data && e.data.type === 'aup_bridge_response' && e.data.id === id) {
|
|
712
|
+
window.removeEventListener('message', handler);
|
|
713
|
+
if (e.data.error) reject(new Error(e.data.error));
|
|
714
|
+
else resolve(e.data.payload);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
window.addEventListener('message', handler);
|
|
718
|
+
parent.postMessage({ type: type, id: id, params: params }, _parentOrigin);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
var _subs = {};
|
|
723
|
+
window.addEventListener('message', function(e) {
|
|
724
|
+
if (!_isTrustedParentMessage(e)) return;
|
|
725
|
+
if (e.data && e.data.type === 'aup_subscribe_event' && e.data.subId && _subs[e.data.subId]) {
|
|
726
|
+
_subs[e.data.subId](e.data.payload);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
window.aup = {
|
|
731
|
+
emit: function(event, data) {
|
|
732
|
+
parent.postMessage({ type: 'aup_event', event: event, data: data }, _parentOrigin);
|
|
733
|
+
},
|
|
734
|
+
on: function(event, fn) {
|
|
735
|
+
window.addEventListener('message', function(e) {
|
|
736
|
+
if (!_isTrustedParentMessage(e)) return;
|
|
737
|
+
if (e.data && e.data.type === 'aup_data' && e.data.event === event) fn(e.data.payload);
|
|
738
|
+
});
|
|
739
|
+
},
|
|
740
|
+
navigate: function(path) {
|
|
741
|
+
parent.postMessage({ type: 'aup_navigate', path: path }, _parentOrigin);
|
|
742
|
+
},
|
|
743
|
+
toast: function(message, intent) {
|
|
744
|
+
parent.postMessage({ type: 'aup_toast', message: message, intent: intent || 'info' }, _parentOrigin);
|
|
745
|
+
},
|
|
746
|
+
fetch: function(path) {
|
|
747
|
+
return _bridgeRequest('aup_bridge_read', { path: path });
|
|
748
|
+
},
|
|
749
|
+
read: function(path) {
|
|
750
|
+
return _bridgeRequest('aup_bridge_read', { path: path });
|
|
751
|
+
},
|
|
752
|
+
list: function(path, options) {
|
|
753
|
+
return _bridgeRequest('aup_bridge_list', { path: path, options: options });
|
|
754
|
+
},
|
|
755
|
+
write: function(path, content, meta) {
|
|
756
|
+
return _bridgeRequest('aup_bridge_write', { path: path, content: content, meta: meta });
|
|
757
|
+
},
|
|
758
|
+
exec: function(path, args) {
|
|
759
|
+
return _bridgeRequest('aup_bridge_exec', { path: path, args: args || {} });
|
|
760
|
+
},
|
|
761
|
+
subscribe: function(filter, callback) {
|
|
762
|
+
var subId = 'bs' + (++_msgId);
|
|
763
|
+
_subs[subId] = callback;
|
|
764
|
+
parent.postMessage({ type: 'aup_bridge_subscribe', subId: subId, filter: filter }, _parentOrigin);
|
|
765
|
+
return function() {
|
|
766
|
+
delete _subs[subId];
|
|
767
|
+
parent.postMessage({ type: 'aup_bridge_unsubscribe', subId: subId }, _parentOrigin);
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
})();
|
|
772
|
+
<\/script>`;
|
|
773
|
+
}
|
|
774
|
+
/** Inject bridge script into HTML — inserts before </head> or at start of <body>. */
|
|
775
|
+
const PAGE_BASE_CSS = `<style data-aup-base>
|
|
776
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
777
|
+
:root {
|
|
778
|
+
--bg: #0a0e14; --surface: #131820; --border: #1d2433;
|
|
779
|
+
--text: #b3b1ad; --dim: #626a73;
|
|
780
|
+
--accent: #e6b450; --accent-bg: #2a2000;
|
|
781
|
+
--font: "Manrope", -apple-system, "Segoe UI", sans-serif;
|
|
782
|
+
--font-mono: "JetBrains Mono", "Fira Code", monospace;
|
|
783
|
+
--radius: 8px;
|
|
784
|
+
color-scheme: dark;
|
|
785
|
+
}
|
|
786
|
+
@media (prefers-color-scheme: light) {
|
|
787
|
+
:root {
|
|
788
|
+
--bg: #f5f3ef; --surface: #fefdfb; --border: #e0dcd4;
|
|
789
|
+
--text: #2c2418; --dim: #8a7e6e;
|
|
790
|
+
--accent: #b8860b; --accent-bg: #fef7e5;
|
|
791
|
+
color-scheme: light;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
html, body { min-height: 100vh; background: var(--bg); color: var(--text); font-family: var(--font); line-height: 1.6; -webkit-font-smoothing: antialiased; }
|
|
795
|
+
a { color: var(--accent); }
|
|
796
|
+
code, pre { font-family: var(--font-mono); }
|
|
797
|
+
pre { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1em; overflow-x: auto; }
|
|
798
|
+
img, video { max-width: 100%; height: auto; }
|
|
799
|
+
</style>`;
|
|
800
|
+
function injectPageBaseCSS(html) {
|
|
801
|
+
if (html.includes("</head>")) return html.replace("</head>", `${PAGE_BASE_CSS}\n</head>`);
|
|
802
|
+
const headMatch = html.match(/<head[^>]*>/i);
|
|
803
|
+
if (headMatch) return html.replace(headMatch[0], `${headMatch[0]}\n${PAGE_BASE_CSS}`);
|
|
804
|
+
return PAGE_BASE_CSS + html;
|
|
805
|
+
}
|
|
806
|
+
function injectBridgeScript(html, serverOrigin) {
|
|
807
|
+
const script = buildBridgeScript(serverOrigin);
|
|
808
|
+
if (html.includes("</head>")) return html.replace("</head>", `${script}\n</head>`);
|
|
809
|
+
const bodyMatch = html.match(/<body[^>]*>/i);
|
|
810
|
+
if (bodyMatch) return html.replace(bodyMatch[0], `${bodyMatch[0]}\n${script}`);
|
|
811
|
+
return script + html;
|
|
812
|
+
}
|
|
813
|
+
function withTimeout(promise, ms) {
|
|
814
|
+
return new Promise((resolve, reject) => {
|
|
815
|
+
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Input timeout")), ms);
|
|
816
|
+
promise.then((val) => {
|
|
817
|
+
clearTimeout(timer);
|
|
818
|
+
resolve(val);
|
|
819
|
+
}, (err) => {
|
|
820
|
+
clearTimeout(timer);
|
|
821
|
+
reject(err);
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
//#endregion
|
|
827
|
+
exports.WebBackend = WebBackend;
|