@codemem/server 0.30.0-alpha.3 → 0.30.0-alpha.4
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 +137 -34
- 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 +90 -9
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, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, 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, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, 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, 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, deleteProjectScopeSettingsMapping, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, listProjectScopeCandidates, listProjectScopeSettingsMappings, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, negotiateSyncCapability, normalizeSyncCapability, parseStrictInteger, parseSyncScopeRequest, personalScopeGrantStatusForPeer, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, 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";
|
|
@@ -1413,31 +1413,80 @@ function toUsageRow(row, metadata) {
|
|
|
1413
1413
|
metadata_json: metadata
|
|
1414
1414
|
};
|
|
1415
1415
|
}
|
|
1416
|
-
function
|
|
1416
|
+
function metadataMemoryIds(value) {
|
|
1417
1417
|
if (!Array.isArray(value)) return [];
|
|
1418
|
-
return value.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item) &&
|
|
1418
|
+
return value.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item) && item > 0);
|
|
1419
1419
|
}
|
|
1420
|
-
function
|
|
1421
|
-
const
|
|
1420
|
+
function chunkNumbers(values, chunkSize = 500) {
|
|
1421
|
+
const chunks = [];
|
|
1422
|
+
for (let start = 0; start < values.length; start += chunkSize) chunks.push(values.slice(start, start + chunkSize));
|
|
1423
|
+
return chunks;
|
|
1424
|
+
}
|
|
1425
|
+
function visibleMemoryIdSet(store, memoryIds) {
|
|
1426
|
+
if (memoryIds.size === 0) return /* @__PURE__ */ new Set();
|
|
1427
|
+
const filterResult = buildFilterClausesWithContext(null, {
|
|
1422
1428
|
actorId: store.actorId,
|
|
1423
1429
|
deviceId: store.deviceId,
|
|
1424
1430
|
enforceScopeVisibility: true
|
|
1425
1431
|
});
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1432
|
+
const visibleIds = /* @__PURE__ */ new Set();
|
|
1433
|
+
for (const chunk of chunkNumbers([...memoryIds])) {
|
|
1434
|
+
const clauses = [
|
|
1435
|
+
"memory_items.active = 1",
|
|
1436
|
+
`memory_items.id IN (${chunk.map(() => "?").join(", ")})`,
|
|
1437
|
+
...filterResult.clauses
|
|
1438
|
+
];
|
|
1439
|
+
const rows = store.db.prepare(`SELECT memory_items.id FROM memory_items WHERE ${clauses.join(" AND ")}`).all(...chunk, ...filterResult.params);
|
|
1440
|
+
for (const row of rows) visibleIds.add(Number(row.id));
|
|
1441
|
+
}
|
|
1442
|
+
return visibleIds;
|
|
1428
1443
|
}
|
|
1429
|
-
function
|
|
1444
|
+
function visibleSessionIdSet(store, sessionIds) {
|
|
1445
|
+
if (sessionIds.size === 0) return /* @__PURE__ */ new Set();
|
|
1446
|
+
const filterResult = buildFilterClausesWithContext(null, {
|
|
1447
|
+
actorId: store.actorId,
|
|
1448
|
+
deviceId: store.deviceId,
|
|
1449
|
+
enforceScopeVisibility: true
|
|
1450
|
+
});
|
|
1451
|
+
const visibleIds = /* @__PURE__ */ new Set();
|
|
1452
|
+
for (const chunk of chunkNumbers([...sessionIds])) {
|
|
1453
|
+
const clauses = [
|
|
1454
|
+
"memory_items.active = 1",
|
|
1455
|
+
`memory_items.session_id IN (${chunk.map(() => "?").join(", ")})`,
|
|
1456
|
+
...filterResult.clauses
|
|
1457
|
+
];
|
|
1458
|
+
const rows = store.db.prepare(`SELECT DISTINCT memory_items.session_id AS session_id
|
|
1459
|
+
FROM memory_items
|
|
1460
|
+
WHERE ${clauses.join(" AND ")}`).all(...chunk, ...filterResult.params);
|
|
1461
|
+
for (const row of rows) if (typeof row.session_id === "number") visibleIds.add(row.session_id);
|
|
1462
|
+
}
|
|
1463
|
+
return visibleIds;
|
|
1464
|
+
}
|
|
1465
|
+
function buildUsageVisibility(store, rows) {
|
|
1466
|
+
const memoryIds = /* @__PURE__ */ new Set();
|
|
1467
|
+
const sessionIds = /* @__PURE__ */ new Set();
|
|
1468
|
+
for (const row of rows) {
|
|
1469
|
+
if (typeof row.session_id === "number") sessionIds.add(row.session_id);
|
|
1470
|
+
for (const key of MEMORY_ID_METADATA_KEYS) for (const memoryId of metadataMemoryIds(row.metadata_json?.[key])) memoryIds.add(memoryId);
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
visibleMemoryIds: visibleMemoryIdSet(store, memoryIds),
|
|
1474
|
+
visibleSessionIds: visibleSessionIdSet(store, sessionIds)
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function usageRowVisible(row, visibility) {
|
|
1478
|
+
const rowSessionVisible = row.session_id == null || visibility.visibleSessionIds.has(row.session_id);
|
|
1430
1479
|
const packItemIds = row.metadata_json?.pack_item_ids;
|
|
1431
1480
|
if (Array.isArray(packItemIds)) {
|
|
1432
|
-
if (packItemIds.length === 0) return row.session_id != null &&
|
|
1433
|
-
return
|
|
1481
|
+
if (packItemIds.length === 0) return row.session_id != null && visibility.visibleSessionIds.has(row.session_id);
|
|
1482
|
+
return rowSessionVisible && metadataMemoryIds(packItemIds).some((memoryId) => visibility.visibleMemoryIds.has(memoryId));
|
|
1434
1483
|
}
|
|
1435
|
-
return row.session_id != null &&
|
|
1484
|
+
return row.session_id != null && visibility.visibleSessionIds.has(row.session_id);
|
|
1436
1485
|
}
|
|
1437
|
-
function sanitizePackUsageMetadata(
|
|
1486
|
+
function sanitizePackUsageMetadata(metadata, visibility) {
|
|
1438
1487
|
if (!metadata) return null;
|
|
1439
1488
|
const sanitized = { ...metadata };
|
|
1440
|
-
for (const key of MEMORY_ID_METADATA_KEYS) if (Array.isArray(sanitized[key])) sanitized[key] =
|
|
1489
|
+
for (const key of MEMORY_ID_METADATA_KEYS) if (Array.isArray(sanitized[key])) sanitized[key] = metadataMemoryIds(sanitized[key]).filter((memoryId) => visibility.visibleMemoryIds.has(memoryId));
|
|
1441
1490
|
return sanitized;
|
|
1442
1491
|
}
|
|
1443
1492
|
function summarizeUsageEvents(rows) {
|
|
@@ -1529,17 +1578,22 @@ function statsRoutes(getStore) {
|
|
|
1529
1578
|
ORDER BY usage_events.created_at DESC`).all(project) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
|
|
1530
1579
|
created_at, metadata_json
|
|
1531
1580
|
FROM usage_events
|
|
1532
|
-
ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)))
|
|
1581
|
+
ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
|
|
1533
1582
|
};
|
|
1534
|
-
const
|
|
1535
|
-
const
|
|
1583
|
+
const loadedGlobalRows = loadUsageRows();
|
|
1584
|
+
const globalVisibility = buildUsageVisibility(store, loadedGlobalRows);
|
|
1585
|
+
const globalRows = loadedGlobalRows.filter((row) => usageRowVisible(row, globalVisibility));
|
|
1586
|
+
const loadedFilteredRows = projectFilter ? loadUsageRows(projectFilter) : null;
|
|
1587
|
+
const filteredVisibility = loadedFilteredRows ? buildUsageVisibility(store, loadedFilteredRows) : null;
|
|
1588
|
+
const filteredRows = loadedFilteredRows ? loadedFilteredRows.filter((row) => usageRowVisible(row, filteredVisibility ?? globalVisibility)) : null;
|
|
1536
1589
|
const eventsGlobal = summarizeUsageEvents(globalRows);
|
|
1537
1590
|
const totalsGlobal = totalUsageEvents(globalRows);
|
|
1538
1591
|
const eventsFiltered = filteredRows ? summarizeUsageEvents(filteredRows) : null;
|
|
1539
1592
|
const totalsFiltered = filteredRows ? totalUsageEvents(filteredRows) : null;
|
|
1593
|
+
const recentVisibility = filteredVisibility ?? globalVisibility;
|
|
1540
1594
|
const recentPacks = (filteredRows ?? globalRows).filter((row) => row.event === "pack").slice(0, 10).map((row) => ({
|
|
1541
1595
|
...row,
|
|
1542
|
-
metadata_json: sanitizePackUsageMetadata(
|
|
1596
|
+
metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
|
|
1543
1597
|
}));
|
|
1544
1598
|
return c.json({
|
|
1545
1599
|
project: projectFilter,
|
|
@@ -1817,6 +1871,43 @@ function unauthorizedPayload(reason, exposeReason = false) {
|
|
|
1817
1871
|
};
|
|
1818
1872
|
return { error: "unauthorized" };
|
|
1819
1873
|
}
|
|
1874
|
+
var SYNC_AUTH_STORE_BUSY_REASON = "sync_auth_store_busy";
|
|
1875
|
+
function isSqliteBusy(err) {
|
|
1876
|
+
if (!err || typeof err !== "object") return false;
|
|
1877
|
+
const code = "code" in err ? String(err.code ?? "") : "";
|
|
1878
|
+
const message = err instanceof Error ? err.message : "";
|
|
1879
|
+
return code === "SQLITE_BUSY" || message.includes("database is locked");
|
|
1880
|
+
}
|
|
1881
|
+
function recordSyncAuthNonce(store, deviceId, nonce, createdAt) {
|
|
1882
|
+
try {
|
|
1883
|
+
if (!recordNonce(store.db, deviceId, nonce, createdAt)) return {
|
|
1884
|
+
ok: false,
|
|
1885
|
+
reason: "nonce_replay",
|
|
1886
|
+
deviceId
|
|
1887
|
+
};
|
|
1888
|
+
} catch (err) {
|
|
1889
|
+
if (isSqliteBusy(err)) return {
|
|
1890
|
+
ok: false,
|
|
1891
|
+
reason: SYNC_AUTH_STORE_BUSY_REASON,
|
|
1892
|
+
deviceId
|
|
1893
|
+
};
|
|
1894
|
+
throw err;
|
|
1895
|
+
}
|
|
1896
|
+
const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
|
|
1897
|
+
try {
|
|
1898
|
+
cleanupNonces(store.db, cutoff);
|
|
1899
|
+
} catch (err) {
|
|
1900
|
+
if (!isSqliteBusy(err)) throw err;
|
|
1901
|
+
}
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
function isSyncAuthStoreBusy(auth) {
|
|
1905
|
+
return !auth.ok && auth.reason === SYNC_AUTH_STORE_BUSY_REASON;
|
|
1906
|
+
}
|
|
1907
|
+
function syncAuthStoreBusyResponse(c) {
|
|
1908
|
+
c.header("Retry-After", "1");
|
|
1909
|
+
return c.json({ error: SYNC_AUTH_STORE_BUSY_REASON }, 503);
|
|
1910
|
+
}
|
|
1820
1911
|
function authorizeSyncRequest(store, request, body) {
|
|
1821
1912
|
const deviceId = (request.header("X-Opencode-Device") ?? "").trim();
|
|
1822
1913
|
const signature = request.header("X-Opencode-Signature") ?? "";
|
|
@@ -1869,14 +1960,8 @@ function authorizeSyncRequest(store, request, body) {
|
|
|
1869
1960
|
reason: "invalid_signature",
|
|
1870
1961
|
deviceId
|
|
1871
1962
|
};
|
|
1872
|
-
const
|
|
1873
|
-
if (
|
|
1874
|
-
ok: false,
|
|
1875
|
-
reason: "nonce_replay",
|
|
1876
|
-
deviceId
|
|
1877
|
-
};
|
|
1878
|
-
const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
|
|
1879
|
-
cleanupNonces(store.db, cutoff);
|
|
1963
|
+
const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
|
|
1964
|
+
if (nonceResult) return nonceResult;
|
|
1880
1965
|
return {
|
|
1881
1966
|
ok: true,
|
|
1882
1967
|
reason: "ok",
|
|
@@ -1986,14 +2071,8 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
1986
2071
|
reason: "bootstrap_grant_invalid_signature",
|
|
1987
2072
|
deviceId
|
|
1988
2073
|
};
|
|
1989
|
-
const
|
|
1990
|
-
if (
|
|
1991
|
-
ok: false,
|
|
1992
|
-
reason: "nonce_replay",
|
|
1993
|
-
deviceId
|
|
1994
|
-
};
|
|
1995
|
-
const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
|
|
1996
|
-
cleanupNonces(store.db, cutoff);
|
|
2074
|
+
const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
|
|
2075
|
+
if (nonceResult) return nonceResult;
|
|
1997
2076
|
return {
|
|
1998
2077
|
ok: true,
|
|
1999
2078
|
reason: "ok",
|
|
@@ -2367,6 +2446,21 @@ function recentScopeRejectionsByPeer(store) {
|
|
|
2367
2446
|
}
|
|
2368
2447
|
return map;
|
|
2369
2448
|
}
|
|
2449
|
+
function legacySharedReviewSummary(store) {
|
|
2450
|
+
const row = store.db.prepare(`SELECT COUNT(*) AS memory_count,
|
|
2451
|
+
MAX(updated_at) AS last_updated_at
|
|
2452
|
+
FROM memory_items
|
|
2453
|
+
WHERE scope_id = ?
|
|
2454
|
+
AND active = 1
|
|
2455
|
+
AND deleted_at IS NULL`).get(LEGACY_SHARED_REVIEW_SCOPE_ID);
|
|
2456
|
+
const memoryCount = Number(row?.memory_count ?? 0);
|
|
2457
|
+
return {
|
|
2458
|
+
scope_id: LEGACY_SHARED_REVIEW_SCOPE_ID,
|
|
2459
|
+
memory_count: memoryCount,
|
|
2460
|
+
has_data: memoryCount > 0,
|
|
2461
|
+
last_updated_at: row?.last_updated_at ?? null
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2370
2464
|
var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
|
|
2371
2465
|
function recentPeerOps(store) {
|
|
2372
2466
|
const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_DIRECTION_WINDOW_SECONDS * 1e3)).toISOString();
|
|
@@ -2438,7 +2532,8 @@ var PEERS_QUERY = `
|
|
|
2438
2532
|
SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
|
|
2439
2533
|
p.last_seen_at, p.last_sync_at, p.last_error,
|
|
2440
2534
|
p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
|
|
2441
|
-
p.actor_id,
|
|
2535
|
+
p.actor_id, p.discovered_via_coordinator_id, p.discovered_via_group_id,
|
|
2536
|
+
a.display_name AS actor_display_name
|
|
2442
2537
|
FROM sync_peers AS p
|
|
2443
2538
|
LEFT JOIN actors AS a ON a.actor_id = p.actor_id
|
|
2444
2539
|
ORDER BY name, peer_device_id
|
|
@@ -2468,6 +2563,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2468
2563
|
const store = getStore();
|
|
2469
2564
|
return (async () => {
|
|
2470
2565
|
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2566
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2471
2567
|
let preauthChecked = false;
|
|
2472
2568
|
let bootstrapAttempted = false;
|
|
2473
2569
|
if (!auth.ok) {
|
|
@@ -2483,6 +2579,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2483
2579
|
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
2484
2580
|
bootstrapAttempted = true;
|
|
2485
2581
|
}
|
|
2582
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2486
2583
|
if (!auth.ok) {
|
|
2487
2584
|
if (bootstrapAttempted) console.warn(`[sync] bootstrap grant auth failed: reason=${auth.reason} grant=${(c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()} path=${c.req.path}`);
|
|
2488
2585
|
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
@@ -2508,6 +2605,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2508
2605
|
app.get("/v1/ops", (c) => {
|
|
2509
2606
|
const store = getStore();
|
|
2510
2607
|
const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2608
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2511
2609
|
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
|
|
2512
2610
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2513
2611
|
if (limited) return limited;
|
|
@@ -2560,6 +2658,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2560
2658
|
const store = getStore();
|
|
2561
2659
|
return (async () => {
|
|
2562
2660
|
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2661
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2563
2662
|
let preauthChecked = false;
|
|
2564
2663
|
let bootstrapAttempted = false;
|
|
2565
2664
|
if (!auth.ok) {
|
|
@@ -2575,6 +2674,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2575
2674
|
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
2576
2675
|
bootstrapAttempted = true;
|
|
2577
2676
|
}
|
|
2677
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2578
2678
|
if (!auth.ok) {
|
|
2579
2679
|
if (bootstrapAttempted) console.warn(`[sync] bootstrap grant auth failed: reason=${auth.reason} grant=${(c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()} path=${c.req.path}`);
|
|
2580
2680
|
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
@@ -2626,6 +2726,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2626
2726
|
const raw = await readBoundedRequestBytes(c.req.raw, MAX_SYNC_BODY_BYTES);
|
|
2627
2727
|
if (raw == null) return c.json({ error: "payload_too_large" }, 413);
|
|
2628
2728
|
const auth = authorizeSyncRequest(store, c.req, raw);
|
|
2729
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2629
2730
|
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
|
|
2630
2731
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2631
2732
|
if (limited) return limited;
|
|
@@ -2817,6 +2918,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2817
2918
|
ping: {}
|
|
2818
2919
|
};
|
|
2819
2920
|
const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
|
|
2921
|
+
const legacyReview = traceSync("legacySharedReview", () => legacySharedReviewSummary(store));
|
|
2820
2922
|
const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
|
|
2821
2923
|
let joinRequests = [];
|
|
2822
2924
|
if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
|
|
@@ -2851,6 +2953,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2851
2953
|
peers: peersItems,
|
|
2852
2954
|
attempts: attemptsItems.slice(0, 5),
|
|
2853
2955
|
legacy_devices: legacyDevices,
|
|
2956
|
+
legacy_shared_review: legacyReview,
|
|
2854
2957
|
sharing_review: sharingReview,
|
|
2855
2958
|
coordinator
|
|
2856
2959
|
};
|