@codemem/server 0.30.0-alpha.3 → 0.30.0-alpha.5
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 +162 -52
- package/dist/index.js.map +1 -1
- package/dist/routes/memory.d.ts.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";
|
|
@@ -655,6 +655,29 @@ function countVisibleMemoryRows(store, filters) {
|
|
|
655
655
|
const row = store.db.prepare(`SELECT COUNT(*) AS total FROM ${from} WHERE ${clauses.join(" AND ")}`).get(...filterResult.params);
|
|
656
656
|
return Number(row?.total ?? 0);
|
|
657
657
|
}
|
|
658
|
+
function summaryLikeSqlPredicate() {
|
|
659
|
+
return `(
|
|
660
|
+
LOWER(TRIM(COALESCE(memory_items.kind, ''))) = 'session_summary'
|
|
661
|
+
OR (
|
|
662
|
+
json_valid(COALESCE(memory_items.metadata_json, ''))
|
|
663
|
+
AND (
|
|
664
|
+
COALESCE(json_extract(memory_items.metadata_json, '$.is_summary') = 1, 0)
|
|
665
|
+
OR LOWER(TRIM(COALESCE(json_extract(memory_items.metadata_json, '$.source'), ''))) = 'observer_summary'
|
|
666
|
+
)
|
|
667
|
+
)
|
|
668
|
+
)`;
|
|
669
|
+
}
|
|
670
|
+
function countVisibleObservationRows(store, filters) {
|
|
671
|
+
const filterResult = buildViewerMemoryFilters(store, filters);
|
|
672
|
+
const clauses = [
|
|
673
|
+
"memory_items.active = 1",
|
|
674
|
+
`NOT ${summaryLikeSqlPredicate()}`,
|
|
675
|
+
...filterResult.clauses
|
|
676
|
+
];
|
|
677
|
+
const from = filterResult.joinSessions ? "memory_items JOIN sessions ON sessions.id = memory_items.session_id" : "memory_items";
|
|
678
|
+
const row = store.db.prepare(`SELECT COUNT(*) AS total FROM ${from} WHERE ${clauses.join(" AND ")}`).get(...filterResult.params);
|
|
679
|
+
return Number(row?.total ?? 0);
|
|
680
|
+
}
|
|
658
681
|
function sessionAllowsArtifactAccess(store, sessionId) {
|
|
659
682
|
const visibleCount = countVisibleMemoryRows(store, { session_id: sessionId });
|
|
660
683
|
if (visibleCount === 0) return false;
|
|
@@ -838,32 +861,16 @@ function memoryRoutes(getStore) {
|
|
|
838
861
|
let artifacts;
|
|
839
862
|
let memories;
|
|
840
863
|
let observations;
|
|
841
|
-
const countObservations = (scopeProject) => {
|
|
842
|
-
let offset = 0;
|
|
843
|
-
let total = 0;
|
|
844
|
-
while (true) {
|
|
845
|
-
const page = queryMemoryPage(store, {
|
|
846
|
-
limit: 200,
|
|
847
|
-
offset,
|
|
848
|
-
project: scopeProject
|
|
849
|
-
});
|
|
850
|
-
if (page.length === 0) break;
|
|
851
|
-
total += page.filter((item) => !isSummaryLikeMemory$1(item)).length;
|
|
852
|
-
if (page.length < 200) break;
|
|
853
|
-
offset += page.length;
|
|
854
|
-
}
|
|
855
|
-
return total;
|
|
856
|
-
};
|
|
857
864
|
if (project) {
|
|
858
865
|
prompts = countVisiblePromptRows(store, project);
|
|
859
866
|
artifacts = countVisibleArtifactRows(store, project);
|
|
860
867
|
memories = countVisibleMemoryRows(store, { project });
|
|
861
|
-
observations =
|
|
868
|
+
observations = countVisibleObservationRows(store, { project });
|
|
862
869
|
} else {
|
|
863
870
|
prompts = countVisiblePromptRows(store);
|
|
864
871
|
artifacts = countVisibleArtifactRows(store);
|
|
865
872
|
memories = countVisibleMemoryRows(store);
|
|
866
|
-
observations =
|
|
873
|
+
observations = countVisibleObservationRows(store);
|
|
867
874
|
}
|
|
868
875
|
const total = prompts + artifacts + memories;
|
|
869
876
|
return c.json({
|
|
@@ -1413,31 +1420,80 @@ function toUsageRow(row, metadata) {
|
|
|
1413
1420
|
metadata_json: metadata
|
|
1414
1421
|
};
|
|
1415
1422
|
}
|
|
1416
|
-
function
|
|
1423
|
+
function metadataMemoryIds(value) {
|
|
1417
1424
|
if (!Array.isArray(value)) return [];
|
|
1418
|
-
return value.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item) &&
|
|
1425
|
+
return value.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item) && item > 0);
|
|
1426
|
+
}
|
|
1427
|
+
function chunkNumbers(values, chunkSize = 500) {
|
|
1428
|
+
const chunks = [];
|
|
1429
|
+
for (let start = 0; start < values.length; start += chunkSize) chunks.push(values.slice(start, start + chunkSize));
|
|
1430
|
+
return chunks;
|
|
1419
1431
|
}
|
|
1420
|
-
function
|
|
1421
|
-
|
|
1432
|
+
function visibleMemoryIdSet(store, memoryIds) {
|
|
1433
|
+
if (memoryIds.size === 0) return /* @__PURE__ */ new Set();
|
|
1434
|
+
const filterResult = buildFilterClausesWithContext(null, {
|
|
1422
1435
|
actorId: store.actorId,
|
|
1423
1436
|
deviceId: store.deviceId,
|
|
1424
1437
|
enforceScopeVisibility: true
|
|
1425
1438
|
});
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1439
|
+
const visibleIds = /* @__PURE__ */ new Set();
|
|
1440
|
+
for (const chunk of chunkNumbers([...memoryIds])) {
|
|
1441
|
+
const clauses = [
|
|
1442
|
+
"memory_items.active = 1",
|
|
1443
|
+
`memory_items.id IN (${chunk.map(() => "?").join(", ")})`,
|
|
1444
|
+
...filterResult.clauses
|
|
1445
|
+
];
|
|
1446
|
+
const rows = store.db.prepare(`SELECT memory_items.id FROM memory_items WHERE ${clauses.join(" AND ")}`).all(...chunk, ...filterResult.params);
|
|
1447
|
+
for (const row of rows) visibleIds.add(Number(row.id));
|
|
1448
|
+
}
|
|
1449
|
+
return visibleIds;
|
|
1428
1450
|
}
|
|
1429
|
-
function
|
|
1451
|
+
function visibleSessionIdSet(store, sessionIds) {
|
|
1452
|
+
if (sessionIds.size === 0) return /* @__PURE__ */ new Set();
|
|
1453
|
+
const filterResult = buildFilterClausesWithContext(null, {
|
|
1454
|
+
actorId: store.actorId,
|
|
1455
|
+
deviceId: store.deviceId,
|
|
1456
|
+
enforceScopeVisibility: true
|
|
1457
|
+
});
|
|
1458
|
+
const visibleIds = /* @__PURE__ */ new Set();
|
|
1459
|
+
for (const chunk of chunkNumbers([...sessionIds])) {
|
|
1460
|
+
const clauses = [
|
|
1461
|
+
"memory_items.active = 1",
|
|
1462
|
+
`memory_items.session_id IN (${chunk.map(() => "?").join(", ")})`,
|
|
1463
|
+
...filterResult.clauses
|
|
1464
|
+
];
|
|
1465
|
+
const rows = store.db.prepare(`SELECT DISTINCT memory_items.session_id AS session_id
|
|
1466
|
+
FROM memory_items
|
|
1467
|
+
WHERE ${clauses.join(" AND ")}`).all(...chunk, ...filterResult.params);
|
|
1468
|
+
for (const row of rows) if (typeof row.session_id === "number") visibleIds.add(row.session_id);
|
|
1469
|
+
}
|
|
1470
|
+
return visibleIds;
|
|
1471
|
+
}
|
|
1472
|
+
function buildUsageVisibility(store, rows) {
|
|
1473
|
+
const memoryIds = /* @__PURE__ */ new Set();
|
|
1474
|
+
const sessionIds = /* @__PURE__ */ new Set();
|
|
1475
|
+
for (const row of rows) {
|
|
1476
|
+
if (typeof row.session_id === "number") sessionIds.add(row.session_id);
|
|
1477
|
+
for (const key of MEMORY_ID_METADATA_KEYS) for (const memoryId of metadataMemoryIds(row.metadata_json?.[key])) memoryIds.add(memoryId);
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
visibleMemoryIds: visibleMemoryIdSet(store, memoryIds),
|
|
1481
|
+
visibleSessionIds: visibleSessionIdSet(store, sessionIds)
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function usageRowVisible(row, visibility) {
|
|
1485
|
+
const rowSessionVisible = row.session_id == null || visibility.visibleSessionIds.has(row.session_id);
|
|
1430
1486
|
const packItemIds = row.metadata_json?.pack_item_ids;
|
|
1431
1487
|
if (Array.isArray(packItemIds)) {
|
|
1432
|
-
if (packItemIds.length === 0) return row.session_id != null &&
|
|
1433
|
-
return
|
|
1488
|
+
if (packItemIds.length === 0) return row.session_id != null && visibility.visibleSessionIds.has(row.session_id);
|
|
1489
|
+
return rowSessionVisible && metadataMemoryIds(packItemIds).some((memoryId) => visibility.visibleMemoryIds.has(memoryId));
|
|
1434
1490
|
}
|
|
1435
|
-
return row.session_id != null &&
|
|
1491
|
+
return row.session_id != null && visibility.visibleSessionIds.has(row.session_id);
|
|
1436
1492
|
}
|
|
1437
|
-
function sanitizePackUsageMetadata(
|
|
1493
|
+
function sanitizePackUsageMetadata(metadata, visibility) {
|
|
1438
1494
|
if (!metadata) return null;
|
|
1439
1495
|
const sanitized = { ...metadata };
|
|
1440
|
-
for (const key of MEMORY_ID_METADATA_KEYS) if (Array.isArray(sanitized[key])) sanitized[key] =
|
|
1496
|
+
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
1497
|
return sanitized;
|
|
1442
1498
|
}
|
|
1443
1499
|
function summarizeUsageEvents(rows) {
|
|
@@ -1529,17 +1585,22 @@ function statsRoutes(getStore) {
|
|
|
1529
1585
|
ORDER BY usage_events.created_at DESC`).all(project) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
|
|
1530
1586
|
created_at, metadata_json
|
|
1531
1587
|
FROM usage_events
|
|
1532
|
-
ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)))
|
|
1588
|
+
ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
|
|
1533
1589
|
};
|
|
1534
|
-
const
|
|
1535
|
-
const
|
|
1590
|
+
const loadedGlobalRows = loadUsageRows();
|
|
1591
|
+
const globalVisibility = buildUsageVisibility(store, loadedGlobalRows);
|
|
1592
|
+
const globalRows = loadedGlobalRows.filter((row) => usageRowVisible(row, globalVisibility));
|
|
1593
|
+
const loadedFilteredRows = projectFilter ? loadUsageRows(projectFilter) : null;
|
|
1594
|
+
const filteredVisibility = loadedFilteredRows ? buildUsageVisibility(store, loadedFilteredRows) : null;
|
|
1595
|
+
const filteredRows = loadedFilteredRows ? loadedFilteredRows.filter((row) => usageRowVisible(row, filteredVisibility ?? globalVisibility)) : null;
|
|
1536
1596
|
const eventsGlobal = summarizeUsageEvents(globalRows);
|
|
1537
1597
|
const totalsGlobal = totalUsageEvents(globalRows);
|
|
1538
1598
|
const eventsFiltered = filteredRows ? summarizeUsageEvents(filteredRows) : null;
|
|
1539
1599
|
const totalsFiltered = filteredRows ? totalUsageEvents(filteredRows) : null;
|
|
1600
|
+
const recentVisibility = filteredVisibility ?? globalVisibility;
|
|
1540
1601
|
const recentPacks = (filteredRows ?? globalRows).filter((row) => row.event === "pack").slice(0, 10).map((row) => ({
|
|
1541
1602
|
...row,
|
|
1542
|
-
metadata_json: sanitizePackUsageMetadata(
|
|
1603
|
+
metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
|
|
1543
1604
|
}));
|
|
1544
1605
|
return c.json({
|
|
1545
1606
|
project: projectFilter,
|
|
@@ -1817,6 +1878,43 @@ function unauthorizedPayload(reason, exposeReason = false) {
|
|
|
1817
1878
|
};
|
|
1818
1879
|
return { error: "unauthorized" };
|
|
1819
1880
|
}
|
|
1881
|
+
var SYNC_AUTH_STORE_BUSY_REASON = "sync_auth_store_busy";
|
|
1882
|
+
function isSqliteBusy(err) {
|
|
1883
|
+
if (!err || typeof err !== "object") return false;
|
|
1884
|
+
const code = "code" in err ? String(err.code ?? "") : "";
|
|
1885
|
+
const message = err instanceof Error ? err.message : "";
|
|
1886
|
+
return code === "SQLITE_BUSY" || message.includes("database is locked");
|
|
1887
|
+
}
|
|
1888
|
+
function recordSyncAuthNonce(store, deviceId, nonce, createdAt) {
|
|
1889
|
+
try {
|
|
1890
|
+
if (!recordNonce(store.db, deviceId, nonce, createdAt)) return {
|
|
1891
|
+
ok: false,
|
|
1892
|
+
reason: "nonce_replay",
|
|
1893
|
+
deviceId
|
|
1894
|
+
};
|
|
1895
|
+
} catch (err) {
|
|
1896
|
+
if (isSqliteBusy(err)) return {
|
|
1897
|
+
ok: false,
|
|
1898
|
+
reason: SYNC_AUTH_STORE_BUSY_REASON,
|
|
1899
|
+
deviceId
|
|
1900
|
+
};
|
|
1901
|
+
throw err;
|
|
1902
|
+
}
|
|
1903
|
+
const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
|
|
1904
|
+
try {
|
|
1905
|
+
cleanupNonces(store.db, cutoff);
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
if (!isSqliteBusy(err)) throw err;
|
|
1908
|
+
}
|
|
1909
|
+
return null;
|
|
1910
|
+
}
|
|
1911
|
+
function isSyncAuthStoreBusy(auth) {
|
|
1912
|
+
return !auth.ok && auth.reason === SYNC_AUTH_STORE_BUSY_REASON;
|
|
1913
|
+
}
|
|
1914
|
+
function syncAuthStoreBusyResponse(c) {
|
|
1915
|
+
c.header("Retry-After", "1");
|
|
1916
|
+
return c.json({ error: SYNC_AUTH_STORE_BUSY_REASON }, 503);
|
|
1917
|
+
}
|
|
1820
1918
|
function authorizeSyncRequest(store, request, body) {
|
|
1821
1919
|
const deviceId = (request.header("X-Opencode-Device") ?? "").trim();
|
|
1822
1920
|
const signature = request.header("X-Opencode-Signature") ?? "";
|
|
@@ -1869,14 +1967,8 @@ function authorizeSyncRequest(store, request, body) {
|
|
|
1869
1967
|
reason: "invalid_signature",
|
|
1870
1968
|
deviceId
|
|
1871
1969
|
};
|
|
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);
|
|
1970
|
+
const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
|
|
1971
|
+
if (nonceResult) return nonceResult;
|
|
1880
1972
|
return {
|
|
1881
1973
|
ok: true,
|
|
1882
1974
|
reason: "ok",
|
|
@@ -1986,14 +2078,8 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
1986
2078
|
reason: "bootstrap_grant_invalid_signature",
|
|
1987
2079
|
deviceId
|
|
1988
2080
|
};
|
|
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);
|
|
2081
|
+
const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
|
|
2082
|
+
if (nonceResult) return nonceResult;
|
|
1997
2083
|
return {
|
|
1998
2084
|
ok: true,
|
|
1999
2085
|
reason: "ok",
|
|
@@ -2367,6 +2453,21 @@ function recentScopeRejectionsByPeer(store) {
|
|
|
2367
2453
|
}
|
|
2368
2454
|
return map;
|
|
2369
2455
|
}
|
|
2456
|
+
function legacySharedReviewSummary(store) {
|
|
2457
|
+
const row = store.db.prepare(`SELECT COUNT(*) AS memory_count,
|
|
2458
|
+
MAX(updated_at) AS last_updated_at
|
|
2459
|
+
FROM memory_items
|
|
2460
|
+
WHERE scope_id = ?
|
|
2461
|
+
AND active = 1
|
|
2462
|
+
AND deleted_at IS NULL`).get(LEGACY_SHARED_REVIEW_SCOPE_ID);
|
|
2463
|
+
const memoryCount = Number(row?.memory_count ?? 0);
|
|
2464
|
+
return {
|
|
2465
|
+
scope_id: LEGACY_SHARED_REVIEW_SCOPE_ID,
|
|
2466
|
+
memory_count: memoryCount,
|
|
2467
|
+
has_data: memoryCount > 0,
|
|
2468
|
+
last_updated_at: row?.last_updated_at ?? null
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2370
2471
|
var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
|
|
2371
2472
|
function recentPeerOps(store) {
|
|
2372
2473
|
const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_DIRECTION_WINDOW_SECONDS * 1e3)).toISOString();
|
|
@@ -2438,7 +2539,8 @@ var PEERS_QUERY = `
|
|
|
2438
2539
|
SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
|
|
2439
2540
|
p.last_seen_at, p.last_sync_at, p.last_error,
|
|
2440
2541
|
p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
|
|
2441
|
-
p.actor_id,
|
|
2542
|
+
p.actor_id, p.discovered_via_coordinator_id, p.discovered_via_group_id,
|
|
2543
|
+
a.display_name AS actor_display_name
|
|
2442
2544
|
FROM sync_peers AS p
|
|
2443
2545
|
LEFT JOIN actors AS a ON a.actor_id = p.actor_id
|
|
2444
2546
|
ORDER BY name, peer_device_id
|
|
@@ -2468,6 +2570,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2468
2570
|
const store = getStore();
|
|
2469
2571
|
return (async () => {
|
|
2470
2572
|
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2573
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2471
2574
|
let preauthChecked = false;
|
|
2472
2575
|
let bootstrapAttempted = false;
|
|
2473
2576
|
if (!auth.ok) {
|
|
@@ -2483,6 +2586,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2483
2586
|
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
2484
2587
|
bootstrapAttempted = true;
|
|
2485
2588
|
}
|
|
2589
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2486
2590
|
if (!auth.ok) {
|
|
2487
2591
|
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
2592
|
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
@@ -2508,6 +2612,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2508
2612
|
app.get("/v1/ops", (c) => {
|
|
2509
2613
|
const store = getStore();
|
|
2510
2614
|
const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2615
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2511
2616
|
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
|
|
2512
2617
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2513
2618
|
if (limited) return limited;
|
|
@@ -2560,6 +2665,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2560
2665
|
const store = getStore();
|
|
2561
2666
|
return (async () => {
|
|
2562
2667
|
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2668
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2563
2669
|
let preauthChecked = false;
|
|
2564
2670
|
let bootstrapAttempted = false;
|
|
2565
2671
|
if (!auth.ok) {
|
|
@@ -2575,6 +2681,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2575
2681
|
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
2576
2682
|
bootstrapAttempted = true;
|
|
2577
2683
|
}
|
|
2684
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2578
2685
|
if (!auth.ok) {
|
|
2579
2686
|
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
2687
|
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
@@ -2626,6 +2733,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2626
2733
|
const raw = await readBoundedRequestBytes(c.req.raw, MAX_SYNC_BODY_BYTES);
|
|
2627
2734
|
if (raw == null) return c.json({ error: "payload_too_large" }, 413);
|
|
2628
2735
|
const auth = authorizeSyncRequest(store, c.req, raw);
|
|
2736
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2629
2737
|
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
|
|
2630
2738
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2631
2739
|
if (limited) return limited;
|
|
@@ -2817,6 +2925,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2817
2925
|
ping: {}
|
|
2818
2926
|
};
|
|
2819
2927
|
const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
|
|
2928
|
+
const legacyReview = traceSync("legacySharedReview", () => legacySharedReviewSummary(store));
|
|
2820
2929
|
const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
|
|
2821
2930
|
let joinRequests = [];
|
|
2822
2931
|
if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
|
|
@@ -2851,6 +2960,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2851
2960
|
peers: peersItems,
|
|
2852
2961
|
attempts: attemptsItems.slice(0, 5),
|
|
2853
2962
|
legacy_devices: legacyDevices,
|
|
2963
|
+
legacy_shared_review: legacyReview,
|
|
2854
2964
|
sharing_review: sharingReview,
|
|
2855
2965
|
coordinator
|
|
2856
2966
|
};
|