@codemem/server 0.26.1 → 0.27.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 CHANGED
@@ -1,11 +1,11 @@
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";
7
7
  import { RateLimiter } from "limiter";
8
- import { homedir } from "node:os";
8
+ import { homedir, networkInterfaces } from "node:os";
9
9
  import { and, count, desc, eq, inArray, isNotNull, max, ne } from "drizzle-orm";
10
10
  import { drizzle } from "drizzle-orm/better-sqlite3";
11
11
  import { createHash, randomUUID } from "node:crypto";
@@ -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,41 @@ 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 pairingAdvertiseAddresses(config = readCoordinatorSyncConfig()) {
1440
+ const advertise = String(config.syncAdvertise || "auto").trim().toLowerCase();
1441
+ if (advertise && advertise !== "auto" && advertise !== "default") return mergeAddresses([], String(config.syncAdvertise || "").split(",").map((item) => item.trim()).filter(Boolean));
1442
+ if (config.syncHost && config.syncHost !== "0.0.0.0") return [`${config.syncHost}:${config.syncPort}`];
1443
+ const addresses = Object.values(networkInterfaces()).flatMap((entries) => entries ?? []).filter((entry) => !entry.internal).map((entry) => entry.address).filter((address) => address && address !== "127.0.0.1" && address !== "::1").map((address) => `${address}:${config.syncPort}`);
1444
+ return [...new Set(addresses)];
1445
+ }
1446
+ function resolveCoordinatorAdminGroup(requestedGroup, status) {
1447
+ const explicit = String(requestedGroup ?? "").trim();
1448
+ if (explicit) return explicit;
1449
+ return status.active_group;
1450
+ }
1451
+ function parseInviteTtlHours(value) {
1452
+ const ttlHours = Number(String(value ?? 24));
1453
+ if (!Number.isInteger(ttlHours) || ttlHours < 1) return null;
1454
+ return ttlHours;
1455
+ }
1421
1456
  function sortActiveMaintenanceJobs(jobs) {
1422
1457
  return [...jobs].sort((a, b) => {
1423
1458
  const aTime = a.started_at ?? a.updated_at;
@@ -1685,12 +1720,6 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
1685
1720
  deviceId
1686
1721
  };
1687
1722
  }
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
1723
  function parseJsonList(value) {
1695
1724
  if (value == null) return [];
1696
1725
  if (typeof value === "string") try {
@@ -1915,30 +1944,6 @@ async function acceptDiscoveredPeer(store, input) {
1915
1944
  };
1916
1945
  })();
1917
1946
  }
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
1947
  function filterOpsForPeer(store, peerDeviceId, ops) {
1943
1948
  const filters = readPeerProjectFilters(store, peerDeviceId);
1944
1949
  const allowPrivate = peerClaimedLocalActor(store, peerDeviceId);
@@ -1950,11 +1955,11 @@ function filterOpsForPeer(store, peerDeviceId, ops) {
1950
1955
  continue;
1951
1956
  }
1952
1957
  const payload = parseOpPayload(op);
1953
- if (!allowPrivate && !isSharedVisibility(payload)) {
1958
+ if (!allowPrivate && !syncVisibilityAllowed(payload)) {
1954
1959
  skipped++;
1955
1960
  continue;
1956
1961
  }
1957
- if (!projectAllowed(payload && typeof payload.project === "string" ? payload.project : null, filters)) {
1962
+ if (!syncProjectAllowedByFilters(payload && typeof payload.project === "string" ? payload.project : null, filters)) {
1958
1963
  skipped++;
1959
1964
  continue;
1960
1965
  }
@@ -2486,6 +2491,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2486
2491
  redacted: true,
2487
2492
  pairing_filter_hint: PAIRING_FILTER_HINT
2488
2493
  });
2494
+ const config = readCoordinatorSyncConfig();
2489
2495
  const d = drizzle(store.db, { schema });
2490
2496
  const deviceRow = d.select({
2491
2497
  device_id: schema.syncDevice.device_id,
@@ -2513,7 +2519,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2513
2519
  fingerprint,
2514
2520
  public_key: publicKey ?? null,
2515
2521
  pairing_filter_hint: PAIRING_FILTER_HINT,
2516
- addresses: []
2522
+ addresses: pairingAdvertiseAddresses(config)
2517
2523
  });
2518
2524
  }
