@codemem/server 0.22.4 → 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 +270 -81
- 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 +11 -8
- package/static/app.js +18 -11
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, 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";
|
|
@@ -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
|
|
@@ -2171,7 +2319,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2171
2319
|
publicKey = String(deviceRow.public_key);
|
|
2172
2320
|
fingerprint = String(deviceRow.fingerprint);
|
|
2173
2321
|
} else try {
|
|
2174
|
-
const [id, fp] = ensureDeviceIdentity(store.db);
|
|
2322
|
+
const [id, fp] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
2175
2323
|
deviceId = id;
|
|
2176
2324
|
fingerprint = fp;
|
|
2177
2325
|
publicKey = d.select({ public_key: schema.syncDevice.public_key }).from(schema.syncDevice).where(eq(schema.syncDevice.device_id, id)).get()?.public_key ?? "";
|
|
@@ -2552,6 +2700,47 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2552
2700
|
return c.json({ error: message }, message.includes("request_not_found") || message.includes("not found") ? 404 : 400);
|
|
2553
2701
|
}
|
|
2554
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
|
+
});
|
|
2555
2744
|
app.delete("/api/sync/peers/:peer_device_id", (c) => {
|
|
2556
2745
|
const store = getStore();
|
|
2557
2746
|
{
|