@codemem/server 0.22.4 → 0.24.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 +300 -82
- package/dist/index.js.map +1 -1
- package/dist/routes/config.d.ts.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 +363 -96
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";
|
|
@@ -193,6 +193,15 @@ var ALLOWED_KEYS = [
|
|
|
193
193
|
"observer_base_url",
|
|
194
194
|
"observer_provider",
|
|
195
195
|
"observer_model",
|
|
196
|
+
"observer_tier_routing_enabled",
|
|
197
|
+
"observer_simple_model",
|
|
198
|
+
"observer_simple_temperature",
|
|
199
|
+
"observer_rich_model",
|
|
200
|
+
"observer_rich_temperature",
|
|
201
|
+
"observer_rich_openai_use_responses",
|
|
202
|
+
"observer_rich_reasoning_effort",
|
|
203
|
+
"observer_rich_reasoning_summary",
|
|
204
|
+
"observer_rich_max_output_tokens",
|
|
196
205
|
"observer_runtime",
|
|
197
206
|
"observer_auth_source",
|
|
198
207
|
"observer_auth_file",
|
|
@@ -221,6 +230,8 @@ var DEFAULTS = {
|
|
|
221
230
|
claude_command: ["claude"],
|
|
222
231
|
observer_runtime: "api_http",
|
|
223
232
|
observer_auth_source: "auto",
|
|
233
|
+
observer_tier_routing_enabled: false,
|
|
234
|
+
observer_rich_openai_use_responses: false,
|
|
224
235
|
observer_auth_command: [],
|
|
225
236
|
observer_auth_timeout_ms: 1500,
|
|
226
237
|
observer_auth_cache_ttl_s: 300,
|
|
@@ -319,6 +330,13 @@ function parsePositiveInt(value, allowZero = false) {
|
|
|
319
330
|
if (allowZero) return parsed >= 0 ? parsed : null;
|
|
320
331
|
return parsed > 0 ? parsed : null;
|
|
321
332
|
}
|
|
333
|
+
function parseFiniteNumber(value, allowZero = true) {
|
|
334
|
+
if (typeof value === "boolean") return null;
|
|
335
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim().length > 0 ? Number(value.trim()) : NaN;
|
|
336
|
+
if (!Number.isFinite(parsed)) return null;
|
|
337
|
+
if (allowZero) return parsed >= 0 ? parsed : null;
|
|
338
|
+
return parsed > 0 ? parsed : null;
|
|
339
|
+
}
|
|
322
340
|
function asStringMap(value) {
|
|
323
341
|
if (value == null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
324
342
|
const parsed = {};
|
|
@@ -388,13 +406,24 @@ function validateAndApplyUpdate(configData, key, value, providers) {
|
|
|
388
406
|
configData[key] = value;
|
|
389
407
|
return null;
|
|
390
408
|
}
|
|
391
|
-
if (key === "
|
|
409
|
+
if (key === "observer_tier_routing_enabled" || key === "observer_rich_openai_use_responses") {
|
|
410
|
+
if (typeof value !== "boolean") return `${key} must be boolean`;
|
|
411
|
+
configData[key] = value;
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
if (key === "observer_base_url" || key === "observer_model" || key === "observer_simple_model" || key === "observer_rich_model" || key === "observer_rich_reasoning_effort" || key === "observer_rich_reasoning_summary" || key === "observer_auth_file" || key === "sync_host" || key === "sync_coordinator_url" || key === "sync_coordinator_group") {
|
|
392
415
|
if (typeof value !== "string") return `${key} must be string`;
|
|
393
416
|
const trimmed = value.trim();
|
|
394
417
|
if (!trimmed) delete configData[key];
|
|
395
418
|
else configData[key] = trimmed;
|
|
396
419
|
return null;
|
|
397
420
|
}
|
|
421
|
+
if (key === "observer_simple_temperature" || key === "observer_rich_temperature") {
|
|
422
|
+
const parsed = parseFiniteNumber(value, true);
|
|
423
|
+
if (parsed == null) return `${key} must be non-negative number`;
|
|
424
|
+
configData[key] = parsed;
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
398
427
|
const allowZero = key === "observer_auth_cache_ttl_s";
|
|
399
428
|
const parsed = parsePositiveInt(value, allowZero);
|
|
400
429
|
if (parsed == null) return `${key} must be ${allowZero ? "non-negative int" : "int"}`;
|
|
@@ -591,14 +620,15 @@ function queryMemoryPage(store, options) {
|
|
|
591
620
|
ORDER BY memory_items.created_at DESC
|
|
592
621
|
LIMIT ? OFFSET ?`).all(...filterResult.params, options.limit + 1, options.offset).map((row) => ({
|
|
593
622
|
...row,
|
|
623
|
+
kind: canonicalMemoryKind(row.kind ?? null, row.metadata_json),
|
|
594
624
|
metadata_json: fromJson(row.metadata_json ?? null)
|
|
595
625
|
}));
|
|
596
626
|
}
|
|
597
|
-
function isSummaryLikeMemory(item) {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
627
|
+
function isSummaryLikeMemory$1(item) {
|
|
628
|
+
return isSummaryLikeMemory({
|
|
629
|
+
kind: item.kind,
|
|
630
|
+
metadata: item.metadata_json
|
|
631
|
+
});
|
|
602
632
|
}
|
|
603
633
|
function selectMemoryPage(store, options) {
|
|
604
634
|
const pageSize = Math.max(options.limit + options.offset + 10, 50);
|
|
@@ -653,7 +683,7 @@ function memoryRoutes(getStore) {
|
|
|
653
683
|
offset,
|
|
654
684
|
project: c.req.query("project") || void 0,
|
|
655
685
|
scope: normalizeScope(c.req.query("scope")),
|
|
656
|
-
matcher: (item) => !isSummaryLikeMemory(item)
|
|
686
|
+
matcher: (item) => !isSummaryLikeMemory$1(item)
|
|
657
687
|
});
|
|
658
688
|
const hasMore = items.length > limit;
|
|
659
689
|
const result = hasMore ? items.slice(0, limit) : items;
|
|
@@ -680,7 +710,7 @@ function memoryRoutes(getStore) {
|
|
|
680
710
|
offset,
|
|
681
711
|
project: c.req.query("project") || void 0,
|
|
682
712
|
scope: normalizeScope(c.req.query("scope")),
|
|
683
|
-
matcher: (item) => isSummaryLikeMemory(item)
|
|
713
|
+
matcher: (item) => isSummaryLikeMemory$1(item)
|
|
684
714
|
});
|
|
685
715
|
const hasMore = items.length > limit;
|
|
686
716
|
const result = hasMore ? items.slice(0, limit) : items;
|
|
@@ -719,7 +749,7 @@ function memoryRoutes(getStore) {
|
|
|
719
749
|
project: scopeProject
|
|
720
750
|
});
|
|
721
751
|
if (page.length === 0) break;
|
|
722
|
-
total += page.filter((item) => !isSummaryLikeMemory(item)).length;
|
|
752
|
+
total += page.filter((item) => !isSummaryLikeMemory$1(item)).length;
|
|
723
753
|
if (page.length < 200) break;
|
|
724
754
|
offset += page.length;
|
|
725
755
|
}
|
|
@@ -1288,6 +1318,9 @@ var SYNC_STALE_AFTER_SECONDS = 600;
|
|
|
1288
1318
|
var SYNC_PROTOCOL_VERSION = "2";
|
|
1289
1319
|
var LEGACY_SYNC_ACTOR_DISPLAY_NAME = "Legacy synced peer";
|
|
1290
1320
|
var LEGACY_SHARED_WORKSPACE_ID = "shared:legacy";
|
|
1321
|
+
function syncKeysDir() {
|
|
1322
|
+
return process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
|
|
1323
|
+
}
|
|
1291
1324
|
function intEnvOr(name, fallback) {
|
|
1292
1325
|
const value = Number.parseInt(process.env[name] ?? "", 10);
|
|
1293
1326
|
return Number.isFinite(value) ? value : fallback;
|
|
@@ -1324,8 +1357,8 @@ function pathWithQuery(url) {
|
|
|
1324
1357
|
const parsed = new URL(url);
|
|
1325
1358
|
return parsed.search ? `${parsed.pathname}${parsed.search}` : parsed.pathname;
|
|
1326
1359
|
}
|
|
1327
|
-
function unauthorizedPayload(reason) {
|
|
1328
|
-
if (process.env.CODEMEM_SYNC_AUTH_DIAGNOSTICS === "1") return {
|
|
1360
|
+
function unauthorizedPayload(reason, exposeReason = false) {
|
|
1361
|
+
if (exposeReason && process.env.CODEMEM_SYNC_AUTH_DIAGNOSTICS === "1") return {
|
|
1329
1362
|
error: "unauthorized",
|
|
1330
1363
|
reason
|
|
1331
1364
|
};
|
|
@@ -1397,6 +1430,123 @@ function authorizeSyncRequest(store, request, body) {
|
|
|
1397
1430
|
deviceId
|
|
1398
1431
|
};
|
|
1399
1432
|
}
|
|
1433
|
+
async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
1434
|
+
const grantId = (request.header("X-Codemem-Bootstrap-Grant") ?? "").trim();
|
|
1435
|
+
const deviceId = (request.header("X-Opencode-Device") ?? "").trim();
|
|
1436
|
+
const signature = request.header("X-Opencode-Signature") ?? "";
|
|
1437
|
+
const timestamp = request.header("X-Opencode-Timestamp") ?? "";
|
|
1438
|
+
const nonce = request.header("X-Opencode-Nonce") ?? "";
|
|
1439
|
+
if (!grantId || !deviceId || !signature || !timestamp || !nonce) return {
|
|
1440
|
+
ok: false,
|
|
1441
|
+
reason: "missing_bootstrap_grant_headers",
|
|
1442
|
+
deviceId
|
|
1443
|
+
};
|
|
1444
|
+
const config = readCoordinatorSyncConfig();
|
|
1445
|
+
if (!config.syncCoordinatorUrl || !config.syncCoordinatorAdminSecret) return {
|
|
1446
|
+
ok: false,
|
|
1447
|
+
reason: "bootstrap_grant_coordinator_not_configured",
|
|
1448
|
+
deviceId
|
|
1449
|
+
};
|
|
1450
|
+
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
1451
|
+
let verification;
|
|
1452
|
+
try {
|
|
1453
|
+
const [status, payload] = await requestJson("GET", `${buildBaseUrl(config.syncCoordinatorUrl)}/v1/admin/bootstrap-grants/${encodeURIComponent(grantId)}`, {
|
|
1454
|
+
headers: { "X-Codemem-Coordinator-Admin": config.syncCoordinatorAdminSecret },
|
|
1455
|
+
timeoutS: Math.max(1, config.syncCoordinatorTimeoutS)
|
|
1456
|
+
});
|
|
1457
|
+
if (status !== 200 || !payload) return {
|
|
1458
|
+
ok: false,
|
|
1459
|
+
reason: "bootstrap_grant_lookup_failed",
|
|
1460
|
+
deviceId
|
|
1461
|
+
};
|
|
1462
|
+
verification = payload;
|
|
1463
|
+
} catch {
|
|
1464
|
+
return {
|
|
1465
|
+
ok: false,
|
|
1466
|
+
reason: "bootstrap_grant_lookup_failed",
|
|
1467
|
+
deviceId
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
const grant = verification.grant;
|
|
1471
|
+
const workerEnrollment = verification.worker_enrollment;
|
|
1472
|
+
if (!grant || !workerEnrollment) return {
|
|
1473
|
+
ok: false,
|
|
1474
|
+
reason: "bootstrap_grant_invalid_payload",
|
|
1475
|
+
deviceId
|
|
1476
|
+
};
|
|
1477
|
+
if (grant.revoked_at) return {
|
|
1478
|
+
ok: false,
|
|
1479
|
+
reason: "bootstrap_grant_revoked",
|
|
1480
|
+
deviceId
|
|
1481
|
+
};
|
|
1482
|
+
if (String(workerEnrollment.device_id) !== String(grant.worker_device_id)) return {
|
|
1483
|
+
ok: false,
|
|
1484
|
+
reason: "bootstrap_grant_worker_enrollment_mismatch",
|
|
1485
|
+
deviceId
|
|
1486
|
+
};
|
|
1487
|
+
if (String(workerEnrollment.group_id) !== String(grant.group_id)) return {
|
|
1488
|
+
ok: false,
|
|
1489
|
+
reason: "bootstrap_grant_group_mismatch",
|
|
1490
|
+
deviceId
|
|
1491
|
+
};
|
|
1492
|
+
if (Number(workerEnrollment.enabled) !== 1) return {
|
|
1493
|
+
ok: false,
|
|
1494
|
+
reason: "bootstrap_grant_worker_disabled",
|
|
1495
|
+
deviceId
|
|
1496
|
+
};
|
|
1497
|
+
if (grant.worker_device_id !== deviceId) return {
|
|
1498
|
+
ok: false,
|
|
1499
|
+
reason: "bootstrap_grant_worker_mismatch",
|
|
1500
|
+
deviceId
|
|
1501
|
+
};
|
|
1502
|
+
if (grant.seed_device_id !== localDeviceId) return {
|
|
1503
|
+
ok: false,
|
|
1504
|
+
reason: "bootstrap_grant_seed_mismatch",
|
|
1505
|
+
deviceId
|
|
1506
|
+
};
|
|
1507
|
+
if (new Date(grant.expires_at) <= /* @__PURE__ */ new Date()) return {
|
|
1508
|
+
ok: false,
|
|
1509
|
+
reason: "bootstrap_grant_expired",
|
|
1510
|
+
deviceId
|
|
1511
|
+
};
|
|
1512
|
+
let valid = false;
|
|
1513
|
+
try {
|
|
1514
|
+
valid = verifySignature({
|
|
1515
|
+
method: request.method,
|
|
1516
|
+
pathWithQuery: pathWithQuery(request.url),
|
|
1517
|
+
bodyBytes: body,
|
|
1518
|
+
timestamp,
|
|
1519
|
+
nonce,
|
|
1520
|
+
signature,
|
|
1521
|
+
publicKey: String(workerEnrollment.public_key),
|
|
1522
|
+
deviceId
|
|
1523
|
+
});
|
|
1524
|
+
} catch {
|
|
1525
|
+
return {
|
|
1526
|
+
ok: false,
|
|
1527
|
+
reason: "bootstrap_grant_signature_verification_error",
|
|
1528
|
+
deviceId
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
if (!valid) return {
|
|
1532
|
+
ok: false,
|
|
1533
|
+
reason: "bootstrap_grant_invalid_signature",
|
|
1534
|
+
deviceId
|
|
1535
|
+
};
|
|
1536
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1537
|
+
if (!recordNonce(store.db, deviceId, nonce, createdAt)) return {
|
|
1538
|
+
ok: false,
|
|
1539
|
+
reason: "nonce_replay",
|
|
1540
|
+
deviceId
|
|
1541
|
+
};
|
|
1542
|
+
const cutoff = (/* @__PURE__ */ new Date(Date.now() - DEFAULT_TIME_WINDOW_S * 2 * 1e3)).toISOString();
|
|
1543
|
+
cleanupNonces(store.db, cutoff);
|
|
1544
|
+
return {
|
|
1545
|
+
ok: true,
|
|
1546
|
+
reason: "ok",
|
|
1547
|
+
deviceId
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1400
1550
|
function projectBasename(value) {
|
|
1401
1551
|
const project = String(value ?? "").trim().replaceAll("\\", "/");
|
|
1402
1552
|
if (!project) return "";
|
|
@@ -1795,29 +1945,43 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
1795
1945
|
}
|
|
1796
1946
|
app.get("/v1/status", (c) => {
|
|
1797
1947
|
const store = getStore();
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1948
|
+
return (async () => {
|
|
1949
|
+
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
1950
|
+
let preauthChecked = false;
|
|
1951
|
+
let bootstrapAttempted = false;
|
|
1952
|
+
if (!auth.ok) {
|
|
1953
|
+
if ((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()) {
|
|
1954
|
+
preauthChecked = true;
|
|
1955
|
+
const bootstrapLimited = rateLimitedResponse(c, c.req.path, false);
|
|
1956
|
+
if (bootstrapLimited) return bootstrapLimited;
|
|
1957
|
+
} else {
|
|
1958
|
+
preauthChecked = true;
|
|
1959
|
+
const unauthLimited = rateLimitedResponse(c, c.req.path, false);
|
|
1960
|
+
if (unauthLimited) return unauthLimited;
|
|
1961
|
+
}
|
|
1962
|
+
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
1963
|
+
bootstrapAttempted = true;
|
|
1964
|
+
}
|
|
1965
|
+
if (!auth.ok) {
|
|
1966
|
+
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}`);
|
|
1967
|
+
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
1968
|
+
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason), 401);
|
|
1969
|
+
}
|
|
1970
|
+
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
1971
|
+
if (limited) return limited;
|
|
1972
|
+
try {
|
|
1973
|
+
const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
1974
|
+
const syncReset = getSyncResetState(store.db);
|
|
1975
|
+
return c.json({
|
|
1807
1976
|
device_id: deviceId,
|
|
1808
|
-
|
|
1809
|
-
|
|
1977
|
+
protocol_version: SYNC_PROTOCOL_VERSION,
|
|
1978
|
+
fingerprint,
|
|
1979
|
+
sync_reset: syncReset
|
|
1980
|
+
});
|
|
1981
|
+
} catch {
|
|
1982
|
+
return c.json({ error: "internal_error" }, 500);
|
|
1810
1983
|
}
|
|
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
|
-
}
|
|
1984
|
+
})();
|
|
1821
1985
|
});
|
|
1822
1986
|
app.get("/v1/ops", (c) => {
|
|
1823
1987
|
const store = getStore();
|
|
@@ -1834,15 +1998,11 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
1834
1998
|
const snapshotId = c.req.query("snapshot_id") ?? null;
|
|
1835
1999
|
const baselineCursor = c.req.query("baseline_cursor") ?? null;
|
|
1836
2000
|
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
|
-
}
|
|
2001
|
+
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
1842
2002
|
const result = loadReplicationOpsForPeer(store.db, {
|
|
1843
2003
|
since,
|
|
1844
2004
|
limit,
|
|
1845
|
-
deviceId: localDeviceId
|
|
2005
|
+
deviceId: localDeviceId,
|
|
1846
2006
|
generation: Number.isFinite(generation) ? generation : null,
|
|
1847
2007
|
snapshotId,
|
|
1848
2008
|
baselineCursor
|
|
@@ -1869,42 +2029,63 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
1869
2029
|
});
|
|
1870
2030
|
app.get("/v1/snapshot", (c) => {
|
|
1871
2031
|
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
|
-
|
|
2032
|
+
return (async () => {
|
|
2033
|
+
let auth = authorizeSyncRequest(store, c.req, Buffer.alloc(0));
|
|
2034
|
+
let preauthChecked = false;
|
|
2035
|
+
let bootstrapAttempted = false;
|
|
2036
|
+
if (!auth.ok) {
|
|
2037
|
+
if ((c.req.header("X-Codemem-Bootstrap-Grant") ?? "").trim()) {
|
|
2038
|
+
preauthChecked = true;
|
|
2039
|
+
const bootstrapLimited = rateLimitedResponse(c, c.req.path, false);
|
|
2040
|
+
if (bootstrapLimited) return bootstrapLimited;
|
|
2041
|
+
} else {
|
|
2042
|
+
preauthChecked = true;
|
|
2043
|
+
const unauthLimited = rateLimitedResponse(c, c.req.path, false);
|
|
2044
|
+
if (unauthLimited) return unauthLimited;
|
|
2045
|
+
}
|
|
2046
|
+
auth = await authorizeBootstrapGrantRequest(store, c.req, Buffer.alloc(0));
|
|
2047
|
+
bootstrapAttempted = true;
|
|
2048
|
+
}
|
|
2049
|
+
if (!auth.ok) {
|
|
2050
|
+
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}`);
|
|
2051
|
+
const wireReason = bootstrapAttempted ? "bootstrap_grant_invalid" : auth.reason;
|
|
2052
|
+
return (preauthChecked ? null : rateLimitedResponse(c, c.req.path, false)) ?? c.json(unauthorizedPayload(wireReason), 401);
|
|
2053
|
+
}
|
|
2054
|
+
const limited = rateLimitedResponse(c, auth.deviceId, true);
|
|
2055
|
+
if (limited) return limited;
|
|
2056
|
+
try {
|
|
2057
|
+
const rawGeneration = c.req.query("generation");
|
|
2058
|
+
const generation = rawGeneration != null && rawGeneration.trim().length > 0 ? Number.parseInt(rawGeneration, 10) : null;
|
|
2059
|
+
const snapshotId = c.req.query("snapshot_id") ?? null;
|
|
2060
|
+
const baselineCursor = c.req.query("baseline_cursor") ?? null;
|
|
2061
|
+
const pageToken = c.req.query("page_token") ?? null;
|
|
2062
|
+
const rawLimit = Number.parseInt(c.req.query("limit") ?? "200", 10);
|
|
2063
|
+
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.min(rawLimit, 5e3)) : 200;
|
|
2064
|
+
if (generation == null || !Number.isFinite(generation)) return c.json({ error: "missing_generation" }, 400);
|
|
2065
|
+
if (!snapshotId) return c.json({ error: "missing_snapshot_id" }, 400);
|
|
2066
|
+
const result = loadMemorySnapshotPageForPeer(store.db, {
|
|
2067
|
+
generation,
|
|
2068
|
+
snapshotId,
|
|
2069
|
+
baselineCursor,
|
|
2070
|
+
pageToken,
|
|
2071
|
+
limit,
|
|
2072
|
+
peerDeviceId: auth.deviceId
|
|
2073
|
+
});
|
|
2074
|
+
return c.json({
|
|
2075
|
+
generation: result.boundary.generation,
|
|
2076
|
+
snapshot_id: result.boundary.snapshot_id,
|
|
2077
|
+
baseline_cursor: result.boundary.baseline_cursor,
|
|
2078
|
+
retained_floor_cursor: result.boundary.retained_floor_cursor,
|
|
2079
|
+
items: result.items,
|
|
2080
|
+
next_page_token: result.nextPageToken,
|
|
2081
|
+
has_more: result.hasMore
|
|
2082
|
+
});
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
const message = err instanceof Error ? err.message : "";
|
|
2085
|
+
if (message === "generation_mismatch" || message === "boundary_mismatch") return c.json({ error: message }, 409);
|
|
2086
|
+
return c.json({ error: "internal_error" }, 500);
|
|
2087
|
+
}
|
|
2088
|
+
})();
|
|
1908
2089
|
});
|
|
1909
2090
|
app.post("/v1/ops", async (c) => {
|
|
1910
2091
|
const store = getStore();
|
|
@@ -1931,13 +2112,9 @@ function syncProtocolRoutes(getStore, opts = {}) {
|
|
|
1931
2112
|
reason: "device_id_mismatch",
|
|
1932
2113
|
op_id: op.op_id
|
|
1933
2114
|
}, 400);
|
|
1934
|
-
|
|
1935
|
-
if (!localDeviceId) {
|
|
1936
|
-
const [deviceId] = ensureDeviceIdentity(store.db);
|
|
1937
|
-
localDeviceId = { device_id: deviceId };
|
|
1938
|
-
}
|
|
2115
|
+
const [localDeviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
1939
2116
|
const filteredInbound = filterOpsForPeer(store, peerDeviceId, normalizedOps);
|
|
1940
|
-
const result = applyReplicationOps(store.db, filteredInbound.allowed, localDeviceId
|
|
2117
|
+
const result = applyReplicationOps(store.db, filteredInbound.allowed, localDeviceId);
|
|
1941
2118
|
return c.json({
|
|
1942
2119
|
...result,
|
|
1943
2120
|
skipped: result.skipped + filteredInbound.skipped
|
|
@@ -2171,7 +2348,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2171
2348
|
publicKey = String(deviceRow.public_key);
|
|
2172
2349
|
fingerprint = String(deviceRow.fingerprint);
|
|
2173
2350
|
} else try {
|
|
2174
|
-
const [id, fp] = ensureDeviceIdentity(store.db);
|
|
2351
|
+
const [id, fp] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
|
|
2175
2352
|
deviceId = id;
|
|
2176
2353
|
fingerprint = fp;
|
|
2177
2354
|
publicKey = d.select({ public_key: schema.syncDevice.public_key }).from(schema.syncDevice).where(eq(schema.syncDevice.device_id, id)).get()?.public_key ?? "";
|
|
@@ -2552,6 +2729,47 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2552
2729
|
return c.json({ error: message }, message.includes("request_not_found") || message.includes("not found") ? 404 : 400);
|
|
2553
2730
|
}
|
|
2554
2731
|
});
|
|
2732
|
+
app.get("/api/sync/bootstrap-grants", async (c) => {
|
|
2733
|
+
const groupId = String(c.req.query("group_id") ?? "").trim();
|
|
2734
|
+
if (!groupId) return c.json({ error: "group_id_required" }, 400);
|
|
2735
|
+
const config = readCoordinatorSyncConfig();
|
|
2736
|
+
try {
|
|
2737
|
+
const items = await coordinatorListBootstrapGrantsAction({
|
|
2738
|
+
groupId,
|
|
2739
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
2740
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
2741
|
+
});
|
|
2742
|
+
return c.json({ items });
|
|
2743
|
+
} catch (error) {
|
|
2744
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
2745
|
+
return c.json({ error: message }, 500);
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
app.post("/api/sync/bootstrap-grants/revoke", async (c) => {
|
|
2749
|
+
let body;
|
|
2750
|
+
try {
|
|
2751
|
+
body = await c.req.json();
|
|
2752
|
+
} catch {
|
|
2753
|
+
return c.json({ error: "invalid json" }, 400);
|
|
2754
|
+
}
|
|
2755
|
+
const grantId = String(body.grant_id ?? "").trim();
|
|
2756
|
+
if (!grantId) return c.json({ error: "grant_id_required" }, 400);
|
|
2757
|
+
const config = readCoordinatorSyncConfig();
|
|
2758
|
+
try {
|
|
2759
|
+
if (!await coordinatorRevokeBootstrapGrantAction({
|
|
2760
|
+
grantId,
|
|
2761
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
2762
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
2763
|
+
})) return c.json({ error: "grant_not_found" }, 404);
|
|
2764
|
+
return c.json({
|
|
2765
|
+
ok: true,
|
|
2766
|
+
grant_id: grantId
|
|
2767
|
+
});
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
2770
|
+
return c.json({ error: message }, 500);
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2555
2773
|
app.delete("/api/sync/peers/:peer_device_id", (c) => {
|
|
2556
2774
|
const store = getStore();
|
|
2557
2775
|
{
|