2519
2525
  });
@@ -2810,11 +2816,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2810
2816
  const groupId = String(body.group_id ?? "").trim();
2811
2817
  const coordinatorUrl = body.coordinator_url == null ? null : String(body.coordinator_url ?? "");
2812
2818
  const policy = String(body.policy ?? "auto_admit").trim();
2813
- const ttlHours = Number.parseInt(String(body.ttl_hours ?? 24), 10);
2819
+ const ttlHours = parseInviteTtlHours(body.ttl_hours);
2814
2820
  if (!groupId) return c.json({ error: "group_id required" }, 400);
2815
2821
  if (body.coordinator_url != null && typeof body.coordinator_url !== "string") return c.json({ error: "coordinator_url must be string" }, 400);
2816
2822
  if (!["auto_admit", "approval_required"].includes(policy)) return c.json({ error: "policy must be auto_admit or approval_required" }, 400);
2817
- if (!Number.isFinite(ttlHours)) return c.json({ error: "ttl_hours must be int" }, 400);
2823
+ if (ttlHours == null) return c.json({ error: "ttl_hours must be an integer >= 1" }, 400);
2818
2824
  try {
2819
2825
  const config = readCoordinatorSyncConfig();
2820
2826
  const result = await coordinatorCreateInviteAction({
@@ -2851,34 +2857,571 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2851
2857
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
2852
2858
  }
2853
2859
  });
