@codemem/server 0.26.0 → 0.26.2
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 +612 -66
- package/dist/index.js.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/app.js +3928 -3292
- package/static/index.html +229 -0
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, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, 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, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncProjectAllowedByFilters, syncVisibilityAllowed, 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";
|
|
@@ -609,7 +609,7 @@ function attachSessionFields(store, items) {
|
|
|
609
609
|
const bySession = /* @__PURE__ */ new Map();
|
|
610
610
|
for (const row of rows) {
|
|
611
611
|
const projectRaw = String(row.project ?? "").trim();
|
|
612
|
-
const project = projectRaw ? projectBasename
|
|
612
|
+
const project = projectRaw ? projectBasename(projectRaw) : "";
|
|
613
613
|
const cwd = String(row.cwd ?? "");
|
|
614
614
|
bySession.set(row.id, {
|
|
615
615
|
project,
|
|
@@ -629,7 +629,7 @@ function attachSessionFields(store, items) {
|
|
|
629
629
|
* Extract the basename of a project path.
|
|
630
630
|
* Strips "fatal:" prefixed values.
|
|
631
631
|
*/
|
|
632
|
-
function projectBasename
|
|
632
|
+
function projectBasename(raw) {
|
|
633
633
|
if (raw.toLowerCase().startsWith("fatal:")) return "";
|
|
634
634
|
const parts = raw.replace(/\\/g, "/").split("/");
|
|
635
635
|
return parts[parts.length - 1] ?? raw;
|
|
@@ -696,7 +696,7 @@ function memoryRoutes(getStore) {
|
|
|
696
696
|
const store = getStore();
|
|
697
697
|
{
|
|
698
698
|
const rows = drizzle(store.db, { schema }).selectDistinct({ project: schema.sessions.project }).from(schema.sessions).where(isNotNull(schema.sessions.project)).all();
|
|
699
|
-
const projects = [...new Set(rows.map((r) => String(r.project ?? "").trim()).filter((p) => p && !p.toLowerCase().startsWith("fatal:")).map((p) => projectBasename
|
|
699
|
+
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();
|
|
700
700
|
return c.json({ projects });
|
|
701
701
|
}
|
|
702
702
|
});
|
|
@@ -1418,6 +1418,34 @@ var SYNC_STALE_AFTER_SECONDS = 600;
|
|
|
1418
1418
|
var SYNC_PROTOCOL_VERSION = "2";
|
|
1419
1419
|
var LEGACY_SYNC_ACTOR_DISPLAY_NAME = "Legacy synced peer";
|
|
1420
1420
|
var LEGACY_SHARED_WORKSPACE_ID = "shared:legacy";
|
|
1421
|
+
function coordinatorAdminStatusPayload(config = readCoordinatorSyncConfig()) {
|
|
1422
|
+
const groups = config.syncCoordinatorGroups;
|
|
1423
|
+
const activeGroup = groups[0] ?? null;
|
|
1424
|
+
const hasUrl = Boolean(config.syncCoordinatorUrl);
|
|
1425
|
+
const hasGroups = groups.length > 0;
|
|
1426
|
+
const hasAdminSecret = Boolean(config.syncCoordinatorAdminSecret);
|
|
1427
|
+
let readiness = "ready";
|
|
1428
|
+
if (!hasUrl) readiness = "not_configured";
|
|
1429
|
+
else if (!hasGroups || !hasAdminSecret) readiness = "partial";
|
|
1430
|
+
return {
|
|
1431
|
+
readiness,
|
|
1432
|
+
coordinator_url: config.syncCoordinatorUrl || null,
|
|
1433
|
+
groups,
|
|
1434
|
+
active_group: activeGroup,
|
|
1435
|
+
has_admin_secret: hasAdminSecret,
|
|
1436
|
+
has_groups: hasGroups
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function resolveCoordinatorAdminGroup(requestedGroup, status) {
|
|
1440
|
+
const explicit = String(requestedGroup ?? "").trim();
|
|
1441
|
+
if (explicit) return explicit;
|
|
1442
|
+
return status.active_group;
|
|
1443
|
+
}
|
|
1444
|
+
function parseInviteTtlHours(value) {
|
|
1445
|
+
const ttlHours = Number(String(value ?? 24));
|
|
1446
|
+
if (!Number.isInteger(ttlHours) || ttlHours < 1) return null;
|
|
1447
|
+
return ttlHours;
|
|
1448
|
+
}
|
|
1421
1449
|
function sortActiveMaintenanceJobs(jobs) {
|
|
1422
1450
|
return [...jobs].sort((a, b) => {
|
|
1423
1451
|
const aTime = a.started_at ?? a.updated_at;
|
|
@@ -1685,12 +1713,6 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
1685
1713
|
deviceId
|
|
1686
1714
|
};
|
|
1687
1715
|
}
|
|
1688
|
-
function projectBasename(value) {
|
|
1689
|
-
const project = String(value ?? "").trim().replaceAll("\\", "/");
|
|
1690
|
-
if (!project) return "";
|
|
1691
|
-
const parts = project.split("/").filter(Boolean);
|
|
1692
|
-
return parts.length > 0 ? parts[parts.length - 1] ?? "" : "";
|
|
1693
|
-
}
|
|
1694
1716
|
function parseJsonList(value) {
|
|
1695
1717
|
if (value == null) return [];
|
|
1696
1718
|
if (typeof value === "string") try {
|
|
@@ -1915,30 +1937,6 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1915
1937
|
};
|
|
1916
1938
|
})();
|
|
1917
1939
|
}
|
|
1918
|
-
function isSharedVisibility(payload) {
|
|
1919
|
-
if (!payload) return false;
|
|
1920
|
-
let visibility = String(payload.visibility ?? "").trim().toLowerCase();
|
|
1921
|
-
const metadata = payload.metadata_json && typeof payload.metadata_json === "object" && !Array.isArray(payload.metadata_json) ? payload.metadata_json : {};
|
|
1922
|
-
const metadataVisibility = String(metadata.visibility ?? "").trim().toLowerCase();
|
|
1923
|
-
if (!visibility && metadataVisibility) visibility = metadataVisibility;
|
|
1924
|
-
if (!visibility) {
|
|
1925
|
-
let workspaceKind = String(payload.workspace_kind ?? "").trim().toLowerCase();
|
|
1926
|
-
let workspaceId = String(payload.workspace_id ?? "").trim().toLowerCase();
|
|
1927
|
-
if (!workspaceKind) workspaceKind = String(metadata.workspace_kind ?? "").trim().toLowerCase();
|
|
1928
|
-
if (!workspaceId) workspaceId = String(metadata.workspace_id ?? "").trim().toLowerCase();
|
|
1929
|
-
if (workspaceKind === "shared" || workspaceId.startsWith("shared:")) visibility = "shared";
|
|
1930
|
-
else return true;
|
|
1931
|
-
}
|
|
1932
|
-
return visibility === "shared";
|
|
1933
|
-
}
|
|
1934
|
-
function projectAllowed(projectValue, filters) {
|
|
1935
|
-
const value = String(projectValue ?? "").trim();
|
|
1936
|
-
const valueBase = projectBasename(value);
|
|
1937
|
-
for (const blocked of filters.exclude) if (blocked === value || blocked === valueBase) return false;
|
|
1938
|
-
if (filters.include.length === 0) return true;
|
|
1939
|
-
for (const allowed of filters.include) if (allowed === value || allowed === valueBase) return true;
|
|
1940
|
-
return false;
|
|
1941
|
-
}
|
|
1942
1940
|
function filterOpsForPeer(store, peerDeviceId, ops) {
|
|
1943
1941
|
const filters = readPeerProjectFilters(store, peerDeviceId);
|
|
1944
1942
|
const allowPrivate = peerClaimedLocalActor(store, peerDeviceId);
|
|
@@ -1950,11 +1948,11 @@ function filterOpsForPeer(store, peerDeviceId, ops) {
|
|
|
1950
1948
|
continue;
|
|
1951
1949
|
}
|
|
1952
1950
|
const payload = parseOpPayload(op);
|
|
1953
|
-
if (!allowPrivate && !
|
|
1951
|
+
if (!allowPrivate && !syncVisibilityAllowed(payload)) {
|
|
1954
1952
|
skipped++;
|
|
1955
1953
|
continue;
|
|
1956
1954
|
}
|
|
1957
|
-
if (!
|
|
1955
|
+
if (!syncProjectAllowedByFilters(payload && typeof payload.project === "string" ? payload.project : null, filters)) {
|
|
1958
1956
|
skipped++;
|
|
1959
1957
|
continue;
|
|
1960
1958
|
}
|
|
@@ -2272,30 +2270,40 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2272
2270
|
app.get("/api/sync/status", async (c) => {
|
|
2273
2271
|
const store = getStore();
|
|
2274
2272
|
{
|
|
2273
|
+
const traceSync = (label, fn) => {
|
|
2274
|
+
if (process.env.CODEMEM_TRACE_SYNC_STATUS !== "1") return fn();
|
|
2275
|
+
const startedAt = Date.now();
|
|
2276
|
+
console.warn(`[codemem sync-status] ${label} start`);
|
|
2277
|
+
try {
|
|
2278
|
+
return fn();
|
|
2279
|
+
} finally {
|
|
2280
|
+
console.warn(`[codemem sync-status] ${label} ${Date.now() - startedAt}ms`);
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2275
2283
|
const showDiag = queryBool(c.req.query("includeDiagnostics"));
|
|
2276
2284
|
const includeJoinRequests = queryBool(c.req.query("includeJoinRequests"));
|
|
2277
2285
|
const project = c.req.query("project") || null;
|
|
2278
|
-
const config = readCoordinatorSyncConfig();
|
|
2279
|
-
const syncReset = getSyncResetState(store.db);
|
|
2286
|
+
const config = traceSync("readCoordinatorSyncConfig", () => readCoordinatorSyncConfig());
|
|
2287
|
+
const syncReset = traceSync("getSyncResetState", () => getSyncResetState(store.db));
|
|
2280
2288
|
const d = drizzle(store.db, { schema });
|
|
2281
|
-
const deviceRow = d.select({
|
|
2289
|
+
const deviceRow = traceSync("deviceRow", () => d.select({
|
|
2282
2290
|
device_id: schema.syncDevice.device_id,
|
|
2283
2291
|
fingerprint: schema.syncDevice.fingerprint
|
|
2284
|
-
}).from(schema.syncDevice).limit(1).get();
|
|
2285
|
-
const daemonState = d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get();
|
|
2286
|
-
const peerCountRow = d.select({ total: count() }).from(schema.syncPeers).get();
|
|
2292
|
+
}).from(schema.syncDevice).limit(1).get());
|
|
2293
|
+
const daemonState = traceSync("daemonState", () => d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get());
|
|
2294
|
+
const peerCountRow = traceSync("peerCountRow", () => d.select({ total: count() }).from(schema.syncPeers).get());
|
|
2287
2295
|
let retentionState;
|
|
2288
2296
|
try {
|
|
2289
|
-
retentionState = d.select().from(schema.syncRetentionState).where(eq(schema.syncRetentionState.id, 1)).get();
|
|
2297
|
+
retentionState = traceSync("retentionState", () => d.select().from(schema.syncRetentionState).where(eq(schema.syncRetentionState.id, 1)).get());
|
|
2290
2298
|
} catch {
|
|
2291
2299
|
retentionState = void 0;
|
|
2292
2300
|
}
|
|
2293
|
-
const lastSyncRow = d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get();
|
|
2294
|
-
const semanticIndex = redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(store.db), showDiag);
|
|
2301
|
+
const lastSyncRow = traceSync("lastSyncRow", () => d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get());
|
|
2302
|
+
const semanticIndex = traceSync("semanticIndex", () => redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(store.db, { fastCounts: !showDiag }), showDiag));
|
|
2295
2303
|
const lastError = daemonState?.last_error;
|
|
2296
2304
|
const lastErrorAt = daemonState?.last_error_at;
|
|
2297
2305
|
const lastOkAt = daemonState?.last_ok_at;
|
|
2298
|
-
const viewerBinding = readViewerBinding(store.dbPath);
|
|
2306
|
+
const viewerBinding = traceSync("readViewerBinding", () => readViewerBinding(store.dbPath));
|
|
2299
2307
|
const daemonRunning = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
|
|
2300
2308
|
const daemonDetail = viewerBinding ? daemonRunning ? `viewer pidfile at ${viewerBinding.host}:${viewerBinding.port}` : `pidfile present but ${viewerBinding.host}:${viewerBinding.port} is unreachable` : null;
|
|
2301
2309
|
let daemonStateValue = "ok";
|
|
@@ -2346,14 +2354,14 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2346
2354
|
statusPayload.daemon_running = runtimeStatus.phase === "starting" || daemonRunning;
|
|
2347
2355
|
statusPayload.daemon_detail = runtimeStatus.detail ?? daemonDetail;
|
|
2348
2356
|
}
|
|
2349
|
-
const peersItems = store.db.prepare(PEERS_QUERY).all().map((row) => {
|
|
2357
|
+
const peersItems = traceSync("peerRows", () => store.db.prepare(PEERS_QUERY).all()).map((row) => {
|
|
2350
2358
|
const peer = mapPeerRow(store, row, showDiag);
|
|
2351
2359
|
peer.status = peerStatus(peer);
|
|
2352
2360
|
return peer;
|
|
2353
2361
|
});
|
|
2354
2362
|
const peersMap = {};
|
|
2355
2363
|
for (const peer of peersItems) peersMap[String(peer.peer_device_id)] = peer.status;
|
|
2356
|
-
const attemptRows = d.select({
|
|
2364
|
+
const attemptRows = traceSync("attemptRows", () => d.select({
|
|
2357
2365
|
peer_device_id: schema.syncAttempts.peer_device_id,
|
|
2358
2366
|
ok: schema.syncAttempts.ok,
|
|
2359
2367
|
error: schema.syncAttempts.error,
|
|
@@ -2361,9 +2369,9 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2361
2369
|
finished_at: schema.syncAttempts.finished_at,
|
|
2362
2370
|
ops_in: schema.syncAttempts.ops_in,
|
|
2363
2371
|
ops_out: schema.syncAttempts.ops_out
|
|
2364
|
-
}).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(25).all();
|
|
2372
|
+
}).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(25).all());
|
|
2365
2373
|
const peerAddressMap = /* @__PURE__ */ new Map();
|
|
2366
|
-
store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all().forEach((peerRow) => {
|
|
2374
|
+
traceSync("peerAddressRows", () => store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all()).forEach((peerRow) => {
|
|
2367
2375
|
const addrs = safeJsonList(peerRow.addresses_json);
|
|
2368
2376
|
if (addrs.length) peerAddressMap.set(String(peerRow.peer_device_id ?? ""), addrs);
|
|
2369
2377
|
});
|
|
@@ -2384,9 +2392,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2384
2392
|
sync: {},
|
|
2385
2393
|
ping: {}
|
|
2386
2394
|
};
|
|
2387
|
-
const legacyDevices = store.claimableLegacyDeviceIds();
|
|
2388
|
-
const sharingReview = store.sharingReviewSummary(project);
|
|
2389
|
-
const
|
|
2395
|
+
const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
|
|
2396
|
+
const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
|
|
2397
|
+
const coordinatorSnapshot = await coordinatorStatusSnapshot(store, config);
|
|
2398
|
+
const coordinator = traceSync("coordinator", () => redactCoordinatorStatus(coordinatorSnapshot, showDiag));
|
|
2390
2399
|
let joinRequests = [];
|
|
2391
2400
|
if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
|
|
2392
2401
|
joinRequests = await listCoordinatorJoinRequests(config);
|
|
@@ -2799,11 +2808,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2799
2808
|
const groupId = String(body.group_id ?? "").trim();
|
|
2800
2809
|
const coordinatorUrl = body.coordinator_url == null ? null : String(body.coordinator_url ?? "");
|
|
2801
2810
|
const policy = String(body.policy ?? "auto_admit").trim();
|
|
2802
|
-
const ttlHours =
|
|
2811
|
+
const ttlHours = parseInviteTtlHours(body.ttl_hours);
|
|
2803
2812
|
if (!groupId) return c.json({ error: "group_id required" }, 400);
|
|
2804
2813
|
if (body.coordinator_url != null && typeof body.coordinator_url !== "string") return c.json({ error: "coordinator_url must be string" }, 400);
|
|
2805
2814
|
if (!["auto_admit", "approval_required"].includes(policy)) return c.json({ error: "policy must be auto_admit or approval_required" }, 400);
|
|
2806
|
-
if (
|
|
2815
|
+
if (ttlHours == null) return c.json({ error: "ttl_hours must be an integer >= 1" }, 400);
|
|
2807
2816
|
try {
|
|
2808
2817
|
const config = readCoordinatorSyncConfig();
|
|
2809
2818
|
const result = await coordinatorCreateInviteAction({
|
|
@@ -2840,34 +2849,571 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
|
|
|
2840
2849
|
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
2841
2850
|
}
|
|
2842
2851
|
});
|
|
2843
|
-
app.
|
|
2852
|
+
app.get("/api/coordinator/admin/status", async (c) => {
|
|
2853
|
+
const status = coordinatorAdminStatusPayload();
|
|
2854
|
+
return c.json(status);
|
|
2855
|
+
});
|
|
2856
|
+
app.get("/api/coordinator/admin/groups", async (c) => {
|
|
2857
|
+
const config = readCoordinatorSyncConfig();
|
|
2858
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
2859
|
+
if (status.readiness === "not_configured") return c.json({
|
|
2860
|
+
error: "coordinator_not_configured",
|
|
2861
|
+
status
|
|
2862
|
+
}, 400);
|
|
2863
|
+
if (!status.has_admin_secret) return c.json({
|
|
2864
|
+
error: "coordinator_admin_secret_missing",
|
|
2865
|
+
status
|
|
2866
|
+
}, 400);
|
|
2867
|
+
const includeArchived = queryBool(c.req.query("include_archived"));
|
|
2868
|
+
try {
|
|
2869
|
+
const items = await coordinatorListGroupsAction({
|
|
2870
|
+
includeArchived,
|
|
2871
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
2872
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
2873
|
+
});
|
|
2874
|
+
return c.json({
|
|
2875
|
+
items,
|
|
2876
|
+
status
|
|
2877
|
+
});
|
|
2878
|
+
} catch (error) {
|
|
2879
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
2880
|
+
return c.json({
|
|
2881
|
+
error: message,
|
|
2882
|
+
status
|
|
2883
|
+
}, 502);
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
app.post("/api/coordinator/admin/groups", async (c) => {
|
|
2887
|
+
const config = readCoordinatorSyncConfig();
|
|
2888
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
2889
|
+
if (status.readiness === "not_configured") return c.json({
|
|
2890
|
+
error: "coordinator_not_configured",
|
|
2891
|
+
status
|
|
2892
|
+
}, 400);
|
|
2893
|
+
if (!status.has_admin_secret) return c.json({
|
|
2894
|
+
error: "coordinator_admin_secret_missing",
|
|
2895
|
+
status
|
|
2896
|
+
}, 400);
|
|
2844
2897
|
let body;
|
|
2845
2898
|
try {
|
|
2846
2899
|
body = await c.req.json();
|
|
2847
2900
|
} catch {
|
|
2848
|
-
return c.json({
|
|
2901
|
+
return c.json({
|
|
2902
|
+
error: "invalid json",
|
|
2903
|
+
status
|
|
2904
|
+
}, 400);
|
|
2849
2905
|
}
|
|
2850
|
-
const
|
|
2851
|
-
const
|
|
2852
|
-
if (!
|
|
2853
|
-
|
|
2906
|
+
const groupId = String(body.group_id ?? "").trim();
|
|
2907
|
+
const displayName = String(body.display_name ?? "").trim() || null;
|
|
2908
|
+
if (!groupId) return c.json({
|
|
2909
|
+
error: "group_id required",
|
|
2910
|
+
status
|
|
2911
|
+
}, 400);
|
|
2912
|
+
try {
|
|
2913
|
+
const group = await coordinatorCreateGroupAction({
|
|
2914
|
+
groupId,
|
|
2915
|
+
displayName,
|
|
2916
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
2917
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
2918
|
+
});
|
|
2919
|
+
return c.json({
|
|
2920
|
+
ok: true,
|
|
2921
|
+
group,
|
|
2922
|
+
status
|
|
2923
|
+
});
|
|
2924
|
+
} catch (error) {
|
|
2925
|
+
return c.json({
|
|
2926
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2927
|
+
status
|
|
2928
|
+
}, 400);
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
app.post("/api/coordinator/admin/groups/:group_id/rename", async (c) => {
|
|
2932
|
+
const config = readCoordinatorSyncConfig();
|
|
2933
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
2934
|
+
if (status.readiness === "not_configured") return c.json({
|
|
2935
|
+
error: "coordinator_not_configured",
|
|
2936
|
+
status
|
|
2937
|
+
}, 400);
|
|
2938
|
+
if (!status.has_admin_secret) return c.json({
|
|
2939
|
+
error: "coordinator_admin_secret_missing",
|
|
2940
|
+
status
|
|
2941
|
+
}, 400);
|
|
2942
|
+
const groupId = String(c.req.param("group_id") ?? "").trim();
|
|
2943
|
+
if (!groupId) return c.json({
|
|
2944
|
+
error: "group_id required",
|
|
2945
|
+
status
|
|
2946
|
+
}, 400);
|
|
2947
|
+
let body;
|
|
2948
|
+
try {
|
|
2949
|
+
body = await c.req.json();
|
|
2950
|
+
} catch {
|
|
2951
|
+
return c.json({
|
|
2952
|
+
error: "invalid json",
|
|
2953
|
+
status
|
|
2954
|
+
}, 400);
|
|
2955
|
+
}
|
|
2956
|
+
const displayName = String(body.display_name ?? "").trim();
|
|
2957
|
+
if (!displayName) return c.json({
|
|
2958
|
+
error: "display_name required",
|
|
2959
|
+
status
|
|
2960
|
+
}, 400);
|
|
2961
|
+
try {
|
|
2962
|
+
const group = await coordinatorRenameGroupAction({
|
|
2963
|
+
groupId,
|
|
2964
|
+
displayName,
|
|
2965
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
2966
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
2967
|
+
});
|
|
2968
|
+
if (!group) return c.json({
|
|
2969
|
+
error: "group_not_found",
|
|
2970
|
+
status
|
|
2971
|
+
}, 404);
|
|
2972
|
+
return c.json({
|
|
2973
|
+
ok: true,
|
|
2974
|
+
group,
|
|
2975
|
+
status
|
|
2976
|
+
});
|
|
2977
|
+
} catch (error) {
|
|
2978
|
+
return c.json({
|
|
2979
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2980
|
+
status
|
|
2981
|
+
}, 400);
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
app.post("/api/coordinator/admin/groups/:group_id/archive", async (c) => {
|
|
2985
|
+
const config = readCoordinatorSyncConfig();
|
|
2986
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
2987
|
+
if (status.readiness === "not_configured") return c.json({
|
|
2988
|
+
error: "coordinator_not_configured",
|
|
2989
|
+
status
|
|
2990
|
+
}, 400);
|
|
2991
|
+
if (!status.has_admin_secret) return c.json({
|
|
2992
|
+
error: "coordinator_admin_secret_missing",
|
|
2993
|
+
status
|
|
2994
|
+
}, 400);
|
|
2995
|
+
const groupId = String(c.req.param("group_id") ?? "").trim();
|
|
2996
|
+
if (!groupId) return c.json({
|
|
2997
|
+
error: "group_id required",
|
|
2998
|
+
status
|
|
2999
|
+
}, 400);
|
|
3000
|
+
try {
|
|
3001
|
+
const group = await coordinatorArchiveGroupAction({
|
|
3002
|
+
groupId,
|
|
3003
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
3004
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
3005
|
+
});
|
|
3006
|
+
if (!group) return c.json({
|
|
3007
|
+
error: "group_not_found_or_already_archived",
|
|
3008
|
+
status
|
|
3009
|
+
}, 404);
|
|
3010
|
+
return c.json({
|
|
3011
|
+
ok: true,
|
|
3012
|
+
group,
|
|
3013
|
+
status
|
|
3014
|
+
});
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
return c.json({
|
|
3017
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3018
|
+
status
|
|
3019
|
+
}, 400);
|
|
3020
|
+
}
|
|
3021
|
+
});
|
|
3022
|
+
app.post("/api/coordinator/admin/groups/:group_id/unarchive", async (c) => {
|
|
3023
|
+
const config = readCoordinatorSyncConfig();
|
|
3024
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
3025
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3026
|
+
error: "coordinator_not_configured",
|
|
3027
|
+
status
|
|
3028
|
+
}, 400);
|
|
3029
|
+
if (!status.has_admin_secret) return c.json({
|
|
3030
|
+
error: "coordinator_admin_secret_missing",
|
|
3031
|
+
status
|
|
3032
|
+
}, 400);
|
|
3033
|
+
const groupId = String(c.req.param("group_id") ?? "").trim();
|
|
3034
|
+
if (!groupId) return c.json({
|
|
3035
|
+
error: "group_id required",
|
|
3036
|
+
status
|
|
3037
|
+
}, 400);
|
|
3038
|
+
try {
|
|
3039
|
+
const group = await coordinatorUnarchiveGroupAction({
|
|
3040
|
+
groupId,
|
|
3041
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
3042
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
3043
|
+
});
|
|
3044
|
+
if (!group) return c.json({
|
|
3045
|
+
error: "group_not_found_or_not_archived",
|
|
3046
|
+
status
|
|
3047
|
+
}, 404);
|
|
3048
|
+
return c.json({
|
|
3049
|
+
ok: true,
|
|
3050
|
+
group,
|
|
3051
|
+
status
|
|
3052
|
+
});
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
return c.json({
|
|
3055
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3056
|
+
status
|
|
3057
|
+
}, 400);
|
|
3058
|
+
}
|
|
3059
|
+
});
|
|
3060
|
+
app.post("/api/coordinator/admin/invites", async (c) => {
|
|
3061
|
+
const config = readCoordinatorSyncConfig();
|
|
3062
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
3063
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3064
|
+
error: "coordinator_not_configured",
|
|
3065
|
+
status
|
|
3066
|
+
}, 400);
|
|
3067
|
+
if (!status.has_admin_secret) return c.json({
|
|
3068
|
+
error: "coordinator_admin_secret_missing",
|
|
3069
|
+
status
|
|
3070
|
+
}, 400);
|
|
3071
|
+
let body;
|
|
3072
|
+
try {
|
|
3073
|
+
body = await c.req.json();
|
|
3074
|
+
} catch {
|
|
3075
|
+
return c.json({
|
|
3076
|
+
error: "invalid json",
|
|
3077
|
+
status
|
|
3078
|
+
}, 400);
|
|
3079
|
+
}
|
|
3080
|
+
const groupId = resolveCoordinatorAdminGroup(String(body.group_id ?? "").trim(), status);
|
|
3081
|
+
const coordinatorUrl = body.coordinator_url == null ? null : String(body.coordinator_url ?? "");
|
|
3082
|
+
const policy = String(body.policy ?? "auto_admit").trim();
|
|
3083
|
+
const ttlHours = parseInviteTtlHours(body.ttl_hours);
|
|
3084
|
+
if (!groupId) return c.json({
|
|
3085
|
+
error: "group_id required",
|
|
3086
|
+
status
|
|
3087
|
+
}, 400);
|
|
3088
|
+
if (body.coordinator_url != null && typeof body.coordinator_url !== "string") return c.json({
|
|
3089
|
+
error: "coordinator_url must be string",
|
|
3090
|
+
status
|
|
3091
|
+
}, 400);
|
|
3092
|
+
if (!["auto_admit", "approval_required"].includes(policy)) return c.json({
|
|
3093
|
+
error: "policy must be auto_admit or approval_required",
|
|
3094
|
+
status
|
|
3095
|
+
}, 400);
|
|
3096
|
+
if (ttlHours == null) return c.json({
|
|
3097
|
+
error: "ttl_hours must be an integer >= 1",
|
|
3098
|
+
status
|
|
3099
|
+
}, 400);
|
|
3100
|
+
try {
|
|
3101
|
+
const result = await coordinatorCreateInviteAction({
|
|
3102
|
+
groupId,
|
|
3103
|
+
coordinatorUrl,
|
|
3104
|
+
policy,
|
|
3105
|
+
ttlHours,
|
|
3106
|
+
createdBy: null,
|
|
3107
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
3108
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
3109
|
+
});
|
|
3110
|
+
return c.json({
|
|
3111
|
+
...result,
|
|
3112
|
+
status
|
|
3113
|
+
});
|
|
3114
|
+
} catch (error) {
|
|
3115
|
+
return c.json({
|
|
3116
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3117
|
+
status
|
|
3118
|
+
}, 400);
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
app.get("/api/coordinator/admin/join-requests", async (c) => {
|
|
3122
|
+
const config = readCoordinatorSyncConfig();
|
|
3123
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
3124
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3125
|
+
error: "coordinator_not_configured",
|
|
3126
|
+
status
|
|
3127
|
+
}, 400);
|
|
3128
|
+
if (!status.has_admin_secret) return c.json({
|
|
3129
|
+
error: "coordinator_admin_secret_missing",
|
|
3130
|
+
status
|
|
3131
|
+
}, 400);
|
|
3132
|
+
const groupId = resolveCoordinatorAdminGroup(c.req.query("group_id"), status);
|
|
3133
|
+
if (!groupId) return c.json({
|
|
3134
|
+
error: "group_id_required",
|
|
3135
|
+
status
|
|
3136
|
+
}, 400);
|
|
3137
|
+
try {
|
|
3138
|
+
const items = await coordinatorListJoinRequestsAction({
|
|
3139
|
+
groupId,
|
|
3140
|
+
remoteUrl: status.coordinator_url,
|
|
3141
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
3142
|
+
});
|
|
3143
|
+
return c.json({
|
|
3144
|
+
items,
|
|
3145
|
+
group_id: groupId,
|
|
3146
|
+
status
|
|
3147
|
+
});
|
|
3148
|
+
} catch (error) {
|
|
3149
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
3150
|
+
return c.json({
|
|
3151
|
+
error: message,
|
|
3152
|
+
status
|
|
3153
|
+
}, 502);
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
app.post("/api/coordinator/admin/join-requests/:request_id/approve", async (c) => {
|
|
3157
|
+
const config = readCoordinatorSyncConfig();
|
|
3158
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
3159
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3160
|
+
error: "coordinator_not_configured",
|
|
3161
|
+
status
|
|
3162
|
+
}, 400);
|
|
3163
|
+
if (!status.has_admin_secret) return c.json({
|
|
3164
|
+
error: "coordinator_admin_secret_missing",
|
|
3165
|
+
status
|
|
3166
|
+
}, 400);
|
|
3167
|
+
const requestId = String(c.req.param("request_id") ?? "").trim();
|
|
3168
|
+
if (!requestId) return c.json({
|
|
3169
|
+
error: "request_id required",
|
|
3170
|
+
status
|
|
3171
|
+
}, 400);
|
|
2854
3172
|
try {
|
|
2855
|
-
const config = readCoordinatorSyncConfig();
|
|
2856
3173
|
const result = await coordinatorReviewJoinRequestAction({
|
|
2857
3174
|
requestId,
|
|
2858
|
-
approve:
|
|
3175
|
+
approve: true,
|
|
2859
3176
|
reviewedBy: null,
|
|
2860
3177
|
remoteUrl: config.syncCoordinatorUrl || null,
|
|
2861
3178
|
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
2862
3179
|
});
|
|
2863
|
-
if (!result) return c.json({
|
|
3180
|
+
if (!result) return c.json({
|
|
3181
|
+
error: "join request not found",
|
|
3182
|
+
status
|
|
3183
|
+
}, 404);
|
|
2864
3184
|
return c.json({
|
|
2865
3185
|
ok: true,
|
|
2866
|
-
request: result
|
|
3186
|
+
request: result,
|
|
3187
|
+
status
|
|
2867
3188
|
});
|
|
2868
3189
|
} catch (error) {
|
|
2869
3190
|
const message = error instanceof Error ? error.message : String(error);
|
|
2870
|
-
return c.json({
|
|
3191
|
+
return c.json({
|
|
3192
|
+
error: message,
|
|
3193
|
+
status
|
|
3194
|
+
}, message.includes("request_not_found") || message.includes("not found") ? 404 : 400);
|
|
3195
|
+
}
|
|
3196
|
+
});
|
|
3197
|
+
app.post("/api/coordinator/admin/join-requests/:request_id/deny", async (c) => {
|
|
3198
|
+
const config = readCoordinatorSyncConfig();
|
|
3199
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
3200
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3201
|
+
error: "coordinator_not_configured",
|
|
3202
|
+
status
|
|
3203
|
+
}, 400);
|
|
3204
|
+
if (!status.has_admin_secret) return c.json({
|
|
3205
|
+
error: "coordinator_admin_secret_missing",
|
|
3206
|
+
status
|
|
3207
|
+
}, 400);
|
|
3208
|
+
const requestId = String(c.req.param("request_id") ?? "").trim();
|
|
3209
|
+
if (!requestId) return c.json({
|
|
3210
|
+
error: "request_id required",
|
|
3211
|
+
status
|
|
3212
|
+
}, 400);
|
|
3213
|
+
try {
|
|
3214
|
+
const result = await coordinatorReviewJoinRequestAction({
|
|
3215
|
+
requestId,
|
|
3216
|
+
approve: false,
|
|
3217
|
+
reviewedBy: null,
|
|
3218
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
3219
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
3220
|
+
});
|
|
3221
|
+
if (!result) return c.json({
|
|
3222
|
+
error: "join request not found",
|
|
3223
|
+
status
|
|
3224
|
+
}, 404);
|
|
3225
|
+
return c.json({
|
|
3226
|
+
ok: true,
|
|
3227
|
+
request: result,
|
|
3228
|
+
status
|
|
3229
|
+
});
|
|
3230
|
+
} catch (error) {
|
|
3231
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3232
|
+
return c.json({
|
|
3233
|
+
error: message,
|
|
3234
|
+
status
|
|
3235
|
+
}, message.includes("request_not_found") || message.includes("not found") ? 404 : 400);
|
|
3236
|
+
}
|
|
3237
|
+
});
|
|
3238
|
+
app.get("/api/coordinator/admin/devices", async (c) => {
|
|
3239
|
+
const config = readCoordinatorSyncConfig();
|
|
3240
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
3241
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3242
|
+
error: "coordinator_not_configured",
|
|
3243
|
+
status
|
|
3244
|
+
}, 400);
|
|
3245
|
+
if (!status.has_admin_secret) return c.json({
|
|
3246
|
+
error: "coordinator_admin_secret_missing",
|
|
3247
|
+
status
|
|
3248
|
+
}, 400);
|
|
3249
|
+
const groupId = resolveCoordinatorAdminGroup(c.req.query("group_id"), status);
|
|
3250
|
+
if (!groupId) return c.json({
|
|
3251
|
+
error: "group_id_required",
|
|
3252
|
+
status
|
|
3253
|
+
}, 400);
|
|
3254
|
+
try {
|
|
3255
|
+
const items = await coordinatorListDevicesAction({
|
|
3256
|
+
groupId,
|
|
3257
|
+
includeDisabled: queryBool(c.req.query("include_disabled")),
|
|
3258
|
+
remoteUrl: status.coordinator_url,
|
|
3259
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
3260
|
+
});
|
|
3261
|
+
return c.json({
|
|
3262
|
+
items,
|
|
3263
|
+
group_id: groupId,
|
|
3264
|
+
status
|
|
3265
|
+
});
|
|
3266
|
+
} catch (error) {
|
|
3267
|
+
const message = error instanceof Error ? error.message : "unknown";
|
|
3268
|
+
return c.json({
|
|
3269
|
+
error: message,
|
|
3270
|
+
status
|
|
3271
|
+
}, 502);
|
|
3272
|
+
}
|
|
3273
|
+
});
|
|
3274
|
+
app.post("/api/coordinator/admin/devices/:device_id/rename", async (c) => {
|
|
3275
|
+
const config = readCoordinatorSyncConfig();
|
|
3276
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
3277
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3278
|
+
error: "coordinator_not_configured",
|
|
3279
|
+
status
|
|
3280
|
+
}, 400);
|
|
3281
|
+
if (!status.has_admin_secret) return c.json({
|
|
3282
|
+
error: "coordinator_admin_secret_missing",
|
|
3283
|
+
status
|
|
3284
|
+
}, 400);
|
|
3285
|
+
const deviceId = String(c.req.param("device_id") ?? "").trim();
|
|
3286
|
+
if (!deviceId) return c.json({
|
|
3287
|
+
error: "device_id required",
|
|
3288
|
+
status
|
|
3289
|
+
}, 400);
|
|
3290
|
+
let body;
|
|
3291
|
+
try {
|
|
3292
|
+
body = await c.req.json();
|
|
3293
|
+
} catch {
|
|
3294
|
+
return c.json({
|
|
3295
|
+
error: "invalid json",
|
|
3296
|
+
status
|
|
3297
|
+
}, 400);
|
|
3298
|
+
}
|
|
3299
|
+
const groupId = resolveCoordinatorAdminGroup(String(body.group_id ?? "").trim(), status);
|
|
3300
|
+
const displayName = String(body.display_name ?? "").trim();
|
|
3301
|
+
if (!groupId) return c.json({
|
|
3302
|
+
error: "group_id required",
|
|
3303
|
+
status
|
|
3304
|
+
}, 400);
|
|
3305
|
+
if (!displayName) return c.json({
|
|
3306
|
+
error: "display_name required",
|
|
3307
|
+
status
|
|
3308
|
+
}, 400);
|
|
3309
|
+
try {
|
|
3310
|
+
const device = await coordinatorRenameDeviceAction({
|
|
3311
|
+
groupId,
|
|
3312
|
+
deviceId,
|
|
3313
|
+
displayName,
|
|
3314
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
3315
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
3316
|
+
});
|
|
3317
|
+
if (!device) return c.json({
|
|
3318
|
+
error: "device_not_found",
|
|
3319
|
+
status
|
|
3320
|
+
}, 404);
|
|
3321
|
+
return c.json({
|
|
3322
|
+
ok: true,
|
|
3323
|
+
device,
|
|
3324
|
+
status
|
|
3325
|
+
});
|
|
3326
|
+
} catch (error) {
|
|
3327
|
+
return c.json({
|
|
3328
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3329
|
+
status
|
|
3330
|
+
}, 400);
|
|
3331
|
+
}
|
|
3332
|
+
});
|
|
3333
|
+
app.post("/api/coordinator/admin/devices/:device_id/disable", async (c) => {
|
|
3334
|
+
const config = readCoordinatorSyncConfig();
|
|
3335
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
3336
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3337
|
+
error: "coordinator_not_configured",
|
|
3338
|
+
status
|
|
3339
|
+
}, 400);
|
|
3340
|
+
if (!status.has_admin_secret) return c.json({
|
|
3341
|
+
error: "coordinator_admin_secret_missing",
|
|
3342
|
+
status
|
|
3343
|
+
}, 400);
|
|
3344
|
+
const deviceId = String(c.req.param("device_id") ?? "").trim();
|
|
3345
|
+
if (!deviceId) return c.json({
|
|
3346
|
+
error: "device_id required",
|
|
3347
|
+
status
|
|
3348
|
+
}, 400);
|
|
3349
|
+
const groupId = resolveCoordinatorAdminGroup(c.req.query("group_id"), status);
|
|
3350
|
+
if (!groupId) return c.json({
|
|
3351
|
+
error: "group_id required",
|
|
3352
|
+
status
|
|
3353
|
+
}, 400);
|
|
3354
|
+
try {
|
|
3355
|
+
if (!await coordinatorDisableDeviceAction({
|
|
3356
|
+
groupId,
|
|
3357
|
+
deviceId,
|
|
3358
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
3359
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
3360
|
+
})) return c.json({
|
|
3361
|
+
error: "device_not_found",
|
|
3362
|
+
status
|
|
3363
|
+
}, 404);
|
|
3364
|
+
return c.json({
|
|
3365
|
+
ok: true,
|
|
3366
|
+
device_id: deviceId,
|
|
3367
|
+
status
|
|
3368
|
+
});
|
|
3369
|
+
} catch (error) {
|
|
3370
|
+
return c.json({
|
|
3371
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3372
|
+
status
|
|
3373
|
+
}, 400);
|
|
3374
|
+
}
|
|
3375
|
+
});
|
|
3376
|
+
app.post("/api/coordinator/admin/devices/:device_id/remove", async (c) => {
|
|
3377
|
+
const config = readCoordinatorSyncConfig();
|
|
3378
|
+
const status = coordinatorAdminStatusPayload(config);
|
|
3379
|
+
if (status.readiness === "not_configured") return c.json({
|
|
3380
|
+
error: "coordinator_not_configured",
|
|
3381
|
+
status
|
|
3382
|
+
}, 400);
|
|
3383
|
+
if (!status.has_admin_secret) return c.json({
|
|
3384
|
+
error: "coordinator_admin_secret_missing",
|
|
3385
|
+
status
|
|
3386
|
+
}, 400);
|
|
3387
|
+
const deviceId = String(c.req.param("device_id") ?? "").trim();
|
|
3388
|
+
if (!deviceId) return c.json({
|
|
3389
|
+
error: "device_id required",
|
|
3390
|
+
status
|
|
3391
|
+
}, 400);
|
|
3392
|
+
const groupId = resolveCoordinatorAdminGroup(c.req.query("group_id"), status);
|
|
3393
|
+
if (!groupId) return c.json({
|
|
3394
|
+
error: "group_id required",
|
|
3395
|
+
status
|
|
3396
|
+
}, 400);
|
|
3397
|
+
try {
|
|
3398
|
+
if (!await coordinatorRemoveDeviceAction({
|
|
3399
|
+
groupId,
|
|
3400
|
+
deviceId,
|
|
3401
|
+
remoteUrl: config.syncCoordinatorUrl || null,
|
|
3402
|
+
adminSecret: config.syncCoordinatorAdminSecret || null
|
|
3403
|
+
})) return c.json({
|
|
3404
|
+
error: "device_not_found",
|
|
3405
|
+
status
|
|
3406
|
+
}, 404);
|
|
3407
|
+
return c.json({
|
|
3408
|
+
ok: true,
|
|
3409
|
+
device_id: deviceId,
|
|
3410
|
+
status
|
|
3411
|
+
});
|
|
3412
|
+
} catch (error) {
|
|
3413
|
+
return c.json({
|
|
3414
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3415
|
+
status
|
|
3416
|
+
}, 400);
|
|
2871
3417
|
}
|
|
2872
3418
|
});
|
|
2873
3419
|
app.get("/api/sync/bootstrap-grants", async (c) => {
|