@codemem/server 0.30.0-alpha.2 → 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 +460 -98
- 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 +3255 -2344
- package/static/index.html +53 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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_SYNC_CAPABILITY, MemoryStore, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, addSyncScopeToBoundary, 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, ensureDeviceIdentity, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorGroupPreference, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listInboundScopeRejections, listMaintenanceJobs, listObserverProviderOptions, 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, 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";
|
|
7
7
|
import { RateLimiter } from "limiter";
|
|
8
8
|
import { homedir, networkInterfaces } from "node:os";
|
|
9
|
-
import { and, count, desc, eq, inArray,
|
|
9
|
+
import { and, count, desc, eq, inArray, max, ne } from "drizzle-orm";
|
|
10
10
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
12
|
//#region src/middleware.ts
|
|
@@ -641,14 +641,65 @@ function normalizeScope(raw) {
|
|
|
641
641
|
const value = String(raw ?? "").trim().toLowerCase();
|
|
642
642
|
if (value === "mine" || value === "theirs") return value;
|
|
643
643
|
}
|
|
644
|
+
function buildViewerMemoryFilters(store, filters) {
|
|
645
|
+
return buildFilterClausesWithContext(filters, {
|
|
646
|
+
actorId: store.actorId,
|
|
647
|
+
deviceId: store.deviceId,
|
|
648
|
+
enforceScopeVisibility: true
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function countVisibleMemoryRows(store, filters) {
|
|
652
|
+
const filterResult = buildViewerMemoryFilters(store, filters);
|
|
653
|
+
const clauses = ["memory_items.active = 1", ...filterResult.clauses];
|
|
654
|
+
const from = filterResult.joinSessions ? "memory_items JOIN sessions ON sessions.id = memory_items.session_id" : "memory_items";
|
|
655
|
+
const row = store.db.prepare(`SELECT COUNT(*) AS total FROM ${from} WHERE ${clauses.join(" AND ")}`).get(...filterResult.params);
|
|
656
|
+
return Number(row?.total ?? 0);
|
|
657
|
+
}
|
|
658
|
+
function sessionAllowsArtifactAccess(store, sessionId) {
|
|
659
|
+
const visibleCount = countVisibleMemoryRows(store, { session_id: sessionId });
|
|
660
|
+
if (visibleCount === 0) return false;
|
|
661
|
+
const row = store.db.prepare(`SELECT COUNT(*) AS total FROM memory_items
|
|
662
|
+
WHERE session_id = ? AND active = 1`).get(sessionId);
|
|
663
|
+
return visibleCount === Number(row?.total ?? 0);
|
|
664
|
+
}
|
|
665
|
+
function countVisiblePromptRows(store, project) {
|
|
666
|
+
const filterResult = buildViewerMemoryFilters(store, null);
|
|
667
|
+
const clauses = ["user_prompts.session_id IS NOT NULL", `EXISTS (
|
|
668
|
+
SELECT 1 FROM memory_items
|
|
669
|
+
WHERE memory_items.session_id = user_prompts.session_id
|
|
670
|
+
AND memory_items.active = 1
|
|
671
|
+
AND ${filterResult.clauses.join(" AND ")}
|
|
672
|
+
)`];
|
|
673
|
+
const params = [...filterResult.params];
|
|
674
|
+
if (project) {
|
|
675
|
+
clauses.unshift("user_prompts.project = ?");
|
|
676
|
+
params.unshift(project);
|
|
677
|
+
}
|
|
678
|
+
const row = store.db.prepare(`SELECT COUNT(*) AS total FROM user_prompts WHERE ${clauses.join(" AND ")}`).get(...params);
|
|
679
|
+
return Number(row?.total ?? 0);
|
|
680
|
+
}
|
|
681
|
+
function countVisibleArtifactRows(store, project) {
|
|
682
|
+
const filterResult = buildViewerMemoryFilters(store, null);
|
|
683
|
+
const clauses = [`EXISTS (
|
|
684
|
+
SELECT 1 FROM memory_items
|
|
685
|
+
WHERE memory_items.session_id = artifacts.session_id
|
|
686
|
+
AND memory_items.active = 1
|
|
687
|
+
AND ${filterResult.clauses.join(" AND ")}
|
|
688
|
+
)`];
|
|
689
|
+
const params = [...filterResult.params];
|
|
690
|
+
const from = project ? "artifacts JOIN sessions ON sessions.id = artifacts.session_id" : "artifacts";
|
|
691
|
+
if (project) {
|
|
692
|
+
clauses.unshift("sessions.project = ?");
|
|
693
|
+
params.unshift(project);
|
|
694
|
+
}
|
|
695
|
+
const row = store.db.prepare(`SELECT COUNT(*) AS total FROM ${from} WHERE ${clauses.join(" AND ")}`).get(...params);
|
|
696
|
+
return Number(row?.total ?? 0);
|
|
697
|
+
}
|
|
644
698
|
function queryMemoryPage(store, options) {
|
|
645
699
|
const filters = {};
|
|
646
700
|
if (options.project) filters.project = options.project;
|
|
647
701
|
if (options.scope) filters.ownership_scope = options.scope;
|
|
648
|
-
const filterResult =
|
|
649
|
-
actorId: store.actorId,
|
|
650
|
-
deviceId: store.deviceId
|
|
651
|
-
});
|
|
702
|
+
const filterResult = buildViewerMemoryFilters(store, filters);
|
|
652
703
|
const where = ["memory_items.active = 1", ...filterResult.clauses].join(" AND ");
|
|
653
704
|
const from = filterResult.joinSessions ? "memory_items JOIN sessions ON sessions.id = memory_items.session_id" : "memory_items";
|
|
654
705
|
const rows = store.db.prepare(`SELECT memory_items.* FROM ${from}
|
|
@@ -688,9 +739,18 @@ function memoryRoutes(getStore) {
|
|
|
688
739
|
const store = getStore();
|
|
689
740
|
{
|
|
690
741
|
const limit = queryInt(c.req.query("limit"), 20);
|
|
691
|
-
const
|
|
742
|
+
const filterResult = buildViewerMemoryFilters(store, null);
|
|
743
|
+
const clauses = [
|
|
744
|
+
"memory_items.session_id = sessions.id",
|
|
745
|
+
"memory_items.active = 1",
|
|
746
|
+
...filterResult.clauses
|
|
747
|
+
];
|
|
748
|
+
const items = store.db.prepare(`SELECT sessions.* FROM sessions
|
|
749
|
+
WHERE EXISTS (SELECT 1 FROM memory_items WHERE ${clauses.join(" AND ")})
|
|
750
|
+
ORDER BY sessions.started_at DESC
|
|
751
|
+
LIMIT ?`).all(...filterResult.params, limit).map((row) => ({
|
|
692
752
|
...row,
|
|
693
|
-
metadata_json: fromJson(row.metadata_json)
|
|
753
|
+
metadata_json: fromJson(row.metadata_json ?? null)
|
|
694
754
|
}));
|
|
695
755
|
return c.json({ items });
|
|
696
756
|
}
|
|
@@ -698,7 +758,16 @@ function memoryRoutes(getStore) {
|
|
|
698
758
|
app.get("/api/projects", (c) => {
|
|
699
759
|
const store = getStore();
|
|
700
760
|
{
|
|
701
|
-
const
|
|
761
|
+
const filterResult = buildViewerMemoryFilters(store, null);
|
|
762
|
+
const clauses = [
|
|
763
|
+
"memory_items.session_id = sessions.id",
|
|
764
|
+
"memory_items.active = 1",
|
|
765
|
+
"sessions.project IS NOT NULL",
|
|
766
|
+
...filterResult.clauses
|
|
767
|
+
];
|
|
768
|
+
const rows = store.db.prepare(`SELECT DISTINCT sessions.project AS project FROM sessions
|
|
769
|
+
JOIN memory_items ON memory_items.session_id = sessions.id
|
|
770
|
+
WHERE ${clauses.join(" AND ")}`).all(...filterResult.params);
|
|
702
771
|
const projects = [...new Set(rows.map((r) => String(r.project ?? "").trim()).filter((p) => p && !p.toLowerCase().startsWith("fatal:")).map((p) => projectBasename(p)).filter(Boolean))].sort();
|
|
703
772
|
return c.json({ projects });
|
|
704
773
|
}
|
|
@@ -765,10 +834,6 @@ function memoryRoutes(getStore) {
|
|
|
765
834
|
const store = getStore();
|
|
766
835
|
{
|
|
767
836
|
const project = c.req.query("project") || null;
|
|
768
|
-
const count = (sql, ...params) => {
|
|
769
|
-
const row = store.db.prepare(sql).get(...params);
|
|
770
|
-
return Number(row?.total ?? 0);
|
|
771
|
-
};
|
|
772
837
|
let prompts;
|
|
773
838
|
let artifacts;
|
|
774
839
|
let memories;
|
|
@@ -790,18 +855,14 @@ function memoryRoutes(getStore) {
|
|
|
790
855
|
return total;
|
|
791
856
|
};
|
|
792
857
|
if (project) {
|
|
793
|
-
prompts =
|
|
794
|
-
artifacts =
|
|
795
|
-
|
|
796
|
-
WHERE sessions.project = ?`, project);
|
|
797
|
-
memories = count(`SELECT COUNT(*) AS total FROM memory_items
|
|
798
|
-
JOIN sessions ON sessions.id = memory_items.session_id
|
|
799
|
-
WHERE sessions.project = ?`, project);
|
|
858
|
+
prompts = countVisiblePromptRows(store, project);
|
|
859
|
+
artifacts = countVisibleArtifactRows(store, project);
|
|
860
|
+
memories = countVisibleMemoryRows(store, { project });
|
|
800
861
|
observations = countObservations(project);
|
|
801
862
|
} else {
|
|
802
|
-
prompts =
|
|
803
|
-
artifacts =
|
|
804
|
-
memories =
|
|
863
|
+
prompts = countVisiblePromptRows(store);
|
|
864
|
+
artifacts = countVisibleArtifactRows(store);
|
|
865
|
+
memories = countVisibleMemoryRows(store);
|
|
805
866
|
observations = countObservations();
|
|
806
867
|
}
|
|
807
868
|
const total = prompts + artifacts + memories;
|
|
@@ -878,6 +939,7 @@ function memoryRoutes(getStore) {
|
|
|
878
939
|
if (!sessionIdStr) return c.json({ error: "session_id required" }, 400);
|
|
879
940
|
const sessionId = parseStrictInteger(sessionIdStr);
|
|
880
941
|
if (sessionId == null) return c.json({ error: "session_id must be int" }, 400);
|
|
942
|
+
if (!sessionAllowsArtifactAccess(store, sessionId)) return c.json({ error: "session not found" }, 404);
|
|
881
943
|
const rows = drizzle(store.db, { schema }).select().from(schema.artifacts).where(eq(schema.artifacts.session_id, sessionId)).all();
|
|
882
944
|
return c.json({ items: rows });
|
|
883
945
|
}
|
|
@@ -892,6 +954,7 @@ function memoryRoutes(getStore) {
|
|
|
892
954
|
}
|
|
893
955
|
const memoryId = parseStrictInteger(typeof body.memory_id === "string" ? body.memory_id : String(body.memory_id ?? ""));
|
|
894
956
|
if (memoryId == null || memoryId <= 0) return c.json({ error: "memory_id must be int" }, 400);
|
|
957
|
+
if (!store.get(memoryId)) return c.json({ error: "memory not found" }, 404);
|
|
895
958
|
const visibility = String(body.visibility ?? "").trim();
|
|
896
959
|
if (visibility !== "private" && visibility !== "shared") return c.json({ error: "visibility must be private or shared" }, 400);
|
|
897
960
|
try {
|
|
@@ -916,6 +979,7 @@ function memoryRoutes(getStore) {
|
|
|
916
979
|
if (memoryId == null || memoryId <= 0) return c.json({ error: "memory_id must be int" }, 400);
|
|
917
980
|
const project = String(body.project ?? "").trim();
|
|
918
981
|
if (!project) return c.json({ error: "project must be a non-empty string" }, 400);
|
|
982
|
+
if (!store.get(memoryId)) return c.json({ error: "memory not found" }, 404);
|
|
919
983
|
try {
|
|
920
984
|
const result = store.moveMemoryProject(memoryId, project);
|
|
921
985
|
return c.json(result);
|
|
@@ -936,6 +1000,7 @@ function memoryRoutes(getStore) {
|
|
|
936
1000
|
}
|
|
937
1001
|
const memoryId = parseStrictInteger(typeof body.memory_id === "string" ? body.memory_id : String(body.memory_id ?? ""));
|
|
938
1002
|
if (memoryId == null || memoryId <= 0) return c.json({ error: "memory_id must be int" }, 400);
|
|
1003
|
+
if (!store.get(memoryId)) return c.json({ error: "memory not found" }, 404);
|
|
939
1004
|
const row = drizzle(store.db, { schema }).select().from(schema.memoryItems).where(eq(schema.memoryItems.id, memoryId)).get();
|
|
940
1005
|
if (!row) return c.json({ error: "memory not found" }, 404);
|
|
941
1006
|
if (!memoryOwnedBySelf(row, buildOwnershipContext(store))) return c.json({ error: "memory not owned by this device" }, 403);
|
|
@@ -1330,6 +1395,131 @@ function sortActiveMaintenanceJobs$1(jobs) {
|
|
|
1330
1395
|
return aKind.localeCompare(bKind);
|
|
1331
1396
|
});
|
|
1332
1397
|
}
|
|
1398
|
+
var MEMORY_ID_METADATA_KEYS = [
|
|
1399
|
+
"pack_item_ids",
|
|
1400
|
+
"added_ids",
|
|
1401
|
+
"removed_ids",
|
|
1402
|
+
"retained_ids"
|
|
1403
|
+
];
|
|
1404
|
+
function toUsageRow(row, metadata) {
|
|
1405
|
+
return {
|
|
1406
|
+
id: Number(row.id ?? 0),
|
|
1407
|
+
session_id: row.session_id == null ? null : Number(row.session_id),
|
|
1408
|
+
event: String(row.event ?? "pack"),
|
|
1409
|
+
tokens_read: Number(row.tokens_read ?? 0),
|
|
1410
|
+
tokens_written: Number(row.tokens_written ?? 0),
|
|
1411
|
+
tokens_saved: Number(row.tokens_saved ?? 0),
|
|
1412
|
+
created_at: String(row.created_at ?? ""),
|
|
1413
|
+
metadata_json: metadata
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function metadataMemoryIds(value) {
|
|
1417
|
+
if (!Array.isArray(value)) return [];
|
|
1418
|
+
return value.map((item) => typeof item === "number" ? item : Number(item)).filter((item) => Number.isInteger(item) && item > 0);
|
|
1419
|
+
}
|
|
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, {
|
|
1428
|
+
actorId: store.actorId,
|
|
1429
|
+
deviceId: store.deviceId,
|
|
1430
|
+
enforceScopeVisibility: true
|
|
1431
|
+
});
|
|
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;
|
|
1443
|
+
}
|
|
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);
|
|
1479
|
+
const packItemIds = row.metadata_json?.pack_item_ids;
|
|
1480
|
+
if (Array.isArray(packItemIds)) {
|
|
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));
|
|
1483
|
+
}
|
|
1484
|
+
return row.session_id != null && visibility.visibleSessionIds.has(row.session_id);
|
|
1485
|
+
}
|
|
1486
|
+
function sanitizePackUsageMetadata(metadata, visibility) {
|
|
1487
|
+
if (!metadata) return null;
|
|
1488
|
+
const sanitized = { ...metadata };
|
|
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));
|
|
1490
|
+
return sanitized;
|
|
1491
|
+
}
|
|
1492
|
+
function summarizeUsageEvents(rows) {
|
|
1493
|
+
const byEvent = /* @__PURE__ */ new Map();
|
|
1494
|
+
for (const row of rows) {
|
|
1495
|
+
const summary = byEvent.get(row.event) ?? {
|
|
1496
|
+
event: row.event,
|
|
1497
|
+
total_tokens_read: 0,
|
|
1498
|
+
total_tokens_written: 0,
|
|
1499
|
+
total_tokens_saved: 0,
|
|
1500
|
+
count: 0
|
|
1501
|
+
};
|
|
1502
|
+
summary.total_tokens_read += row.tokens_read;
|
|
1503
|
+
summary.total_tokens_written += row.tokens_written;
|
|
1504
|
+
summary.total_tokens_saved += row.tokens_saved;
|
|
1505
|
+
summary.count += 1;
|
|
1506
|
+
byEvent.set(row.event, summary);
|
|
1507
|
+
}
|
|
1508
|
+
return [...byEvent.values()].sort((a, b) => a.event.localeCompare(b.event));
|
|
1509
|
+
}
|
|
1510
|
+
function totalUsageEvents(rows) {
|
|
1511
|
+
return rows.reduce((acc, row) => ({
|
|
1512
|
+
tokens_read: Number(acc.tokens_read) + row.tokens_read,
|
|
1513
|
+
tokens_written: Number(acc.tokens_written) + row.tokens_written,
|
|
1514
|
+
tokens_saved: Number(acc.tokens_saved) + row.tokens_saved,
|
|
1515
|
+
count: Number(acc.count) + 1
|
|
1516
|
+
}), {
|
|
1517
|
+
tokens_read: 0,
|
|
1518
|
+
tokens_written: 0,
|
|
1519
|
+
tokens_saved: 0,
|
|
1520
|
+
count: 0
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1333
1523
|
/**
|
|
1334
1524
|
* Create stats routes. The store factory is called per-request to get a
|
|
1335
1525
|
* fresh connection (matching the Python viewer pattern).
|
|
@@ -1378,58 +1568,32 @@ function statsRoutes(getStore) {
|
|
|
1378
1568
|
const store = getStore();
|
|
1379
1569
|
{
|
|
1380
1570
|
const projectFilter = c.req.query("project") || null;
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
eventsFiltered = store.db.prepare(`SELECT event,
|
|
1408
|
-
SUM(tokens_read) AS total_tokens_read,
|
|
1409
|
-
SUM(tokens_written) AS total_tokens_written,
|
|
1410
|
-
SUM(tokens_saved) AS total_tokens_saved,
|
|
1411
|
-
COUNT(*) AS count
|
|
1412
|
-
FROM usage_events
|
|
1413
|
-
JOIN sessions ON sessions.id = usage_events.session_id
|
|
1414
|
-
WHERE sessions.project = ?
|
|
1415
|
-
GROUP BY event ORDER BY event`).all(projectFilter);
|
|
1416
|
-
totalsFiltered = store.db.prepare(`SELECT COALESCE(SUM(tokens_read),0) AS tokens_read,
|
|
1417
|
-
COALESCE(SUM(tokens_written),0) AS tokens_written,
|
|
1418
|
-
COALESCE(SUM(tokens_saved),0) AS tokens_saved,
|
|
1419
|
-
COUNT(*) AS count
|
|
1420
|
-
FROM usage_events
|
|
1421
|
-
JOIN sessions ON sessions.id = usage_events.session_id
|
|
1422
|
-
WHERE sessions.project = ?`).get(projectFilter);
|
|
1423
|
-
}
|
|
1424
|
-
const recentPacks = (projectFilter ? recentPacksQuery.all(projectFilter) : recentPacksQuery.all()).map((row) => ({
|
|
1425
|
-
id: Number(row.id ?? 0),
|
|
1426
|
-
session_id: row.session_id == null ? null : Number(row.session_id),
|
|
1427
|
-
event: String(row.event ?? "pack"),
|
|
1428
|
-
tokens_read: Number(row.tokens_read ?? 0),
|
|
1429
|
-
tokens_written: Number(row.tokens_written ?? 0),
|
|
1430
|
-
tokens_saved: Number(row.tokens_saved ?? 0),
|
|
1431
|
-
created_at: String(row.created_at ?? ""),
|
|
1432
|
-
metadata_json: parseMetadataJson(row.metadata_json)
|
|
1571
|
+
const loadUsageRows = (project) => {
|
|
1572
|
+
return (project ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
|
|
1573
|
+
usage_events.tokens_read, usage_events.tokens_written, usage_events.tokens_saved,
|
|
1574
|
+
usage_events.created_at, usage_events.metadata_json
|
|
1575
|
+
FROM usage_events
|
|
1576
|
+
JOIN sessions ON sessions.id = usage_events.session_id
|
|
1577
|
+
WHERE sessions.project = ?
|
|
1578
|
+
ORDER BY usage_events.created_at DESC`).all(project) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
|
|
1579
|
+
created_at, metadata_json
|
|
1580
|
+
FROM usage_events
|
|
1581
|
+
ORDER BY created_at DESC`).all()).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
|
|
1582
|
+
};
|
|
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;
|
|
1589
|
+
const eventsGlobal = summarizeUsageEvents(globalRows);
|
|
1590
|
+
const totalsGlobal = totalUsageEvents(globalRows);
|
|
1591
|
+
const eventsFiltered = filteredRows ? summarizeUsageEvents(filteredRows) : null;
|
|
1592
|
+
const totalsFiltered = filteredRows ? totalUsageEvents(filteredRows) : null;
|
|
1593
|
+
const recentVisibility = filteredVisibility ?? globalVisibility;
|
|
1594
|
+
const recentPacks = (filteredRows ?? globalRows).filter((row) => row.event === "pack").slice(0, 10).map((row) => ({
|
|
1595
|
+
...row,
|
|
1596
|
+
metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
|
|
1433
1597
|
}));
|
|
1434
1598
|
return c.json({
|
|
1435
1599
|
project: projectFilter,
|
|
@@ -1450,6 +1614,16 @@ function statsRoutes(getStore) {
|
|
|
1450
1614
|
/**
|
|
1451
1615
|
* Sync routes — status, peers, actors, attempts, pairing, mutations.
|
|
1452
1616
|
*/
|
|
1617
|
+
function safeSkippedSyncDetail(detail) {
|
|
1618
|
+
if (!detail) return null;
|
|
1619
|
+
return {
|
|
1620
|
+
reason: detail.reason,
|
|
1621
|
+
skipped_count: detail.skipped_count,
|
|
1622
|
+
scope_id: detail.scope_id ?? null,
|
|
1623
|
+
project: detail.project ?? null,
|
|
1624
|
+
visibility: detail.visibility ?? null
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1453
1627
|
var SYNC_STALE_AFTER_SECONDS = 600;
|
|
1454
1628
|
var SYNC_PROTOCOL_VERSION = "2";
|
|
1455
1629
|
var LEGACY_SYNC_ACTOR_DISPLAY_NAME = "Legacy synced peer";
|
|
@@ -1559,12 +1733,37 @@ function optionalViewerString(body, key) {
|
|
|
1559
1733
|
if (value == null) return null;
|
|
1560
1734
|
return String(value).trim() || null;
|
|
1561
1735
|
}
|
|
1736
|
+
function optionalViewerStrictString(body, key) {
|
|
1737
|
+
const value = body[key];
|
|
1738
|
+
if (value == null) return null;
|
|
1739
|
+
if (typeof value !== "string") throw new Error(`${key} must be string`);
|
|
1740
|
+
return value.trim() || null;
|
|
1741
|
+
}
|
|
1742
|
+
function optionalViewerStringList(body, key) {
|
|
1743
|
+
const value = body[key];
|
|
1744
|
+
if (value == null) return [];
|
|
1745
|
+
if (!Array.isArray(value)) throw new Error(`${key} must be an array of strings`);
|
|
1746
|
+
return [...new Set(value.map((item) => {
|
|
1747
|
+
if (typeof item !== "string") throw new Error(`${key} must be an array of strings`);
|
|
1748
|
+
return item.trim();
|
|
1749
|
+
}).filter(Boolean))];
|
|
1750
|
+
}
|
|
1562
1751
|
function optionalViewerNumber(body, key) {
|
|
1563
1752
|
const value = body[key];
|
|
1564
1753
|
if (value == null || value === "") return null;
|
|
1565
1754
|
const number = typeof value === "number" ? value : Number(value);
|
|
1566
1755
|
return Number.isFinite(number) ? Math.trunc(number) : NaN;
|
|
1567
1756
|
}
|
|
1757
|
+
function optionalViewerInteger(body, key) {
|
|
1758
|
+
const value = body[key];
|
|
1759
|
+
if (value == null || value === "") return null;
|
|
1760
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
1761
|
+
return Number.isInteger(number) ? number : NaN;
|
|
1762
|
+
}
|
|
1763
|
+
function missingGuardrailConfirmations(warnings, confirmedTokens) {
|
|
1764
|
+
const confirmed = new Set(confirmedTokens);
|
|
1765
|
+
return warnings.filter((warning) => warning.requires_confirmation && (!warning.confirmation_token || !confirmed.has(warning.confirmation_token)));
|
|
1766
|
+
}
|
|
1568
1767
|
function coordinatorAdminMutationStatus(message) {
|
|
1569
1768
|
if (message.includes("scope_not_found") || message.includes("membership_not_found") || message.includes("group_not_found") || message.includes("Group not found") || message.includes("Scope not found") || message.includes("device_not_enrolled_for_scope_group") || message.includes("device must be enrolled")) return 404;
|
|
1570
1769
|
if (message.includes("not active") || message.includes("scope_not_active") || message.includes("group_archived") || message.includes("Group is archived")) return 409;
|
|
@@ -1672,6 +1871,43 @@ function unauthorizedPayload(reason, exposeReason = false) {
|
|
|
1672
1871
|
};
|
|
1673
1872
|
return { error: "unauthorized" };
|
|
1674
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
|
+
}
|
|
1675
1911
|
function authorizeSyncRequest(store, request, body) {
|
|
1676
1912
|
const deviceId = (request.header("X-Opencode-Device") ?? "").trim();
|
|
1677
1913
|
const signature = request.header("X-Opencode-Signature") ?? "";
|
|
@@ -1724,14 +1960,8 @@ function authorizeSyncRequest(store, request, body) {
|
|
|
1724
1960
|
reason: "invalid_signature",
|
|
1725
1961
|
deviceId
|
|
1726
1962
|
};
|
|
1727
|
-
const
|
|
1728
|
-
if (
|
|
1729
|
-
ok: false,
|
|
1730
|
-
reason: "nonce_replay",
|
|
1731
|
-
deviceId
|
|
1732
|
-
};
|
|
1733
|
-
const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
|
|
1734
|
-
cleanupNonces(store.db, cutoff);
|
|
1963
|
+
const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
|
|
1964
|
+
if (nonceResult) return nonceResult;
|
|
1735
1965
|
return {
|
|
1736
1966
|
ok: true,
|
|
1737
1967
|
reason: "ok",
|
|
@@ -1841,14 +2071,8 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
1841
2071
|
reason: "bootstrap_grant_invalid_signature",
|
|
1842
2072
|
deviceId
|
|
1843
2073
|
};
|
|
1844
|
-
const
|
|
1845
|
-
if (
|
|
1846
|
-
ok: false,
|
|
1847
|
-
reason: "nonce_replay",
|
|
1848
|
-
deviceId
|
|
1849
|
-
};
|
|
1850
|
-
const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
|
|
1851
|
-
cleanupNonces(store.db, cutoff);
|
|
2074
|
+
const nonceResult = recordSyncAuthNonce(store, deviceId, nonce, (/* @__PURE__ */ new Date()).toISOString());
|
|
2075
|
+
if (nonceResult) return nonceResult;
|
|
1852
2076
|
return {
|
|
1853
2077
|
ok: true,
|
|
1854
2078
|
reason: "ok",
|
|
@@ -1892,6 +2116,42 @@ function currentProjectScope(row, effective) {
|
|
|
1892
2116
|
inherits_global: row.projects_include_json == null && row.projects_exclude_json == null
|
|
1893
2117
|
};
|
|
1894
2118
|
}
|
|
2119
|
+
function cleanPeerScopeText(value) {
|
|
2120
|
+
return String(value ?? "").trim() || null;
|
|
2121
|
+
}
|
|
2122
|
+
function peerAuthorizedScopes(store, peerDeviceId) {
|
|
2123
|
+
const deviceId = peerDeviceId.trim();
|
|
2124
|
+
if (!deviceId) return [];
|
|
2125
|
+
return store.db.prepare(`SELECT
|
|
2126
|
+
rs.scope_id,
|
|
2127
|
+
rs.label,
|
|
2128
|
+
rs.kind,
|
|
2129
|
+
rs.authority_type,
|
|
2130
|
+
rs.coordinator_id,
|
|
2131
|
+
rs.group_id,
|
|
2132
|
+
sm.role,
|
|
2133
|
+
sm.membership_epoch,
|
|
2134
|
+
sm.updated_at
|
|
2135
|
+
FROM scope_memberships sm
|
|
2136
|
+
JOIN replication_scopes rs ON rs.scope_id = sm.scope_id
|
|
2137
|
+
WHERE sm.device_id = ?
|
|
2138
|
+
AND sm.status = 'active'
|
|
2139
|
+
AND rs.status = 'active'
|
|
2140
|
+
AND sm.membership_epoch >= rs.membership_epoch
|
|
2141
|
+
ORDER BY CASE WHEN rs.authority_type = 'local' THEN 1 ELSE 0 END,
|
|
2142
|
+
rs.label COLLATE NOCASE,
|
|
2143
|
+
rs.scope_id`).all(deviceId).map((row) => ({
|
|
2144
|
+
scope_id: String(row.scope_id ?? ""),
|
|
2145
|
+
label: String(row.label ?? row.scope_id ?? ""),
|
|
2146
|
+
kind: String(row.kind ?? "user"),
|
|
2147
|
+
authority_type: String(row.authority_type ?? "local"),
|
|
2148
|
+
coordinator_id: cleanPeerScopeText(row.coordinator_id),
|
|
2149
|
+
group_id: cleanPeerScopeText(row.group_id),
|
|
2150
|
+
role: String(row.role ?? "member"),
|
|
2151
|
+
membership_epoch: Number(row.membership_epoch ?? 0),
|
|
2152
|
+
updated_at: cleanPeerScopeText(row.updated_at)
|
|
2153
|
+
}));
|
|
2154
|
+
}
|
|
1895
2155
|
function ensureLocalActorRecord(store) {
|
|
1896
2156
|
const d = drizzle(store.db, { schema });
|
|
1897
2157
|
if (d.select({ actor_id: schema.actors.actor_id }).from(schema.actors).where(eq(schema.actors.actor_id, store.actorId)).get()) return;
|
|
@@ -2114,7 +2374,8 @@ function filterOpsForPeer(store, peerDeviceId, localDeviceId, ops, options = {})
|
|
|
2114
2374
|
});
|
|
2115
2375
|
return {
|
|
2116
2376
|
allowed,
|
|
2117
|
-
skipped: skipped?.skipped_count ?? 0
|
|
2377
|
+
skipped: skipped?.skipped_count ?? 0,
|
|
2378
|
+
skippedDetail: skipped ? safeSkippedSyncDetail(skipped) : null
|
|
2118
2379
|
};
|
|
2119
2380
|
}
|
|
2120
2381
|
function claimedLocalActorScopeStatus(store, row) {
|
|
@@ -2155,9 +2416,10 @@ function mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer
|
|
|
2155
2416
|
last_error: showDiag ? row.last_error : null,
|
|
2156
2417
|
has_error: Boolean(row.last_error),
|
|
2157
2418
|
claimed_local_actor: Boolean(row.claimed_local_actor),
|
|
2158
|
-
claimed_local_actor_scope:
|
|
2419
|
+
claimed_local_actor_scope: claimedLocalActorScopeStatus(store, row),
|
|
2159
2420
|
actor_id: row.actor_id ?? null,
|
|
2160
2421
|
actor_display_name: row.actor_display_name ?? null,
|
|
2422
|
+
authorized_scopes: peerAuthorizedScopes(store, peerId),
|
|
2161
2423
|
project_scope: { ...currentProjectScope(row, readPeerProjectFilters(store, String(row.peer_device_id ?? ""))) },
|
|
2162
2424
|
recent_ops: {
|
|
2163
2425
|
in: recentOps.in,
|
|
@@ -2184,6 +2446,21 @@ function recentScopeRejectionsByPeer(store) {
|
|
|
2184
2446
|
}
|
|
2185
2447
|
return map;
|
|
2186
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
|
+
}
|
|
2187
2464
|
var SYNC_DIRECTION_WINDOW_SECONDS = 1440 * 60;
|
|
2188
2465
|
function recentPeerOps(store) {
|
|
2189
2466
|
const cutoff = (/* @__PURE__ */ new Date(Date.now() - SYNC_DIRECTION_WINDOW_SECONDS * 1e3)).toISOString();
|
|
@@ -2255,7 +2532,8 @@ var PEERS_QUERY = `
|
|
|
2255
2532
|
SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
|
|
2256
2533
|
p.last_seen_at, p.last_sync_at, p.last_error,
|
|
2257
2534
|
p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
|
|
2258
|
-
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
|
|
2259
2537
|
FROM sync_peers AS p
|
|
2260
2538
|
LEFT JOIN actors AS a ON a.actor_id = p.actor_id
|
|
2261
2539
|
ORDER BY name, peer_device_id
|
|
@@ -2285,6 +2563,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2285
2563
|
const store = getStore();
|
|
2286
2564
|
return (async () => {
|
|
2287
2565
|
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2566
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2288
2567
|
let preauthChecked = false;
|
|
2289
2568
|
let bootstrapAttempted = false;
|
|
2290
2569
|
if (!auth.ok) {
|
|
@@ -2300,6 +2579,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2300
2579
|
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
2301
2580
|
bootstrapAttempted = true;
|
|
2302
2581
|
}
|
|
2582
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2303
2583
|
if (!auth.ok) {
|
|
2304
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}`);
|
|
2305
2585
|
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
@@ -2325,6 +2605,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2325
2605
|
app.get("/v1/ops", (c) => {
|
|
2326
2606
|
const store = getStore();
|
|
2327
2607
|
const auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2608
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2328
2609
|
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
|
|
2329
2610
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2330
2611
|
if (limited) return limited;
|
|
@@ -2366,7 +2647,8 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2366
2647
|
retained_floor_cursor: boundary.retained_floor_cursor,
|
|
2367
2648
|
ops: filtered.allowed,
|
|
2368
2649
|
next_cursor: nextCursor,
|
|
2369
|
-
skipped: filtered.skipped
|
|
2650
|
+
skipped: filtered.skipped,
|
|
2651
|
+
skipped_detail: filtered.skippedDetail
|
|
2370
2652
|
});
|
|
2371
2653
|
} catch {
|
|
2372
2654
|
return c.json({ error: "internal_error" }, 500);
|
|
@@ -2376,6 +2658,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2376
2658
|
const store = getStore();
|
|
2377
2659
|
return (async () => {
|
|
2378
2660
|
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2661
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2379
2662
|
let preauthChecked = false;
|
|
2380
2663
|
let bootstrapAttempted = false;
|
|
2381
2664
|
if (!auth.ok) {
|
|
@@ -2391,6 +2674,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2391
2674
|
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
2392
2675
|
bootstrapAttempted = true;
|
|
2393
2676
|
}
|
|
2677
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2394
2678
|
if (!auth.ok) {
|
|
2395
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}`);
|
|
2396
2680
|
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
@@ -2442,6 +2726,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2442
2726
|
const raw = await readBoundedRequestBytes(c.req.raw, MAX_SYNC_BODY_BYTES);
|
|
2443
2727
|
if (raw == null) return c.json({ error: "payload_too_large" }, 413);
|
|
2444
2728
|
const auth = authorizeSyncRequest(store, c.req, raw);
|
|
2729
|
+
if (isSyncAuthStoreBusy(auth)) return syncAuthStoreBusyResponse(c);
|
|
2445
2730
|
if (!auth.ok) return rateLimitedResponse(c, c.req.path, false) ?? c.json(unauthorizedPayload(auth.reason), 401);
|
|
2446
2731
|
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2447
2732
|
if (limited) return limited;
|
|
@@ -2483,6 +2768,7 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
2483
2768
|
return c.json({
|
|
2484
2769
|
...result,
|
|
2485
2770
|
skipped,
|
|
2771
|
+
skipped_detail: filtered.skippedDetail,
|
|
2486
2772
|
sync_capability: LOCAL_SYNC_CAPABILITY,
|
|
2487
2773
|
scope_id: scopeRequest.scope_id
|
|
2488
2774
|
});
|
|
@@ -2585,6 +2871,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2585
2871
|
statusPayload.daemon_running = runtimeStatus.phase === "starting" || daemonRunning;
|
|
2586
2872
|
statusPayload.daemon_detail = runtimeStatus.detail ?? daemonDetail;
|
|
2587
2873
|
}
|
|
2874
|
+
const coordinatorSnapshot = await coordinatorStatusSnapshot(store, config);
|
|
2875
|
+
const coordinator = traceSync("coordinator", () => redactCoordinatorStatus(coordinatorSnapshot, showDiag));
|
|
2588
2876
|
const peerRows = traceSync("peerRows", () => store.db.prepare(PEERS_QUERY).all());
|
|
2589
2877
|
const recentOpsByPeer = traceSync("recentPeerOps", () => recentPeerOps(store));
|
|
2590
2878
|
const scopeRejectionsByPeer = traceSync("recentScopeRejectionsByPeer", () => recentScopeRejectionsByPeer(store));
|
|
@@ -2630,9 +2918,8 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2630
2918
|
ping: {}
|
|
2631
2919
|
};
|
|
2632
2920
|
const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
|
|
2921
|
+
const legacyReview = traceSync("legacySharedReview", () => legacySharedReviewSummary(store));
|
|
2633
2922
|
const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
|
|
2634
|
-
const coordinatorSnapshot = await coordinatorStatusSnapshot(store, config);
|
|
2635
|
-
const coordinator = traceSync("coordinator", () => redactCoordinatorStatus(coordinatorSnapshot, showDiag));
|
|
2636
2923
|
let joinRequests = [];
|
|
2637
2924
|
if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
|
|
2638
2925
|
joinRequests = await listCoordinatorJoinRequests(config);
|
|
@@ -2666,6 +2953,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2666
2953
|
peers: peersItems,
|
|
2667
2954
|
attempts: attemptsItems.slice(0, 5),
|
|
2668
2955
|
legacy_devices: legacyDevices,
|
|
2956
|
+
legacy_shared_review: legacyReview,
|
|
2669
2957
|
sharing_review: sharingReview,
|
|
2670
2958
|
coordinator
|
|
2671
2959
|
};
|
|
@@ -2821,7 +3109,9 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2821
3109
|
const result = await runSyncPass(store.db, peerId, { scanner: store.scanner });
|
|
2822
3110
|
items.push({
|
|
2823
3111
|
peer_device_id: peerId,
|
|
2824
|
-
...result
|
|
3112
|
+
...result,
|
|
3113
|
+
skippedOut: void 0,
|
|
3114
|
+
skipped_out: safeSkippedSyncDetail(result.skippedOut)
|
|
2825
3115
|
});
|
|
2826
3116
|
}
|
|
2827
3117
|
return c.json({ items });
|
|
@@ -2855,6 +3145,78 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2855
3145
|
project_scope: currentProjectScope(row, readPeerProjectFilters(store, peerDeviceId))
|
|
2856
3146
|
});
|
|
2857
3147
|
});
|
|
3148
|
+
app.get("/api/sync/sharing-domains/settings", (c) => {
|
|
3149
|
+
const store = getStore();
|
|
3150
|
+
const limit = Math.max(1, queryInt(c.req.query("limit"), 250));
|
|
3151
|
+
return c.json({
|
|
3152
|
+
scopes: listSharingDomainSettingsScopes(store.db),
|
|
3153
|
+
mappings: listProjectScopeSettingsMappings(store.db),
|
|
3154
|
+
projects: listProjectScopeCandidates(store.db, { limit }),
|
|
3155
|
+
local_default_scope_id: LOCAL_DEFAULT_SCOPE_ID
|
|
3156
|
+
});
|
|
3157
|
+
});
|
|
3158
|
+
app.put("/api/sync/sharing-domains/project-mappings", async (c) => {
|
|
3159
|
+
const store = getStore();
|
|
3160
|
+
const body = await parseViewerJsonBody(c);
|
|
3161
|
+
if (!body) return c.json({ error: "invalid json" }, 400);
|
|
3162
|
+
try {
|
|
3163
|
+
const id = optionalViewerInteger(body, "id");
|
|
3164
|
+
const priority = optionalViewerInteger(body, "priority");
|
|
3165
|
+
if (Number.isNaN(id)) return c.json({ error: "id must be an integer" }, 400);
|
|
3166
|
+
if (Number.isNaN(priority)) return c.json({ error: "priority must be an integer" }, 400);
|
|
3167
|
+
const workspaceIdentity = optionalViewerStrictString(body, "workspace_identity");
|
|
3168
|
+
const projectPattern = optionalViewerStrictString(body, "project_pattern");
|
|
3169
|
+
const scopeId = optionalViewerStrictString(body, "scope_id") ?? "";
|
|
3170
|
+
const source = optionalViewerStrictString(body, "source") ?? "user";
|
|
3171
|
+
const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
|
|
3172
|
+
const mappingInput = {
|
|
3173
|
+
id,
|
|
3174
|
+
workspace_identity: workspaceIdentity,
|
|
3175
|
+
project_pattern: projectPattern,
|
|
3176
|
+
scope_id: scopeId,
|
|
3177
|
+
priority,
|
|
3178
|
+
source
|
|
3179
|
+
};
|
|
3180
|
+
const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput);
|
|
3181
|
+
if (analysis.requested_workspace_identity?.startsWith("unmapped:") && scopeId !== LOCAL_DEFAULT_SCOPE_ID) return c.json({
|
|
3182
|
+
error: "unmapped_project_local_only",
|
|
3183
|
+
message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a non-local Sharing domain.",
|
|
3184
|
+
guardrail_warnings: analysis.warnings
|
|
3185
|
+
}, 400);
|
|
3186
|
+
const missingConfirmations = missingGuardrailConfirmations(analysis.warnings, confirmedGuardrailTokens);
|
|
3187
|
+
if (missingConfirmations.length > 0) {
|
|
3188
|
+
const missingCodes = [...new Set(missingConfirmations.map((warning) => warning.code))];
|
|
3189
|
+
const missingTokens = [...new Set(missingConfirmations.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0);
|
|
3190
|
+
return c.json({
|
|
3191
|
+
error: "guardrail_confirmation_required",
|
|
3192
|
+
required_guardrails: missingCodes,
|
|
3193
|
+
required_guardrail_tokens: missingTokens,
|
|
3194
|
+
guardrail_warnings: analysis.warnings
|
|
3195
|
+
}, 409);
|
|
3196
|
+
}
|
|
3197
|
+
const mapping = upsertProjectScopeSettingsMapping(store.db, { ...mappingInput });
|
|
3198
|
+
return c.json({
|
|
3199
|
+
ok: true,
|
|
3200
|
+
mapping,
|
|
3201
|
+
guardrail_warnings: analysis.warnings
|
|
3202
|
+
});
|
|
3203
|
+
} catch (error) {
|
|
3204
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => {
|
|
3208
|
+
const store = getStore();
|
|
3209
|
+
const id = Number(c.req.param("id"));
|
|
3210
|
+
try {
|
|
3211
|
+
const deleted = deleteProjectScopeSettingsMapping(store.db, id);
|
|
3212
|
+
return c.json({
|
|
3213
|
+
ok: true,
|
|
3214
|
+
deleted
|
|
3215
|
+
});
|
|
3216
|
+
} catch (error) {
|
|
3217
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
3218
|
+
}
|
|
3219
|
+
});
|
|
2858
3220
|
app.post("/api/sync/peers/identity", async (c) => {
|
|
2859
3221
|
const store = getStore();
|
|
2860
3222
|
ensureLocalActorRecord(store);
|