@codemem/server 0.21.2 → 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 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();
@@ -1733,7 +1828,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
1733
1828
  };
1734
1829
  const legacyDevices = store.claimableLegacyDeviceIds();
1735
1830
  const sharingReview = store.sharingReviewSummary(project);
1736
- const coordinator = await coordinatorStatusSnapshot(store, config);
1831
+ const coordinator = redactCoordinatorStatus(await coordinatorStatusSnapshot(store, config), showDiag);
1737
1832
  let joinRequests = [];
1738
1833
  if (includeJoinRequests && config.syncCoordinatorAdminSecret) try {
1739
1834
  joinRequests = await listCoordinatorJoinRequests(config);
@@ -1866,6 +1961,42 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
1866
1961
  return c.json({ ok: true });
1867
1962
  }
1868
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
+ });
1869
2000
  app.post("/api/sync/invites/create", async (c) => {
1870
2001
  let body;
1871
2002
  try {
@@ -1954,6 +2085,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
1954
2085
  const peerDeviceId = c.req.param("peer_device_id")?.trim();
1955
2086
  if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
1956
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();
1957
2089
  d.delete(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).run();
1958
2090
  return c.json({ ok: true });
1959
2091
  }