framer-dalton 0.0.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.
- package/README.md +25 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +6361 -0
- package/dist/start-relay-server.d.ts +2 -0
- package/dist/start-relay-server.js +718 -0
- package/package.json +38 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import fs2 from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import 'child_process';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
import * as vm from 'vm';
|
|
10
|
+
import { connect } from 'framer-api';
|
|
11
|
+
|
|
12
|
+
/* @framer/ai relay server v0.0.1 */
|
|
13
|
+
var __defProp = Object.defineProperty;
|
|
14
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
15
|
+
function getLogPath() {
|
|
16
|
+
if (process.env.XDG_STATE_HOME) {
|
|
17
|
+
return path.join(process.env.XDG_STATE_HOME, "framer", "relay.log");
|
|
18
|
+
}
|
|
19
|
+
if (process.platform === "win32") {
|
|
20
|
+
return path.join(
|
|
21
|
+
process.env.APPDATA || os.homedir(),
|
|
22
|
+
"framer",
|
|
23
|
+
"relay.log"
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return path.join(os.homedir(), ".local", "state", "framer", "relay.log");
|
|
27
|
+
}
|
|
28
|
+
__name(getLogPath, "getLogPath");
|
|
29
|
+
var logPath = getLogPath();
|
|
30
|
+
var initialized = false;
|
|
31
|
+
function ensureLogDir() {
|
|
32
|
+
if (initialized) return;
|
|
33
|
+
const dir = path.dirname(logPath);
|
|
34
|
+
if (!fs2.existsSync(dir)) {
|
|
35
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
initialized = true;
|
|
38
|
+
}
|
|
39
|
+
__name(ensureLogDir, "ensureLogDir");
|
|
40
|
+
function log(message) {
|
|
41
|
+
ensureLogDir();
|
|
42
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
43
|
+
fs2.appendFileSync(logPath, `${timestamp} ${message}
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
__name(log, "log");
|
|
47
|
+
var __filename$1 = fileURLToPath(import.meta.url);
|
|
48
|
+
path.dirname(__filename$1);
|
|
49
|
+
var VERSION = "0.0.1" ;
|
|
50
|
+
var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19987;
|
|
51
|
+
|
|
52
|
+
// src/connection-errors.ts
|
|
53
|
+
var CONNECTION_ERROR_PATTERNS = [
|
|
54
|
+
"Connection closed",
|
|
55
|
+
// FramerAPIError PROJECT_CLOSED
|
|
56
|
+
"Connection to the server was closed",
|
|
57
|
+
// FramerAPIError PROJECT_CLOSED
|
|
58
|
+
"Connection timeout after",
|
|
59
|
+
// FramerAPIError TIMEOUT
|
|
60
|
+
"No connection to the server",
|
|
61
|
+
// FramerAPIError INTERNAL
|
|
62
|
+
"WebSocket upgrade failed",
|
|
63
|
+
// WebSocket handshake failure
|
|
64
|
+
"Session is not connected",
|
|
65
|
+
// Our own check in executor.ts
|
|
66
|
+
"Execution timed out after",
|
|
67
|
+
// Likely dead connection
|
|
68
|
+
"Session expired"
|
|
69
|
+
// Server-side session expiry
|
|
70
|
+
];
|
|
71
|
+
function isConnectionError(errorMessage) {
|
|
72
|
+
return CONNECTION_ERROR_PATTERNS.some(
|
|
73
|
+
(pattern) => errorMessage.includes(pattern)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
__name(isConnectionError, "isConnectionError");
|
|
77
|
+
var ScopedFS = class {
|
|
78
|
+
static {
|
|
79
|
+
__name(this, "ScopedFS");
|
|
80
|
+
}
|
|
81
|
+
allowedDirs;
|
|
82
|
+
constructor(allowedDirs) {
|
|
83
|
+
const defaultDirs = [process.cwd(), "/tmp", os.tmpdir()];
|
|
84
|
+
const dirs = allowedDirs ?? defaultDirs;
|
|
85
|
+
this.allowedDirs = [...new Set(dirs.map((d) => path.resolve(d)))];
|
|
86
|
+
}
|
|
87
|
+
isPathAllowed(resolved) {
|
|
88
|
+
return this.allowedDirs.some((dir) => {
|
|
89
|
+
return resolved === dir || resolved.startsWith(dir + path.sep);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
resolvePath(filePath) {
|
|
93
|
+
const resolved = path.resolve(filePath);
|
|
94
|
+
if (!this.isPathAllowed(resolved)) {
|
|
95
|
+
const error2 = new Error(
|
|
96
|
+
`EPERM: operation not permitted, access outside allowed directories: ${filePath}`
|
|
97
|
+
);
|
|
98
|
+
error2.code = "EPERM";
|
|
99
|
+
error2.errno = -1;
|
|
100
|
+
error2.syscall = "access";
|
|
101
|
+
error2.path = filePath;
|
|
102
|
+
throw error2;
|
|
103
|
+
}
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
// Sync methods
|
|
107
|
+
readFileSync = /* @__PURE__ */ __name((filePath, options) => {
|
|
108
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
109
|
+
return fs2.readFileSync(resolved, options);
|
|
110
|
+
}, "readFileSync");
|
|
111
|
+
writeFileSync = /* @__PURE__ */ __name((filePath, data, options) => {
|
|
112
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
113
|
+
fs2.writeFileSync(
|
|
114
|
+
resolved,
|
|
115
|
+
data,
|
|
116
|
+
options
|
|
117
|
+
);
|
|
118
|
+
}, "writeFileSync");
|
|
119
|
+
appendFileSync = /* @__PURE__ */ __name((filePath, data, options) => {
|
|
120
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
121
|
+
fs2.appendFileSync(
|
|
122
|
+
resolved,
|
|
123
|
+
data,
|
|
124
|
+
options
|
|
125
|
+
);
|
|
126
|
+
}, "appendFileSync");
|
|
127
|
+
readdirSync = /* @__PURE__ */ __name((dirPath, options) => {
|
|
128
|
+
const resolved = this.resolvePath(dirPath.toString());
|
|
129
|
+
return fs2.readdirSync(resolved, options);
|
|
130
|
+
}, "readdirSync");
|
|
131
|
+
mkdirSync = /* @__PURE__ */ __name((dirPath, options) => {
|
|
132
|
+
const resolved = this.resolvePath(dirPath.toString());
|
|
133
|
+
return fs2.mkdirSync(resolved, options);
|
|
134
|
+
}, "mkdirSync");
|
|
135
|
+
rmdirSync = /* @__PURE__ */ __name((dirPath, options) => {
|
|
136
|
+
const resolved = this.resolvePath(dirPath.toString());
|
|
137
|
+
fs2.rmdirSync(resolved, options);
|
|
138
|
+
}, "rmdirSync");
|
|
139
|
+
unlinkSync = /* @__PURE__ */ __name((filePath) => {
|
|
140
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
141
|
+
fs2.unlinkSync(resolved);
|
|
142
|
+
}, "unlinkSync");
|
|
143
|
+
statSync = /* @__PURE__ */ __name((filePath, options) => {
|
|
144
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
145
|
+
return fs2.statSync(resolved, options);
|
|
146
|
+
}, "statSync");
|
|
147
|
+
lstatSync = /* @__PURE__ */ __name((filePath, options) => {
|
|
148
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
149
|
+
return fs2.lstatSync(resolved, options);
|
|
150
|
+
}, "lstatSync");
|
|
151
|
+
existsSync = /* @__PURE__ */ __name((filePath) => {
|
|
152
|
+
try {
|
|
153
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
154
|
+
return fs2.existsSync(resolved);
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}, "existsSync");
|
|
159
|
+
accessSync = /* @__PURE__ */ __name((filePath, mode) => {
|
|
160
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
161
|
+
fs2.accessSync(resolved, mode);
|
|
162
|
+
}, "accessSync");
|
|
163
|
+
copyFileSync = /* @__PURE__ */ __name((src, dest, mode) => {
|
|
164
|
+
const resolvedSrc = this.resolvePath(src.toString());
|
|
165
|
+
const resolvedDest = this.resolvePath(dest.toString());
|
|
166
|
+
fs2.copyFileSync(resolvedSrc, resolvedDest, mode);
|
|
167
|
+
}, "copyFileSync");
|
|
168
|
+
renameSync = /* @__PURE__ */ __name((oldPath, newPath) => {
|
|
169
|
+
const resolvedOld = this.resolvePath(oldPath.toString());
|
|
170
|
+
const resolvedNew = this.resolvePath(newPath.toString());
|
|
171
|
+
fs2.renameSync(resolvedOld, resolvedNew);
|
|
172
|
+
}, "renameSync");
|
|
173
|
+
rmSync = /* @__PURE__ */ __name((filePath, options) => {
|
|
174
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
175
|
+
fs2.rmSync(resolved, options);
|
|
176
|
+
}, "rmSync");
|
|
177
|
+
// Stream methods
|
|
178
|
+
createReadStream = /* @__PURE__ */ __name((filePath, options) => {
|
|
179
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
180
|
+
return fs2.createReadStream(resolved, options);
|
|
181
|
+
}, "createReadStream");
|
|
182
|
+
createWriteStream = /* @__PURE__ */ __name((filePath, options) => {
|
|
183
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
184
|
+
return fs2.createWriteStream(resolved, options);
|
|
185
|
+
}, "createWriteStream");
|
|
186
|
+
// Promise-based API (fs.promises equivalent)
|
|
187
|
+
get promises() {
|
|
188
|
+
return {
|
|
189
|
+
readFile: /* @__PURE__ */ __name(async (filePath, options) => {
|
|
190
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
191
|
+
return fs2.promises.readFile(
|
|
192
|
+
resolved,
|
|
193
|
+
options
|
|
194
|
+
);
|
|
195
|
+
}, "readFile"),
|
|
196
|
+
writeFile: /* @__PURE__ */ __name(async (filePath, data, options) => {
|
|
197
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
198
|
+
return fs2.promises.writeFile(
|
|
199
|
+
resolved,
|
|
200
|
+
data,
|
|
201
|
+
options
|
|
202
|
+
);
|
|
203
|
+
}, "writeFile"),
|
|
204
|
+
appendFile: /* @__PURE__ */ __name(async (filePath, data, options) => {
|
|
205
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
206
|
+
return fs2.promises.appendFile(
|
|
207
|
+
resolved,
|
|
208
|
+
data,
|
|
209
|
+
options
|
|
210
|
+
);
|
|
211
|
+
}, "appendFile"),
|
|
212
|
+
readdir: /* @__PURE__ */ __name(async (dirPath, options) => {
|
|
213
|
+
const resolved = this.resolvePath(dirPath.toString());
|
|
214
|
+
return fs2.promises.readdir(
|
|
215
|
+
resolved,
|
|
216
|
+
options
|
|
217
|
+
);
|
|
218
|
+
}, "readdir"),
|
|
219
|
+
mkdir: /* @__PURE__ */ __name(async (dirPath, options) => {
|
|
220
|
+
const resolved = this.resolvePath(dirPath.toString());
|
|
221
|
+
return fs2.promises.mkdir(resolved, options);
|
|
222
|
+
}, "mkdir"),
|
|
223
|
+
rmdir: /* @__PURE__ */ __name(async (dirPath, options) => {
|
|
224
|
+
const resolved = this.resolvePath(dirPath.toString());
|
|
225
|
+
return fs2.promises.rmdir(resolved, options);
|
|
226
|
+
}, "rmdir"),
|
|
227
|
+
unlink: /* @__PURE__ */ __name(async (filePath) => {
|
|
228
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
229
|
+
return fs2.promises.unlink(resolved);
|
|
230
|
+
}, "unlink"),
|
|
231
|
+
stat: /* @__PURE__ */ __name(async (filePath, options) => {
|
|
232
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
233
|
+
return fs2.promises.stat(resolved, options);
|
|
234
|
+
}, "stat"),
|
|
235
|
+
access: /* @__PURE__ */ __name(async (filePath, mode) => {
|
|
236
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
237
|
+
return fs2.promises.access(resolved, mode);
|
|
238
|
+
}, "access"),
|
|
239
|
+
copyFile: /* @__PURE__ */ __name(async (src, dest, mode) => {
|
|
240
|
+
const resolvedSrc = this.resolvePath(src.toString());
|
|
241
|
+
const resolvedDest = this.resolvePath(dest.toString());
|
|
242
|
+
return fs2.promises.copyFile(resolvedSrc, resolvedDest, mode);
|
|
243
|
+
}, "copyFile"),
|
|
244
|
+
rename: /* @__PURE__ */ __name(async (oldPath, newPath) => {
|
|
245
|
+
const resolvedOld = this.resolvePath(oldPath.toString());
|
|
246
|
+
const resolvedNew = this.resolvePath(newPath.toString());
|
|
247
|
+
return fs2.promises.rename(resolvedOld, resolvedNew);
|
|
248
|
+
}, "rename"),
|
|
249
|
+
rm: /* @__PURE__ */ __name(async (filePath, options) => {
|
|
250
|
+
const resolved = this.resolvePath(filePath.toString());
|
|
251
|
+
return fs2.promises.rm(resolved, options);
|
|
252
|
+
}, "rm")
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
constants = fs2.constants;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/executor.ts
|
|
259
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
260
|
+
var baseRequire = createRequire(import.meta.url);
|
|
261
|
+
var ALLOWED_MODULES = /* @__PURE__ */ new Set([
|
|
262
|
+
"path",
|
|
263
|
+
"node:path",
|
|
264
|
+
"url",
|
|
265
|
+
"node:url",
|
|
266
|
+
"fs",
|
|
267
|
+
"node:fs",
|
|
268
|
+
"fs/promises",
|
|
269
|
+
"node:fs/promises",
|
|
270
|
+
"crypto",
|
|
271
|
+
"node:crypto",
|
|
272
|
+
"buffer",
|
|
273
|
+
"node:buffer",
|
|
274
|
+
"util",
|
|
275
|
+
"node:util",
|
|
276
|
+
"os",
|
|
277
|
+
"node:os"
|
|
278
|
+
]);
|
|
279
|
+
function createSandboxedRequire(scopedFs) {
|
|
280
|
+
const sandboxedRequire = /* @__PURE__ */ __name(((id) => {
|
|
281
|
+
if (!ALLOWED_MODULES.has(id)) {
|
|
282
|
+
const error2 = new Error(
|
|
283
|
+
`Module "${id}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
|
|
284
|
+
);
|
|
285
|
+
error2.name = "ModuleNotAllowedError";
|
|
286
|
+
throw error2;
|
|
287
|
+
}
|
|
288
|
+
if (id === "fs" || id === "node:fs") {
|
|
289
|
+
return scopedFs;
|
|
290
|
+
}
|
|
291
|
+
if (id === "fs/promises" || id === "node:fs/promises") {
|
|
292
|
+
return scopedFs.promises;
|
|
293
|
+
}
|
|
294
|
+
return baseRequire(id);
|
|
295
|
+
}), "sandboxedRequire");
|
|
296
|
+
sandboxedRequire.resolve = baseRequire.resolve;
|
|
297
|
+
sandboxedRequire.cache = baseRequire.cache;
|
|
298
|
+
sandboxedRequire.extensions = baseRequire.extensions;
|
|
299
|
+
sandboxedRequire.main = baseRequire.main;
|
|
300
|
+
return sandboxedRequire;
|
|
301
|
+
}
|
|
302
|
+
__name(createSandboxedRequire, "createSandboxedRequire");
|
|
303
|
+
async function sandboxedImport(scopedFs, specifier) {
|
|
304
|
+
if (!ALLOWED_MODULES.has(specifier)) {
|
|
305
|
+
const error2 = new Error(
|
|
306
|
+
`Module "${specifier}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
|
|
307
|
+
);
|
|
308
|
+
error2.name = "ModuleNotAllowedError";
|
|
309
|
+
throw error2;
|
|
310
|
+
}
|
|
311
|
+
if (specifier === "fs" || specifier === "node:fs") {
|
|
312
|
+
return scopedFs;
|
|
313
|
+
}
|
|
314
|
+
if (specifier === "fs/promises" || specifier === "node:fs/promises") {
|
|
315
|
+
return scopedFs.promises;
|
|
316
|
+
}
|
|
317
|
+
return import(specifier);
|
|
318
|
+
}
|
|
319
|
+
__name(sandboxedImport, "sandboxedImport");
|
|
320
|
+
async function execute(session, connection, code, options = {}) {
|
|
321
|
+
const { timeout = DEFAULT_TIMEOUT, cwd } = options;
|
|
322
|
+
const output = [];
|
|
323
|
+
const customConsole = {
|
|
324
|
+
log: /* @__PURE__ */ __name((...args) => {
|
|
325
|
+
output.push(args.map((arg) => formatValue(arg)).join(" "));
|
|
326
|
+
}, "log"),
|
|
327
|
+
error: /* @__PURE__ */ __name((...args) => {
|
|
328
|
+
output.push(`[ERROR] ${args.map((arg) => formatValue(arg)).join(" ")}`);
|
|
329
|
+
}, "error"),
|
|
330
|
+
warn: /* @__PURE__ */ __name((...args) => {
|
|
331
|
+
output.push(`[WARN] ${args.map((arg) => formatValue(arg)).join(" ")}`);
|
|
332
|
+
}, "warn"),
|
|
333
|
+
info: /* @__PURE__ */ __name((...args) => {
|
|
334
|
+
output.push(args.map((arg) => formatValue(arg)).join(" "));
|
|
335
|
+
}, "info")
|
|
336
|
+
};
|
|
337
|
+
const scopedFs = cwd ? new ScopedFS([cwd, "/tmp"]) : new ScopedFS();
|
|
338
|
+
const sandboxedRequire = createSandboxedRequire(scopedFs);
|
|
339
|
+
const vmContextObj = {
|
|
340
|
+
// Framer API
|
|
341
|
+
framer: connection,
|
|
342
|
+
state: session.state,
|
|
343
|
+
// Console
|
|
344
|
+
console: customConsole,
|
|
345
|
+
// Module system (sandboxed)
|
|
346
|
+
require: sandboxedRequire,
|
|
347
|
+
import: /* @__PURE__ */ __name((specifier) => sandboxedImport(scopedFs, specifier), "import"),
|
|
348
|
+
// Timers
|
|
349
|
+
setTimeout,
|
|
350
|
+
clearTimeout,
|
|
351
|
+
setInterval,
|
|
352
|
+
clearInterval,
|
|
353
|
+
// Fetch & network
|
|
354
|
+
fetch,
|
|
355
|
+
// Common globals
|
|
356
|
+
Buffer,
|
|
357
|
+
URL,
|
|
358
|
+
URLSearchParams,
|
|
359
|
+
TextEncoder,
|
|
360
|
+
TextDecoder,
|
|
361
|
+
crypto,
|
|
362
|
+
AbortController,
|
|
363
|
+
AbortSignal,
|
|
364
|
+
structuredClone
|
|
365
|
+
};
|
|
366
|
+
const vmContext = vm.createContext(vmContextObj);
|
|
367
|
+
const wrappedCode = `(async () => { ${code} })()`;
|
|
368
|
+
try {
|
|
369
|
+
const script = new vm.Script(wrappedCode, {
|
|
370
|
+
filename: "framer-exec.js"
|
|
371
|
+
});
|
|
372
|
+
const resultPromise = script.runInContext(vmContext, {
|
|
373
|
+
timeout: 5e3
|
|
374
|
+
// Short timeout for sync part
|
|
375
|
+
});
|
|
376
|
+
const result = await Promise.race([
|
|
377
|
+
resultPromise,
|
|
378
|
+
new Promise(
|
|
379
|
+
(_, reject) => setTimeout(
|
|
380
|
+
() => reject(new Error(`Execution timed out after ${timeout}ms`)),
|
|
381
|
+
timeout
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
]);
|
|
385
|
+
if (result !== void 0) {
|
|
386
|
+
output.push(formatValue(result));
|
|
387
|
+
}
|
|
388
|
+
return { output };
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
391
|
+
return {
|
|
392
|
+
output,
|
|
393
|
+
error: errorMessage
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
__name(execute, "execute");
|
|
398
|
+
function formatValue(value) {
|
|
399
|
+
if (value === null) return "null";
|
|
400
|
+
if (value === void 0) return "undefined";
|
|
401
|
+
if (typeof value === "string") return value;
|
|
402
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
403
|
+
return String(value);
|
|
404
|
+
if (typeof value === "function")
|
|
405
|
+
return `[Function: ${value.name || "anonymous"}]`;
|
|
406
|
+
if (value instanceof Error) return value.message;
|
|
407
|
+
if (value instanceof Date) return value.toISOString();
|
|
408
|
+
if (value instanceof Map) return `Map(${value.size})`;
|
|
409
|
+
if (value instanceof Set) return `Set(${value.size})`;
|
|
410
|
+
if (Buffer.isBuffer(value)) return `Buffer(${value.length})`;
|
|
411
|
+
try {
|
|
412
|
+
return JSON.stringify(value, null, 2);
|
|
413
|
+
} catch {
|
|
414
|
+
return String(value);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
__name(formatValue, "formatValue");
|
|
418
|
+
var ConnectionPool = class {
|
|
419
|
+
static {
|
|
420
|
+
__name(this, "ConnectionPool");
|
|
421
|
+
}
|
|
422
|
+
pool = /* @__PURE__ */ new Map();
|
|
423
|
+
/**
|
|
424
|
+
* Acquire a connection for a session.
|
|
425
|
+
* If a connection already exists for the project, the session is added to it.
|
|
426
|
+
* Otherwise, a new connection is created.
|
|
427
|
+
*/
|
|
428
|
+
async acquire(projectId, apiKey, session) {
|
|
429
|
+
const entry = this.pool.get(projectId);
|
|
430
|
+
if (entry) {
|
|
431
|
+
entry.sessions.add(session);
|
|
432
|
+
return entry.connection;
|
|
433
|
+
}
|
|
434
|
+
const connection = await connect(projectId, apiKey);
|
|
435
|
+
this.pool.set(projectId, {
|
|
436
|
+
connection,
|
|
437
|
+
sessions: /* @__PURE__ */ new Set([session])
|
|
438
|
+
});
|
|
439
|
+
return connection;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Get the connection for a project.
|
|
443
|
+
*/
|
|
444
|
+
getConnection(projectId) {
|
|
445
|
+
const entry = this.pool.get(projectId);
|
|
446
|
+
return entry?.connection ?? null;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Reconnect a project's connection (call after catching a connection error).
|
|
450
|
+
* Uses the same connection object but swaps the underlying WebSocket.
|
|
451
|
+
*/
|
|
452
|
+
async reconnect(projectId) {
|
|
453
|
+
const entry = this.pool.get(projectId);
|
|
454
|
+
if (!entry) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
await entry.connection.reconnect();
|
|
458
|
+
return entry.connection;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Release a session from a connection.
|
|
462
|
+
* If no sessions remain, the connection is disconnected and removed.
|
|
463
|
+
*/
|
|
464
|
+
async release(projectId, session) {
|
|
465
|
+
const entry = this.pool.get(projectId);
|
|
466
|
+
if (!entry) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
entry.sessions.delete(session);
|
|
470
|
+
if (entry.sessions.size === 0) {
|
|
471
|
+
await entry.connection.disconnect();
|
|
472
|
+
this.pool.delete(projectId);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Release all connections (for cleanup).
|
|
477
|
+
*/
|
|
478
|
+
async releaseAll() {
|
|
479
|
+
for (const [projectId, entry] of this.pool) {
|
|
480
|
+
await entry.connection.disconnect();
|
|
481
|
+
this.pool.delete(projectId);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
var connectionPool = new ConnectionPool();
|
|
486
|
+
|
|
487
|
+
// src/session-manager.ts
|
|
488
|
+
var SessionManager = class {
|
|
489
|
+
static {
|
|
490
|
+
__name(this, "SessionManager");
|
|
491
|
+
}
|
|
492
|
+
sessions = /* @__PURE__ */ new Map();
|
|
493
|
+
async create(projectId, apiKey) {
|
|
494
|
+
let id = 1;
|
|
495
|
+
while (this.sessions.has(String(id))) {
|
|
496
|
+
id++;
|
|
497
|
+
}
|
|
498
|
+
const session = {
|
|
499
|
+
id: String(id),
|
|
500
|
+
projectId,
|
|
501
|
+
apiKey,
|
|
502
|
+
state: {}
|
|
503
|
+
};
|
|
504
|
+
await connectionPool.acquire(projectId, apiKey, session);
|
|
505
|
+
this.sessions.set(String(id), session);
|
|
506
|
+
return String(id);
|
|
507
|
+
}
|
|
508
|
+
list() {
|
|
509
|
+
return Array.from(this.sessions.values()).map((session) => ({
|
|
510
|
+
id: session.id,
|
|
511
|
+
projectId: session.projectId,
|
|
512
|
+
stateKeys: Object.keys(session.state)
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
get(id) {
|
|
516
|
+
return this.sessions.get(id);
|
|
517
|
+
}
|
|
518
|
+
getConnection(session) {
|
|
519
|
+
return connectionPool.getConnection(session.projectId);
|
|
520
|
+
}
|
|
521
|
+
async reconnect(session) {
|
|
522
|
+
return connectionPool.reconnect(session.projectId);
|
|
523
|
+
}
|
|
524
|
+
async destroy(id) {
|
|
525
|
+
const session = this.sessions.get(id);
|
|
526
|
+
if (!session) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
await connectionPool.release(session.projectId, session);
|
|
530
|
+
this.sessions.delete(id);
|
|
531
|
+
}
|
|
532
|
+
async destroyAll() {
|
|
533
|
+
for (const id of this.sessions.keys()) {
|
|
534
|
+
await this.destroy(id);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
var sessionManager = new SessionManager();
|
|
539
|
+
|
|
540
|
+
// src/relay-server.ts
|
|
541
|
+
function json(res, data, status = 200) {
|
|
542
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
543
|
+
res.end(JSON.stringify(data));
|
|
544
|
+
}
|
|
545
|
+
__name(json, "json");
|
|
546
|
+
function error(res, message, status = 400) {
|
|
547
|
+
json(res, { error: message }, status);
|
|
548
|
+
}
|
|
549
|
+
__name(error, "error");
|
|
550
|
+
async function readBody(req) {
|
|
551
|
+
return new Promise((resolve, reject) => {
|
|
552
|
+
let body = "";
|
|
553
|
+
req.on("data", (chunk) => {
|
|
554
|
+
body += chunk;
|
|
555
|
+
});
|
|
556
|
+
req.on("end", () => {
|
|
557
|
+
try {
|
|
558
|
+
resolve(body ? JSON.parse(body) : {});
|
|
559
|
+
} catch {
|
|
560
|
+
reject(new Error("Invalid JSON body"));
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
req.on("error", reject);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
__name(readBody, "readBody");
|
|
567
|
+
async function startRelayServer(port = RELAY_PORT) {
|
|
568
|
+
const server = http.createServer(async (req, res) => {
|
|
569
|
+
const url = req.url || "/";
|
|
570
|
+
try {
|
|
571
|
+
if (req.method === "GET" && url === "/version") {
|
|
572
|
+
return json(res, { version: VERSION });
|
|
573
|
+
}
|
|
574
|
+
if (req.method === "GET" && url === "/cli/sessions") {
|
|
575
|
+
const sessions = sessionManager.list();
|
|
576
|
+
return json(res, { sessions });
|
|
577
|
+
}
|
|
578
|
+
if (req.method === "POST" && url === "/cli/session/new") {
|
|
579
|
+
const body = await readBody(req);
|
|
580
|
+
const projectId = body.projectId;
|
|
581
|
+
const apiKey = body.apiKey;
|
|
582
|
+
if (!projectId || !apiKey) {
|
|
583
|
+
return error(res, "projectId and apiKey are required");
|
|
584
|
+
}
|
|
585
|
+
const id = await sessionManager.create(projectId, apiKey);
|
|
586
|
+
log(`session.new id=${id} project=${projectId}`);
|
|
587
|
+
return json(res, { id });
|
|
588
|
+
}
|
|
589
|
+
if (req.method === "POST" && url === "/cli/session/destroy") {
|
|
590
|
+
const body = await readBody(req);
|
|
591
|
+
const sessionId = body.sessionId ? String(body.sessionId) : "";
|
|
592
|
+
if (!sessionId) {
|
|
593
|
+
return error(res, "sessionId is required");
|
|
594
|
+
}
|
|
595
|
+
await sessionManager.destroy(sessionId);
|
|
596
|
+
log(`session.destroy id=${sessionId}`);
|
|
597
|
+
return json(res, { success: true });
|
|
598
|
+
}
|
|
599
|
+
if (req.method === "POST" && url === "/cli/exec") {
|
|
600
|
+
const body = await readBody(req);
|
|
601
|
+
const sessionId = String(body.sessionId);
|
|
602
|
+
const code = body.code;
|
|
603
|
+
const timeout = body.timeout || 3e4;
|
|
604
|
+
const cwd = body.cwd;
|
|
605
|
+
if (!sessionId) {
|
|
606
|
+
return error(res, "sessionId is required");
|
|
607
|
+
}
|
|
608
|
+
if (!code) {
|
|
609
|
+
return error(res, "code is required");
|
|
610
|
+
}
|
|
611
|
+
const session = sessionManager.get(sessionId);
|
|
612
|
+
if (!session) {
|
|
613
|
+
return error(res, `Session ${sessionId} not found`, 404);
|
|
614
|
+
}
|
|
615
|
+
log(
|
|
616
|
+
`exec session=${sessionId} code=${JSON.stringify(code).slice(0, 100)}`
|
|
617
|
+
);
|
|
618
|
+
const connection = sessionManager.getConnection(session);
|
|
619
|
+
if (!connection) {
|
|
620
|
+
return json(res, {
|
|
621
|
+
output: [],
|
|
622
|
+
error: "Failed to get connection for session"
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
let result;
|
|
626
|
+
try {
|
|
627
|
+
result = await execute(session, connection, code, { timeout, cwd });
|
|
628
|
+
} catch (execErr) {
|
|
629
|
+
const errMsg = execErr instanceof Error ? execErr.message : String(execErr);
|
|
630
|
+
result = { output: [], error: errMsg };
|
|
631
|
+
}
|
|
632
|
+
if (result.error && isConnectionError(result.error)) {
|
|
633
|
+
log(
|
|
634
|
+
`reconnect session=${sessionId} project=${session.projectId} reason="${result.error}"`
|
|
635
|
+
);
|
|
636
|
+
const newConnection = await sessionManager.reconnect(session);
|
|
637
|
+
if (newConnection) {
|
|
638
|
+
try {
|
|
639
|
+
result = await execute(session, newConnection, code, {
|
|
640
|
+
timeout,
|
|
641
|
+
cwd
|
|
642
|
+
});
|
|
643
|
+
log(`reconnect.success session=${sessionId}`);
|
|
644
|
+
} catch (retryErr) {
|
|
645
|
+
const errMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
646
|
+
log(`reconnect.failed session=${sessionId} error="${errMsg}"`);
|
|
647
|
+
result = { output: [], error: errMsg };
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
log(
|
|
651
|
+
`reconnect.failed session=${sessionId} error="no connection returned"`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (result.error) {
|
|
656
|
+
log(`exec.error session=${sessionId} error="${result.error}"`);
|
|
657
|
+
}
|
|
658
|
+
return json(res, result);
|
|
659
|
+
}
|
|
660
|
+
if (req.method === "POST" && url === "/shutdown") {
|
|
661
|
+
log("shutdown requested");
|
|
662
|
+
json(res, { success: true });
|
|
663
|
+
setTimeout(() => {
|
|
664
|
+
process.exit(0);
|
|
665
|
+
}, 100);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
error(res, "Not found", 404);
|
|
669
|
+
} catch (err) {
|
|
670
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
671
|
+
log(`error: ${message}`);
|
|
672
|
+
error(res, message, 500);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
return new Promise((resolve, reject) => {
|
|
676
|
+
server.on("error", reject);
|
|
677
|
+
server.listen(port, "127.0.0.1", () => {
|
|
678
|
+
resolve(server);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
__name(startRelayServer, "startRelayServer");
|
|
683
|
+
|
|
684
|
+
// src/start-relay-server.ts
|
|
685
|
+
process.title = "framer-relay-server";
|
|
686
|
+
process.on("uncaughtException", (err) => {
|
|
687
|
+
log(`uncaught exception: ${err.message}`);
|
|
688
|
+
console.error("Uncaught Exception:", err);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
});
|
|
691
|
+
process.on("unhandledRejection", (reason) => {
|
|
692
|
+
log(`unhandled rejection: ${reason}`);
|
|
693
|
+
console.error("Unhandled Rejection:", reason);
|
|
694
|
+
process.exit(1);
|
|
695
|
+
});
|
|
696
|
+
async function main() {
|
|
697
|
+
const server = await startRelayServer(RELAY_PORT);
|
|
698
|
+
log(`started v${VERSION} on port ${RELAY_PORT}`);
|
|
699
|
+
console.log(`Framer relay server v${VERSION} running on port ${RELAY_PORT}`);
|
|
700
|
+
process.on("SIGINT", () => {
|
|
701
|
+
log("shutdown SIGINT");
|
|
702
|
+
console.log("\nShutting down...");
|
|
703
|
+
server.close();
|
|
704
|
+
process.exit(0);
|
|
705
|
+
});
|
|
706
|
+
process.on("SIGTERM", () => {
|
|
707
|
+
log("shutdown SIGTERM");
|
|
708
|
+
console.log("\nShutting down...");
|
|
709
|
+
server.close();
|
|
710
|
+
process.exit(0);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
__name(main, "main");
|
|
714
|
+
main().catch((err) => {
|
|
715
|
+
log(`startup failed: ${err.message}`);
|
|
716
|
+
console.error("Failed to start relay server:", err);
|
|
717
|
+
process.exit(1);
|
|
718
|
+
});
|