@codemem/server 0.22.3 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +311 -90
- package/dist/index.js.map +1 -1
- package/dist/routes/memory.d.ts.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +12 -9
- package/static/app.js +81 -15
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, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorReviewJoinRequestAction, coordinatorStatusSnapshot, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSyncResetState, listCoordinatorJoinRequests, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
3
|
+
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
4
4
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { createMiddleware } from "hono/factory";
|
|
7
7
|
import { RateLimiter } from "limiter";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { count, desc, eq, inArray, isNotNull, max, ne } from "drizzle-orm";
|
|
9
|
+
import { and, count, desc, eq, inArray, isNotNull, max, ne } from "drizzle-orm";
|
|
10
10
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
11
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
12
|
import net from "node:net";
|
|
@@ -591,14 +591,15 @@ function queryMemoryPage(store, options) {
|
|
|
591
591
|
ORDER BY memory_items.created_at DESC
|
|
592
592
|
LIMIT ? OFFSET ?`).all(...filterResult.params, options.limit + 1, options.offset).map((row) => ({
|
|
593
593
|
...row,
|
|
594
|
+
kind: canonicalMemoryKind(row.kind ?? null, row.metadata_json),
|
|
594
595
|
metadata_json: fromJson(row.metadata_json ?? null)
|
|
595
596
|
}));
|
|
596
597
|
}
|
|
597
|
-
function isSummaryLikeMemory(item) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
598
|
+
function isSummaryLikeMemory$1(item) {
|
|
599
|
+
return isSummaryLikeMemory({
|
|
600
|
+
kind: item.kind,
|
|
601
|
+
metadata: item.metadata_json
|
|
602
|
+
});
|
|
602
603
|
}
|
|
603
604
|
function selectMemoryPage(store, options) {
|
|
604
605
|
const pageSize = Math.max(options.limit + options.offset + 10, 50);
|
|
@@ -653,7 +654,7 @@ function memoryRoutes(getStore) {
|
|
|
653
654
|
offset,
|
|
654
655
|
project: c.req.query("project") || void 0,
|
|
655
656
|
scope: normalizeScope(c.req.query("scope")),
|
|
656
|
-
matcher: (item) => !isSummaryLikeMemory(item)
|
|
657
|
+
matcher: (item) => !isSummaryLikeMemory$1(item)
|
|
657
658
|
});
|
|
658
659
|
const hasMore = items.length > limit;
|
|
659
660
|
const result = hasMore ? items.slice(0, limit) : items;
|
|
@@ -680,7 +681,7 @@ function memoryRoutes(getStore) {
|
|
|
680
681
|
offset,
|
|
681
682
|
project: c.req.query("project") || void 0,
|
|
682
683
|
scope: normalizeScope(c.req.query("scope")),
|
|
683
|
-
matcher: (item) => isSummaryLikeMemory(item)
|
|
684
|
+
matcher: (item) => isSummaryLikeMemory$1(item)
|
|
684
685
|
});
|
|
685
686
|
const hasMore = items.length > limit;
|
|
686
687
|
const result = hasMore ? items.slice(0, limit) : items;
|
|
@@ -719,7 +720,7 @@ function memoryRoutes(getStore) {
|
|
|
719
720
|
project: scopeProject
|
|
720
721
|
});
|
|
721
722
|
if (page.length === 0) break;
|
|
722
|
-
total += page.filter((item) => !isSummaryLikeMemory(item)).length;
|
|
723
|
+
total += page.filter((item) => !isSummaryLikeMemory$1(item)).length;
|
|
723
724
|
if (page.length < 200) break;
|
|
724
725
|
offset += page.length;
|
|
725
726
|
}
|
|
@@ -1288,6 +1289,9 @@ var SYNC_STALE_AFTER_SECONDS = 600;
|
|
|
1288
1289
|
var SYNC_PROTOCOL_VERSION = "2";
|
|
1289
1290
|
var LEGACY_SYNC_ACTOR_DISPLAY_NAME = "Legacy synced peer";
|
|
1290
1291
|
var LEGACY_SHARED_WORKSPACE_ID = "shared:legacy";
|
|
1292
|
+
function syncKeysDir() {
|
|
1293
|
+
return process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
|
|
1294
|
+
}
|
|
1291
1295
|
function intEnvOr(name, fallback) {
|
|
1292
1296
|
const value = Number.parseInt(process.env[name] ?? "", 10);
|
|
1293
1297
|
return Number.isFinite(value) ? value : fallback;
|
|
@@ -1324,8 +1328,8 @@ function pathWithQuery(url) {
|
|
|
1324
1328
|
const parsed = new URL(url);
|
|
1325
1329
|
return parsed.search ? `${parsed.pathname}${parsed.search}` : parsed.pathname;
|
|
1326
1330
|
}
|
|
1327
|
-
function unauthorizedPayload(reason) {
|
|
1328
|
-
if (process.env.CODEMEM_SYNC_AUTH_DIAGNOSTICS === "1") return {
|
|
1331
|
+
function unauthorizedPayload(reason, exposeReason = false) {
|
|
1332
|
+
if (exposeReason && process.env.CODEMEM_SYNC_AUTH_DIAGNOSTICS === "1") return {
|
|
1329
1333
|
error: "unauthorized",
|
|
1330
1334
|
reason
|
|
1331
1335
|
};
|
|
@@ -1397,6 +1401,123 @@ function authorizeSyncRequest(store, request, body) {
|
|
|
1397
1401
|
deviceId
|
|
1398
1402
|
};
|
|
1399
1403
|
}
|
|
1404
|
+
async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
1405
|
+
const grantId = (request.header("X-Codemem-Bootstrap-Grant") ?? "").trim();
|
|
1406
|
+
const deviceId = (request.header("X-Opencode-Device") ?? "").trim();
|
|
1407
|
+
const signature = request.header("X-Opencode-Signature") ?? "";
|
|
1408
|
+
const timestamp = request.header("X-Opencode-Timestamp") ?? "";
|
|
1409
|
+
const nonce = request.header("X-Opencode-Nonce") ?? "";
|
|
1410
|
+
if (!grantId || !deviceId || !signature || !timestamp || !nonce) return {
|
|
1411
|
+
ok: false,
|
|
1412
|
+
reason: "missing_bootstrap_grant_headers",
|
|
1413
|
+
deviceId
|
|
1414
|
+
};
|
|
1415
|
+
const config = readCoordinatorSyncConfig();
|
|
1416
|
+
if (!config.syncCoordinatorUrl || !config.syncCoordinatorAdminSecret) return {
|
|
1417
|
+
ok: false,
|
|
1418
|
+
reason: "bootstrap_grant_coordinator_not_configured",
|
|
1419
|
+
deviceId
|
|
1420
|
+
};
|
|
1421
|
+
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
1422
|
+
let verification;
|
|
1423
|
+
try {
|
|
1424
|
+
const [status, payload] = await requestJson("GET", `${buildBaseUrl(config.syncCoordinatorUrl)}/v1/admin/bootstrap-grants/${encodeURIComponent(grantId)}`, {
|
|
1425
|
+
headers: { "X-Codemem-Coordinator-Admin": config.syncCoordinatorAdminSecret },
|
|
1426
|
+
timeoutS: Math.max(1, config.syncCoordinatorTimeoutS)
|
|
1427
|
+
});
|
|
1428
|
+
if (status !== 200 || !payload) return {
|
|
1429
|
+
ok: false,
|
|
1430
|
+
reason: "bootstrap_grant_lookup_failed",
|
|
1431
|
+
deviceId
|
|
1432
|
+
};
|
|
1433
|
+
verification = payload;
|
|
1434
|
+
} catch {
|
|
1435
|
+
return {
|
|
1436
|
+
ok: false,
|
|
1437
|
+
reason: "bootstrap_grant_lookup_failed",
|
|
1438
|
+
deviceId
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
const grant = verification.grant;
|
|
1442
|
+
const workerEnrollment = verification.worker_enrollment;
|
|
1443
|
+
if (!grant || !workerEnrollment) return {
|
|
1444
|
+
ok: false,
|
|
1445
|
+
reason: "bootstrap_grant_invalid_payload",
|
|
1446
|
+
deviceId
|
|
1447
|
+
};
|
|
1448
|
+
if (grant.revoked_at) return {
|
|
1449
|
+
ok: false,
|
|
1450
|
+
reason: "bootstrap_grant_revoked",
|
|
1451
|
+
deviceId
|
|
1452
|
+
};
|
|
1453
|
+
if (String(workerEnrollment.device_id) !== String(grant.worker_device_id)) return {
|
|
1454
|
+
ok: false,
|
|
1455
|
+
reason: "bootstrap_grant_worker_enrollment_mismatch",
|
|
1456
|
+
deviceId
|
|
1457
|
+
};
|
|
1458
|
+
if (String(workerEnrollment.group_id) !== String(grant.group_id)) return {
|
|
1459
|
+
ok: false,
|
|
1460
|
+
reason: "bootstrap_grant_group_mismatch",
|
|
1461
|
+
deviceId
|
|
1462
|
+
};
|
|
1463
|
+
if (Number(workerEnrollment.enabled) !== 1) return {
|
|
1464
|
+
ok: false,
|
|
1465
|
+
reason: "bootstrap_grant_worker_disabled",
|
|
1466
|
+
deviceId
|
|
1467
|
+
};
|
|
1468
|
+
if (grant.worker_device_id !== deviceId) return {
|
|
1469
|
+
ok: false,
|
|
1470
|
+
reason: "bootstrap_grant_worker_mismatch",
|
|
1471
|
+
deviceId
|
|
1472
|
+
};
|
|
1473
|
+
if (grant.seed_device_id !== localDeviceId) return {
|
|
1474
|
+
ok: false,
|
|
1475
|
+
reason: "bootstrap_grant_seed_mismatch",
|
|
1476
|
+
deviceId
|
|
1477
|
+
};
|
|
1478
|
+
if (new Date(grant.expires_at) <= /* @__PURE__ */ new Date()) return {
|
|
1479
|
+
ok: false,
|
|
1480
|
+
reason: "bootstrap_grant_expired",
|
|
1481
|
+
deviceId
|
|
1482
|
+
};
|
|
1483
|
+
let valid = false;
|
|
1484
|
+
try {
|
|
1485
|
+
valid = verifySignature({
|
|
1486
|
+
method: request.method,
|
|
1487
|
+
pathWithQuery: pathWithQuery(request.url),
|
|
1488
|
+
bodyBytes: body,
|
|
1489
|
+
timestamp,
|
|
1490
|
+
nonce,
|
|
1491
|
+
signature,
|
|
1492
|
+
publicKey: String(workerEnrollment.public_key),
|
|
1493
|
+
deviceId
|
|
1494
|
+
});
|
|
1495
|
+
} catch {
|
|
1496
|
+
return {
|
|
1497
|
+
ok: false,
|
|
1498
|
+
reason: "bootstrap_grant_signature_verification_error",
|
|
1499
|
+
deviceId
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
if (!valid) return {
|
|
1503
|
+
ok: false,
|
|
1504
|
+
reason: "bootstrap_grant_invalid_signature",
|
|
1505
|
+
deviceId
|
|
1506
|
+
};
|
|
1507
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1508
|
+
if (!recordNonce(store.db, deviceId, nonce, createdAt)) return {
|
|
1509
|
+
ok: false,
|
|
1510
|
+
reason: "nonce_replay",
|
|
1511
|
+
deviceId
|
|
1512
|
+
};
|
|
1513
|
+
const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
|
|
1514
|
+
cleanupNonces(store.db, cutoff);
|
|
1515
|
+
return {
|
|
1516
|
+
ok: true,
|
|
1517
|
+
reason: "ok",
|
|
1518
|
+
deviceId
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1400
1521
|
function projectBasename(value) {
|
|
1401
1522
|
const project = String(value ?? "").trim().replaceAll("\\", "/");
|
|
1402
1523
|
if (!project) return "";
|
|
@@ -1795,29 +1916,43 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
1795
1916
|
}
|
|
1796
1917
|
app.get("/v1/status", (c) => {
|
|
1797
1918
|
const store = getStore();
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1919
|
+
return (async () => {
|
|
1920
|
+
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
1921
|
+
let preauthChecked = false;
|
|
1922
|
+
let bootstrapAttempted = false;
|
|
1923
|
+
if (!auth.ok) {
|
|
1924
|
+
if ((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()) {
|
|
1925
|
+
preauthChecked = true;
|
|
1926
|
+
const bootstrapLimited = rateLimitedResponse(c, c.req.path, false);
|
|
1927
|
+
if (bootstrapLimited) return bootstrapLimited;
|
|
1928
|
+
} else {
|
|
1929
|
+
preauthChecked = true;
|
|
1930
|
+
const unauthLimited = rateLimitedResponse(c, c.req.path, false);
|
|
1931
|
+
if (unauthLimited) return unauthLimited;
|
|
1932
|
+
}
|
|
1933
|
+
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
1934
|
+
bootstrapAttempted = true;
|
|
1935
|
+
}
|
|
1936
|
+
if (!auth.ok) {
|
|
1937
|
+
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}`);
|
|
1938
|
+
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
1939
|
+
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason), 401);
|
|
1940
|
+
}
|
|
1941
|
+
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
1942
|
+
if (limited) return limited;
|
|
1943
|
+
try {
|
|
1944
|
+
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
1945
|
+
const syncReset = getSyncResetState(store.db);
|
|
1946
|
+
return c.json({
|
|
1807
1947
|
device_id: deviceId,
|
|
1808
|
-
|
|
1809
|
-
|
|
1948
|
+
protocol_version: SYNC_PROTOCOL_VERSION,
|
|
1949
|
+
fingerprint,
|
|
1950
|
+
sync_reset: syncReset
|
|
1951
|
+
});
|
|
1952
|
+
} catch {
|
|
1953
|
+
return c.json({ error: "internal_error" }, 500);
|
|
1810
1954
|
}
|
|
1811
|
-
|
|
1812
|
-
return c.json({
|
|
1813
|
-
device_id: device.device_id,
|
|
1814
|
-
protocol_version: SYNC_PROTOCOL_VERSION,
|
|
1815
|
-
fingerprint: device.fingerprint,
|
|
1816
|
-
sync_reset: syncReset
|
|
1817
|
-
});
|
|
1818
|
-
} catch {
|
|
1819
|
-
return c.json({ error: "internal_error" }, 500);
|
|
1820
|
-
}
|
|
1955
|
+
})();
|
|
1821
1956
|
});
|
|
1822
1957
|
app.get("/v1/ops", (c) => {
|
|
1823
1958
|
const store = getStore();
|
|
@@ -1834,15 +1969,11 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
1834
1969
|
const snapshotId = c.req.query("snapshot_id") ?? null;
|
|
1835
1970
|
const baselineCursor = c.req.query("baseline_cursor") ?? null;
|
|
1836
1971
|
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 1e3)) : 200;
|
|
1837
|
-
|
|
1838
|
-
if (!localDeviceId) {
|
|
1839
|
-
const [deviceId] = ensureDeviceIdentity(store.db);
|
|
1840
|
-
localDeviceId = { device_id: deviceId };
|
|
1841
|
-
}
|
|
1972
|
+
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
1842
1973
|
const result = loadReplicationOpsForPeer(store.db, {
|
|
1843
1974
|
since,
|
|
1844
1975
|
limit,
|
|
1845
|
-
deviceId: localDeviceId
|
|
1976
|
+
deviceId: localDeviceId,
|
|
1846
1977
|
generation: Number.isFinite(generation) ? generation : null,
|
|
1847
1978
|
snapshotId,
|
|
1848
1979
|
baselineCursor
|
|
@@ -1869,42 +2000,63 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
1869
2000
|
});
|
|
1870
2001
|
app.get("/v1/snapshot", (c) => {
|
|
1871
2002
|
const store = getStore();
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2003
|
+
return (async () => {
|
|
2004
|
+
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2005
|
+
let preauthChecked = false;
|
|
2006
|
+
let bootstrapAttempted = false;
|
|
2007
|
+
if (!auth.ok) {
|
|
2008
|
+
if ((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()) {
|
|
2009
|
+
preauthChecked = true;
|
|
2010
|
+
const bootstrapLimited = rateLimitedResponse(c, c.req.path, false);
|
|
2011
|
+
if (bootstrapLimited) return bootstrapLimited;
|
|
2012
|
+
} else {
|
|
2013
|
+
preauthChecked = true;
|
|
2014
|
+
const unauthLimited = rateLimitedResponse(c, c.req.path, false);
|
|
2015
|
+
if (unauthLimited) return unauthLimited;
|
|
2016
|
+
}
|
|
2017
|
+
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
2018
|
+
bootstrapAttempted = true;
|
|
2019
|
+
}
|
|
2020
|
+
if (!auth.ok) {
|
|
2021
|
+
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}`);
|
|
2022
|
+
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
2023
|
+
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason), 401);
|
|
2024
|
+
}
|
|
2025
|
+
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2026
|
+
if (limited) return limited;
|
|
2027
|
+
try {
|
|
2028
|
+
const rawGeneration = c.req.query("generation");
|
|
2029
|
+
const generation = rawGeneration != null && rawGeneration.trim().length > 0 ? Number.parseInt(rawGeneration, 10) : null;
|
|
2030
|
+
const snapshotId = c.req.query("snapshot_id") ?? null;
|
|
2031
|
+
const baselineCursor = c.req.query("baseline_cursor") ?? null;
|
|
2032
|
+
const pageToken = c.req.query("page_token") ?? null;
|
|
2033
|
+
const rawLimit = Number.parseInt(c.req.query("limit") ?? "200", 10);
|
|
2034
|
+
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 5e3)) : 200;
|
|
2035
|
+
if (generation == null || !Number.isFinite(generation)) return c.json({ error: "missing_generation" }, 400);
|
|
2036
|
+
if (!snapshotId) return c.json({ error: "missing_snapshot_id" }, 400);
|
|
2037
|
+
const result = loadMemorySnapshotPageForPeer(store.db, {
|
|
2038
|
+
generation,
|
|
2039
|
+
snapshotId,
|
|
2040
|
+
baselineCursor,
|
|
2041
|
+
pageToken,
|
|
2042
|
+
limit,
|
|
2043
|
+
peerDeviceId: auth.deviceId
|
|
2044
|
+
});
|
|
2045
|
+
return c.json({
|
|
2046
|
+
generation: result.boundary.generation,
|
|
2047
|
+
snapshot_id: result.boundary.snapshot_id,
|
|
2048
|
+
baseline_cursor: result.boundary.baseline_cursor,
|
|
2049
|
+
retained_floor_cursor: result.boundary.retained_floor_cursor,
|
|
2050
|
+
items: result.items,
|
|
2051
|
+
next_page_token: result.nextPageToken,
|
|
2052
|
+
has_more: result.hasMore
|
|
2053
|
+
});
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
const message = err instanceof Error ? err.message : "";
|
|
2056
|
+
if (message === "generation_mismatch" || message === "boundary_mismatch") return c.json({ error: message }, 409);
|
|
2057
|
+
return c.json({ error: "internal_error" }, 500);
|
|
2058
|
+
}
|
|
2059
|
+
})();
|
|
1908
2060
|
});
|
|
1909
2061
|
app.post("/v1/ops", async (c) => {
|
|
1910
2062
|
const store = getStore();
|
|
@@ -1931,13 +2083,9 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
1931
2083
|
reason: "device_id_mismatch",
|
|
1932
2084
|
op_id: op.op_id
|
|
1933
2085
|
}, 400);
|
|
1934
|
-
|
|
1935
|
-
if (!localDeviceId) {
|
|
1936
|
-
const [deviceId] = ensureDeviceIdentity(store.db);
|
|
1937
|
-
localDeviceId = { device_id: deviceId };
|
|
1938
|
-
}
|
|
2086
|
+
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
1939
2087
|
const filteredInbound = filterOpsForPeer(store, peerDeviceId, normalizedOps);
|
|
1940
|
-
const result = applyReplicationOps(store.db, filteredInbound.allowed, localDeviceId
|
|
2088
|
+
const result = applyReplicationOps(store.db, filteredInbound.allowed, localDeviceId);
|
|
1941
2089
|
return c.json({
|
|
1942
2090
|
...result,
|
|
1943
2091
|
skipped: result.skipped + filteredInbound.skipped
|
|
@@ -2036,7 +2184,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2036
2184
|
});
|
|
2037
2185
|
const peersMap = {};
|
|
2038
2186
|
for (const peer of peersItems) peersMap[String(peer.peer_device_id)] = peer.status;
|
|
2039
|
-
const
|
|
2187
|
+
const attemptRows = d.select({
|
|
2040
2188
|
peer_device_id: schema.syncAttempts.peer_device_id,
|
|
2041
2189
|
ok: schema.syncAttempts.ok,
|
|
2042
2190
|
error: schema.syncAttempts.error,
|
|
@@ -2044,11 +2192,20 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2044
2192
|
finished_at: schema.syncAttempts.finished_at,
|
|
2045
2193
|
ops_in: schema.syncAttempts.ops_in,
|
|
2046
2194
|
ops_out: schema.syncAttempts.ops_out
|
|
2047
|
-
}).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(25).all()
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2195
|
+
}).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(25).all();
|
|
2196
|
+
const peerAddressMap = /* @__PURE__ */ new Map();
|
|
2197
|
+
store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all().forEach((peerRow) => {
|
|
2198
|
+
const addrs = safeJsonList(peerRow.addresses_json);
|
|
2199
|
+
if (addrs.length) peerAddressMap.set(String(peerRow.peer_device_id ?? ""), addrs);
|
|
2200
|
+
});
|
|
2201
|
+
const attemptsItems = attemptRows.map((row) => {
|
|
2202
|
+
const addrs = showDiag ? peerAddressMap.get(String(row.peer_device_id ?? "")) : void 0;
|
|
2203
|
+
return {
|
|
2204
|
+
...row,
|
|
2205
|
+
status: attemptStatus(row),
|
|
2206
|
+
address: addrs?.length ? addrs[0] : null
|
|
2207
|
+
};
|
|
2208
|
+
});
|
|
2052
2209
|
const latestAttemptError = String(attemptsItems[0]?.error || "").trim();
|
|
2053
2210
|
const statusBlock = {
|
|
2054
2211
|
...statusPayload,
|
|
@@ -2118,7 +2275,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2118
2275
|
const d = drizzle(store.db, { schema });
|
|
2119
2276
|
const includeMerged = queryBool(c.req.query("includeMerged"));
|
|
2120
2277
|
const query = d.select().from(schema.actors);
|
|
2121
|
-
const rows = includeMerged ? query.orderBy(schema.actors.display_name).all() : query.where(ne(schema.actors.status, "merged")).orderBy(schema.actors.display_name).all();
|
|
2278
|
+
const rows = includeMerged ? query.orderBy(schema.actors.display_name).all() : query.where(and(ne(schema.actors.status, "merged"), ne(schema.actors.status, "deactivated"))).orderBy(schema.actors.display_name).all();
|
|
2122
2279
|
return c.json({ items: rows });
|
|
2123
2280
|
}
|
|
2124
2281
|
});
|
|
@@ -2162,7 +2319,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2162
2319
|
publicKey = String(deviceRow.public_key);
|
|
2163
2320
|
fingerprint = String(deviceRow.fingerprint);
|
|
2164
2321
|
} else try {
|
|
2165
|
-
const [id, fp] = ensureDeviceIdentity(store.db);
|
|
2322
|
+
const [id, fp] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
2166
2323
|
deviceId = id;
|
|
2167
2324
|
fingerprint = fp;
|
|
2168
2325
|
publicKey = d.select({ public_key: schema.syncDevice.public_key }).from(schema.syncDevice).where(eq(schema.syncDevice.device_id, id)).get()?.public_key ?? "";
|
|
@@ -2371,7 +2528,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2371
2528
|
if (!primary || !secondary) return c.json({ error: "actor not found" }, 404);
|
|
2372
2529
|
if (primary.status !== "active") return c.json({ error: "primary actor not active" }, 409);
|
|
2373
2530
|
if (secondary.status !== "active") return c.json({ error: "secondary actor not active" }, 409);
|
|
2374
|
-
if (secondary.is_local) return c.json({ error: "cannot merge local actor" }, 409);
|
|
2531
|
+
if (secondary.is_local && secondary.actor_id === store.actorId) return c.json({ error: "cannot merge this device's own local actor" }, 409);
|
|
2375
2532
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2376
2533
|
const mergedCount = store.db.transaction(() => {
|
|
2377
2534
|
const peerUpdate = d.update(schema.syncPeers).set({ actor_id: primaryActorId }).where(eq(schema.syncPeers.actor_id, secondaryActorId)).run();
|
|
@@ -2385,6 +2542,29 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2385
2542
|
})();
|
|
2386
2543
|
return c.json({ merged_count: mergedCount });
|
|
2387
2544
|
});
|
|
2545
|
+
app.post("/api/sync/actors/deactivate", async (c) => {
|
|
2546
|
+
const store = getStore();
|
|
2547
|
+
let body;
|
|
2548
|
+
try {
|
|
2549
|
+
body = await c.req.json();
|
|
2550
|
+
} catch {
|
|
2551
|
+
return c.json({ error: "invalid json" }, 400);
|
|
2552
|
+
}
|
|
2553
|
+
const actorId = String(body.actor_id ?? "").trim();
|
|
2554
|
+
if (!actorId) return c.json({ error: "actor_id required" }, 400);
|
|
2555
|
+
if (actorId === store.actorId) return c.json({ error: "cannot deactivate this device's own local actor" }, 409);
|
|
2556
|
+
const d = drizzle(store.db, { schema });
|
|
2557
|
+
const actor = d.select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
|
|
2558
|
+
if (!actor) return c.json({ error: "actor not found" }, 404);
|
|
2559
|
+
if (actor.status !== "active") return c.json({ error: "actor not active" }, 409);
|
|
2560
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2561
|
+
d.update(schema.actors).set({
|
|
2562
|
+
status: "deactivated",
|
|
2563
|
+
updated_at: now
|
|
2564
|
+
}).where(eq(schema.actors.actor_id, actorId)).run();
|
|
2565
|
+
d.update(schema.syncPeers).set({ actor_id: null }).where(eq(schema.syncPeers.actor_id, actorId)).run();
|
|
2566
|
+
return c.json({ ok: true });
|
|
2567
|
+
});
|
|
2388
2568
|
app.post("/api/sync/legacy-devices/claim", async (c) => {
|
|
2389
2569
|
const store = getStore();
|
|
2390
2570
|
let body;
|
|
@@ -2520,6 +2700,47 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2520
2700
|
return c.json({ error: message }, message.includes("request_not_found") || message.includes("not found") ? 404 : 400);
|
|
2521
2701
|
}
|
|
2522
2702
|
});
|
|
2703
|
+
app.get("/api/sync/bootstrap-grants", async (c) => {
|
|
2704
|
+
const groupId = String(c.req.query("group_id") ?? "").trim();
|
|
2705
|
+
if (!groupId) return c.json({ error: "group_id_required" }, 400);
|
|
2706
|
+
const config = readCoordinatorSyncConfig();
|
|
2707
|
+
try {
|
|
2708
|
+
const items = await coordinatorListBootstrapGrantsAction({
|
|
2709
|
+
groupId,
|
|
2710
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
2711
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
2712
|
+
});
|
|
2713
|
+
return c.json({ items });
|
|
2714
|
+
} catch (error) {
|
|
2715
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
2716
|
+
return c.json({ error: message }, 500);
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
app.post("/api/sync/bootstrap-grants/revoke", async (c) => {
|
|
2720
|
+
let body;
|
|
2721
|
+
try {
|
|
2722
|
+
body = await c.req.json();
|
|
2723
|
+
} catch {
|
|
2724
|
+
return c.json({ error: "invalid json" }, 400);
|
|
2725
|
+
}
|
|
2726
|
+
const grantId = String(body.grant_id ?? "").trim();
|
|
2727
|
+
if (!grantId) return c.json({ error: "grant_id_required" }, 400);
|
|
2728
|
+
const config = readCoordinatorSyncConfig();
|
|
2729
|
+
try {
|
|
2730
|
+
if (!await coordinatorRevokeBootstrapGrantAction({
|
|
2731
|
+
grantId,
|
|
2732
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
2733
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
2734
|
+
})) return c.json({ error: "grant_not_found" }, 404);
|
|
2735
|
+
return c.json({
|
|
2736
|
+
ok: true,
|
|
2737
|
+
grant_id: grantId
|
|
2738
|
+
});
|
|
2739
|
+
} catch (error) {
|
|
2740
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
2741
|
+
return c.json({ error: message }, 500);
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2523
2744
|
app.delete("/api/sync/peers/:peer_device_id", (c) => {
|
|
2524
2745
|
const store = getStore();
|
|
2525
2746
|
{
|