@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 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$1(projectRaw) : "";
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$1(raw) {
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$1(p)).filter(Boolean))].sort();
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 && !isSharedVisibility(payload)) {
1951
+ if (!allowPrivate && !syncVisibilityAllowed(payload)) {
1954
1952
  skipped++;
1955
1953
  continue;
1956
1954
  }
1957
- if (!projectAllowed(payload && typeof payload.project === "string" ? payload.project : null, filters)) {
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 coordinator = redactCoordinatorStatus(await coordinatorStatusSnapshot(store, config), showDiag);
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 = Number.parseInt(String(body.ttl_hours ?? 24), 10);
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 (!Number.isFinite(ttlHours)) return c.json({ error: "ttl_hours must be int" }, 400);
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.post("/api/sync/join-requests/review", async (c) => {
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({ error: "invalid json" }, 400);
2901
+ return c.json({
2902
+ error: "invalid json",
2903
+ status
2904
+ }, 400);
2849
2905
  }
2850
- const requestId = String(body.request_id ?? "").trim();
2851
- const action = String(body.action ?? "").trim();
2852
- if (!requestId) return c.json({ error: "request_id required" }, 400);
2853
- if (!["approve", "deny"].includes(action)) return c.json({ error: "action must be approve or deny" }, 400);
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: action === "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({ error: "join request not found" }, 404);
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({ error: message }, message.includes("request_not_found") || message.includes("not found") ? 404 : 400);
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) => {