@codemem/server 0.28.1 → 0.29.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/dist/index.js +368 -45
- package/dist/index.js.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 +2 -2
- package/static/app.js +2053 -952
- package/static/index.html +518 -23
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",
|
|
@@ -905,6 +904,28 @@ function memoryRoutes(getStore) {
|
|
|
905
904
|
return c.json({ error: msg }, 400);
|
|
906
905
|
}
|
|
907
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
|
+
});
|
|
908
929
|
app.post("/api/memories/forget", async (c) => {
|
|
909
930
|
const store = getStore();
|
|
910
931
|
let body;
|
|
@@ -1323,20 +1344,31 @@ function statsRoutes(getStore) {
|
|
|
1323
1344
|
return null;
|
|
1324
1345
|
}
|
|
1325
1346
|
};
|
|
1347
|
+
const RECENTLY_COMPLETED_WINDOW_MS = 12e4;
|
|
1326
1348
|
app.get("/api/stats", (c) => {
|
|
1327
1349
|
const store = getStore();
|
|
1328
|
-
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) => ({
|
|
1329
1360
|
kind: job.kind,
|
|
1330
1361
|
title: job.title,
|
|
1331
1362
|
status: job.status,
|
|
1332
1363
|
message: job.message,
|
|
1333
1364
|
progress: job.progress,
|
|
1365
|
+
finished_at: job.finished_at,
|
|
1334
1366
|
error: job.error
|
|
1335
1367
|
}));
|
|
1336
1368
|
return c.json({
|
|
1337
1369
|
...store.stats(),
|
|
1338
1370
|
viewer_pid: process.pid,
|
|
1339
|
-
maintenance_jobs:
|
|
1371
|
+
maintenance_jobs: surfacedJobs
|
|
1340
1372
|
});
|
|
1341
1373
|
});
|
|
1342
1374
|
app.get("/api/runtime", (c) => {
|
|
@@ -1422,6 +1454,60 @@ var SYNC_STALE_AFTER_SECONDS = 600;
|
|
|
1422
1454
|
var SYNC_PROTOCOL_VERSION = "2";
|
|
1423
1455
|
var LEGACY_SYNC_ACTOR_DISPLAY_NAME = "Legacy synced peer";
|
|
1424
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
|
+
}
|
|
1425
1511
|
function coordinatorAdminStatusPayload(config = readCoordinatorSyncConfig()) {
|
|
1426
1512
|
const groups = config.syncCoordinatorGroups;
|
|
1427
1513
|
const activeGroup = groups[0] ?? null;
|
|
@@ -1878,19 +1964,31 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1878
1964
|
detail: "This discovered device is missing its coordinator public key. Refresh sync status and try again."
|
|
1879
1965
|
};
|
|
1880
1966
|
const groupIds = Array.isArray(match.groups) ? match.groups.map((value) => String(value ?? "").trim()).filter(Boolean) : [];
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
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
|
+
}
|
|
1888
1985
|
const d = drizzle(store.db, { schema });
|
|
1889
1986
|
const existing = d.select({
|
|
1890
1987
|
peer_device_id: schema.syncPeers.peer_device_id,
|
|
1891
1988
|
pinned_fingerprint: schema.syncPeers.pinned_fingerprint,
|
|
1892
1989
|
public_key: schema.syncPeers.public_key,
|
|
1893
|
-
addresses_json: schema.syncPeers.addresses_json
|
|
1990
|
+
addresses_json: schema.syncPeers.addresses_json,
|
|
1991
|
+
discovered_via_group_id: schema.syncPeers.discovered_via_group_id
|
|
1894
1992
|
}).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, input.peerDeviceId)).get();
|
|
1895
1993
|
const existingFingerprint = String(existing?.pinned_fingerprint ?? "").trim();
|
|
1896
1994
|
if (existing && existingFingerprint && existingFingerprint !== nextFingerprint) return {
|
|
@@ -1912,6 +2010,26 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1912
2010
|
groupId,
|
|
1913
2011
|
requestedDeviceId: input.peerDeviceId
|
|
1914
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
|
+
}
|
|
1915
2033
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1916
2034
|
return store.db.transaction(() => {
|
|
1917
2035
|
if (!existing) {
|
|
@@ -1921,15 +2039,20 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1921
2039
|
pinned_fingerprint: nextFingerprint || null,
|
|
1922
2040
|
public_key: nextPublicKey,
|
|
1923
2041
|
addresses_json: addressesJson,
|
|
2042
|
+
projects_include_json: scopeInclude ?? null,
|
|
2043
|
+
projects_exclude_json: scopeExclude ?? null,
|
|
1924
2044
|
created_at: now,
|
|
1925
|
-
last_seen_at: now
|
|
2045
|
+
last_seen_at: now,
|
|
2046
|
+
discovered_via_coordinator_id: coordinatorUrl,
|
|
2047
|
+
discovered_via_group_id: groupId
|
|
1926
2048
|
}).run();
|
|
1927
2049
|
return {
|
|
1928
2050
|
ok: true,
|
|
1929
2051
|
peer_device_id: input.peerDeviceId,
|
|
1930
2052
|
created: true,
|
|
1931
2053
|
updated: false,
|
|
1932
|
-
name: nextName
|
|
2054
|
+
name: nextName,
|
|
2055
|
+
group_id: groupId
|
|
1933
2056
|
};
|
|
1934
2057
|
}
|
|
1935
2058
|
d.update(schema.syncPeers).set({
|
|
@@ -1937,14 +2060,19 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1937
2060
|
pinned_fingerprint: nextFingerprint || existing.pinned_fingerprint || null,
|
|
1938
2061
|
public_key: nextPublicKey || existing.public_key || null,
|
|
1939
2062
|
addresses_json: addressesJson,
|
|
1940
|
-
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
|
+
}
|
|
1941
2068
|
}).where(eq(schema.syncPeers.peer_device_id, input.peerDeviceId)).run();
|
|
1942
2069
|
return {
|
|
1943
2070
|
ok: true,
|
|
1944
2071
|
peer_device_id: input.peerDeviceId,
|
|
1945
2072
|
created: false,
|
|
1946
2073
|
updated: true,
|
|
1947
|
-
name: nextName
|
|
2074
|
+
name: nextName,
|
|
2075
|
+
group_id: groupId
|
|
1948
2076
|
};
|
|
1949
2077
|
})();
|
|
1950
2078
|
}
|
|
@@ -2002,7 +2130,9 @@ function mapPeerRow(store, row, showDiag, recentOpsByPeer) {
|
|
|
2002
2130
|
recent_ops: {
|
|
2003
2131
|
in: recentOps.in,
|
|
2004
2132
|
out: recentOps.out
|
|
2005
|
-
}
|
|
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
|
|
2006
2136
|
};
|
|
2007
2137
|
}
|
|
2008
2138
|
var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
|
|
@@ -2072,23 +2202,6 @@ function readViewerBinding(dbPath) {
|
|
|
2072
2202
|
} catch {}
|
|
2073
2203
|
return null;
|
|
2074
2204
|
}
|
|
2075
|
-
async function portOpen(host, port) {
|
|
2076
|
-
return new Promise((resolve) => {
|
|
2077
|
-
const socket = net.createConnection({
|
|
2078
|
-
host,
|
|
2079
|
-
port
|
|
2080
|
-
});
|
|
2081
|
-
const done = (ok) => {
|
|
2082
|
-
socket.removeAllListeners();
|
|
2083
|
-
socket.destroy();
|
|
2084
|
-
resolve(ok);
|
|
2085
|
-
};
|
|
2086
|
-
socket.setTimeout(300);
|
|
2087
|
-
socket.once("connect", () => done(true));
|
|
2088
|
-
socket.once("timeout", () => done(false));
|
|
2089
|
-
socket.once("error", () => done(false));
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
2205
|
var PEERS_QUERY = `
|
|
2093
2206
|
SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
|
|
2094
2207
|
p.last_seen_at, p.last_sync_at, p.last_error,
|
|
@@ -2339,13 +2452,13 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2339
2452
|
retentionState = void 0;
|
|
2340
2453
|
}
|
|
2341
2454
|
const lastSyncRow = traceSync("lastSyncRow", () => d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get());
|
|
2342
|
-
const semanticIndex = traceSync("semanticIndex", () => redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(store.db
|
|
2455
|
+
const semanticIndex = traceSync("semanticIndex", () => redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(store.db), showDiag));
|
|
2343
2456
|
const lastError = daemonState?.last_error;
|
|
2344
2457
|
const lastErrorAt = daemonState?.last_error_at;
|
|
2345
2458
|
const lastOkAt = daemonState?.last_ok_at;
|
|
2346
2459
|
const viewerBinding = traceSync("readViewerBinding", () => readViewerBinding(store.dbPath));
|
|
2347
|
-
const daemonRunning =
|
|
2348
|
-
const daemonDetail = viewerBinding ?
|
|
2460
|
+
const daemonRunning = Boolean(viewerBinding);
|
|
2461
|
+
const daemonDetail = viewerBinding ? `viewer pidfile at ${viewerBinding.host}:${viewerBinding.port}` : null;
|
|
2349
2462
|
let daemonStateValue = "ok";
|
|
2350
2463
|
if (!config.syncEnabled) daemonStateValue = "disabled";
|
|
2351
2464
|
else if (lastError && (!lastOkAt || String(lastOkAt) < String(lastErrorAt ?? ""))) daemonStateValue = "error";
|
|
@@ -2524,10 +2637,6 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2524
2637
|
app.get("/api/sync/pairing", (c) => {
|
|
2525
2638
|
const store = getStore();
|
|
2526
2639
|
{
|
|
2527
|
-
if (!queryBool(c.req.query("includeDiagnostics"))) return c.json({
|
|
2528
|
-
redacted: true,
|
|
2529
|
-
pairing_filter_hint: PAIRING_FILTER_HINT
|
|
2530
|
-
});
|
|
2531
2640
|
const config = readCoordinatorSyncConfig();
|
|
2532
2641
|
const d = drizzle(store.db, { schema });
|
|
2533
2642
|
const deviceRow = d.select({
|
|
@@ -2882,14 +2991,32 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2882
2991
|
} catch {
|
|
2883
2992
|
return c.json({ error: "invalid json" }, 400);
|
|
2884
2993
|
}
|
|
2885
|
-
const
|
|
2886
|
-
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
|
+
}
|
|
2887
3011
|
try {
|
|
2888
3012
|
const result = await coordinatorImportInviteAction({
|
|
2889
|
-
inviteValue,
|
|
3013
|
+
inviteValue: rawValue,
|
|
2890
3014
|
dbPath: store.dbPath
|
|
2891
3015
|
});
|
|
2892
|
-
return c.json(
|
|
3016
|
+
return c.json({
|
|
3017
|
+
...result,
|
|
3018
|
+
type: "team_join"
|
|
3019
|
+
});
|
|
2893
3020
|
} catch (error) {
|
|
2894
3021
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
2895
3022
|
}
|
|
@@ -3461,6 +3588,202 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3461
3588
|
}, 400);
|
|
3462
3589
|
}
|
|
3463
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
|
+
});
|
|
3464
3787
|
app.post("/api/coordinator/admin/devices/:device_id/remove", async (c) => {
|
|
3465
3788
|
const config = readCoordinatorSyncConfig();
|
|
3466
3789
|
const status = coordinatorAdminStatusPayload(config);
|