@codemem/server 0.21.2 → 0.22.0-alpha.2
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 +171 -45
- package/dist/index.js.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";
|
|
@@ -1309,6 +1309,101 @@ function parseOpPayload(op) {
|
|
|
1309
1309
|
return null;
|
|
1310
1310
|
}
|
|
1311
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
|
+
}
|
|
1312
1407
|
function isSharedVisibility(payload) {
|
|
1313
1408
|
if (!payload) return false;
|
|
1314
1409
|
let visibility = String(payload.visibility ?? "").trim().toLowerCase();
|
|
@@ -1539,6 +1634,43 @@ function syncProtocolRoutes(getStore) {
|
|
|
1539
1634
|
return c.json({ error: "internal_error" }, 500);
|
|
1540
1635
|
}
|
|
1541
1636
|
});
|
|
1637
|
+
app.get("/v1/snapshot", (c) => {
|
|
1638
|
+
const store = getStore();
|
|
1639
|
+
const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
1640
|
+
if (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);
|
|
1641
|
+
try {
|
|
1642
|
+
const rawGeneration = c.req.query("generation");
|
|
1643
|
+
const generation = rawGeneration != null && rawGeneration.trim().length > 0 ? Number.parseInt(rawGeneration, 10) : null;
|
|
1644
|
+
const snapshotId = c.req.query("snapshot_id") ?? null;
|
|
1645
|
+
const baselineCursor = c.req.query("baseline_cursor") ?? null;
|
|
1646
|
+
const pageToken = c.req.query("page_token") ?? null;
|
|
1647
|
+
const rawLimit = Number.parseInt(c.req.query("limit") ?? "200", 10);
|
|
1648
|
+
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 1e3)) : 200;
|
|
1649
|
+
if (generation == null || !Number.isFinite(generation)) return c.json({ error: "missing_generation" }, 400);
|
|
1650
|
+
if (!snapshotId) return c.json({ error: "missing_snapshot_id" }, 400);
|
|
1651
|
+
const result = loadMemorySnapshotPageForPeer(store.db, {
|
|
1652
|
+
generation,
|
|
1653
|
+
snapshotId,
|
|
1654
|
+
baselineCursor,
|
|
1655
|
+
pageToken,
|
|
1656
|
+
limit,
|
|
1657
|
+
peerDeviceId: auth.deviceId
|
|
1658
|
+
});
|
|
1659
|
+
return c.json({
|
|
1660
|
+
generation: result.boundary.generation,
|
|
1661
|
+
snapshot_id: result.boundary.snapshot_id,
|
|
1662
|
+
baseline_cursor: result.boundary.baseline_cursor,
|
|
1663
|
+
retained_floor_cursor: result.boundary.retained_floor_cursor,
|
|
1664
|
+
items: result.items,
|
|
1665
|
+
next_page_token: result.nextPageToken,
|
|
1666
|
+
has_more: result.hasMore
|
|
1667
|
+
});
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
const message = err instanceof Error ? err.message : "";
|
|
1670
|
+
if (message === "generation_mismatch" || message === "boundary_mismatch") return c.json({ error: message }, 409);
|
|
1671
|
+
return c.json({ error: "internal_error" }, 500);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1542
1674
|
app.post("/v1/ops", async (c) => {
|
|
1543
1675
|
const store = getStore();
|
|
1544
1676
|
const raw = Buffer.from(await c.req.arrayBuffer());
|
|
@@ -1574,49 +1706,6 @@ function syncProtocolRoutes(getStore) {
|
|
|
1574
1706
|
skipped: result.skipped + filteredInbound.skipped
|
|
1575
1707
|
});
|
|
1576
1708
|
});
|
|
1577
|
-
app.get("/v1/bootstrap/memories", (c) => {
|
|
1578
|
-
const store = getStore();
|
|
1579
|
-
const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
1580
|
-
if (!auth.ok) return c.json(unauthorizedPayload(auth.reason), 401);
|
|
1581
|
-
const peerDeviceId = auth.deviceId;
|
|
1582
|
-
try {
|
|
1583
|
-
const rawLimit = Number.parseInt(c.req.query("limit") ?? "200", 10);
|
|
1584
|
-
const rawGeneration = c.req.query("generation");
|
|
1585
|
-
const generation = rawGeneration != null && rawGeneration.trim().length > 0 ? Number.parseInt(rawGeneration, 10) : null;
|
|
1586
|
-
const snapshotId = c.req.query("snapshot_id") ?? null;
|
|
1587
|
-
const baselineCursor = c.req.query("baseline_cursor") ?? null;
|
|
1588
|
-
const pageToken = c.req.query("page_token") ?? null;
|
|
1589
|
-
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 1e3)) : 200;
|
|
1590
|
-
const page = loadMemorySnapshotPageForPeer(store.db, {
|
|
1591
|
-
limit,
|
|
1592
|
-
pageToken,
|
|
1593
|
-
peerDeviceId,
|
|
1594
|
-
generation: Number.isFinite(generation) ? generation : null,
|
|
1595
|
-
snapshotId,
|
|
1596
|
-
baselineCursor
|
|
1597
|
-
});
|
|
1598
|
-
return c.json({
|
|
1599
|
-
generation: page.boundary.generation,
|
|
1600
|
-
snapshot_id: page.boundary.snapshot_id,
|
|
1601
|
-
baseline_cursor: page.boundary.baseline_cursor,
|
|
1602
|
-
retained_floor_cursor: page.boundary.retained_floor_cursor,
|
|
1603
|
-
items: page.items,
|
|
1604
|
-
next_page_token: page.nextPageToken,
|
|
1605
|
-
has_more: page.hasMore
|
|
1606
|
-
});
|
|
1607
|
-
} catch (err) {
|
|
1608
|
-
if (err instanceof Error && (err.message === "generation_mismatch" || err.message === "boundary_mismatch")) {
|
|
1609
|
-
const boundary = getSyncResetState(store.db);
|
|
1610
|
-
return c.json({
|
|
1611
|
-
error: "reset_required",
|
|
1612
|
-
reset_required: true,
|
|
1613
|
-
reason: err.message,
|
|
1614
|
-
...boundary
|
|
1615
|
-
}, 409);
|
|
1616
|
-
}
|
|
1617
|
-
return c.json({ error: "internal_error" }, 500);
|
|
1618
|
-
}
|
|
1619
|
-
});
|
|
1620
1709
|
return app;
|
|
1621
1710
|
}
|
|
1622
1711
|
/**
|
|
@@ -1733,7 +1822,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
1733
1822
|
};
|
|
1734
1823
|
const legacyDevices = store.claimableLegacyDeviceIds();
|
|
1735
1824
|
const sharingReview = store.sharingReviewSummary(project);
|
|
1736
|
-
const coordinator = await coordinatorStatusSnapshot(store, config);
|
|
1825
|
+
const coordinator = redactCoordinatorStatus(await coordinatorStatusSnapshot(store, config), showDiag);
|
|
1737
1826
|
let joinRequests = [];
|
|
1738
1827
|
if (includeJoinRequests && config.syncCoordinatorAdminSecret) try {
|
|
1739
1828
|
joinRequests = await listCoordinatorJoinRequests(config);
|
|
@@ -1866,6 +1955,42 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
1866
1955
|
return c.json({ ok: true });
|
|
1867
1956
|
}
|
|
1868
1957
|
});
|
|
1958
|
+
app.post("/api/sync/peers/accept-discovered", async (c) => {
|
|
1959
|
+
const store = getStore();
|
|
1960
|
+
let body;
|
|
1961
|
+
try {
|
|
1962
|
+
body = await c.req.json();
|
|
1963
|
+
} catch {
|
|
1964
|
+
return c.json({ error: "invalid json" }, 400);
|
|
1965
|
+
}
|
|
1966
|
+
const peerDeviceId = String(body.peer_device_id ?? "").trim();
|
|
1967
|
+
const fingerprint = String(body.fingerprint ?? "").trim();
|
|
1968
|
+
if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
|
|
1969
|
+
if (!fingerprint) return c.json({ error: "fingerprint required" }, 400);
|
|
1970
|
+
try {
|
|
1971
|
+
const result = await acceptDiscoveredPeer(store, {
|
|
1972
|
+
peerDeviceId,
|
|
1973
|
+
fingerprint
|
|
1974
|
+
});
|
|
1975
|
+
if (!result.ok) return c.json({
|
|
1976
|
+
error: result.error,
|
|
1977
|
+
detail: result.detail
|
|
1978
|
+
}, { status: result.status });
|
|
1979
|
+
return c.json({
|
|
1980
|
+
ok: true,
|
|
1981
|
+
peer_device_id: result.peer_device_id,
|
|
1982
|
+
created: result.created,
|
|
1983
|
+
updated: result.updated,
|
|
1984
|
+
name: result.name,
|
|
1985
|
+
needs_scope_review: true
|
|
1986
|
+
});
|
|
1987
|
+
} catch (error) {
|
|
1988
|
+
return c.json({
|
|
1989
|
+
error: "coordinator_lookup_failed",
|
|
1990
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1991
|
+
}, { status: 502 });
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1869
1994
|
app.post("/api/sync/invites/create", async (c) => {
|
|
1870
1995
|
let body;
|
|
1871
1996
|
try {
|
|
@@ -1954,6 +2079,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
1954
2079
|
const peerDeviceId = c.req.param("peer_device_id")?.trim();
|
|
1955
2080
|
if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
|
|
1956
2081
|
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);
|
|
2082
|
+
d.delete(schema.replicationCursors).where(eq(schema.replicationCursors.peer_device_id, peerDeviceId)).run();
|
|
1957
2083
|
d.delete(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();
|
|
1958
2084
|
return c.json({ ok: true });
|
|
1959
2085
|
}
|