@cryptiklemur/lattice 5.10.0 → 5.11.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.
- package/dist/client/assets/{angular-html-BDIcxkJq.js → angular-html-BoFzmWT8.js} +1 -1
- package/dist/client/assets/{angular-ts-Bt22ouNH.js → angular-ts-DZnI8rKE.js} +1 -1
- package/dist/client/assets/{apl-p8qkxzEK.js → apl-DstVmncE.js} +1 -1
- package/dist/client/assets/{astro-CIaMc49M.js → astro-DTPCjzEx.js} +1 -1
- package/dist/client/assets/{blade-BR56EAMD.js → blade-6q42Ss3F.js} +1 -1
- package/dist/client/assets/{c-Dli0HzAh.js → c-BQDGJ-nQ.js} +1 -1
- package/dist/client/assets/{cobol-Cad15ECy.js → cobol-Dlh0WvsZ.js} +1 -1
- package/dist/client/assets/{coffee-DpyATEbF.js → coffee-DdQv129j.js} +1 -1
- package/dist/client/assets/{cpp-KN8_NFsf.js → cpp-DhbQJIv4.js} +1 -1
- package/dist/client/assets/{crystal-CuyGv0kh.js → crystal-C22kERUB.js} +1 -1
- package/dist/client/assets/{css-Cm3q4bxn.js → css-n31O5kHj.js} +1 -1
- package/dist/client/assets/{dist-BjxsMc4u.js → dist-D8okl7lw.js} +2 -2
- package/dist/client/assets/{edge-B6S7CSbx.js → edge-Cgwx-o_7.js} +1 -1
- package/dist/client/assets/{elixir-CNUy9H8T.js → elixir-DAGM2WKD.js} +1 -1
- package/dist/client/assets/{elm-CNfcWmb9.js → elm-BLw_7oO9.js} +1 -1
- package/dist/client/assets/{erb-DWebzDaI.js → erb-DCaNhYa7.js} +1 -1
- package/dist/client/assets/{git-rebase-B_Pt2ZBK.js → git-rebase-CNNhb8-g.js} +1 -1
- package/dist/client/assets/{glimmer-js-CVwoOd72.js → glimmer-js-BnZd88Wi.js} +1 -1
- package/dist/client/assets/{glimmer-ts-CjtFSxjz.js → glimmer-ts-DvFNbZu-.js} +1 -1
- package/dist/client/assets/{glsl-CP4rggAA.js → glsl-Dnrk_Jnx.js} +1 -1
- package/dist/client/assets/{graphql-Dbm6sAtp.js → graphql-DlWTPvCG.js} +1 -1
- package/dist/client/assets/{hack-Bj9y3SGf.js → hack-DQg1Ek33.js} +1 -1
- package/dist/client/assets/{haml-DRGrdf3f.js → haml-DSk45qIE.js} +1 -1
- package/dist/client/assets/{handlebars-CFKjcBMg.js → handlebars-DuLvATB2.js} +1 -1
- package/dist/client/assets/{html-Vcd4eHHg.js → html-D4DiUnLg.js} +1 -1
- package/dist/client/assets/{html-derivative-BF0YbD4L.js → html-derivative-CS5MZ6d9.js} +1 -1
- package/dist/client/assets/{http-CGVTa2NT.js → http-CkDncfer.js} +1 -1
- package/dist/client/assets/{hurl-B0GrsGqd.js → hurl-DU39oO3U.js} +1 -1
- package/dist/client/assets/{index-CX1tudsF.js → index-CHPfE1Zl.js} +129 -129
- package/dist/client/assets/index-DHUKmLLC.css +2 -0
- package/dist/client/assets/{java-BJHQqHsm.js → java-lntACKEu.js} +1 -1
- package/dist/client/assets/{javascript-CmuMsKrc.js → javascript-CxkFc6nV.js} +1 -1
- package/dist/client/assets/{jinja-JxCLeq1j.js → jinja-DolO2zO7.js} +1 -1
- package/dist/client/assets/{jison-BdgAUhei.js → jison-Cok5FPev.js} +1 -1
- package/dist/client/assets/{json-DtPissHL.js → json-BebuQPrq.js} +1 -1
- package/dist/client/assets/{jsx-DUAxxDkP.js → jsx-iLBaUyXr.js} +1 -1
- package/dist/client/assets/{julia-DxDlbL6e.js → julia-C5Dsc7cH.js} +1 -1
- package/dist/client/assets/{just-CVmAAx2R.js → just-DJYqq_9R.js} +1 -1
- package/dist/client/assets/{latex-uwxggTWA.js → latex-BTTYiKj1.js} +1 -1
- package/dist/client/assets/{liquid-xsETAJJy.js → liquid-DpAKCrOB.js} +1 -1
- package/dist/client/assets/{lua-B2Hh8PgD.js → lua-BZ6b1hko.js} +1 -1
- package/dist/client/assets/{marko-yDeGxD87.js → marko-D8VK6iGt.js} +1 -1
- package/dist/client/assets/{mdc-QMp4ieYR.js → mdc-Paa3XzwY.js} +1 -1
- package/dist/client/assets/{nginx-7gmRmcqz.js → nginx-C5k9mWtJ.js} +1 -1
- package/dist/client/assets/{nim-CA8SNY_7.js → nim-Dst6YSnE.js} +1 -1
- package/dist/client/assets/{perl-lx5nW4VC.js → perl-XhiCjgBp.js} +1 -1
- package/dist/client/assets/{php-DgHiW953.js → php-BcsPLnLU.js} +1 -1
- package/dist/client/assets/{pug-CbbB1vwb.js → pug-GLH9-eAJ.js} +1 -1
- package/dist/client/assets/{qml-COrzwCIh.js → qml-Cj_lJioE.js} +1 -1
- package/dist/client/assets/{r-Dv7pZJDH.js → r-B70aGYK5.js} +1 -1
- package/dist/client/assets/{razor-D2m8EDP5.js → razor-R3gub_zy.js} +1 -1
- package/dist/client/assets/{regexp-BXLT-jPc.js → regexp-itC0dIUJ.js} +1 -1
- package/dist/client/assets/{rst-_S6rrUYh.js → rst-DdyoV8E2.js} +1 -1
- package/dist/client/assets/{ruby-C3XO7tYY.js → ruby-BYBZsv66.js} +1 -1
- package/dist/client/assets/{sas-DP2k4iuN.js → sas-fqfqXqj1.js} +1 -1
- package/dist/client/assets/{scss-lhLFMXGn.js → scss-B-ELv6mu.js} +1 -1
- package/dist/client/assets/{shellscript-BYlBPHen.js → shellscript-BgB8TNw6.js} +1 -1
- package/dist/client/assets/{shellsession-CbVyQKWZ.js → shellsession-BLK2Dgkm.js} +1 -1
- package/dist/client/assets/{soy-Be8a0lHq.js → soy-C7_RmNrp.js} +1 -1
- package/dist/client/assets/{sql-2KxvU9YS.js → sql-AUgbUJq4.js} +1 -1
- package/dist/client/assets/{stata-BxlWftTS.js → stata-CIVqSIOr.js} +1 -1
- package/dist/client/assets/{surrealql-CJ-q86nR.js → surrealql-BzRQzc5S.js} +1 -1
- package/dist/client/assets/{svelte-Q1ml0OiY.js → svelte-BCIwEwtb.js} +1 -1
- package/dist/client/assets/{templ-BbfPZhtu.js → templ-C1hbwe4u.js} +1 -1
- package/dist/client/assets/{tex-Dcth4Gi6.js → tex-CI4tIsaP.js} +1 -1
- package/dist/client/assets/{ts-tags-BKhSOXI3.js → ts-tags-SUeikhEp.js} +1 -1
- package/dist/client/assets/{tsx-CS6iQ0XH.js → tsx-xkp7aIZs.js} +1 -1
- package/dist/client/assets/{twig-BHp31ZxS.js → twig-CGgBSAyc.js} +1 -1
- package/dist/client/assets/{typescript-16YJBTaO.js → typescript-O2YMTl_s.js} +1 -1
- package/dist/client/assets/{vue-CMKwTi4r.js → vue-DsNRxos1.js} +1 -1
- package/dist/client/assets/{vue-html-Dr8VUA2G.js → vue-html-CuY3t7bs.js} +1 -1
- package/dist/client/assets/{vue-vine-DZUqDerl.js → vue-vine-C6kSCKwY.js} +1 -1
- package/dist/client/assets/{xml-CBbBKKDC.js → xml-DafwzOLY.js} +1 -1
- package/dist/client/assets/{xsl-DWEX6PKX.js → xsl-1SGGZibr.js} +1 -1
- package/dist/client/assets/{yaml-DvKvvh3X.js → yaml-DSVhzmhr.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/sw.js +1 -1
- package/dist/server/analytics/engine.js +241 -241
- package/dist/server/assets.js +4 -4
- package/dist/server/auth/passphrase.js +13 -13
- package/dist/server/config.js +7 -7
- package/dist/server/daemon.js +93 -93
- package/dist/server/features/brainstorm.js +42 -42
- package/dist/server/features/ralph-loop.js +33 -33
- package/dist/server/features/scheduler.js +53 -53
- package/dist/server/features/specs.js +54 -54
- package/dist/server/features/sticky-notes.js +17 -17
- package/dist/server/features/superpowers.js +24 -24
- package/dist/server/handlers/analytics.js +1 -1
- package/dist/server/handlers/attachment.js +32 -32
- package/dist/server/handlers/bookmarks.js +4 -4
- package/dist/server/handlers/brainstorm.js +4 -4
- package/dist/server/handlers/chat.js +54 -54
- package/dist/server/handlers/editor.js +13 -13
- package/dist/server/handlers/fs.js +51 -51
- package/dist/server/handlers/hooks.js +20 -20
- package/dist/server/handlers/loop.js +6 -6
- package/dist/server/handlers/memory.js +44 -44
- package/dist/server/handlers/mesh.js +60 -60
- package/dist/server/handlers/notes.js +7 -7
- package/dist/server/handlers/plugins.js +174 -174
- package/dist/server/handlers/project-settings.js +26 -26
- package/dist/server/handlers/scheduler.js +6 -6
- package/dist/server/handlers/session.js +24 -24
- package/dist/server/handlers/settings.js +21 -21
- package/dist/server/handlers/skills.js +91 -91
- package/dist/server/handlers/specs.js +51 -28
- package/dist/server/handlers/terminal.js +13 -13
- package/dist/server/handlers/themes.js +21 -21
- package/dist/server/handlers/update.js +17 -17
- package/dist/server/hooks/event_forward.sh +34 -0
- package/dist/server/hooks/post_tool_use.sh +26 -0
- package/dist/server/hooks/statusline.sh +26 -0
- package/dist/server/identity.js +6 -6
- package/dist/server/index.js +111 -111
- package/dist/server/logger.js +1 -1
- package/dist/server/mesh/connector.js +78 -78
- package/dist/server/mesh/crypto.js +20 -20
- package/dist/server/mesh/discovery.js +14 -14
- package/dist/server/mesh/pairing.js +30 -30
- package/dist/server/mesh/peers.js +10 -10
- package/dist/server/mesh/proxy.js +14 -14
- package/dist/server/mesh/session-sync.js +23 -23
- package/dist/server/project/bookmarks.js +11 -11
- package/dist/server/project/context-breakdown.js +70 -70
- package/dist/server/project/file-browser.js +17 -17
- package/dist/server/project/project-files.js +68 -68
- package/dist/server/project/registry.js +10 -10
- package/dist/server/project/sdk-bridge.js +157 -157
- package/dist/server/project/session.js +201 -199
- package/dist/server/project/terminal.js +15 -15
- package/dist/server/project/warmup.js +37 -37
- package/dist/server/push.js +11 -11
- package/dist/server/runtime.js +1 -1
- package/dist/server/tls.js +15 -15
- package/dist/server/tui.js +15 -15
- package/dist/server/update-checker.js +21 -21
- package/dist/server/ws/broadcast.js +18 -18
- package/dist/server/ws/router.js +17 -17
- package/dist/shared/constants.js +8 -8
- package/package.json +2 -2
- package/dist/client/assets/index-DlfI20Gn.css +0 -2
package/dist/server/assets.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { join, dirname } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const __dirname_local = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const CONTENT_TYPES = {
|
|
6
6
|
".html": "text/html; charset=utf-8",
|
|
7
7
|
".js": "application/javascript",
|
|
8
8
|
".css": "text/css",
|
|
@@ -28,12 +28,12 @@ export function hasEmbeddedAssets() {
|
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
30
|
export function getClientDir() {
|
|
31
|
-
|
|
31
|
+
const distPath = join(__dirname_local, "../../dist/client");
|
|
32
32
|
if (existsSync(distPath))
|
|
33
33
|
return distPath;
|
|
34
34
|
return join(__dirname_local, "../../dist/client");
|
|
35
35
|
}
|
|
36
36
|
export function guessContentType(path) {
|
|
37
|
-
|
|
37
|
+
const ext = path.slice(path.lastIndexOf("."));
|
|
38
38
|
return CONTENT_TYPES[ext] || "application/octet-stream";
|
|
39
39
|
}
|
|
@@ -9,24 +9,24 @@ function scryptAsync(password, salt, keylen) {
|
|
|
9
9
|
});
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const TOKEN_TTL = 86400000;
|
|
13
|
+
const CLEANUP_INTERVAL = 600000;
|
|
14
|
+
const activeSessions = new Map();
|
|
15
15
|
export async function hashPassphrase(passphrase) {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const salt = randomBytes(16).toString("hex");
|
|
17
|
+
const hash = (await scryptAsync(passphrase, salt, 64)).toString("hex");
|
|
18
18
|
return salt + ":" + hash;
|
|
19
19
|
}
|
|
20
20
|
export async function verifyPassphrase(passphrase, storedHash) {
|
|
21
|
-
|
|
21
|
+
const parts = storedHash.split(":");
|
|
22
22
|
if (parts.length !== 2) {
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const salt = parts[0];
|
|
26
|
+
const hash = parts[1];
|
|
27
27
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const derived = await scryptAsync(passphrase, salt, 64);
|
|
29
|
+
const expected = Buffer.from(hash, "hex");
|
|
30
30
|
if (derived.length !== expected.length) {
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
@@ -46,7 +46,7 @@ export function removeSession(token) {
|
|
|
46
46
|
activeSessions.delete(token);
|
|
47
47
|
}
|
|
48
48
|
export function isValidSession(token) {
|
|
49
|
-
|
|
49
|
+
const createdAt = activeSessions.get(token);
|
|
50
50
|
if (createdAt === undefined) {
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
@@ -59,8 +59,8 @@ export function isValidSession(token) {
|
|
|
59
59
|
export function clearSessions() {
|
|
60
60
|
activeSessions.clear();
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const cleanupInterval = setInterval(function () {
|
|
63
|
+
const now = Date.now();
|
|
64
64
|
activeSessions.forEach(function (createdAt, token) {
|
|
65
65
|
if (now - createdAt > TOKEN_TTL) {
|
|
66
66
|
activeSessions.delete(token);
|
package/dist/server/config.js
CHANGED
|
@@ -2,8 +2,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir, hostname } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { DEFAULT_PORT, LATTICE_HOME_DIR } from "#shared";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const home = process.env.LATTICE_HOME || join(homedir(), LATTICE_HOME_DIR);
|
|
6
|
+
let cachedConfig = null;
|
|
7
7
|
export function getLatticeHome() {
|
|
8
8
|
if (!existsSync(home)) {
|
|
9
9
|
mkdirSync(home, { recursive: true });
|
|
@@ -17,12 +17,12 @@ export function loadConfig() {
|
|
|
17
17
|
if (cachedConfig) {
|
|
18
18
|
return cachedConfig;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
const configPath = getConfigPath();
|
|
21
21
|
if (!existsSync(configPath)) {
|
|
22
22
|
return createDefaultConfig();
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
26
|
cachedConfig = {
|
|
27
27
|
port: parsed.port ?? DEFAULT_PORT,
|
|
28
28
|
name: parsed.name ?? hostname(),
|
|
@@ -35,7 +35,7 @@ export function loadConfig() {
|
|
|
35
35
|
return cachedConfig;
|
|
36
36
|
}
|
|
37
37
|
export function saveConfig(config) {
|
|
38
|
-
|
|
38
|
+
const configPath = getConfigPath();
|
|
39
39
|
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
40
40
|
cachedConfig = config;
|
|
41
41
|
}
|
|
@@ -43,7 +43,7 @@ export function invalidateConfigCache() {
|
|
|
43
43
|
cachedConfig = null;
|
|
44
44
|
}
|
|
45
45
|
function createDefaultConfig() {
|
|
46
|
-
|
|
46
|
+
const config = {
|
|
47
47
|
port: DEFAULT_PORT,
|
|
48
48
|
name: hostname(),
|
|
49
49
|
tls: false,
|
package/dist/server/daemon.js
CHANGED
|
@@ -58,21 +58,21 @@ import { initSuperpowers } from "./features/superpowers.js";
|
|
|
58
58
|
import { cleanupClientTerminals } from "./handlers/terminal.js";
|
|
59
59
|
import { cleanupClient as cleanupClientAttachments } from "./handlers/attachment.js";
|
|
60
60
|
import { initPush, getVapidPublicKey, addPushSubscription } from "./push.js";
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const RATE_LIMIT_WINDOW = 10000;
|
|
62
|
+
const RATE_LIMIT_MAX = 100;
|
|
63
|
+
const clientRateLimits = new Map();
|
|
64
|
+
const wsClientIds = new WeakMap();
|
|
65
65
|
function parseCookies(cookieHeader) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
const map = new Map();
|
|
67
|
+
const parts = cookieHeader.split(";");
|
|
68
|
+
for (let i = 0; i < parts.length; i++) {
|
|
69
|
+
const part = parts[i].trim();
|
|
70
|
+
const eqIdx = part.indexOf("=");
|
|
71
71
|
if (eqIdx === -1) {
|
|
72
72
|
continue;
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const key = part.slice(0, eqIdx).trim();
|
|
75
|
+
const value = part.slice(eqIdx + 1).trim();
|
|
76
76
|
map.set(key, value);
|
|
77
77
|
}
|
|
78
78
|
return map;
|
|
@@ -81,9 +81,9 @@ function isAuthenticatedReq(req, passphraseHash) {
|
|
|
81
81
|
if (!passphraseHash) {
|
|
82
82
|
return true;
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const cookieHeader = req.headers.cookie || "";
|
|
85
|
+
const cookies = parseCookies(cookieHeader);
|
|
86
|
+
const token = cookies.get("lattice_auth");
|
|
87
87
|
if (!token) {
|
|
88
88
|
return false;
|
|
89
89
|
}
|
|
@@ -182,9 +182,9 @@ function buildLoginPage() {
|
|
|
182
182
|
<script>
|
|
183
183
|
document.getElementById('form').addEventListener('submit', async function(e) {
|
|
184
184
|
e.preventDefault();
|
|
185
|
-
|
|
185
|
+
const passphrase = document.getElementById('passphrase').value;
|
|
186
186
|
try {
|
|
187
|
-
|
|
187
|
+
const res = await fetch('/auth', {
|
|
188
188
|
method: 'POST',
|
|
189
189
|
headers: { 'Content-Type': 'application/json' },
|
|
190
190
|
body: JSON.stringify({ passphrase: passphrase })
|
|
@@ -203,8 +203,8 @@ function buildLoginPage() {
|
|
|
203
203
|
</html>`;
|
|
204
204
|
}
|
|
205
205
|
function getMimeType(filePath) {
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
const ext = filePath.split(".").pop() || "";
|
|
207
|
+
const mimeTypes = {
|
|
208
208
|
html: "text/html; charset=utf-8",
|
|
209
209
|
js: "application/javascript",
|
|
210
210
|
css: "text/css",
|
|
@@ -226,19 +226,19 @@ function handleWsOpen(ws, clientId) {
|
|
|
226
226
|
addClient(clientId, ws);
|
|
227
227
|
log.ws("Client connected: %s", clientId);
|
|
228
228
|
sendTo(clientId, { type: "mesh:nodes", nodes: buildNodesMessage() });
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
const connectConfig = loadConfig();
|
|
230
|
+
const connectIdentity = loadOrCreateIdentity();
|
|
231
|
+
const localProjects = connectConfig.projects.map(function (p) {
|
|
232
232
|
return { slug: p.slug, path: p.path, title: p.title, nodeId: connectIdentity.id, nodeName: connectConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path), activeSessions: getActiveStreamCountForProject(p.slug) };
|
|
233
233
|
});
|
|
234
|
-
|
|
234
|
+
const connectRemoteProjects = getAllRemoteProjects(connectIdentity.id);
|
|
235
235
|
sendTo(clientId, {
|
|
236
236
|
type: "projects:list",
|
|
237
237
|
projects: localProjects.concat(connectRemoteProjects),
|
|
238
238
|
});
|
|
239
239
|
if (isWarmupComplete()) {
|
|
240
240
|
sendTo(clientId, { type: "warmup:models", models: getWarmupModels() });
|
|
241
|
-
|
|
241
|
+
const accountInfo = getWarmupAccountInfo();
|
|
242
242
|
if (accountInfo) {
|
|
243
243
|
sendTo(clientId, {
|
|
244
244
|
type: "warmup:account",
|
|
@@ -249,9 +249,9 @@ function handleWsOpen(ws, clientId) {
|
|
|
249
249
|
apiProvider: accountInfo.apiProvider,
|
|
250
250
|
});
|
|
251
251
|
}
|
|
252
|
-
|
|
253
|
-
for (
|
|
254
|
-
|
|
252
|
+
const cachedRateLimits = getWarmupRateLimits();
|
|
253
|
+
for (let rli = 0; rli < cachedRateLimits.length; rli++) {
|
|
254
|
+
const rl = cachedRateLimits[rli];
|
|
255
255
|
sendTo(clientId, {
|
|
256
256
|
type: "chat:rate_limit",
|
|
257
257
|
status: rl.status,
|
|
@@ -264,8 +264,8 @@ function handleWsOpen(ws, clientId) {
|
|
|
264
264
|
});
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
|
-
for (
|
|
268
|
-
|
|
267
|
+
for (let pi = 0; pi < connectConfig.projects.length; pi++) {
|
|
268
|
+
const proj = connectConfig.projects[pi];
|
|
269
269
|
subscribeClientToProject(clientId, proj.slug);
|
|
270
270
|
void listSessions(proj.slug, { limit: 40 }).then(function (result) {
|
|
271
271
|
sendTo(clientId, {
|
|
@@ -281,11 +281,11 @@ function handleWsOpen(ws, clientId) {
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
function handleWsMessage(ws, data) {
|
|
284
|
-
|
|
284
|
+
const clientId = wsClientIds.get(ws);
|
|
285
285
|
if (!clientId)
|
|
286
286
|
return;
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
let limit = clientRateLimits.get(clientId);
|
|
289
289
|
if (!limit || now - limit.windowStart > RATE_LIMIT_WINDOW) {
|
|
290
290
|
limit = { count: 0, windowStart: now };
|
|
291
291
|
clientRateLimits.set(clientId, limit);
|
|
@@ -295,9 +295,9 @@ function handleWsMessage(ws, data) {
|
|
|
295
295
|
sendTo(clientId, { type: "chat:error", message: "Rate limit exceeded, please slow down" });
|
|
296
296
|
return;
|
|
297
297
|
}
|
|
298
|
-
|
|
298
|
+
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString() : Buffer.from(data).toString();
|
|
299
299
|
try {
|
|
300
|
-
|
|
300
|
+
const msg = JSON.parse(text);
|
|
301
301
|
routeMessage(clientId, msg);
|
|
302
302
|
}
|
|
303
303
|
catch (err) {
|
|
@@ -305,7 +305,7 @@ function handleWsMessage(ws, data) {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
function handleWsClose(ws) {
|
|
308
|
-
|
|
308
|
+
const clientId = wsClientIds.get(ws);
|
|
309
309
|
if (!clientId)
|
|
310
310
|
return;
|
|
311
311
|
clearActiveSession(clientId);
|
|
@@ -321,24 +321,24 @@ function handleWsClose(ws) {
|
|
|
321
321
|
log.ws("Client disconnected: %s", clientId);
|
|
322
322
|
}
|
|
323
323
|
export async function startDaemon(portOverride, tlsOverride) {
|
|
324
|
-
|
|
325
|
-
|
|
324
|
+
const config = loadConfig();
|
|
325
|
+
const effectivePort = (portOverride && !isNaN(portOverride)) ? portOverride : config.port;
|
|
326
326
|
if (tlsOverride !== null && tlsOverride !== undefined) {
|
|
327
327
|
config.tls = tlsOverride;
|
|
328
328
|
}
|
|
329
|
-
|
|
329
|
+
const identity = loadOrCreateIdentity();
|
|
330
330
|
log.server("Node: %s (%s)", config.name, identity.id);
|
|
331
331
|
log.server("Home: %s", getLatticeHome());
|
|
332
|
-
|
|
333
|
-
|
|
332
|
+
const clientDir = getClientDir();
|
|
333
|
+
const app = express();
|
|
334
334
|
app.use(express.json());
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
335
|
+
const authAttempts = new Map();
|
|
336
|
+
const AUTH_RATE_LIMIT = 5;
|
|
337
|
+
const AUTH_RATE_WINDOW = 60000;
|
|
338
338
|
app.post("/auth", async function (req, res) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
339
|
+
const ip = req.ip || req.socket.remoteAddress || "unknown";
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
let attempts = authAttempts.get(ip) || [];
|
|
342
342
|
attempts = attempts.filter(function (t) { return now - t < AUTH_RATE_WINDOW; });
|
|
343
343
|
if (attempts.length >= AUTH_RATE_LIMIT) {
|
|
344
344
|
res.status(429).json({ ok: false, error: "Too many attempts. Try again later." });
|
|
@@ -346,9 +346,9 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
346
346
|
}
|
|
347
347
|
attempts.push(now);
|
|
348
348
|
authAttempts.set(ip, attempts);
|
|
349
|
-
|
|
350
|
-
if (!config.passphraseHash || await verifyPassphrase(passphrase, config.passphraseHash)) {
|
|
351
|
-
|
|
349
|
+
const passphrase = req.body.passphrase || "";
|
|
350
|
+
if (!config.passphraseHash || (await verifyPassphrase(passphrase, config.passphraseHash))) {
|
|
351
|
+
const token = generateSessionToken();
|
|
352
352
|
addSession(token);
|
|
353
353
|
res.setHeader("Set-Cookie", "lattice_auth=" + token + "; HttpOnly; Path=/; SameSite=Strict");
|
|
354
354
|
res.json({ ok: true });
|
|
@@ -374,7 +374,7 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
374
374
|
});
|
|
375
375
|
app.post("/api/push-subscribe", function (req, res) {
|
|
376
376
|
try {
|
|
377
|
-
|
|
377
|
+
const pushBody = req.body;
|
|
378
378
|
addPushSubscription(pushBody);
|
|
379
379
|
res.json({ ok: true });
|
|
380
380
|
}
|
|
@@ -407,15 +407,15 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
407
407
|
res.json(session);
|
|
408
408
|
});
|
|
409
409
|
app.get("/api/file", async function (req, res) {
|
|
410
|
-
|
|
410
|
+
const reqFilePath = req.query.path;
|
|
411
411
|
if (!reqFilePath) {
|
|
412
412
|
res.status(400).send("Missing path parameter");
|
|
413
413
|
return;
|
|
414
414
|
}
|
|
415
|
-
|
|
416
|
-
for (
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
let resolved = null;
|
|
416
|
+
for (let pi = 0; pi < config.projects.length; pi++) {
|
|
417
|
+
const projectPath = resolve(config.projects[pi].path);
|
|
418
|
+
const candidate = resolve(projectPath, reqFilePath);
|
|
419
419
|
if (candidate.startsWith(projectPath + "/") && existsSync(candidate)) {
|
|
420
420
|
resolved = candidate;
|
|
421
421
|
break;
|
|
@@ -425,16 +425,16 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
425
425
|
res.status(404).send("File not found");
|
|
426
426
|
return;
|
|
427
427
|
}
|
|
428
|
-
|
|
428
|
+
const fileStat = await fsStat(resolved);
|
|
429
429
|
res.setHeader("Content-Type", getMimeType(resolved));
|
|
430
430
|
res.setHeader("Content-Length", fileStat.size);
|
|
431
431
|
createReadStream(resolved).pipe(res);
|
|
432
432
|
});
|
|
433
|
-
|
|
433
|
+
let tlsOptions;
|
|
434
434
|
if (config.tls) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
435
|
+
const certsDir = join(getLatticeHome(), "certs");
|
|
436
|
+
const certPath = join(certsDir, "cert.pem");
|
|
437
|
+
const keyPath = join(certsDir, "key.pem");
|
|
438
438
|
if (existsSync(certPath) && existsSync(keyPath)) {
|
|
439
439
|
try {
|
|
440
440
|
tlsOptions = {
|
|
@@ -456,15 +456,15 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
456
456
|
console.error("[lattice] TLS enabled but no certs found. Run 'lattice setup-tls' to generate them.");
|
|
457
457
|
}
|
|
458
458
|
}
|
|
459
|
-
|
|
460
|
-
|
|
459
|
+
const protocol = tlsOptions ? "https" : "http";
|
|
460
|
+
const httpServer = tlsOptions
|
|
461
461
|
? createHttpsServer(tlsOptions, app)
|
|
462
462
|
: createHttpServer(app);
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
464
|
+
const { loadAllThemes } = await import("./handlers/themes.js");
|
|
465
465
|
async function getThemeInjectionScript() {
|
|
466
466
|
try {
|
|
467
|
-
|
|
467
|
+
const customThemes = await loadAllThemes();
|
|
468
468
|
if (customThemes.length === 0)
|
|
469
469
|
return "";
|
|
470
470
|
return "<script>window.__LATTICE_CUSTOM_THEMES__=" + JSON.stringify(customThemes) + "</script>";
|
|
@@ -474,8 +474,8 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
476
|
if (isDev) {
|
|
477
|
-
|
|
478
|
-
|
|
477
|
+
const { createServer: createViteServer } = await import("vite");
|
|
478
|
+
const vite = await createViteServer({
|
|
479
479
|
server: {
|
|
480
480
|
middlewareMode: true,
|
|
481
481
|
hmr: { server: httpServer },
|
|
@@ -486,7 +486,7 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
486
486
|
transformIndexHtml: {
|
|
487
487
|
order: "post",
|
|
488
488
|
handler: async function () {
|
|
489
|
-
|
|
489
|
+
const script = await getThemeInjectionScript();
|
|
490
490
|
if (!script)
|
|
491
491
|
return [];
|
|
492
492
|
return [{ tag: "script", attrs: {}, children: "window.__LATTICE_CUSTOM_THEMES__=" + JSON.stringify(await loadAllThemes()), injectTo: "head" }];
|
|
@@ -500,10 +500,10 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
500
500
|
else if (clientDir && existsSync(clientDir)) {
|
|
501
501
|
app.use(express.static(clientDir, { dotfiles: "allow" }));
|
|
502
502
|
app.get("/{*path}", async function (_req, res) {
|
|
503
|
-
|
|
503
|
+
const indexPath = join(clientDir, "index.html");
|
|
504
504
|
if (existsSync(indexPath)) {
|
|
505
|
-
|
|
506
|
-
|
|
505
|
+
let html = readFileSync(indexPath, "utf-8");
|
|
506
|
+
const injection = await getThemeInjectionScript();
|
|
507
507
|
if (injection) {
|
|
508
508
|
html = html.replace("</head>", injection + "</head>");
|
|
509
509
|
}
|
|
@@ -515,7 +515,7 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
515
515
|
}
|
|
516
516
|
});
|
|
517
517
|
}
|
|
518
|
-
|
|
518
|
+
const wss = new WebSocketServer({
|
|
519
519
|
noServer: true,
|
|
520
520
|
perMessageDeflate: {
|
|
521
521
|
zlibDeflateOptions: { level: 1 },
|
|
@@ -523,7 +523,7 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
523
523
|
},
|
|
524
524
|
});
|
|
525
525
|
httpServer.on("upgrade", function (req, socket, head) {
|
|
526
|
-
|
|
526
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
527
527
|
if (url.pathname !== "/ws") {
|
|
528
528
|
return;
|
|
529
529
|
}
|
|
@@ -532,15 +532,15 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
532
532
|
return;
|
|
533
533
|
}
|
|
534
534
|
wss.handleUpgrade(req, socket, head, function (ws) {
|
|
535
|
-
|
|
535
|
+
const clientId = crypto.randomUUID();
|
|
536
536
|
handleWsOpen(ws, clientId);
|
|
537
537
|
ws.on("pong", function () { markClientAlive(clientId); });
|
|
538
538
|
ws.on("message", function (data) { handleWsMessage(ws, data); });
|
|
539
539
|
ws.on("close", function () { handleWsClose(ws); });
|
|
540
540
|
});
|
|
541
541
|
});
|
|
542
|
-
|
|
543
|
-
for (
|
|
542
|
+
const maxRetries = 10;
|
|
543
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
544
544
|
try {
|
|
545
545
|
await new Promise(function (resolveP, reject) {
|
|
546
546
|
httpServer.once("error", function (err) {
|
|
@@ -585,9 +585,9 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
585
585
|
loadSessionHistory();
|
|
586
586
|
loadSpecs();
|
|
587
587
|
onSpecsReloaded(function () {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
for (
|
|
588
|
+
const allSpecs = listSpecs();
|
|
589
|
+
const slugs = new Set(allSpecs.map(function (s) { return s.projectSlug; }));
|
|
590
|
+
for (const slug of slugs) {
|
|
591
591
|
broadcastToProject(slug, { type: "specs:list_result", specs: listSpecs(slug) });
|
|
592
592
|
}
|
|
593
593
|
});
|
|
@@ -595,7 +595,7 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
595
595
|
startBrainstormWatchers();
|
|
596
596
|
initSuperpowers();
|
|
597
597
|
startPeriodicUpdateCheck();
|
|
598
|
-
|
|
598
|
+
const hookResult = installHooks();
|
|
599
599
|
if (hookResult.success) {
|
|
600
600
|
log.server("Claude Code hooks installed/updated");
|
|
601
601
|
}
|
|
@@ -604,14 +604,14 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
604
604
|
}
|
|
605
605
|
loadInterruptedSessions();
|
|
606
606
|
initPush();
|
|
607
|
-
|
|
607
|
+
const firstProject = config.projects[0];
|
|
608
608
|
if (firstProject) {
|
|
609
609
|
void runWarmup(firstProject.path);
|
|
610
610
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
611
|
+
let meshDirty = true;
|
|
612
|
+
let lastNodesJson = "";
|
|
613
|
+
let lastProjectsJson = "";
|
|
614
|
+
let broadcastTick = 0;
|
|
615
615
|
onPeerConnected(function (nodeId) {
|
|
616
616
|
meshDirty = true;
|
|
617
617
|
broadcast({ type: "mesh:node_online", nodeId: nodeId });
|
|
@@ -633,28 +633,28 @@ export async function startDaemon(portOverride, tlsOverride) {
|
|
|
633
633
|
setInterval(function () {
|
|
634
634
|
broadcastTick++;
|
|
635
635
|
if (meshDirty) {
|
|
636
|
-
|
|
637
|
-
|
|
636
|
+
const nodesPayload = buildNodesMessage();
|
|
637
|
+
const nodesJson = JSON.stringify(nodesPayload);
|
|
638
638
|
if (nodesJson !== lastNodesJson) {
|
|
639
639
|
lastNodesJson = nodesJson;
|
|
640
640
|
broadcast({ type: "mesh:nodes", nodes: nodesPayload });
|
|
641
641
|
}
|
|
642
642
|
meshDirty = false;
|
|
643
643
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
644
|
+
const currentConfig = loadConfig();
|
|
645
|
+
const currentIdentity = loadOrCreateIdentity();
|
|
646
|
+
const localProjects = currentConfig.projects.map(function (p) {
|
|
647
647
|
return { slug: p.slug, path: p.path, title: p.title, nodeId: currentIdentity.id, nodeName: currentConfig.name, isRemote: false, ideProjectName: detectIdeProjectName(p.path), activeSessions: getActiveStreamCountForProject(p.slug) };
|
|
648
648
|
});
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
649
|
+
const remoteProjects = getAllRemoteProjects(currentIdentity.id);
|
|
650
|
+
const allProjects = localProjects.concat(remoteProjects);
|
|
651
|
+
const projectsJson = JSON.stringify(allProjects);
|
|
652
652
|
if (projectsJson !== lastProjectsJson) {
|
|
653
653
|
lastProjectsJson = projectsJson;
|
|
654
654
|
broadcast({ type: "projects:list", projects: allProjects });
|
|
655
655
|
}
|
|
656
656
|
if (broadcastTick % 3 === 0) {
|
|
657
|
-
|
|
657
|
+
const updateInfo = getCachedUpdateInfo();
|
|
658
658
|
if (updateInfo && updateInfo.updateAvailable) {
|
|
659
659
|
broadcast({
|
|
660
660
|
type: "update:status",
|