2854
- app.post("/api/sync/join-requests/review", async (c) => {
2860
+ app.get("/api/coordinator/admin/status", async (c) => {
2861
+ const status = coordinatorAdminStatusPayload();
2862
+ return c.json(status);
2863
+ });
2864
+ app.get("/api/coordinator/admin/groups", async (c) => {
2865
+ const config = readCoordinatorSyncConfig();
2866
+ const status = coordinatorAdminStatusPayload(config);
2867
+ if (status.readiness === "not_configured") return c.json({
2868
+ error: "coordinator_not_configured",
2869
+ status
2870
+ }, 400);
2871
+ if (!status.has_admin_secret) return c.json({
2872
+ error: "coordinator_admin_secret_missing",
2873
+ status
2874
+ }, 400);
2875
+ const includeArchived = queryBool(c.req.query("include_archived"));
2876
+ try {
2877
+ const items = await coordinatorListGroupsAction({
2878
+ includeArchived,
2879
+ remoteUrl: config.syncCoordinatorUrl || null,
2880
+ adminSecret: config.syncCoordinatorAdminSecret || null
2881
+ });
2882
+ return c.json({
2883
+ items,
2884
+ status
2885
+ });
2886
+ } catch (error) {
2887
+ const message = error instanceof Error ? error.message : "unknown";
2888
+ return c.json({
2889
+ error: message,
2890
+ status
2891
+ }, 502);
2892
+ }
2893
+ });
2894
+ app.post("/api/coordinator/admin/groups", async (c) => {
2895
+ const config = readCoordinatorSyncConfig();
2896
+ const status = coordinatorAdminStatusPayload(config);
2897
+ if (status.readiness === "not_configured") return c.json({
2898
+ error: "coordinator_not_configured",
2899
+ status
2900
+ }, 400);
2901
+ if (!status.has_admin_secret) return c.json({
2902
+ error: "coordinator_admin_secret_missing",
2903
+ status
2904
+ }, 400);
2855
2905
  let body;
2856
2906
  try {
2857
2907
  body = await c.req.json();
2858
2908
  } catch {
2859
- return c.json({ error: "invalid json" }, 400);
2909
+ return c.json({
2910
+ error: "invalid json",
2911
+ status
2912
+ }, 400);
2860
2913
  }
2861
- const requestId = String(body.request_id ?? "").trim();
2862
- const action = String(body.action ?? "").trim();
2863
- if (!requestId) return c.json({ error: "request_id required" }, 400);
2864
- if (!["approve", "deny"].includes(action)) return c.json({ error: "action must be approve or deny" }, 400);
2914
+ const groupId = String(body.group_id ?? "").trim();
2915
+ const displayName = String(body.display_name ?? "").trim() || null;
2916
+ if (!groupId) return c.json({
2917
+ error: "group_id required",
2918
+ status
2919
+ }, 400);
2920
+ try {
2921
+ const group = await coordinatorCreateGroupAction({
2922
+ groupId,
2923
+ displayName,
2924
+ remoteUrl: config.syncCoordinatorUrl || null,
2925
+ adminSecret: config.syncCoordinatorAdminSecret || null
2926
+ });
2927
+ return c.json({
2928
+ ok: true,
2929
+ group,
2930
+ status
2931
+ });
2932
+ } catch (error) {
2933
+ return c.json({
2934
+ error: error instanceof Error ? error.message : String(error),
2935
+ status
2936
+ }, 400);
2937
+ }
2938
+ });
2939
+ app.post("/api/coordinator/admin/groups/:group_id/rename", async (c) => {
2940
+ const config = readCoordinatorSyncConfig();
2941
+ const status = coordinatorAdminStatusPayload(config);
2942
+ if (status.readiness === "not_configured") return c.json({
2943
+ error: "coordinator_not_configured",
2944
+ status
2945
+ }, 400);
2946
+ if (!status.has_admin_secret) return c.json({
2947
+ error: "coordinator_admin_secret_missing",
2948
+ status
2949
+ }, 400);
2950
+ const groupId = String(c.req.param("group_id") ?? "").trim();
2951
+ if (!groupId) return c.json({
2952
+ error: "group_id required",
2953
+ status
2954
+ }, 400);
2955
+ let body;
2956
+ try {
2957
+ body = await c.req.json();
2958
+ } catch {
2959
+ return c.json({
2960
+ error: "invalid json",
2961
+ status
2962
+ }, 400);
2963
+ }
2964
+ const displayName = String(body.display_name ?? "").trim();
2965
+ if (!displayName) return c.json({
2966
+ error: "display_name required",
2967
+ status
2968
+ }, 400);
2969
+ try {
2970
+ const group = await coordinatorRenameGroupAction({
2971
+ groupId,
2972
+ displayName,
2973
+ remoteUrl: config.syncCoordinatorUrl || null,
2974
+ adminSecret: config.syncCoordinatorAdminSecret || null
2975
+ });
2976
+ if (!group) return c.json({
2977
+ error: "group_not_found",
2978
+ status
2979
+ }, 404);
2980
+ return c.json({
2981
+ ok: true,
2982
+ group,
2983
+ status
2984
+ });
2985
+ } catch (error) {
2986
+ return c.json({
2987
+ error: error instanceof Error ? error.message : String(error),
2988
+ status
2989
+ }, 400);
2990
+ }
2991
+ });
2992
+ app.post("/api/coordinator/admin/groups/:group_id/archive", async (c) => {
2993
+ const config = readCoordinatorSyncConfig();
2994
+ const status = coordinatorAdminStatusPayload(config);
2995
+ if (status.readiness === "not_configured") return c.json({
2996
+ error: "coordinator_not_configured",
2997
+ status
2998
+ }, 400);
2999
+ if (!status.has_admin_secret) return c.json({
3000
+ error: "coordinator_admin_secret_missing",
3001
+ status
3002
+ }, 400);
3003
+ const groupId = String(c.req.param("group_id") ?? "").trim();
3004
+ if (!groupId) return c.json({
3005
+ error: "group_id required",
3006
+ status
3007
+ }, 400);
3008
+ try {
3009
+ const group = await coordinatorArchiveGroupAction({
3010
+ groupId,
3011
+ remoteUrl: config.syncCoordinatorUrl || null,
3012
+ adminSecret: config.syncCoordinatorAdminSecret || null
3013
+ });
3014
+ if (!group) return c.json({
3015
+ error: "group_not_found_or_already_archived",
3016
+ status
3017
+ }, 404);
3018
+ return c.json({
3019
+ ok: true,
3020
+ group,
3021
+ status
3022
+ });
3023
+ } catch (error) {
3024
+ return c.json({
3025
+ error: error instanceof Error ? error.message : String(error),
3026
+ status
3027
+ }, 400);
3028
+ }
3029
+ });
3030
+ app.post("/api/coordinator/admin/groups/:group_id/unarchive", async (c) => {
3031
+ const config = readCoordinatorSyncConfig();
3032
+ const status = coordinatorAdminStatusPayload(config);
3033
+ if (status.readiness === "not_configured") return c.json({
3034
+ error: "coordinator_not_configured",
3035
+ status
3036
+ }, 400);
3037
+ if (!status.has_admin_secret) return c.json({
3038
+ error: "coordinator_admin_secret_missing",
3039
+ status
3040
+ }, 400);
3041
+ const groupId = String(c.req.param("group_id") ?? "").trim();
3042
+ if (!groupId) return c.json({
3043
+ error: "group_id required",
3044
+ status
3045
+ }, 400);
3046
+ try {
3047
+ const group = await coordinatorUnarchiveGroupAction({
3048
+ groupId,
3049
+ remoteUrl: config.syncCoordinatorUrl || null,
3050
+ adminSecret: config.syncCoordinatorAdminSecret || null
3051
+ });
3052
+ if (!group) return c.json({
3053
+ error: "group_not_found_or_not_archived",
3054
+ status
3055
+ }, 404);
3056
+ return c.json({
3057
+ ok: true,
3058
+ group,
3059
+ status
3060
+ });
3061
+ } catch (error) {
3062
+ return c.json({
3063
+ error: error instanceof Error ? error.message : String(error),
3064
+ status
3065
+ }, 400);
3066
+ }
3067
+ });
3068
+ app.post("/api/coordinator/admin/invites", async (c) => {
3069
+ const config = readCoordinatorSyncConfig();
3070
+ const status = coordinatorAdminStatusPayload(config);
3071
+ if (status.readiness === "not_configured") return c.json({
3072
+ error: "coordinator_not_configured",
3073
+ status
3074
+ }, 400);
3075
+ if (!status.has_admin_secret) return c.json({
3076
+ error: "coordinator_admin_secret_missing",
3077
+ status
3078
+ }, 400);
3079
+ let body;
3080
+ try {
3081
+ body = await c.req.json();
3082
+ } catch {
3083
+ return c.json({
3084
+ error: "invalid json",
3085
+ status
3086
+ }, 400);
3087
+ }
3088
+ const groupId = resolveCoordinatorAdminGroup(String(body.group_id ?? "").trim(), status);
3089
+ const coordinatorUrl = body.coordinator_url == null ? null : String(body.coordinator_url ?? "");
3090
+ const policy = String(body.policy ?? "auto_admit").trim();
3091
+ const ttlHours = parseInviteTtlHours(body.ttl_hours);
3092
+ if (!groupId) return c.json({
3093
+ error: "group_id required",
3094
+ status
3095
+ }, 400);
3096
+ if (body.coordinator_url != null && typeof body.coordinator_url !== "string") return c.json({
3097
+ error: "coordinator_url must be string",
3098
+ status
3099
+ }, 400);
3100
+ if (!["auto_admit", "approval_required"].includes(policy)) return c.json({
3101
+ error: "policy must be auto_admit or approval_required",
3102
+ status
3103
+ }, 400);
3104
+ if (ttlHours == null) return c.json({
3105
+ error: "ttl_hours must be an integer >= 1",
3106
+ status
3107
+ }, 400);
3108
+ try {
3109
+ const result = await coordinatorCreateInviteAction({
3110
+ groupId,
3111
+ coordinatorUrl,
3112
+ policy,
3113
+ ttlHours,
3114
+ createdBy: null,
3115
+ remoteUrl: config.syncCoordinatorUrl || null,
3116
+ adminSecret: config.syncCoordinatorAdminSecret || null
3117
+ });
3118
+ return c.json({
3119
+ ...result,
3120
+ status
3121
+ });
3122
+ } catch (error) {
3123
+ return c.json({
3124
+ error: error instanceof Error ? error.message : String(error),
3125
+ status
3126
+ }, 400);
3127
+ }
3128
+ });
3129
+ app.get("/api/coordinator/admin/join-requests", async (c) => {
3130
+ const config = readCoordinatorSyncConfig();
3131
+ const status = coordinatorAdminStatusPayload(config);
3132
+ if (status.readiness === "not_configured") return c.json({
3133
+ error: "coordinator_not_configured",
3134
+ status
3135
+ }, 400);
3136
+ if (!status.has_admin_secret) return c.json({
3137
+ error: "coordinator_admin_secret_missing",
3138
+ status
3139
+ }, 400);
3140
+ const groupId = resolveCoordinatorAdminGroup(c.req.query("group_id"), status);
3141
+ if (!groupId) return c.json({
3142
+ error: "group_id_required",
3143
+ status
3144
+ }, 400);
3145
+ try {
3146
+ const items = await coordinatorListJoinRequestsAction({
3147
+ groupId,
3148
+ remoteUrl: status.coordinator_url,
3149
+ adminSecret: config.syncCoordinatorAdminSecret || null
3150
+ });
3151
+ return c.json({
3152
+ items,
3153
+ group_id: groupId,
3154
+ status
3155
+ });
3156
+ } catch (error) {
3157
+ const message = error instanceof Error ? error.message : "unknown";
3158
+ return c.json({
3159
+ error: message,
3160
+ status
3161
+ }, 502);
3162
+ }
3163
+ });
3164
+ app.post("/api/coordinator/admin/join-requests/:request_id/approve", async (c) => {
3165
+ const config = readCoordinatorSyncConfig();
3166
+ const status = coordinatorAdminStatusPayload(config);
3167
+ if (status.readiness === "not_configured") return c.json({
3168
+ error: "coordinator_not_configured",
3169
+ status
3170
+ }, 400);
3171
+ if (!status.has_admin_secret) return c.json({
3172
+ error: "coordinator_admin_secret_missing",
3173
+ status
3174
+ }, 400);
3175
+ const requestId = String(c.req.param("request_id") ?? "").trim();
3176
+ if (!requestId) return c.json({
3177
+ error: "request_id required",
3178
+ status
3179
+ }, 400);
3180
+ try {
3181
+ const result = await coordinatorReviewJoinRequestAction({
3182
+ requestId,
3183
+ approve: true,
3184
+ reviewedBy: null,
3185
+ remoteUrl: config.syncCoordinatorUrl || null,
3186
+ adminSecret: config.syncCoordinatorAdminSecret || null
3187
+ });
3188
+ if (!result) return c.json({
3189
+ error: "join request not found",
3190
+ status
3191
+ }, 404);
3192
+ return c.json({
3193
+ ok: true,
3194
+ request: result,
3195
+ status
3196
+ });
3197
+ } catch (error) {
3198
+ const message = error instanceof Error ? error.message : String(error);
3199
+ return c.json({
3200
+ error: message,
3201
+ status
3202
+ }, message.includes("request_not_found") || message.includes("not found") ? 404 : 400);
3203
+ }
3204
+ });
3205
+ app.post("/api/coordinator/admin/join-requests/:request_id/deny", async (c) => {
3206
+ const config = readCoordinatorSyncConfig();
3207
+ const status = coordinatorAdminStatusPayload(config);
3208
+ if (status.readiness === "not_configured") return c.json({
3209
+ error: "coordinator_not_configured",
3210
+ status
3211
+ }, 400);
3212
+ if (!status.has_admin_secret) return c.json({
3213
+ error: "coordinator_admin_secret_missing",
3214
+ status
3215
+ }, 400);
3216
+ const requestId = String(c.req.param("request_id") ?? "").trim();
3217
+ if (!requestId) return c.json({
3218
+ error: "request_id required",
3219
+ status
3220
+ }, 400);
2865
3221
  try {
2866
- const config = readCoordinatorSyncConfig();
2867
3222
  const result = await coordinatorReviewJoinRequestAction({
2868
3223
  requestId,
2869
- approve: action === "approve",
3224
+ approve: false,
2870
3225
  reviewedBy: null,
2871
3226
  remoteUrl: config.syncCoordinatorUrl || null,
2872
3227
  adminSecret: config.syncCoordinatorAdminSecret || null
2873
3228
  });
2874
- if (!result) return c.json({ error: "join request not found" }, 404);
3229
+ if (!result) return c.json({
3230
+ error: "join request not found",
3231
+ status
3232
+ }, 404);
2875
3233
  return c.json({
2876
3234
  ok: true,
2877
- request: result
3235
+ request: result,
3236
+ status
2878
3237
  });
2879
3238
  } catch (error) {
2880
3239
  const message = error instanceof Error ? error.message : String(error);
2881
- return c.json({ error: message }, message.includes("request_not_found") || message.includes("not found") ? 404 : 400);
3240
+ return c.json({
3241
+ error: message,
3242
+ status
3243
+ }, message.includes("request_not_found") || message.includes("not found") ? 404 : 400);
3244
+ }
3245
+ });
3246
+ app.get("/api/coordinator/admin/devices", async (c) => {
3247
+ const config = readCoordinatorSyncConfig();
3248
+ const status = coordinatorAdminStatusPayload(config);
3249
+ if (status.readiness === "not_configured") return c.json({
3250
+ error: "coordinator_not_configured",
3251
+ status
3252
+ }, 400);
3253
+ if (!status.has_admin_secret) return c.json({
3254
+ error: "coordinator_admin_secret_missing",
3255
+ status
3256
+ }, 400);
3257
+ const groupId = resolveCoordinatorAdminGroup(c.req.query("group_id"), status);
3258
+ if (!groupId) return c.json({
3259
+ error: "group_id_required",
3260
+ status
3261
+ }, 400);
3262
+ try {
3263
+ const items = await coordinatorListDevicesAction({
3264
+ groupId,
3265
+ includeDisabled: queryBool(c.req.query("include_disabled")),
3266
+ remoteUrl: status.coordinator_url,
3267
+ adminSecret: config.syncCoordinatorAdminSecret || null
3268
+ });
3269
+ return c.json({
3270
+ items,
3271
+ group_id: groupId,
3272
+ status
3273
+ });
3274
+ } catch (error) {
3275
+ const message = error instanceof Error ? error.message : "unknown";
3276
+ return c.json({
3277
+ error: message,
3278
+ status
3279
+ }, 502);
3280
+ }
3281
+ });
3282
+ app.post("/api/coordinator/admin/devices/:device_id/rename", async (c) => {
3283
+ const config = readCoordinatorSyncConfig();
3284
+ const status = coordinatorAdminStatusPayload(config);
3285
+ if (status.readiness === "not_configured") return c.json({
3286
+ error: "coordinator_not_configured",
3287
+ status
3288
+ }, 400);
3289
+ if (!status.has_admin_secret) return c.json({
3290
+ error: "coordinator_admin_secret_missing",
3291
+ status
3292
+ }, 400);
3293
+ const deviceId = String(c.req.param("device_id") ?? "").trim();
3294
+ if (!deviceId) return c.json({
3295
+ error: "device_id required",
3296
+ status
3297
+ }, 400);
3298
+ let body;
3299
+ try {
3300
+ body = await c.req.json();
3301
+ } catch {
3302
+ return c.json({
3303
+ error: "invalid json",
3304
+ status
3305
+ }, 400);
3306
+ }
3307
+ const groupId = resolveCoordinatorAdminGroup(String(body.group_id ?? "").trim(), status);
3308
+ const displayName = String(body.display_name ?? "").trim();
3309
+ if (!groupId) return c.json({
3310
+ error: "group_id required",
3311
+ status
3312
+ }, 400);
3313
+ if (!displayName) return c.json({
3314
+ error: "display_name required",
3315
+ status
3316
+ }, 400);
3317
+ try {
3318
+ const device = await coordinatorRenameDeviceAction({
3319
+ groupId,
3320
+ deviceId,
3321
+ displayName,
3322
+ remoteUrl: config.syncCoordinatorUrl || null,
3323
+ adminSecret: config.syncCoordinatorAdminSecret || null
3324
+ });
3325
+ if (!device) return c.json({
3326
+ error: "device_not_found",
3327
+ status
3328
+ }, 404);
3329
+ return c.json({
3330
+ ok: true,
3331
+ device,
3332
+ status
3333
+ });
3334
+ } catch (error) {
3335
+ return c.json({
3336
+ error: error instanceof Error ? error.message : String(error),
3337
+ status
3338
+ }, 400);
3339
+ }
3340
+ });
3341
+ app.post("/api/coordinator/admin/devices/:device_id/disable", async (c) => {
3342
+ const config = readCoordinatorSyncConfig();
3343
+ const status = coordinatorAdminStatusPayload(config);
3344
+ if (status.readiness === "not_configured") return c.json({
3345
+ error: "coordinator_not_configured",
3346
+ status
3347
+ }, 400);
3348
+ if (!status.has_admin_secret) return c.json({
3349
+ error: "coordinator_admin_secret_missing",
3350
+ status
3351
+ }, 400);
3352
+ const deviceId = String(c.req.param("device_id") ?? "").trim();
3353
+ if (!deviceId) return c.json({
3354
+ error: "device_id required",
3355
+ status
3356
+ }, 400);
3357
+ const groupId = resolveCoordinatorAdminGroup(c.req.query("group_id"), status);
3358
+ if (!groupId) return c.json({
3359
+ error: "group_id required",
3360
+ status
3361
+ }, 400);
3362
+ try {
3363
+ if (!await coordinatorDisableDeviceAction({
3364
+ groupId,
3365
+ deviceId,
3366
+ remoteUrl: config.syncCoordinatorUrl || null,
3367
+ adminSecret: config.syncCoordinatorAdminSecret || null
3368
+ })) return c.json({
3369
+ error: "device_not_found",
3370
+ status
3371
+ }, 404);
3372
+ return c.json({
3373
+ ok: true,
3374
+ device_id: deviceId,
3375
+ status
3376
+ });
3377
+ } catch (error) {
3378
+ return c.json({
3379
+ error: error instanceof Error ? error.message : String(error),
3380
+ status
3381
+ }, 400);
3382
+ }
3383
+ });
3384
+ app.post("/api/coordinator/admin/devices/:device_id/remove", async (c) => {
3385
+ const config = readCoordinatorSyncConfig();
3386
+ const status = coordinatorAdminStatusPayload(config);
3387
+ if (status.readiness === "not_configured") return c.json({
3388
+ error: "coordinator_not_configured",
3389
+ status
3390
+ }, 400);
3391
+ if (!status.has_admin_secret) return c.json({
3392
+ error: "coordinator_admin_secret_missing",
3393
+ status
3394
+ }, 400);
3395
+ const deviceId = String(c.req.param("device_id") ?? "").trim();
3396
+ if (!deviceId) return c.json({
3397
+ error: "device_id required",
3398
+ status
3399
+ }, 400);
3400
+ const groupId = resolveCoordinatorAdminGroup(c.req.query("group_id"), status);
3401
+ if (!groupId) return c.json({
3402
+ error: "group_id required",
3403
+ status
3404
+ }, 400);
3405
+ try {
3406
+ if (!await coordinatorRemoveDeviceAction({
3407
+ groupId,
3408
+ deviceId,
3409
+ remoteUrl: config.syncCoordinatorUrl || null,
3410
+ adminSecret: config.syncCoordinatorAdminSecret || null
3411
+ })) return c.json({
3412
+ error: "device_not_found",
3413
+ status
3414
+ }, 404);
3415
+ return c.json({
3416
+ ok: true,
3417
+ device_id: deviceId,
3418
+ status
3419
+ });
3420
+ } catch (error) {
3421
+ return c.json({
3422
+ error: error instanceof Error ? error.message : String(error),
3423
+ status
3424
+ }, 400);
2882
3425
  }
2883
3426
  });
2884
3427
  app.get("/api/sync/bootstrap-grants", async (c) => {