@codemem/server 0.26.1 → 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
  }
@@ -2810,11 +2808,11 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2810
2808
  const groupId = String(body.group_id ?? "").trim();
2811
2809
  const coordinatorUrl = body.coordinator_url == null ? null : String(body.coordinator_url ?? "");
2812
2810
  const policy = String(body.policy ?? "auto_admit").trim();
2813
- const ttlHours = Number.parseInt(String(body.ttl_hours ?? 24), 10);
2811
+ const ttlHours = parseInviteTtlHours(body.ttl_hours);
2814
2812
  if (!groupId) return c.json({ error: "group_id required" }, 400);
2815
2813
  if (body.coordinator_url != null && typeof body.coordinator_url !== "string") return c.json({ error: "coordinator_url must be string" }, 400);
2816
2814
  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);
2815
+ if (ttlHours == null) return c.json({ error: "ttl_hours must be an integer >= 1" }, 400);
2818
2816
  try {
2819
2817
  const config = readCoordinatorSyncConfig();
2820
2818
  const result = await coordinatorCreateInviteAction({
@@ -2851,34 +2849,571 @@ function syncRoutes(getStore, getSyncRuntimeStatus) {
2851
2849
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
2852
2850
  }
2853
2851
  });
2854
- 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);
2855
2897
  let body;
2856
2898
  try {
2857
2899
  body = await c.req.json();
2858
2900
  } catch {
2859
- return c.json({ error: "invalid json" }, 400);
2901
+ return c.json({
2902
+ error: "invalid json",
2903
+ status
2904
+ }, 400);
2860
2905
  }
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);
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);
2865
3172
  try {
2866
- const config = readCoordinatorSyncConfig();
2867
3173
  const result = await coordinatorReviewJoinRequestAction({
2868
3174
  requestId,
2869
- approve: action === "approve",
3175
+ approve: true,
2870
3176
  reviewedBy: null,
2871
3177
  remoteUrl: config.syncCoordinatorUrl || null,
2872
3178
  adminSecret: config.syncCoordinatorAdminSecret || null
2873
3179
  });
2874
- 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);
2875
3184
  return c.json({
2876
3185
  ok: true,
2877
- request: result
3186
+ request: result,
3187
+ status
2878
3188
  });
2879
3189
  } catch (error) {
2880
3190
  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);
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);
2882
3417
  }
2883
3418
  });
2884
3419
  app.get("/api/sync/bootstrap-grants", async (c) => {