@codemem/server 0.21.1 → 0.22.0-alpha.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 +165 -3
- package/dist/index.js.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 +223 -5
- package/static/index.html +5 -0
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, applyReplicationOps, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorReviewJoinRequestAction, coordinatorStatusSnapshot, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSyncResetState, listCoordinatorJoinRequests, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, resolveDbPath, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
3
|
+
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, applyReplicationOps, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorReviewJoinRequestAction, coordinatorStatusSnapshot, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSyncResetState, listCoordinatorJoinRequests, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, resolveDbPath, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, 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";
|
|
@@ -1084,6 +1084,14 @@ function rawEventsRoutes(getStore, sweeper) {
|
|
|
1084
1084
|
*/
|
|
1085
1085
|
function statsRoutes(getStore) {
|
|
1086
1086
|
const app = new Hono();
|
|
1087
|
+
const parseMetadataJson = (value) => {
|
|
1088
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
1089
|
+
try {
|
|
1090
|
+
return JSON.parse(value);
|
|
1091
|
+
} catch {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1087
1095
|
app.get("/api/stats", (c) => {
|
|
1088
1096
|
const store = getStore();
|
|
1089
1097
|
return c.json({
|
|
@@ -1095,6 +1103,18 @@ function statsRoutes(getStore) {
|
|
|
1095
1103
|
const store = getStore();
|
|
1096
1104
|
{
|
|
1097
1105
|
const projectFilter = c.req.query("project") || null;
|
|
1106
|
+
const recentPacksQuery = projectFilter ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
|
|
1107
|
+
usage_events.tokens_read, usage_events.tokens_written, usage_events.tokens_saved,
|
|
1108
|
+
usage_events.created_at, usage_events.metadata_json
|
|
1109
|
+
FROM usage_events
|
|
1110
|
+
JOIN sessions ON sessions.id = usage_events.session_id
|
|
1111
|
+
WHERE usage_events.event = 'pack' AND sessions.project = ?
|
|
1112
|
+
ORDER BY usage_events.created_at DESC
|
|
1113
|
+
LIMIT 10`) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved, created_at, metadata_json
|
|
1114
|
+
FROM usage_events
|
|
1115
|
+
WHERE event = 'pack'
|
|
1116
|
+
ORDER BY created_at DESC
|
|
1117
|
+
LIMIT 10`);
|
|
1098
1118
|
const eventsGlobal = store.db.prepare(`SELECT event,
|
|
1099
1119
|
SUM(tokens_read) AS total_tokens_read,
|
|
1100
1120
|
SUM(tokens_written) AS total_tokens_written,
|
|
@@ -1126,6 +1146,16 @@ function statsRoutes(getStore) {
|
|
|
1126
1146
|
JOIN sessions ON sessions.id = usage_events.session_id
|
|
1127
1147
|
WHERE sessions.project = ?`).get(projectFilter);
|
|
1128
1148
|
}
|
|
1149
|
+
const recentPacks = (projectFilter ? recentPacksQuery.all(projectFilter) : recentPacksQuery.all()).map((row) => ({
|
|
1150
|
+
id: Number(row.id ?? 0),
|
|
1151
|
+
session_id: row.session_id == null ? null : Number(row.session_id),
|
|
1152
|
+
event: String(row.event ?? "pack"),
|
|
1153
|
+
tokens_read: Number(row.tokens_read ?? 0),
|
|
1154
|
+
tokens_written: Number(row.tokens_written ?? 0),
|
|
1155
|
+
tokens_saved: Number(row.tokens_saved ?? 0),
|
|
1156
|
+
created_at: String(row.created_at ?? ""),
|
|
1157
|
+
metadata_json: parseMetadataJson(row.metadata_json)
|
|
1158
|
+
}));
|
|
1129
1159
|
return c.json({
|
|
1130
1160
|
project: projectFilter,
|
|
1131
1161
|
events: projectFilter ? eventsFiltered : eventsGlobal,
|
|
@@ -1134,7 +1164,7 @@ function statsRoutes(getStore) {
|
|
|
1134
1164
|
totals_global: totalsGlobal,
|
|
1135
1165
|
events_filtered: eventsFiltered,
|
|
1136
1166
|
totals_filtered: totalsFiltered,
|
|
1137
|
-
recent_packs:
|
|
1167
|
+
recent_packs: recentPacks
|
|
1138
1168
|
});
|
|
1139
1169
|
}
|
|
1140
1170
|
});
|
|
@@ -1279,6 +1309,101 @@ function parseOpPayload(op) {
|
|
|
1279
1309
|
return null;
|
|
1280
1310
|
}
|
|
1281
1311
|
}
|
|
1312
|
+
function redactCoordinatorStatus(coordinator, showDiag) {
|
|
1313
|
+
if (showDiag) return coordinator;
|
|
1314
|
+
const discovered = Array.isArray(coordinator.discovered_devices) ? coordinator.discovered_devices.map((item) => {
|
|
1315
|
+
if (!item || typeof item !== "object") return item;
|
|
1316
|
+
const record = item;
|
|
1317
|
+
return {
|
|
1318
|
+
device_id: record.device_id,
|
|
1319
|
+
display_name: record.display_name ?? null,
|
|
1320
|
+
groups: Array.isArray(record.groups) ? record.groups : [],
|
|
1321
|
+
last_seen_at: record.last_seen_at ?? null,
|
|
1322
|
+
expires_at: record.expires_at ?? null,
|
|
1323
|
+
stale: Boolean(record.stale),
|
|
1324
|
+
fingerprint: null,
|
|
1325
|
+
addresses: []
|
|
1326
|
+
};
|
|
1327
|
+
}) : [];
|
|
1328
|
+
return {
|
|
1329
|
+
...coordinator,
|
|
1330
|
+
discovered_devices: discovered
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
async function acceptDiscoveredPeer(store, input) {
|
|
1334
|
+
const config = readCoordinatorSyncConfig();
|
|
1335
|
+
if (!config.syncEnabled || !config.syncCoordinatorUrl || config.syncCoordinatorGroups.length === 0) return {
|
|
1336
|
+
ok: false,
|
|
1337
|
+
status: 400,
|
|
1338
|
+
error: "coordinator_not_configured",
|
|
1339
|
+
detail: "Coordinator must be configured before accepting discovered peers."
|
|
1340
|
+
};
|
|
1341
|
+
const match = (await lookupCoordinatorPeers(store, config)).find((peer) => String(peer.device_id ?? "").trim() === input.peerDeviceId && String(peer.fingerprint ?? "").trim() === input.fingerprint);
|
|
1342
|
+
if (!match) return {
|
|
1343
|
+
ok: false,
|
|
1344
|
+
status: 404,
|
|
1345
|
+
error: "discovered_peer_not_found",
|
|
1346
|
+
detail: "That discovered device is no longer available. Refresh sync status and try again."
|
|
1347
|
+
};
|
|
1348
|
+
const nextFingerprint = String(match.fingerprint ?? "").trim();
|
|
1349
|
+
const nextName = String(match.display_name ?? "").trim() || null;
|
|
1350
|
+
const nextAddresses = Array.isArray(match.addresses) ? match.addresses.filter((value) => typeof value === "string") : [];
|
|
1351
|
+
const d = drizzle(store.db, { schema });
|
|
1352
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1353
|
+
return store.db.transaction(() => {
|
|
1354
|
+
const existing = d.select({
|
|
1355
|
+
peer_device_id: schema.syncPeers.peer_device_id,
|
|
1356
|
+
pinned_fingerprint: schema.syncPeers.pinned_fingerprint,
|
|
1357
|
+
addresses_json: schema.syncPeers.addresses_json
|
|
1358
|
+
}).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, input.peerDeviceId)).get();
|
|
1359
|
+
const existingFingerprint = String(existing?.pinned_fingerprint ?? "").trim();
|
|
1360
|
+
if (existing && existingFingerprint && existingFingerprint !== nextFingerprint) return {
|
|
1361
|
+
ok: false,
|
|
1362
|
+
status: 409,
|
|
1363
|
+
error: "peer_conflict",
|
|
1364
|
+
detail: "An existing peer with this device id is pinned to a different fingerprint. Remove or repair the old peer before accepting this discovered device."
|
|
1365
|
+
};
|
|
1366
|
+
const existingAddresses = (() => {
|
|
1367
|
+
try {
|
|
1368
|
+
const raw = JSON.parse(String(existing?.addresses_json ?? "[]"));
|
|
1369
|
+
return Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : [];
|
|
1370
|
+
} catch {
|
|
1371
|
+
return [];
|
|
1372
|
+
}
|
|
1373
|
+
})();
|
|
1374
|
+
const addressesJson = JSON.stringify(mergeAddresses(existingAddresses, nextAddresses));
|
|
1375
|
+
if (!existing) {
|
|
1376
|
+
d.insert(schema.syncPeers).values({
|
|
1377
|
+
peer_device_id: input.peerDeviceId,
|
|
1378
|
+
name: nextName,
|
|
1379
|
+
pinned_fingerprint: nextFingerprint || null,
|
|
1380
|
+
addresses_json: addressesJson,
|
|
1381
|
+
created_at: now,
|
|
1382
|
+
last_seen_at: now
|
|
1383
|
+
}).run();
|
|
1384
|
+
return {
|
|
1385
|
+
ok: true,
|
|
1386
|
+
peer_device_id: input.peerDeviceId,
|
|
1387
|
+
created: true,
|
|
1388
|
+
updated: false,
|
|
1389
|
+
name: nextName
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
d.update(schema.syncPeers).set({
|
|
1393
|
+
name: nextName,
|
|
1394
|
+
pinned_fingerprint: nextFingerprint || existing.pinned_fingerprint || null,
|
|
1395
|
+
addresses_json: addressesJson,
|
|
1396
|
+
last_seen_at: now
|
|
1397
|
+
}).where(eq(schema.syncPeers.peer_device_id, input.peerDeviceId)).run();
|
|
1398
|
+
return {
|
|
1399
|
+
ok: true,
|
|
1400
|
+
peer_device_id: input.peerDeviceId,
|
|
1401
|
+
created: false,
|
|
1402
|
+
updated: true,
|
|
1403
|
+
name: nextName
|
|
1404
|
+
};
|
|
1405
|
+
})();
|
|
1406
|
+
}
|
|
1282
1407
|
function isSharedVisibility(payload) {
|
|
1283
1408
|
if (!payload) return false;
|
|
1284
1409
|
let visibility = String(payload.visibility ?? "").trim().toLowerCase();
|
|
@@ -1703,7 +1828,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
1703
1828
|
};
|
|
1704
1829
|
const legacyDevices = store.claimableLegacyDeviceIds();
|
|
1705
1830
|
const sharingReview = store.sharingReviewSummary(project);
|
|
1706
|
-
const coordinator = await coordinatorStatusSnapshot(store, config);
|
|
1831
|
+
const coordinator = redactCoordinatorStatus(await coordinatorStatusSnapshot(store, config), showDiag);
|
|
1707
1832
|
let joinRequests = [];
|
|
1708
1833
|
if (includeJoinRequests && config.syncCoordinatorAdminSecret) try {
|
|
1709
1834
|
joinRequests = await listCoordinatorJoinRequests(config);
|
|
@@ -1836,6 +1961,42 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
1836
1961
|
return c.json({ ok: true });
|
|
1837
1962
|
}
|
|
1838
1963
|
});
|
|
1964
|
+
app.post("/api/sync/peers/accept-discovered", async (c) => {
|
|
1965
|
+
const store = getStore();
|
|
1966
|
+
let body;
|
|
1967
|
+
try {
|
|
1968
|
+
body = await c.req.json();
|
|
1969
|
+
} catch {
|
|
1970
|
+
return c.json({ error: "invalid json" }, 400);
|
|
1971
|
+
}
|
|
1972
|
+
const peerDeviceId = String(body.peer_device_id ?? "").trim();
|
|
1973
|
+
const fingerprint = String(body.fingerprint ?? "").trim();
|
|
1974
|
+
if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
|
|
1975
|
+
if (!fingerprint) return c.json({ error: "fingerprint required" }, 400);
|
|
1976
|
+
try {
|
|
1977
|
+
const result = await acceptDiscoveredPeer(store, {
|
|
1978
|
+
peerDeviceId,
|
|
1979
|
+
fingerprint
|
|
1980
|
+
});
|
|
1981
|
+
if (!result.ok) return c.json({
|
|
1982
|
+
error: result.error,
|
|
1983
|
+
detail: result.detail
|
|
1984
|
+
}, { status: result.status });
|
|
1985
|
+
return c.json({
|
|
1986
|
+
ok: true,
|
|
1987
|
+
peer_device_id: result.peer_device_id,
|
|
1988
|
+
created: result.created,
|
|
1989
|
+
updated: result.updated,
|
|
1990
|
+
name: result.name,
|
|
1991
|
+
needs_scope_review: true
|
|
1992
|
+
});
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
return c.json({
|
|
1995
|
+
error: "coordinator_lookup_failed",
|
|
1996
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1997
|
+
}, { status: 502 });
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
1839
2000
|
app.post("/api/sync/invites/create", async (c) => {
|
|
1840
2001
|
let body;
|
|
1841
2002
|
try {
|
|
@@ -1924,6 +2085,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
1924
2085
|
const peerDeviceId = c.req.param("peer_device_id")?.trim();
|
|
1925
2086
|
if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
|
|
1926
2087
|
if (!d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get()) return c.json({ error: "peer not found" }, 404);
|
|
2088
|
+
d.delete(schema.replicationCursors).where(eq(schema.replicationCursors.peer_device_id, peerDeviceId)).run();
|
|
1927
2089
|
d.delete(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();
|
|
1928
2090
|
return c.json({ ok: true });
|
|
1929
2091
|
}
|