@codemem/server 0.28.0 → 0.29.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/index.js +377 -50
- package/dist/index.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/stats.d.ts.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +7 -7
- package/static/app.js +2065 -978
- package/static/index.html +531 -30
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncProjectAllowedByFilters, syncVisibilityAllowed, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
3
|
+
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncProjectAllowedByFilters, syncVisibilityAllowed, updatePeerAddresses, upsertCoordinatorGroupPreference, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
4
4
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { createMiddleware } from "hono/factory";
|
|
@@ -9,7 +9,6 @@ import { homedir, networkInterfaces } from "node:os";
|
|
|
9
9
|
import { and, count, desc, eq, inArray, isNotNull, max, ne } from "drizzle-orm";
|
|
10
10
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
-
import net from "node:net";
|
|
13
12
|
//#region src/middleware.ts
|
|
14
13
|
var LOOPBACK_HOSTS = new Set([
|
|
15
14
|
"127.0.0.1",
|
|
@@ -188,6 +187,7 @@ var SECRET_CONFIG_KEYS = new Set([
|
|
|
188
187
|
"observer_headers",
|
|
189
188
|
"sync_coordinator_admin_secret"
|
|
190
189
|
]);
|
|
190
|
+
var REMOVED_CONFIG_KEYS = new Set(["observer_rich_openai_use_responses"]);
|
|
191
191
|
var ALLOWED_KEYS = [
|
|
192
192
|
"claude_command",
|
|
193
193
|
"observer_base_url",
|
|
@@ -198,7 +198,6 @@ var ALLOWED_KEYS = [
|
|
|
198
198
|
"observer_simple_temperature",
|
|
199
199
|
"observer_rich_model",
|
|
200
200
|
"observer_rich_temperature",
|
|
201
|
-
"observer_rich_openai_use_responses",
|
|
202
201
|
"observer_rich_reasoning_effort",
|
|
203
202
|
"observer_rich_reasoning_summary",
|
|
204
203
|
"observer_rich_max_output_tokens",
|
|
@@ -231,7 +230,6 @@ var DEFAULTS = {
|
|
|
231
230
|
observer_runtime: "api_http",
|
|
232
231
|
observer_auth_source: "auto",
|
|
233
232
|
observer_tier_routing_enabled: false,
|
|
234
|
-
observer_rich_openai_use_responses: false,
|
|
235
233
|
observer_auth_command: [],
|
|
236
234
|
observer_auth_timeout_ms: 1500,
|
|
237
235
|
observer_auth_cache_ttl_s: 300,
|
|
@@ -275,6 +273,11 @@ function readConfigFile(configPath) {
|
|
|
275
273
|
return {};
|
|
276
274
|
}
|
|
277
275
|
}
|
|
276
|
+
function withoutRemovedConfigKeys(configData) {
|
|
277
|
+
const next = { ...configData };
|
|
278
|
+
for (const key of REMOVED_CONFIG_KEYS) delete next[key];
|
|
279
|
+
return next;
|
|
280
|
+
}
|
|
278
281
|
function getEffectiveConfig(configData) {
|
|
279
282
|
const effective = {
|
|
280
283
|
...DEFAULTS,
|
|
@@ -406,7 +409,7 @@ function validateAndApplyUpdate(configData, key, value, providers) {
|
|
|
406
409
|
configData[key] = value;
|
|
407
410
|
return null;
|
|
408
411
|
}
|
|
409
|
-
if (key === "observer_tier_routing_enabled"
|
|
412
|
+
if (key === "observer_tier_routing_enabled") {
|
|
410
413
|
if (typeof value !== "boolean") return `${key} must be boolean`;
|
|
411
414
|
configData[key] = value;
|
|
412
415
|
return null;
|
|
@@ -446,7 +449,7 @@ function configRoutes(opts = {}) {
|
|
|
446
449
|
const app = new Hono();
|
|
447
450
|
app.get("/api/config", (c) => {
|
|
448
451
|
const configPath = getConfigPath();
|
|
449
|
-
const configData = readConfigFile(configPath);
|
|
452
|
+
const configData = withoutRemovedConfigKeys(readConfigFile(configPath));
|
|
450
453
|
const effective = getEffectiveConfig(configData);
|
|
451
454
|
return c.json({
|
|
452
455
|
path: configPath,
|
|
@@ -469,7 +472,7 @@ function configRoutes(opts = {}) {
|
|
|
469
472
|
if ("config" in payload && payload.config != null && (typeof payload.config !== "object" || Array.isArray(payload.config))) return c.json({ error: "config must be an object" }, 400);
|
|
470
473
|
const updates = "config" in payload && payload.config != null && typeof payload.config === "object" && !Array.isArray(payload.config) ? payload.config : payload;
|
|
471
474
|
const configPath = getCodememConfigPath();
|
|
472
|
-
const beforeConfig = readCodememConfigFile();
|
|
475
|
+
const beforeConfig = withoutRemovedConfigKeys(readCodememConfigFile());
|
|
473
476
|
const { allowedUpdates, error: protectedKeyError } = partitionViewerUpdates(updates, beforeConfig);
|
|
474
477
|
if (protectedKeyError) return c.json({ error: protectedKeyError }, 403);
|
|
475
478
|
const beforeEffective = getEffectiveConfig(beforeConfig);
|
|
@@ -901,6 +904,28 @@ function memoryRoutes(getStore) {
|
|
|
901
904
|
return c.json({ error: msg }, 400);
|
|
902
905
|
}
|
|
903
906
|
});
|
|
907
|
+
app.post("/api/memories/project", async (c) => {
|
|
908
|
+
const store = getStore();
|
|
909
|
+
let body;
|
|
910
|
+
try {
|
|
911
|
+
body = await c.req.json();
|
|
912
|
+
} catch {
|
|
913
|
+
return c.json({ error: "invalid JSON" }, 400);
|
|
914
|
+
}
|
|
915
|
+
const memoryId = parseStrictInteger(typeof body.memory_id === "string" ? body.memory_id : String(body.memory_id ?? ""));
|
|
916
|
+
if (memoryId == null || memoryId <= 0) return c.json({ error: "memory_id must be int" }, 400);
|
|
917
|
+
const project = String(body.project ?? "").trim();
|
|
918
|
+
if (!project) return c.json({ error: "project must be a non-empty string" }, 400);
|
|
919
|
+
try {
|
|
920
|
+
const result = store.moveMemoryProject(memoryId, project);
|
|
921
|
+
return c.json(result);
|
|
922
|
+
} catch (err) {
|
|
923
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
924
|
+
if (msg.includes("not found")) return c.json({ error: msg }, 404);
|
|
925
|
+
if (msg.includes("not owned")) return c.json({ error: msg }, 403);
|
|
926
|
+
return c.json({ error: msg }, 400);
|
|
927
|
+
}
|
|
928
|
+
});
|
|
904
929
|
app.post("/api/memories/forget", async (c) => {
|
|
905
930
|
const store = getStore();
|
|
906
931
|
let body;
|
|
@@ -1319,20 +1344,31 @@ function statsRoutes(getStore) {
|
|
|
1319
1344
|
return null;
|
|
1320
1345
|
}
|
|
1321
1346
|
};
|
|
1347
|
+
const RECENTLY_COMPLETED_WINDOW_MS = 12e4;
|
|
1322
1348
|
app.get("/api/stats", (c) => {
|
|
1323
1349
|
const store = getStore();
|
|
1324
|
-
const
|
|
1350
|
+
const jobs = listMaintenanceJobs(store.db);
|
|
1351
|
+
const now = Date.now();
|
|
1352
|
+
const surfacedJobs = sortActiveMaintenanceJobs$1(jobs.filter((job) => {
|
|
1353
|
+
if (job.status === "pending" || job.status === "running" || job.status === "failed") return true;
|
|
1354
|
+
if (job.status === "completed" && job.finished_at) {
|
|
1355
|
+
const finished = Date.parse(job.finished_at);
|
|
1356
|
+
if (Number.isFinite(finished) && now - finished <= RECENTLY_COMPLETED_WINDOW_MS) return true;
|
|
1357
|
+
}
|
|
1358
|
+
return false;
|
|
1359
|
+
})).map((job) => ({
|
|
1325
1360
|
kind: job.kind,
|
|
1326
1361
|
title: job.title,
|
|
1327
1362
|
status: job.status,
|
|
1328
1363
|
message: job.message,
|
|
1329
1364
|
progress: job.progress,
|
|
1365
|
+
finished_at: job.finished_at,
|
|
1330
1366
|
error: job.error
|
|
1331
1367
|
}));
|
|
1332
1368
|
return c.json({
|
|
1333
1369
|
...store.stats(),
|
|
1334
1370
|
viewer_pid: process.pid,
|
|
1335
|
-
maintenance_jobs:
|
|
1371
|
+
maintenance_jobs: surfacedJobs
|
|
1336
1372
|
});
|
|
1337
1373
|
});
|
|
1338
1374
|
app.get("/api/runtime", (c) => {
|
|
@@ -1418,6 +1454,60 @@ var SYNC_STALE_AFTER_SECONDS = 600;
|
|
|
1418
1454
|
var SYNC_PROTOCOL_VERSION = "2";
|
|
1419
1455
|
var LEGACY_SYNC_ACTOR_DISPLAY_NAME = "Legacy synced peer";
|
|
1420
1456
|
var LEGACY_SHARED_WORKSPACE_ID = "shared:legacy";
|
|
1457
|
+
/**
|
|
1458
|
+
* Attempt to decode a pasted string as a device-pairing payload.
|
|
1459
|
+
*
|
|
1460
|
+
* Returns:
|
|
1461
|
+
* - `{ kind: "pair", ... }` when the payload base64-decodes to JSON with
|
|
1462
|
+
* the expected pairing shape AND the fingerprint matches the public
|
|
1463
|
+
* key.
|
|
1464
|
+
* - `{ kind: "invalid-pair", error }` when the payload looked like a
|
|
1465
|
+
* pairing payload but failed validation — the caller should 400 this
|
|
1466
|
+
* rather than falling back to coordinator-invite handling.
|
|
1467
|
+
* - `null` when the string is not a pairing payload at all.
|
|
1468
|
+
*/
|
|
1469
|
+
function tryParsePairingPayload(value) {
|
|
1470
|
+
let parsed;
|
|
1471
|
+
if (value.trimStart().startsWith("{")) try {
|
|
1472
|
+
parsed = JSON.parse(value);
|
|
1473
|
+
} catch {
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
let decoded;
|
|
1478
|
+
try {
|
|
1479
|
+
decoded = Buffer.from(value, "base64").toString("utf8");
|
|
1480
|
+
} catch {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
if (!decoded?.trimStart().startsWith("{")) return null;
|
|
1484
|
+
try {
|
|
1485
|
+
parsed = JSON.parse(decoded);
|
|
1486
|
+
} catch {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
const deviceId = String(parsed.device_id ?? "").trim();
|
|
1491
|
+
const fingerprint = String(parsed.fingerprint ?? "").trim();
|
|
1492
|
+
const publicKey = String(parsed.public_key ?? "").trim();
|
|
1493
|
+
if (!deviceId && !fingerprint && !publicKey) return null;
|
|
1494
|
+
const addresses = (Array.isArray(parsed.addresses) ? parsed.addresses : []).filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
|
|
1495
|
+
if (!deviceId || !fingerprint || !publicKey || addresses.length === 0) return {
|
|
1496
|
+
kind: "invalid-pair",
|
|
1497
|
+
error: "Pairing payload missing device_id, fingerprint, public_key, or addresses"
|
|
1498
|
+
};
|
|
1499
|
+
if (fingerprintPublicKey(publicKey) !== fingerprint) return {
|
|
1500
|
+
kind: "invalid-pair",
|
|
1501
|
+
error: "Pairing payload fingerprint mismatch"
|
|
1502
|
+
};
|
|
1503
|
+
return {
|
|
1504
|
+
kind: "pair",
|
|
1505
|
+
device_id: deviceId,
|
|
1506
|
+
fingerprint,
|
|
1507
|
+
public_key: publicKey,
|
|
1508
|
+
addresses
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1421
1511
|
function coordinatorAdminStatusPayload(config = readCoordinatorSyncConfig()) {
|
|
1422
1512
|
const groups = config.syncCoordinatorGroups;
|
|
1423
1513
|
const activeGroup = groups[0] ?? null;
|
|
@@ -1874,19 +1964,31 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1874
1964
|
detail: "This discovered device is missing its coordinator public key. Refresh sync status and try again."
|
|
1875
1965
|
};
|
|
1876
1966
|
const groupIds = Array.isArray(match.groups) ? match.groups.map((value) => String(value ?? "").trim()).filter(Boolean) : [];
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1967
|
+
let groupId;
|
|
1968
|
+
if (input.expectedGroupId) {
|
|
1969
|
+
if (!groupIds.includes(input.expectedGroupId)) return {
|
|
1970
|
+
ok: false,
|
|
1971
|
+
status: 409,
|
|
1972
|
+
error: "peer_not_in_group",
|
|
1973
|
+
detail: "This discovered device is not visible through the specified coordinator group. Refresh sync status and try again."
|
|
1974
|
+
};
|
|
1975
|
+
groupId = input.expectedGroupId;
|
|
1976
|
+
} else {
|
|
1977
|
+
if (groupIds.length !== 1) return {
|
|
1978
|
+
ok: false,
|
|
1979
|
+
status: 409,
|
|
1980
|
+
error: "ambiguous_coordinator_group",
|
|
1981
|
+
detail: groupIds.length > 1 ? "This device is visible through multiple coordinator groups. Review the team setup before approving it here." : "This device is missing coordinator group context. Refresh sync status and try again."
|
|
1982
|
+
};
|
|
1983
|
+
groupId = groupIds[0];
|
|
1984
|
+
}
|
|
1884
1985
|
const d = drizzle(store.db, { schema });
|
|
1885
1986
|
const existing = d.select({
|
|
1886
1987
|
peer_device_id: schema.syncPeers.peer_device_id,
|
|
1887
1988
|
pinned_fingerprint: schema.syncPeers.pinned_fingerprint,
|
|
1888
1989
|
public_key: schema.syncPeers.public_key,
|
|
1889
|
-
addresses_json: schema.syncPeers.addresses_json
|
|
1990
|
+
addresses_json: schema.syncPeers.addresses_json,
|
|
1991
|
+
discovered_via_group_id: schema.syncPeers.discovered_via_group_id
|
|
1890
1992
|
}).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, input.peerDeviceId)).get();
|
|
1891
1993
|
const existingFingerprint = String(existing?.pinned_fingerprint ?? "").trim();
|
|
1892
1994
|
if (existing && existingFingerprint && existingFingerprint !== nextFingerprint) return {
|
|
@@ -1908,6 +2010,26 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1908
2010
|
groupId,
|
|
1909
2011
|
requestedDeviceId: input.peerDeviceId
|
|
1910
2012
|
});
|
|
2013
|
+
const coordinatorUrl = config.syncCoordinatorUrl || null;
|
|
2014
|
+
let scopeInclude;
|
|
2015
|
+
let scopeExclude;
|
|
2016
|
+
if (!existing) {
|
|
2017
|
+
let templateInclude = null;
|
|
2018
|
+
let templateExclude = null;
|
|
2019
|
+
if (coordinatorUrl) {
|
|
2020
|
+
const prefs = getCoordinatorGroupPreference(store.db, coordinatorUrl, groupId);
|
|
2021
|
+
if (prefs?.auto_seed_scope) {
|
|
2022
|
+
templateInclude = prefs.projects_include ?? null;
|
|
2023
|
+
templateExclude = prefs.projects_exclude ?? null;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
const overrideInclude = input.scopeOverride?.projects_include;
|
|
2027
|
+
const overrideExclude = input.scopeOverride?.projects_exclude;
|
|
2028
|
+
const resolvedInclude = overrideInclude === void 0 ? templateInclude : overrideInclude;
|
|
2029
|
+
const resolvedExclude = overrideExclude === void 0 ? templateExclude : overrideExclude;
|
|
2030
|
+
scopeInclude = resolvedInclude ? JSON.stringify(resolvedInclude) : null;
|
|
2031
|
+
scopeExclude = resolvedExclude ? JSON.stringify(resolvedExclude) : null;
|
|
2032
|
+
}
|
|
1911
2033
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1912
2034
|
return store.db.transaction(() => {
|
|
1913
2035
|
if (!existing) {
|
|
@@ -1917,15 +2039,20 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1917
2039
|
pinned_fingerprint: nextFingerprint || null,
|
|
1918
2040
|
public_key: nextPublicKey,
|
|
1919
2041
|
addresses_json: addressesJson,
|
|
2042
|
+
projects_include_json: scopeInclude ?? null,
|
|
2043
|
+
projects_exclude_json: scopeExclude ?? null,
|
|
1920
2044
|
created_at: now,
|
|
1921
|
-
last_seen_at: now
|
|
2045
|
+
last_seen_at: now,
|
|
2046
|
+
discovered_via_coordinator_id: coordinatorUrl,
|
|
2047
|
+
discovered_via_group_id: groupId
|
|
1922
2048
|
}).run();
|
|
1923
2049
|
return {
|
|
1924
2050
|
ok: true,
|
|
1925
2051
|
peer_device_id: input.peerDeviceId,
|
|
1926
2052
|
created: true,
|
|
1927
2053
|
updated: false,
|
|
1928
|
-
name: nextName
|
|
2054
|
+
name: nextName,
|
|
2055
|
+
group_id: groupId
|
|
1929
2056
|
};
|
|
1930
2057
|
}
|
|
1931
2058
|
d.update(schema.syncPeers).set({
|
|
@@ -1933,14 +2060,19 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1933
2060
|
pinned_fingerprint: nextFingerprint || existing.pinned_fingerprint || null,
|
|
1934
2061
|
public_key: nextPublicKey || existing.public_key || null,
|
|
1935
2062
|
addresses_json: addressesJson,
|
|
1936
|
-
last_seen_at: now
|
|
2063
|
+
last_seen_at: now,
|
|
2064
|
+
...existing.discovered_via_group_id ? {} : {
|
|
2065
|
+
discovered_via_group_id: groupId,
|
|
2066
|
+
discovered_via_coordinator_id: coordinatorUrl
|
|
2067
|
+
}
|
|
1937
2068
|
}).where(eq(schema.syncPeers.peer_device_id, input.peerDeviceId)).run();
|
|
1938
2069
|
return {
|
|
1939
2070
|
ok: true,
|
|
1940
2071
|
peer_device_id: input.peerDeviceId,
|
|
1941
2072
|
created: false,
|
|
1942
2073
|
updated: true,
|
|
1943
|
-
name: nextName
|
|
2074
|
+
name: nextName,
|
|
2075
|
+
group_id: groupId
|
|
1944
2076
|
};
|
|
1945
2077
|
})();
|
|
1946
2078
|
}
|
|
@@ -1998,7 +2130,9 @@ function mapPeerRow(store, row, showDiag, recentOpsByPeer) {
|
|
|
1998
2130
|
recent_ops: {
|
|
1999
2131
|
in: recentOps.in,
|
|
2000
2132
|
out: recentOps.out
|
|
2001
|
-
}
|
|
2133
|
+
},
|
|
2134
|
+
discovered_via_coordinator_id: typeof row.discovered_via_coordinator_id === "string" ? row.discovered_via_coordinator_id : null,
|
|
2135
|
+
discovered_via_group_id: typeof row.discovered_via_group_id === "string" ? row.discovered_via_group_id : null
|
|
2002
2136
|
};
|
|
2003
2137
|
}
|
|
2004
2138
|
var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
|
|
@@ -2068,23 +2202,6 @@ function readViewerBinding(dbPath) {
|
|
|
2068
2202
|
} catch {}
|
|
2069
2203
|
return null;
|
|
2070
2204
|
}
|
|
2071
|
-
async function portOpen(host, port) {
|
|
2072
|
-
return new Promise((resolve) => {
|
|
2073
|
-
const socket = net.createConnection({
|
|
2074
|
-
host,
|
|
2075
|
-
port
|
|
2076
|
-
});
|
|
2077
|
-
const done = (ok) => {
|
|
2078
|
-
socket.removeAllListeners();
|
|
2079
|
-
socket.destroy();
|
|
2080
|
-
resolve(ok);
|
|
2081
|
-
};
|
|
2082
|
-
socket.setTimeout(300);
|
|
2083
|
-
socket.once("connect", () => done(true));
|
|
2084
|
-
socket.once("timeout", () => done(false));
|
|
2085
|
-
socket.once("error", () => done(false));
|
|
2086
|
-
});
|
|
2087
|
-
}
|
|
2088
2205
|
var PEERS_QUERY = `
|
|
2089
2206
|
SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
|
|
2090
2207
|
p.last_seen_at, p.last_sync_at, p.last_error,
|
|
@@ -2335,13 +2452,13 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2335
2452
|
retentionState = void 0;
|
|
2336
2453
|
}
|
|
2337
2454
|
const lastSyncRow = traceSync("lastSyncRow", () => d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get());
|
|
2338
|
-
const semanticIndex = traceSync("semanticIndex", () => redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(store.db
|
|
2455
|
+
const semanticIndex = traceSync("semanticIndex", () => redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(store.db), showDiag));
|
|
2339
2456
|
const lastError = daemonState?.last_error;
|
|
2340
2457
|
const lastErrorAt = daemonState?.last_error_at;
|
|
2341
2458
|
const lastOkAt = daemonState?.last_ok_at;
|
|
2342
2459
|
const viewerBinding = traceSync("readViewerBinding", () => readViewerBinding(store.dbPath));
|
|
2343
|
-
const daemonRunning =
|
|
2344
|
-
const daemonDetail = viewerBinding ?
|
|
2460
|
+
const daemonRunning = Boolean(viewerBinding);
|
|
2461
|
+
const daemonDetail = viewerBinding ? `viewer pidfile at ${viewerBinding.host}:${viewerBinding.port}` : null;
|
|
2345
2462
|
let daemonStateValue = "ok";
|
|
2346
2463
|
if (!config.syncEnabled) daemonStateValue = "disabled";
|
|
2347
2464
|
else if (lastError && (!lastOkAt || String(lastOkAt) < String(lastErrorAt ?? ""))) daemonStateValue = "error";
|
|
@@ -2520,10 +2637,6 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2520
2637
|
app.get("/api/sync/pairing", (c) => {
|
|
2521
2638
|
const store = getStore();
|
|
2522
2639
|
{
|
|
2523
|
-
if (!queryBool(c.req.query("includeDiagnostics"))) return c.json({
|
|
2524
|
-
redacted: true,
|
|
2525
|
-
pairing_filter_hint: PAIRING_FILTER_HINT
|
|
2526
|
-
});
|
|
2527
2640
|
const config = readCoordinatorSyncConfig();
|
|
2528
2641
|
const d = drizzle(store.db, { schema });
|
|
2529
2642
|
const deviceRow = d.select({
|
|
@@ -2878,14 +2991,32 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2878
2991
|
} catch {
|
|
2879
2992
|
return c.json({ error: "invalid json" }, 400);
|
|
2880
2993
|
}
|
|
2881
|
-
const
|
|
2882
|
-
if (!
|
|
2994
|
+
const rawValue = String(body.invite ?? "").trim();
|
|
2995
|
+
if (!rawValue) return c.json({ error: "invite required" }, 400);
|
|
2996
|
+
const pairingResult = tryParsePairingPayload(rawValue.match(/echo\s+['"]([A-Za-z0-9+/=]+)['"]\s*\|\s*base64\s+-d\s*\|\s*codemem/)?.[1]?.trim() || rawValue);
|
|
2997
|
+
if (pairingResult?.kind === "invalid-pair") return c.json({ error: pairingResult.error }, 400);
|
|
2998
|
+
if (pairingResult?.kind === "pair") try {
|
|
2999
|
+
updatePeerAddresses(store.db, pairingResult.device_id, pairingResult.addresses, {
|
|
3000
|
+
pinnedFingerprint: pairingResult.fingerprint,
|
|
3001
|
+
publicKey: pairingResult.public_key
|
|
3002
|
+
});
|
|
3003
|
+
return c.json({
|
|
3004
|
+
ok: true,
|
|
3005
|
+
type: "pair",
|
|
3006
|
+
peer_device_id: pairingResult.device_id
|
|
3007
|
+
});
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3010
|
+
}
|
|
2883
3011
|
try {
|
|
2884
3012
|
const result = await coordinatorImportInviteAction({
|
|
2885
|
-
inviteValue,
|
|
3013
|
+
inviteValue: rawValue,
|
|
2886
3014
|
dbPath: store.dbPath
|
|
2887
3015
|
});
|
|
2888
|
-
return c.json(
|
|
3016
|
+
return c.json({
|
|
3017
|
+
...result,
|
|
3018
|
+
type: "team_join"
|
|
3019
|
+
});
|
|
2889
3020
|
} catch (error) {
|
|
2890
3021
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
2891
3022
|
}
|
|
@@ -3457,6 +3588,202 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3457
3588
|
}, 400);
|
|
3458
3589
|
}
|
|
3459
3590
|
});
|
|
3591
|
+
app.post("/api/coordinator/admin/groups/:group_id/enroll-peer", async (c) => {
|
|
3592
|
+
const status = coordinatorAdminStatusPayload(readCoordinatorSyncConfig());
|
|
3593
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3594
|
+
error: "coordinator_not_configured",
|
|
3595
|
+
status
|
|
3596
|
+
}, 400);
|
|
3597
|
+
const rawGroupId = String(c.req.param("group_id") ?? "").trim();
|
|
3598
|
+
if (!rawGroupId) return c.json({ error: "group_id required" }, 400);
|
|
3599
|
+
let body;
|
|
3600
|
+
try {
|
|
3601
|
+
body = await c.req.json();
|
|
3602
|
+
} catch {
|
|
3603
|
+
return c.json({ error: "invalid_json" }, 400);
|
|
3604
|
+
}
|
|
3605
|
+
const rawMode = String(body.mode ?? "").trim().toLowerCase();
|
|
3606
|
+
if (rawMode && rawMode !== "manual" && rawMode !== "discovered") return c.json({
|
|
3607
|
+
error: "invalid_mode",
|
|
3608
|
+
detail: `mode must be 'discovered' or 'manual', got ${rawMode}`
|
|
3609
|
+
}, 400);
|
|
3610
|
+
const mode = rawMode === "manual" ? "manual" : "discovered";
|
|
3611
|
+
const normalizeOverride = (value) => {
|
|
3612
|
+
if (value === void 0) return void 0;
|
|
3613
|
+
if (value === null) return null;
|
|
3614
|
+
if (!Array.isArray(value)) return void 0;
|
|
3615
|
+
const cleaned = value.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0);
|
|
3616
|
+
return cleaned.length === 0 ? null : cleaned;
|
|
3617
|
+
};
|
|
3618
|
+
const overrideInclude = normalizeOverride(body.projects_include);
|
|
3619
|
+
const overrideExclude = normalizeOverride(body.projects_exclude);
|
|
3620
|
+
const scopeOverride = overrideInclude === void 0 && overrideExclude === void 0 ? void 0 : {
|
|
3621
|
+
projects_include: overrideInclude,
|
|
3622
|
+
projects_exclude: overrideExclude
|
|
3623
|
+
};
|
|
3624
|
+
const store = getStore();
|
|
3625
|
+
if (mode === "manual") {
|
|
3626
|
+
const peerDeviceId = String(body.peer_device_id ?? "").trim();
|
|
3627
|
+
const publicKey = String(body.peer_public_key ?? "").trim();
|
|
3628
|
+
const name = String(body.name ?? "").trim() || null;
|
|
3629
|
+
const fingerprint = String(body.fingerprint ?? "").trim() || null;
|
|
3630
|
+
const addresses = (Array.isArray(body.peer_addresses) ? body.peer_addresses : []).map((item) => String(item ?? "").trim()).filter((item) => item.length > 0);
|
|
3631
|
+
if (!peerDeviceId || !publicKey) return c.json({ error: "peer_device_id_and_public_key_required" }, 400);
|
|
3632
|
+
const manualInclude = overrideInclude && overrideInclude.length > 0 ? JSON.stringify(overrideInclude) : null;
|
|
3633
|
+
const manualExclude = overrideExclude && overrideExclude.length > 0 ? JSON.stringify(overrideExclude) : null;
|
|
3634
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3635
|
+
const d = drizzle(store.db, { schema });
|
|
3636
|
+
try {
|
|
3637
|
+
if (!store.db.transaction(() => {
|
|
3638
|
+
if (d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get()) return false;
|
|
3639
|
+
d.insert(schema.syncPeers).values({
|
|
3640
|
+
peer_device_id: peerDeviceId,
|
|
3641
|
+
name,
|
|
3642
|
+
pinned_fingerprint: fingerprint,
|
|
3643
|
+
public_key: publicKey,
|
|
3644
|
+
addresses_json: JSON.stringify(addresses),
|
|
3645
|
+
projects_include_json: manualInclude,
|
|
3646
|
+
projects_exclude_json: manualExclude,
|
|
3647
|
+
created_at: now,
|
|
3648
|
+
last_seen_at: now,
|
|
3649
|
+
discovered_via_coordinator_id: null,
|
|
3650
|
+
discovered_via_group_id: null
|
|
3651
|
+
}).run();
|
|
3652
|
+
return true;
|
|
3653
|
+
})()) return c.json({
|
|
3654
|
+
error: "peer_exists",
|
|
3655
|
+
detail: "Peer is already enrolled."
|
|
3656
|
+
}, 409);
|
|
3657
|
+
} catch (error) {
|
|
3658
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : "";
|
|
3659
|
+
if (msg.includes("unique") || msg.includes("primary key")) return c.json({
|
|
3660
|
+
error: "peer_exists",
|
|
3661
|
+
detail: "Peer is already enrolled."
|
|
3662
|
+
}, 409);
|
|
3663
|
+
throw error;
|
|
3664
|
+
}
|
|
3665
|
+
return c.json({
|
|
3666
|
+
ok: true,
|
|
3667
|
+
peer_device_id: peerDeviceId,
|
|
3668
|
+
created: true,
|
|
3669
|
+
updated: false,
|
|
3670
|
+
name,
|
|
3671
|
+
group_id: null
|
|
3672
|
+
});
|
|
3673
|
+
}
|
|
3674
|
+
if (!status.has_admin_secret) return c.json({
|
|
3675
|
+
error: "coordinator_admin_secret_missing",
|
|
3676
|
+
status
|
|
3677
|
+
}, 400);
|
|
3678
|
+
const peerDeviceId = String(body.peer_device_id ?? body.discovered_device_id ?? "").trim();
|
|
3679
|
+
const fingerprint = String(body.fingerprint ?? "").trim();
|
|
3680
|
+
if (!peerDeviceId || !fingerprint) return c.json({ error: "peer_device_id_and_fingerprint_required" }, 400);
|
|
3681
|
+
try {
|
|
3682
|
+
const result = await acceptDiscoveredPeer(store, {
|
|
3683
|
+
peerDeviceId,
|
|
3684
|
+
fingerprint,
|
|
3685
|
+
expectedGroupId: rawGroupId,
|
|
3686
|
+
scopeOverride
|
|
3687
|
+
});
|
|
3688
|
+
if (result.ok) return c.json(result);
|
|
3689
|
+
return c.json({
|
|
3690
|
+
error: result.error,
|
|
3691
|
+
detail: result.detail
|
|
3692
|
+
}, result.status);
|
|
3693
|
+
} catch (error) {
|
|
3694
|
+
return c.json({
|
|
3695
|
+
error: "coordinator_lookup_failed",
|
|
3696
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
3697
|
+
}, 502);
|
|
3698
|
+
}
|
|
3699
|
+
});
|
|
3700
|
+
app.get("/api/coordinator/admin/groups/:group_id/preferences", (c) => {
|
|
3701
|
+
const status = coordinatorAdminStatusPayload(readCoordinatorSyncConfig());
|
|
3702
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3703
|
+
error: "coordinator_not_configured",
|
|
3704
|
+
status
|
|
3705
|
+
}, 400);
|
|
3706
|
+
const groupId = String(c.req.param("group_id") ?? "").trim();
|
|
3707
|
+
if (!groupId) return c.json({
|
|
3708
|
+
error: "group_id required",
|
|
3709
|
+
status
|
|
3710
|
+
}, 400);
|
|
3711
|
+
const coordinatorId = status.coordinator_url;
|
|
3712
|
+
if (!coordinatorId) return c.json({
|
|
3713
|
+
error: "coordinator_not_configured",
|
|
3714
|
+
status
|
|
3715
|
+
}, 400);
|
|
3716
|
+
const existing = getCoordinatorGroupPreference(getStore().db, coordinatorId, groupId);
|
|
3717
|
+
if (existing) return c.json({
|
|
3718
|
+
preferences: existing,
|
|
3719
|
+
status
|
|
3720
|
+
});
|
|
3721
|
+
return c.json({
|
|
3722
|
+
preferences: {
|
|
3723
|
+
coordinator_id: coordinatorId,
|
|
3724
|
+
group_id: groupId,
|
|
3725
|
+
projects_include: null,
|
|
3726
|
+
projects_exclude: null,
|
|
3727
|
+
auto_seed_scope: true,
|
|
3728
|
+
updated_at: null
|
|
3729
|
+
},
|
|
3730
|
+
status
|
|
3731
|
+
});
|
|
3732
|
+
});
|
|
3733
|
+
app.put("/api/coordinator/admin/groups/:group_id/preferences", async (c) => {
|
|
3734
|
+
const status = coordinatorAdminStatusPayload(readCoordinatorSyncConfig());
|
|
3735
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3736
|
+
error: "coordinator_not_configured",
|
|
3737
|
+
status
|
|
3738
|
+
}, 400);
|
|
3739
|
+
const groupId = String(c.req.param("group_id") ?? "").trim();
|
|
3740
|
+
if (!groupId) return c.json({
|
|
3741
|
+
error: "group_id required",
|
|
3742
|
+
status
|
|
3743
|
+
}, 400);
|
|
3744
|
+
const coordinatorId = status.coordinator_url;
|
|
3745
|
+
if (!coordinatorId) return c.json({
|
|
3746
|
+
error: "coordinator_not_configured",
|
|
3747
|
+
status
|
|
3748
|
+
}, 400);
|
|
3749
|
+
let body;
|
|
3750
|
+
try {
|
|
3751
|
+
body = await c.req.json();
|
|
3752
|
+
} catch {
|
|
3753
|
+
return c.json({
|
|
3754
|
+
error: "invalid_json",
|
|
3755
|
+
status
|
|
3756
|
+
}, 400);
|
|
3757
|
+
}
|
|
3758
|
+
const normalizeList = (value) => {
|
|
3759
|
+
if (value === void 0) return void 0;
|
|
3760
|
+
if (value === null) return null;
|
|
3761
|
+
if (!Array.isArray(value)) return void 0;
|
|
3762
|
+
const cleaned = value.map((item) => String(item ?? "").trim()).filter((item) => item.length > 0);
|
|
3763
|
+
return cleaned.length === 0 ? null : cleaned;
|
|
3764
|
+
};
|
|
3765
|
+
const autoSeed = typeof body.auto_seed_scope === "boolean" ? body.auto_seed_scope : void 0;
|
|
3766
|
+
const store = getStore();
|
|
3767
|
+
try {
|
|
3768
|
+
const preferences = upsertCoordinatorGroupPreference(store.db, {
|
|
3769
|
+
coordinator_id: coordinatorId,
|
|
3770
|
+
group_id: groupId,
|
|
3771
|
+
projects_include: normalizeList(body.projects_include),
|
|
3772
|
+
projects_exclude: normalizeList(body.projects_exclude),
|
|
3773
|
+
auto_seed_scope: autoSeed
|
|
3774
|
+
});
|
|
3775
|
+
return c.json({
|
|
3776
|
+
preferences,
|
|
3777
|
+
status
|
|
3778
|
+
});
|
|
3779
|
+
} catch (error) {
|
|
3780
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3781
|
+
return c.json({
|
|
3782
|
+
error: msg,
|
|
3783
|
+
status
|
|
3784
|
+
}, 400);
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3460
3787
|
app.post("/api/coordinator/admin/devices/:device_id/remove", async (c) => {
|
|
3461
3788
|
const config = readCoordinatorSyncConfig();
|
|
3462
3789
|
const status = coordinatorAdminStatusPayload(config);
|