@lifeaitools/clauth 1.5.74 → 1.5.76

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.
@@ -17,9 +17,10 @@ import ora from "ora";
17
17
  import { execSync as execSyncTop } from "child_process";
18
18
  import Conf from "conf";
19
19
  import { getConfOptions } from "../conf-path.js";
20
- import { readdir, readFile, writeFile, rm, mkdir, stat, rename } from "node:fs/promises";
21
- import fg from "fast-glob";
22
- import { rgPath } from "@vscode/ripgrep";
20
+ import { readdir, readFile, writeFile, rm, mkdir, stat, rename } from "node:fs/promises";
21
+ import fg from "fast-glob";
22
+ import { rgPath } from "@vscode/ripgrep";
23
+ import { createStudioDebugRuntime } from "../studio-debug.js";
23
24
 
24
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
26
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
@@ -2405,7 +2406,7 @@ function readBody(req) {
2405
2406
  }
2406
2407
 
2407
2408
  // ── Server logic (shared by foreground + daemon) ─────────────
2408
- function createServer(initPassword, whitelist, port, tunnelHostnameInit = null, isStaged = false) {
2409
+ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null, isStaged = false) {
2409
2410
  // tunnelHostname may be updated at runtime (fetched from DB after unlock)
2410
2411
  let tunnelHostname = tunnelHostnameInit;
2411
2412
 
@@ -2435,11 +2436,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2435
2436
  // Rotation engine — starts after unlock
2436
2437
  const rotationEngine = createRotationEngine(initPassword, machineHash, LOG_FILE);
2437
2438
 
2438
- const CORS = {
2439
+ const CORS = {
2439
2440
  "Access-Control-Allow-Origin": "*",
2440
2441
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
2441
2442
  "Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id, mcp-protocol-version, mcp-session-id",
2442
- };
2443
+ };
2444
+ const studioDebugRuntime = createStudioDebugRuntime({ port, logFile: LOG_FILE });
2443
2445
 
2444
2446
  // ── MCP SSE session tracking ──────────────────────────────
2445
2447
  const sseSessions = new Map(); // sessionId → { res, initialized }
@@ -2840,11 +2842,14 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2840
2842
  return strike(res, 403, `Rejected non-local address: ${remote}`);
2841
2843
  }
2842
2844
 
2843
- // CORS preflight
2844
- if (req.method === "OPTIONS") {
2845
- res.writeHead(204, CORS);
2846
- return res.end();
2847
- }
2845
+ // CORS preflight
2846
+ if (req.method === "OPTIONS") {
2847
+ res.writeHead(204, CORS);
2848
+ return res.end();
2849
+ }
2850
+
2851
+ const studioDebugHandled = await studioDebugRuntime.handle(req, res, url, CORS);
2852
+ if (studioDebugHandled !== false) return;
2848
2853
 
2849
2854
  // ── Hosts that bypass OAuth (fresh domains for claude.ai compatibility) ──
2850
2855
  const NOAUTH_HOSTS = ["fs.regendevcorp.com", "clauth.regendevcorp.com", "chitchat.regendevcorp.com"];
