@codemem/server 0.32.1 → 0.32.3
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 +91 -38
- 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 +36 -17
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, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, createCoordinatorReciprocalApproval, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
3
|
+
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, canonicalWorkspaceIdentity, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, createCoordinatorReciprocalApproval, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeAddress, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recordNonce, rejectInboundScopeFailures, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, summarizeInboundScopeRejections, syncScopeResetRequiredPayload, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, 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";
|
|
@@ -1729,7 +1729,7 @@ async function ensureDefaultSpaceForTeam(opts) {
|
|
|
1729
1729
|
remoteUrl: opts.config.syncCoordinatorUrl || null,
|
|
1730
1730
|
adminSecret: opts.config.syncCoordinatorAdminSecret || null
|
|
1731
1731
|
});
|
|
1732
|
-
const [deviceId] = ensureDeviceIdentity(opts.store.db);
|
|
1732
|
+
const [deviceId] = ensureDeviceIdentity(opts.store.db, { keysDir: syncKeysDir() });
|
|
1733
1733
|
return {
|
|
1734
1734
|
scope,
|
|
1735
1735
|
membership: await coordinatorGrantScopeMembershipAction({
|
|
@@ -1876,9 +1876,9 @@ function coordinatorAdminMutationStatus(message) {
|
|
|
1876
1876
|
}
|
|
1877
1877
|
function pairingAdvertiseAddresses(config = readCoordinatorSyncConfig()) {
|
|
1878
1878
|
const advertise = String(config.syncAdvertise || "auto").trim().toLowerCase();
|
|
1879
|
-
if (advertise && advertise !== "auto" && advertise !== "default") return mergeAddresses([], String(config.syncAdvertise || "").split(",").map((item) => item.trim()).filter(Boolean));
|
|
1880
|
-
if (config.syncHost && config.syncHost !== "0.0.0.0") return [
|
|
1881
|
-
const addresses = Object.values(networkInterfaces()).flatMap((entries) => entries ?? []).filter((entry) => !entry.internal).map((entry) => entry.address).filter((address) => address && address !== "127.0.0.1" && address !== "::1").map((address) =>
|
|
1879
|
+
if (advertise && advertise !== "auto" && advertise !== "default") return mergeAddresses([], String(config.syncAdvertise || "").split(",").map((item) => item.trim()).filter(Boolean), { defaultHttpPort: config.syncPort });
|
|
1880
|
+
if (config.syncHost && config.syncHost !== "0.0.0.0") return [normalizeAddress(formatHostPort(config.syncHost, config.syncPort))].filter(Boolean);
|
|
1881
|
+
const addresses = Object.values(networkInterfaces()).flatMap((entries) => entries ?? []).filter((entry) => !entry.internal).map((entry) => entry.address).filter((address) => address && address !== "127.0.0.1" && address !== "::1").map((address) => normalizeAddress(formatHostPort(address, config.syncPort))).filter(Boolean);
|
|
1882
1882
|
return [...new Set(addresses)];
|
|
1883
1883
|
}
|
|
1884
1884
|
function resolveCoordinatorAdminGroup(requestedGroup, status) {
|
|
@@ -2313,6 +2313,7 @@ function redactCoordinatorStatus(coordinator, showDiag) {
|
|
|
2313
2313
|
const discovered = Array.isArray(coordinator.discovered_devices) ? coordinator.discovered_devices.map((item) => {
|
|
2314
2314
|
if (!item || typeof item !== "object") return item;
|
|
2315
2315
|
const record = item;
|
|
2316
|
+
const addressCount = Array.isArray(record.addresses) ? record.addresses.length : 0;
|
|
2316
2317
|
return {
|
|
2317
2318
|
device_id: record.device_id,
|
|
2318
2319
|
display_name: record.display_name ?? null,
|
|
@@ -2325,7 +2326,8 @@ function redactCoordinatorStatus(coordinator, showDiag) {
|
|
|
2325
2326
|
incoming_reciprocal_request_id: record.incoming_reciprocal_request_id ?? null,
|
|
2326
2327
|
outgoing_reciprocal_request_id: record.outgoing_reciprocal_request_id ?? null,
|
|
2327
2328
|
fingerprint: null,
|
|
2328
|
-
addresses: []
|
|
2329
|
+
addresses: [],
|
|
2330
|
+
address_count: addressCount
|
|
2329
2331
|
};
|
|
2330
2332
|
}) : [];
|
|
2331
2333
|
return {
|
|
@@ -2341,13 +2343,32 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
2341
2343
|
error: "coordinator_not_configured",
|
|
2342
2344
|
detail: "Coordinator must be configured before accepting discovered peers."
|
|
2343
2345
|
};
|
|
2344
|
-
const
|
|
2346
|
+
const discovered = await lookupCoordinatorPeers(store, config);
|
|
2347
|
+
const inputFingerprint = String(input.fingerprint ?? "").trim();
|
|
2348
|
+
const candidates = discovered.filter((peer) => String(peer.device_id ?? "").trim() === input.peerDeviceId);
|
|
2349
|
+
const matchingCandidates = inputFingerprint ? candidates.filter((peer) => String(peer.fingerprint ?? "").trim() === inputFingerprint) : candidates;
|
|
2350
|
+
const uniqueFingerprints = new Set(matchingCandidates.map((peer) => String(peer.fingerprint ?? "").trim()).filter(Boolean));
|
|
2351
|
+
if (!inputFingerprint && uniqueFingerprints.size > 1) return {
|
|
2352
|
+
ok: false,
|
|
2353
|
+
status: 409,
|
|
2354
|
+
error: "ambiguous_discovered_peer",
|
|
2355
|
+
detail: "That discovered device has multiple coordinator fingerprints. Enable diagnostics, refresh sync status, and choose the intended device before trusting it."
|
|
2356
|
+
};
|
|
2357
|
+
const match = matchingCandidates[0];
|
|
2345
2358
|
if (!match) return {
|
|
2346
2359
|
ok: false,
|
|
2347
2360
|
status: 404,
|
|
2348
2361
|
error: "discovered_peer_not_found",
|
|
2349
2362
|
detail: "That discovered device is no longer available. Refresh sync status and try again."
|
|
2350
2363
|
};
|
|
2364
|
+
const expiresAt = String(match.expires_at ?? "").trim();
|
|
2365
|
+
const expired = expiresAt ? Date.parse(expiresAt) <= Date.now() : false;
|
|
2366
|
+
if (match.stale || expired) return {
|
|
2367
|
+
ok: false,
|
|
2368
|
+
status: 409,
|
|
2369
|
+
error: "discovered_peer_stale",
|
|
2370
|
+
detail: "This discovered device's coordinator presence is stale. Wait for it to come online and refresh sync status before trusting it."
|
|
2371
|
+
};
|
|
2351
2372
|
const nextFingerprint = String(match.fingerprint ?? "").trim();
|
|
2352
2373
|
const nextPublicKey = String(match.public_key ?? "").trim();
|
|
2353
2374
|
const nextName = String(match.display_name ?? "").trim() || null;
|
|
@@ -2358,7 +2379,14 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
2358
2379
|
error: "discovered_peer_missing_public_key",
|
|
2359
2380
|
detail: "This discovered device is missing its coordinator public key. Refresh sync status and try again."
|
|
2360
2381
|
};
|
|
2382
|
+
if (!nextFingerprint || fingerprintPublicKey(nextPublicKey) !== nextFingerprint) return {
|
|
2383
|
+
ok: false,
|
|
2384
|
+
status: 409,
|
|
2385
|
+
error: "discovered_peer_fingerprint_mismatch",
|
|
2386
|
+
detail: "This discovered device's coordinator public key does not match its fingerprint. Refresh sync status or re-enroll the device before trusting it."
|
|
2387
|
+
};
|
|
2361
2388
|
const groupIds = Array.isArray(match.groups) ? match.groups.map((value) => String(value ?? "").trim()).filter(Boolean) : [];
|
|
2389
|
+
const freshGroupIds = Array.isArray(match.fresh_groups) ? match.fresh_groups.map((value) => String(value ?? "").trim()).filter(Boolean) : [];
|
|
2362
2390
|
let groupId;
|
|
2363
2391
|
if (input.expectedGroupId) {
|
|
2364
2392
|
if (!groupIds.includes(input.expectedGroupId)) return {
|
|
@@ -2367,6 +2395,12 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
2367
2395
|
error: "peer_not_in_group",
|
|
2368
2396
|
detail: "This discovered device is not visible through the specified coordinator group. Refresh sync status and try again."
|
|
2369
2397
|
};
|
|
2398
|
+
if (!freshGroupIds.includes(input.expectedGroupId)) return {
|
|
2399
|
+
ok: false,
|
|
2400
|
+
status: 409,
|
|
2401
|
+
error: "discovered_peer_stale",
|
|
2402
|
+
detail: "This discovered device's coordinator presence is stale for the specified group. Wait for it to come online and refresh sync status before trusting it."
|
|
2403
|
+
};
|
|
2370
2404
|
groupId = input.expectedGroupId;
|
|
2371
2405
|
} else {
|
|
2372
2406
|
if (groupIds.length !== 1) return {
|
|
@@ -2400,7 +2434,7 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
2400
2434
|
return [];
|
|
2401
2435
|
}
|
|
2402
2436
|
})();
|
|
2403
|
-
const addressesJson = JSON.stringify(mergeAddresses(
|
|
2437
|
+
const addressesJson = JSON.stringify(mergeAddresses(nextAddresses, existingAddresses));
|
|
2404
2438
|
await createCoordinatorReciprocalApproval(store, config, {
|
|
2405
2439
|
groupId,
|
|
2406
2440
|
requestedDeviceId: input.peerDeviceId
|
|
@@ -2509,12 +2543,14 @@ function mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer
|
|
|
2509
2543
|
out: 0
|
|
2510
2544
|
};
|
|
2511
2545
|
const scopeRejections = scopeRejectionsByPeer?.get(peerId);
|
|
2546
|
+
const addresses = safeJsonList(row.addresses_json);
|
|
2512
2547
|
return {
|
|
2513
2548
|
peer_device_id: row.peer_device_id,
|
|
2514
2549
|
name: row.name,
|
|
2515
2550
|
fingerprint: showDiag ? row.pinned_fingerprint : null,
|
|
2516
2551
|
pinned: Boolean(row.pinned_fingerprint),
|
|
2517
|
-
addresses: showDiag ?
|
|
2552
|
+
addresses: showDiag ? addresses : [],
|
|
2553
|
+
address_count: addresses.length,
|
|
2518
2554
|
last_seen_at: row.last_seen_at,
|
|
2519
2555
|
last_sync_at: row.last_sync_at,
|
|
2520
2556
|
last_error: showDiag ? row.last_error : null,
|
|
@@ -2538,6 +2574,18 @@ function mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer
|
|
|
2538
2574
|
discovered_via_group_id: typeof row.discovered_via_group_id === "string" ? row.discovered_via_group_id : null
|
|
2539
2575
|
};
|
|
2540
2576
|
}
|
|
2577
|
+
function redactSyncAttemptError(error) {
|
|
2578
|
+
if (!String(error ?? "").trim()) return null;
|
|
2579
|
+
return "sync attempt failed; enable diagnostics for details";
|
|
2580
|
+
}
|
|
2581
|
+
function mapSyncAttemptRow(row, showDiag, addresses) {
|
|
2582
|
+
return {
|
|
2583
|
+
...row,
|
|
2584
|
+
error: showDiag ? row.error : redactSyncAttemptError(row.error),
|
|
2585
|
+
status: attemptStatus(row),
|
|
2586
|
+
address: showDiag && addresses?.length ? addresses[0] : null
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2541
2589
|
var SYNC_SCOPE_REJECTION_WINDOW_SECONDS = 1440 * 60;
|
|
2542
2590
|
function recentScopeRejectionsByPeer(store) {
|
|
2543
2591
|
const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_SCOPE_REJECTION_WINDOW_SECONDS * 1e3)).toISOString();
|
|
@@ -2895,9 +2943,12 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2895
2943
|
}
|
|
2896
2944
|
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2897
2945
|
if (!auth.ok) {
|
|
2898
|
-
if (bootstrapAttempted)
|
|
2946
|
+
if (bootstrapAttempted) {
|
|
2947
|
+
const grantPresent = Boolean((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim());
|
|
2948
|
+
console.warn(`[sync] bootstrap grant auth failed: reason=${auth.reason} grant_present=${grantPresent} path=${c.req.path}`);
|
|
2949
|
+
}
|
|
2899
2950
|
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
2900
|
-
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason), 401);
|
|
2951
|
+
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason, true), 401);
|
|
2901
2952
|
}
|
|
2902
2953
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2903
2954
|
if (limited) return limited;
|
|
@@ -2920,7 +2971,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2920
2971
|
const store = getStore();
|
|
2921
2972
|
const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2922
2973
|
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2923
|
-
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
|
|
2974
|
+
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason, true), 401);
|
|
2924
2975
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2925
2976
|
if (limited) return limited;
|
|
2926
2977
|
const peerDeviceId = auth.deviceId;
|
|
@@ -2990,9 +3041,12 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2990
3041
|
}
|
|
2991
3042
|
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2992
3043
|
if (!auth.ok) {
|
|
2993
|
-
if (bootstrapAttempted)
|
|
3044
|
+
if (bootstrapAttempted) {
|
|
3045
|
+
const grantPresent = Boolean((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim());
|
|
3046
|
+
console.warn(`[sync] bootstrap grant auth failed: reason=${auth.reason} grant_present=${grantPresent} path=${c.req.path}`);
|
|
3047
|
+
}
|
|
2994
3048
|
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
2995
|
-
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason), 401);
|
|
3049
|
+
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason, true), 401);
|
|
2996
3050
|
}
|
|
2997
3051
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2998
3052
|
if (limited) return limited;
|
|
@@ -3041,7 +3095,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
3041
3095
|
if (raw == null) return c.json({ error: "payload_too_large" }, 413);
|
|
3042
3096
|
const auth = authorizeSyncRequest(store, c.req, raw);
|
|
3043
3097
|
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
3044
|
-
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
|
|
3098
|
+
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason, true), 401);
|
|
3045
3099
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
3046
3100
|
if (limited) return limited;
|
|
3047
3101
|
const peerDeviceId = auth.deviceId;
|
|
@@ -3215,14 +3269,9 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3215
3269
|
if (addrs.length) peerAddressMap.set(String(peerRow.peer_device_id ?? ""), addrs);
|
|
3216
3270
|
});
|
|
3217
3271
|
const attemptsItems = attemptRows.map((row) => {
|
|
3218
|
-
|
|
3219
|
-
return {
|
|
3220
|
-
...row,
|
|
3221
|
-
status: attemptStatus(row),
|
|
3222
|
-
address: addrs?.length ? addrs[0] : null
|
|
3223
|
-
};
|
|
3272
|
+
return mapSyncAttemptRow(row, showDiag, showDiag ? peerAddressMap.get(String(row.peer_device_id ?? "")) : void 0);
|
|
3224
3273
|
});
|
|
3225
|
-
const latestAttemptError = String(
|
|
3274
|
+
const latestAttemptError = String(attemptRows[0]?.error || "").trim();
|
|
3226
3275
|
const statusBlock = {
|
|
3227
3276
|
...statusPayload,
|
|
3228
3277
|
background_maintenance: summarizeMaintenanceJobs(listMaintenanceJobs(store.db), showDiag),
|
|
@@ -3348,7 +3397,12 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3348
3397
|
peer_sync_capability: schema.syncAttempts.peer_sync_capability,
|
|
3349
3398
|
negotiated_sync_capability: schema.syncAttempts.negotiated_sync_capability
|
|
3350
3399
|
}).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(limit).all();
|
|
3351
|
-
|
|
3400
|
+
const showDiag = queryBool(c.req.query("includeDiagnostics"));
|
|
3401
|
+
const items = rows.map((row) => mapSyncAttemptRow(row, showDiag));
|
|
3402
|
+
return c.json({
|
|
3403
|
+
items,
|
|
3404
|
+
redacted: !showDiag
|
|
3405
|
+
});
|
|
3352
3406
|
}
|
|
3353
3407
|
});
|
|
3354
3408
|
app.get("/api/sync/pairing", (c) => {
|
|
@@ -3356,19 +3410,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3356
3410
|
{
|
|
3357
3411
|
const config = readCoordinatorSyncConfig();
|
|
3358
3412
|
const d = drizzle(store.db, { schema });
|
|
3359
|
-
const deviceRow = d.select({
|
|
3360
|
-
device_id: schema.syncDevice.device_id,
|
|
3361
|
-
public_key: schema.syncDevice.public_key,
|
|
3362
|
-
fingerprint: schema.syncDevice.fingerprint
|
|
3363
|
-
}).from(schema.syncDevice).limit(1).get();
|
|
3364
3413
|
let deviceId;
|
|
3365
3414
|
let publicKey;
|
|
3366
3415
|
let fingerprint;
|
|
3367
|
-
|
|
3368
|
-
deviceId = String(deviceRow.device_id);
|
|
3369
|
-
publicKey = String(deviceRow.public_key);
|
|
3370
|
-
fingerprint = String(deviceRow.fingerprint);
|
|
3371
|
-
} else try {
|
|
3416
|
+
try {
|
|
3372
3417
|
const [id, fp] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
3373
3418
|
deviceId = id;
|
|
3374
3419
|
fingerprint = fp;
|
|
@@ -3829,11 +3874,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3829
3874
|
const peerDeviceId = String(body.peer_device_id ?? "").trim();
|
|
3830
3875
|
const fingerprint = String(body.fingerprint ?? "").trim();
|
|
3831
3876
|
if (!peerDeviceId) return c.json({ error: "peer_device_id required" }, 400);
|
|
3832
|
-
if (!fingerprint) return c.json({ error: "fingerprint required" }, 400);
|
|
3833
3877
|
try {
|
|
3834
3878
|
const result = await acceptDiscoveredPeer(store, {
|
|
3835
3879
|
peerDeviceId,
|
|
3836
|
-
fingerprint
|
|
3880
|
+
fingerprint: fingerprint || void 0
|
|
3837
3881
|
});
|
|
3838
3882
|
if (!result.ok) return c.json({
|
|
3839
3883
|
error: result.error,
|
|
@@ -3898,9 +3942,16 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
3898
3942
|
const pairingResult = tryParsePairingPayload(rawValue.match(/echo\s+['"]([A-Za-z0-9+/=]+)['"]\s*\|\s*base64\s+-d\s*\|\s*codemem/)?.[1]?.trim() || rawValue);
|
|
3899
3943
|
if (pairingResult?.kind === "invalid-pair") return c.json({ error: pairingResult.error }, 400);
|
|
3900
3944
|
if (pairingResult?.kind === "pair") try {
|
|
3945
|
+
const existing = drizzle(store.db, { schema }).select({ pinned_fingerprint: schema.syncPeers.pinned_fingerprint }).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, pairingResult.device_id)).get();
|
|
3946
|
+
const existingFingerprint = String(existing?.pinned_fingerprint ?? "").trim();
|
|
3947
|
+
if (existingFingerprint && existingFingerprint !== pairingResult.fingerprint) return c.json({
|
|
3948
|
+
error: "peer_conflict",
|
|
3949
|
+
detail: "Pairing payload conflicts with the existing trusted fingerprint for this device. Remove or repair the old peer before accepting this pairing payload."
|
|
3950
|
+
}, 409);
|
|
3901
3951
|
updatePeerAddresses(store.db, pairingResult.device_id, pairingResult.addresses, {
|
|
3902
3952
|
pinnedFingerprint: pairingResult.fingerprint,
|
|
3903
|
-
publicKey: pairingResult.public_key
|
|
3953
|
+
publicKey: pairingResult.public_key,
|
|
3954
|
+
replaceTrust: true
|
|
3904
3955
|
});
|
|
3905
3956
|
return c.json({
|
|
3906
3957
|
ok: true,
|
|
@@ -4866,9 +4917,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
4866
4917
|
const peerDeviceId = String(body.peer_device_id ?? "").trim();
|
|
4867
4918
|
const publicKey = String(body.peer_public_key ?? "").trim();
|
|
4868
4919
|
const name = String(body.name ?? "").trim() || null;
|
|
4869
|
-
const
|
|
4870
|
-
const addresses = (Array.isArray(body.peer_addresses) ? body.peer_addresses : []).map((item) => String(item ?? "").trim()).filter((item) => item.length > 0);
|
|
4920
|
+
const requestedFingerprint = String(body.fingerprint ?? "").trim();
|
|
4921
|
+
const addresses = mergeAddresses([], (Array.isArray(body.peer_addresses) ? body.peer_addresses : []).map((item) => String(item ?? "").trim()).filter((item) => item.length > 0));
|
|
4871
4922
|
if (!peerDeviceId || !publicKey) return c.json({ error: "peer_device_id_and_public_key_required" }, 400);
|
|
4923
|
+
const fingerprint = requestedFingerprint || fingerprintPublicKey(publicKey);
|
|
4924
|
+
if (fingerprintPublicKey(publicKey) !== fingerprint) return c.json({ error: "fingerprint_mismatch" }, 400);
|
|
4872
4925
|
const manualInclude = overrideInclude && overrideInclude.length > 0 ? JSON.stringify(overrideInclude) : null;
|
|
4873
4926
|
const manualExclude = overrideExclude && overrideExclude.length > 0 ? JSON.stringify(overrideExclude) : null;
|
|
4874
4927
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|