@@ -0,0 +1,410 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { spawn } from "child_process";
6
+
7
+ const DEFAULT_POLL_TIMEOUT = 600_000;
8
+ const MAX_POLL_TIMEOUT = 600_000;
9
+ const MAX_EVENT_QUEUE = 100;
10
+ const MAX_SNIPPET = 2_000;
11
+
12
+ const APP_TARGETS = {
13
+ prt: {
14
+ appSlug: "prt",
15
+ brandSlug: "prt",
16
+ packageName: "@regen/prt-portal",
17
+ command: "pnpm --filter @regen/prt-portal dev",
18
+ url: "http://localhost:3006",
19
+ },
20
+ "prt-portal": {
21
+ appSlug: "prt",
22
+ brandSlug: "prt",
23
+ packageName: "@regen/prt-portal",
24
+ command: "pnpm --filter @regen/prt-portal dev",
25
+ url: "http://localhost:3006",
26
+ },
27
+ };
28
+
29
+ function nowIso() {
30
+ return new Date().toISOString();
31
+ }
32
+
33
+ function safeString(value, max = MAX_SNIPPET) {
34
+ if (typeof value !== "string") return value;
35
+ return value.length > max ? value.slice(0, max) : value;
36
+ }
37
+
38
+ function normalizeEvent(session, body) {
39
+ const type = body.type || body.mode || "direct_edit";
40
+ const id = body.id || `evt_${crypto.randomUUID()}`;
41
+ const target = body.target && typeof body.target === "object" ? { ...body.target } : {};
42
+ if (typeof target.textSnippet === "string") target.textSnippet = safeString(target.textSnippet, 500);
43
+ if (typeof target.outerHTMLSnippet === "string") target.outerHTMLSnippet = safeString(target.outerHTMLSnippet, MAX_SNIPPET);
44
+ if (typeof target.outerHTML === "string" && !target.outerHTMLSnippet) {
45
+ target.outerHTMLSnippet = safeString(target.outerHTML, MAX_SNIPPET);
46
+ delete target.outerHTML;
47
+ }
48
+
49
+ return {
50
+ type,
51
+ id,
52
+ mode: body.mode || type,
53
+ sessionId: session.sessionId,
54
+ brandSlug: session.brandSlug,
55
+ appSlug: session.appSlug,
56
+ target,
57
+ reference: body.reference || null,
58
+ instruction: safeString(body.instruction || body.prompt || "", 4_000),
59
+ createdAt: nowIso(),
60
+ };
61
+ }
62
+
63
+ function readBody(req, maxBytes = 128 * 1024) {
64
+ return new Promise((resolve, reject) => {
65
+ let data = "";
66
+ req.on("data", chunk => {
67
+ data += chunk;
68
+ if (data.length > maxBytes) reject(new Error("Body too large"));
69
+ });
70
+ req.on("end", () => {
71
+ if (!data.trim()) return resolve({});
72
+ try {
73
+ resolve(JSON.parse(data));
74
+ } catch {
75
+ reject(new Error("Invalid JSON"));
76
+ }
77
+ });
78
+ req.on("error", reject);
79
+ });
80
+ }
81
+
82
+ function writeJson(res, status, data, cors) {
83
+ res.writeHead(status, { "Content-Type": "application/json", ...cors });
84
+ res.end(JSON.stringify(data));
85
+ }
86
+
87
+ async function isUrlReachable(url) {
88
+ try {
89
+ const response = await fetch(url, {
90
+ method: "GET",
91
+ signal: AbortSignal.timeout(1_500),
92
+ });
93
+ return response.status < 500;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function resolveTarget(input = {}) {
100
+ const slug = String(input.appSlug || input.brandSlug || "prt").toLowerCase();
101
+ const configured = APP_TARGETS[slug];
102
+ if (!configured && !input.devCommand && !input.devUrl) {
103
+ return {
104
+ error: "unknown_app",
105
+ message: `No local debug target is configured for appSlug "${slug}".`,
106
+ appSlug: slug,
107
+ };
108
+ }
109
+
110
+ const base = configured || {
111
+ appSlug: slug,
112
+ brandSlug: input.brandSlug || slug,
113
+ command: input.devCommand,
114
+ url: input.devUrl,
115
+ };
116
+
117
+ return {
118
+ appSlug: input.appSlug || base.appSlug,
119
+ brandSlug: input.brandSlug || base.brandSlug || input.appSlug || base.appSlug,
120
+ command: input.devCommand || base.command,
121
+ url: input.devUrl || base.url,
122
+ cwd: input.cwd || input.repoRoot || process.cwd(),
123
+ };
124
+ }
125
+
126
+ function splitCommand(command) {
127
+ if (!command || typeof command !== "string") return null;
128
+ const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
129
+ return parts.map(part => part.replace(/^"|"$/g, ""));
130
+ }
131
+
132
+ function startDevProcess(target, logFile) {
133
+ const parts = splitCommand(target.command);
134
+ if (!parts || parts.length === 0) {
135
+ return { started: false, error: "missing_command" };
136
+ }
137
+
138
+ const logDir = path.join(os.tmpdir(), "clauth-studio-debug");
139
+ fs.mkdirSync(logDir, { recursive: true });
140
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
141
+ const outPath = path.join(logDir, `${target.appSlug}-${stamp}.out.log`);
142
+ const errPath = path.join(logDir, `${target.appSlug}-${stamp}.err.log`);
143
+ const out = fs.openSync(outPath, "a");
144
+ const err = fs.openSync(errPath, "a");
145
+
146
+ const proc = spawn(parts[0], parts.slice(1), {
147
+ cwd: target.cwd,
148
+ env: process.env,
149
+ stdio: ["ignore", out, err],
150
+ shell: process.platform === "win32",
151
+ detached: true,
152
+ windowsHide: true,
153
+ });
154
+ proc.unref();
155
+ try {
156
+ fs.appendFileSync(logFile, `[${nowIso()}] studio-debug dev start pid=${proc.pid} command=${target.command}\n`);
157
+ } catch {}
158
+
159
+ return {
160
+ started: true,
161
+ pid: proc.pid,
162
+ command: target.command,
163
+ url: target.url,
164
+ stdout: outPath,
165
+ stderr: errPath,
166
+ process: proc,
167
+ };
168
+ }
169
+
170
+ export class StudioDebugSessionStore {
171
+ constructor({ port = 52437, logFile = path.join(os.tmpdir(), "clauth-serve.log") } = {}) {
172
+ this.port = port;
173
+ this.logFile = logFile;
174
+ this.sessions = new Map();
175
+ }
176
+
177
+ async start(input = {}) {
178
+ const target = resolveTarget(input);
179
+ if (target.error) {
180
+ return { ok: false, error: target.error, message: target.message, status: "error" };
181
+ }
182
+
183
+ const reachable = await isUrlReachable(target.url);
184
+ const shouldLaunch = input.launchDevServer !== false;
185
+ const launch = reachable
186
+ ? { started: false, command: target.command, url: target.url, alreadyRunning: true }
187
+ : shouldLaunch
188
+ ? startDevProcess(target, this.logFile)
189
+ : { started: false, command: target.command, url: target.url, skipped: true };
190
+
191
+ const sessionId = input.sessionId || `studio-${crypto.randomUUID()}`;
192
+ const token = crypto.randomBytes(24).toString("base64url");
193
+ const session = {
194
+ sessionId,
195
+ token,
196
+ brandSlug: target.brandSlug,
197
+ appSlug: target.appSlug,
198
+ repoRoot: input.repoRoot || target.cwd,
199
+ cwd: target.cwd,
200
+ devCommand: target.command,
201
+ devUrl: target.url,
202
+ modeDefault: input.modeDefault || "direct_edit",
203
+ status: "waiting_for_agent",
204
+ createdAt: nowIso(),
205
+ updatedAt: nowIso(),
206
+ stopped: false,
207
+ events: [],
208
+ replies: [],
209
+ pendingPolls: [],
210
+ launch,
211
+ };
212
+ this.sessions.set(sessionId, session);
213
+
214
+ return {
215
+ ok: true,
216
+ sessionId,
217
+ token,
218
+ devUrl: target.url,
219
+ relayBaseUrl: `http://127.0.0.1:${this.port}/studio/debug/${sessionId}`,
220
+ status: session.status,
221
+ launch: {
222
+ started: !!launch.started,
223
+ alreadyRunning: !!launch.alreadyRunning,
224
+ command: launch.command,
225
+ url: launch.url,
226
+ pid: launch.pid || null,
227
+ },
228
+ pollCommand: `node scripts/studio-debug-poll.mjs --session ${sessionId} --token ${token} --relay http://127.0.0.1:${this.port}/studio/claude/${sessionId}`,
229
+ claudeBaseUrl: `http://127.0.0.1:${this.port}/studio/claude/${sessionId}`,
230
+ };
231
+ }
232
+
233
+ get(sessionId) {
234
+ return this.sessions.get(sessionId) || null;
235
+ }
236
+
237
+ authenticate(sessionId, token) {
238
+ const session = this.get(sessionId);
239
+ if (!session) return { error: "not_found" };
240
+ if (session.token !== token) return { error: "unauthorized" };
241
+ return { session };
242
+ }
243
+
244
+ submitEvent(sessionId, body) {
245
+ const auth = this.authenticate(sessionId, body.token);
246
+ if (auth.error) return auth;
247
+ const session = auth.session;
248
+ const event = normalizeEvent(session, body);
249
+
250
+ session.updatedAt = nowIso();
251
+ session.status = "event_pending";
252
+ if (session.pendingPolls.length > 0) {
253
+ const poll = session.pendingPolls.shift();
254
+ poll(event);
255
+ } else {
256
+ session.events.push(event);
257
+ if (session.events.length > MAX_EVENT_QUEUE) session.events.shift();
258
+ }
259
+ return { ok: true, eventId: event.id, status: session.status };
260
+ }
261
+
262
+ poll(sessionId, token, timeoutMs = DEFAULT_POLL_TIMEOUT) {
263
+ const auth = this.authenticate(sessionId, token);
264
+ if (auth.error) return Promise.resolve(auth);
265
+ const session = auth.session;
266
+ if (session.events.length > 0) {
267
+ session.status = "agent_received";
268
+ session.updatedAt = nowIso();
269
+ return Promise.resolve(session.events.shift());
270
+ }
271
+ if (session.stopped) return Promise.resolve({ type: "stopped" });
272
+
273
+ const timeout = Math.min(Math.max(Number(timeoutMs) || DEFAULT_POLL_TIMEOUT, 1), MAX_POLL_TIMEOUT);
274
+ session.status = "waiting_for_event";
275
+ session.updatedAt = nowIso();
276
+
277
+ return new Promise(resolve => {
278
+ const timer = setTimeout(() => {
279
+ session.pendingPolls = session.pendingPolls.filter(fn => fn !== finish);
280
+ resolve({ type: "timeout" });
281
+ }, timeout);
282
+ const finish = event => {
283
+ clearTimeout(timer);
284
+ session.status = "agent_received";
285
+ session.updatedAt = nowIso();
286
+ resolve(event);
287
+ };
288
+ session.pendingPolls.push(finish);
289
+ });
290
+ }
291
+
292
+ reply(sessionId, body) {
293
+ const auth = this.authenticate(sessionId, body.token);
294
+ if (auth.error) return auth;
295
+ const session = auth.session;
296
+ const reply = {
297
+ eventId: body.eventId,
298
+ status: body.status || body.type || "done",
299
+ message: body.message || "",
300
+ filesChanged: Array.isArray(body.filesChanged)
301
+ ? body.filesChanged
302
+ : body.file
303
+ ? [body.file]
304
+ : [],
305
+ createdAt: nowIso(),
306
+ };
307
+ session.replies.push(reply);
308
+ session.status = reply.status === "done" ? "done" : reply.status;
309
+ session.updatedAt = nowIso();
310
+ return { ok: true, status: session.status, reply };
311
+ }
312
+
313
+ status(sessionId, token) {
314
+ const auth = this.authenticate(sessionId, token);
315
+ if (auth.error) return auth;
316
+ const session = auth.session;
317
+ return {
318
+ ok: true,
319
+ sessionId: session.sessionId,
320
+ brandSlug: session.brandSlug,
321
+ appSlug: session.appSlug,
322
+ devUrl: session.devUrl,
323
+ status: session.status,
324
+ createdAt: session.createdAt,
325
+ updatedAt: session.updatedAt,
326
+ pendingEvents: session.events.length,
327
+ pendingPolls: session.pendingPolls.length,
328
+ replies: session.replies,
329
+ launch: {
330
+ started: !!session.launch?.started,
331
+ alreadyRunning: !!session.launch?.alreadyRunning,
332
+ command: session.launch?.command || session.devCommand,
333
+ url: session.launch?.url || session.devUrl,
334
+ pid: session.launch?.pid || null,
335
+ },
336
+ };
337
+ }
338
+
339
+ stop(sessionId, token) {
340
+ const auth = this.authenticate(sessionId, token);
341
+ if (auth.error) return auth;
342
+ const session = auth.session;
343
+ session.stopped = true;
344
+ session.status = "stopped";
345
+ session.updatedAt = nowIso();
346
+ if (session.launch?.process && !session.launch.process.killed) {
347
+ try {
348
+ session.launch.process.kill();
349
+ } catch {}
350
+ }
351
+ for (const poll of session.pendingPolls.splice(0)) poll({ type: "stopped" });
352
+ this.sessions.delete(sessionId);
353
+ return { ok: true, status: "stopped", sessionId };
354
+ }
355
+ }
356
+
357
+ export function createStudioDebugRuntime(options) {
358
+ const store = new StudioDebugSessionStore(options);
359
+
360
+ async function handle(req, res, url, cors) {
361
+ const reqPath = url.pathname;
362
+ const method = req.method;
363
+
364
+ if (method === "POST" && reqPath === "/studio/debug/start") {
365
+ try {
366
+ const body = await readBody(req);
367
+ const result = await store.start(body);
368
+ return writeJson(res, result.ok ? 200 : 400, result, cors);
369
+ } catch (err) {
370
+ return writeJson(res, 400, { ok: false, error: err.message }, cors);
371
+ }
372
+ }
373
+
374
+ const debugMatch = reqPath.match(/^\/studio\/debug\/([^/]+)\/(events|status|stop)$/);
375
+ const claudeMatch = reqPath.match(/^\/studio\/claude\/([^/]+)\/(poll|reply|status)$/);
376
+ if (!debugMatch && !claudeMatch) return false;
377
+ const [, sessionId, action] = debugMatch || claudeMatch;
378
+
379
+ try {
380
+ if (method === "POST" && action === "events") {
381
+ const result = store.submitEvent(sessionId, await readBody(req));
382
+ return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
383
+ }
384
+ if (method === "GET" && action === "poll") {
385
+ const result = await store.poll(sessionId, url.searchParams.get("token"), url.searchParams.get("timeout"));
386
+ return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
387
+ }
388
+ if (method === "POST" && action === "reply") {
389
+ const result = store.reply(sessionId, await readBody(req));
390
+ return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
391
+ }
392
+ if (method === "GET" && action === "status") {
393
+ const result = store.status(sessionId, url.searchParams.get("token"));
394
+ return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
395
+ }
396
+ if (method === "POST" && action === "stop") {
397
+ const body = await readBody(req);
398
+ const result = store.stop(sessionId, body.token || url.searchParams.get("token"));
399
+ return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
400
+ }
401
+ return writeJson(res, 405, { ok: false, error: "method_not_allowed" }, cors);
402
+ } catch (err) {
403
+ return writeJson(res, 400, { ok: false, error: err.message }, cors);
404
+ }
405
+ }
406
+
407
+ return { store, handle };
408
+ }
409
+
410
+ export const studioDebugTargets = APP_TARGETS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.5.74",
3
+ "version": "1.5.76",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {