@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 +584 -49
- package/dist/index.js.map +1 -1
- package/dist/routes/sync.d.ts.map +1 -1
- package/package.json +2 -2
- package/static/app.js +3926 -3290
- package/static/index.html +229 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
3
|
+
import { CODEMEM_CONFIG_ENV_OVERRIDES, DEFAULT_TIME_WINDOW_S, MemoryStore, VERSION, VERSION as VERSION$1, applyReplicationOps, buildBaseUrl, buildFilterClausesWithContext, buildRawEventEnvelopeFromHook, canonicalMemoryKind, cleanupNonces, coordinatorArchiveGroupAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, createCoordinatorReciprocalApproval, ensureDeviceIdentity, extractReplicationOps, fingerprintPublicKey, fromJson, getCodememConfigPath, getCodememEnvOverrides, getSemanticIndexDiagnostics, getSyncResetState, isSummaryLikeMemory, listCoordinatorJoinRequests, listMaintenanceJobs, listObserverProviderOptions, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, mergeAddresses, parseStrictInteger, probeAvailableCredentials, readCodememConfigFile, readCoordinatorSyncConfig, recordNonce, requestJson, resolveDbPath, runSyncPass, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncProjectAllowedByFilters, syncVisibilityAllowed, verifySignature, writeCodememConfigFile } from "@codemem/core";
|
|
4
4
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { createMiddleware } from "hono/factory";
|
|
@@ -609,7 +609,7 @@ function attachSessionFields(store, items) {
|
|
|
609
609
|
const bySession = /* @__PURE__ */ new Map();
|
|
610
610
|
for (const row of rows) {
|
|
611
611
|
const projectRaw = String(row.project ?? "").trim();
|
|
612
|
-
const project = projectRaw ? projectBasename
|
|
612
|
+
const project = projectRaw ? projectBasename(projectRaw) : "";
|
|
613
613
|
const cwd = String(row.cwd ?? "");
|
|
614
614
|
bySession.set(row.id, {
|
|
615
615
|
project,
|
|
@@ -629,7 +629,7 @@ function attachSessionFields(store, items) {
|
|
|
629
629
|
* Extract the basename of a project path.
|
|
630
630
|
* Strips "fatal:" prefixed values.
|
|
631
631
|
*/
|
|
632
|
-
function projectBasename
|
|
632
|
+
function projectBasename(raw) {
|
|
633
633
|
if (raw.toLowerCase().startsWith("fatal:")) return "";
|
|
634
634
|
const parts = raw.replace(/\\/g, "/").split("/");
|
|
635
635
|
return parts[parts.length - 1] ?? raw;
|
|
@@ -696,7 +696,7 @@ function memoryRoutes(getStore) {
|
|
|
696
696
|
const store = getStore();
|
|
697
697
|
{
|
|
698
698
|
const rows = drizzle(store.db, { schema }).selectDistinct({ project: schema.sessions.project }).from(schema.sessions).where(isNotNull(schema.sessions.project)).all();
|
|
699
|
-
const projects = [...new Set(rows.map((r) => String(r.project ?? "").trim()).filter((p) => p && !p.toLowerCase().startsWith("fatal:")).map((p) => projectBasename
|
|
699
|
+
const projects = [...new Set(rows.map((r) => String(r.project ?? "").trim()).filter((p) => p && !p.toLowerCase().startsWith("fatal:")).map((p) => projectBasename(p)).filter(Boolean))].sort();
|
|
700
700
|
return c.json({ projects });
|
|
701
701
|
}
|
|
702
702
|
});
|
|
@@ -1418,6 +1418,34 @@ var SYNC_STALE_AFTER_SECONDS = 600;
|
|
|
1418
1418
|
var SYNC_PROTOCOL_VERSION = "2";
|
|
1419
1419
|
var LEGACY_SYNC_ACTOR_DISPLAY_NAME = "Legacy synced peer";
|
|
1420
1420
|
var LEGACY_SHARED_WORKSPACE_ID = "shared:legacy";
|
|
1421
|
+
function coordinatorAdminStatusPayload(config = readCoordinatorSyncConfig()) {
|
|
1422
|
+
const groups = config.syncCoordinatorGroups;
|
|
1423
|
+
const activeGroup = groups[0] ?? null;
|
|
1424
|
+
const hasUrl = Boolean(config.syncCoordinatorUrl);
|
|
1425
|
+
const hasGroups = groups.length > 0;
|
|
1426
|
+
const hasAdminSecret = Boolean(config.syncCoordinatorAdminSecret);
|
|
1427
|
+
let readiness = "ready";
|
|
1428
|
+
if (!hasUrl) readiness = "not_configured";
|
|
1429
|
+
else if (!hasGroups || !hasAdminSecret) readiness = "partial";
|
|
1430
|
+
return {
|
|
1431
|
+
readiness,
|
|
1432
|
+
coordinator_url: config.syncCoordinatorUrl || null,
|
|
1433
|
+
groups,
|
|
1434
|
+
active_group: activeGroup,
|
|
1435
|
+
has_admin_secret: hasAdminSecret,
|
|
1436
|
+
has_groups: hasGroups
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function resolveCoordinatorAdminGroup(requestedGroup, status) {
|
|
1440
|
+
const explicit = String(requestedGroup ?? "").trim();
|
|
1441
|
+
if (explicit) return explicit;
|
|
1442
|
+
return status.active_group;
|
|
1443
|
+
}
|
|
1444
|
+
function parseInviteTtlHours(value) {
|
|
1445
|
+
const ttlHours = Number(String(value ?? 24));
|
|
1446
|
+
if (!Number.isInteger(ttlHours) || ttlHours < 1) return null;
|
|
1447
|
+
return ttlHours;
|
|
1448
|
+
}
|
|
1421
1449
|
function sortActiveMaintenanceJobs(jobs) {
|
|
1422
1450
|
return [...jobs].sort((a, b) => {
|
|
1423
1451
|
const aTime = a.started_at ?? a.updated_at;
|
|
@@ -1685,12 +1713,6 @@ async function authorizeBootstrapGrantRequest(store, request, body) {
|
|
|
1685
1713
|
deviceId
|
|
1686
1714
|
};
|
|
1687
1715
|
}
|
|
1688
|
-
function projectBasename(value) {
|
|
1689
|
-
const project = String(value ?? "").trim().replaceAll("\\", "/");
|
|
1690
|
-
if (!project) return "";
|
|
1691
|
-
const parts = project.split("/").filter(Boolean);
|
|
1692
|
-
return parts.length > 0 ? parts[parts.length - 1] ?? "" : "";
|
|
1693
|
-
}
|
|
1694
1716
|
function parseJsonList(value) {
|
|
1695
1717
|
if (value == null) return [];
|
|
1696
1718
|
if (typeof value === "string") try {
|
|
@@ -1915,30 +1937,6 @@ async function acceptDiscoveredPeer(store, input) {
|
|
|
1915
1937
|
};
|
|
1916
1938
|
})();
|
|
1917
1939
|
}
|
|
1918
|
-
function isSharedVisibility(payload) {
|
|
1919
|
-
if (!payload) return false;
|
|
1920
|
-
let visibility = String(payload.visibility ?? "").trim().toLowerCase();
|
|
1921
|
-
const metadata = payload.metadata_json && typeof payload.metadata_json === "object" && !Array.isArray(payload.metadata_json) ? payload.metadata_json : {};
|
|
1922
|
-
const metadataVisibility = String(metadata.visibility ?? "").trim().toLowerCase();
|
|
1923
|
-
if (!visibility && metadataVisibility) visibility = metadataVisibility;
|
|
1924
|
-
if (!visibility) {
|
|
1925
|
-
let workspaceKind = String(payload.workspace_kind ?? "").trim().toLowerCase();
|
|
1926
|
-
let workspaceId = String(payload.workspace_id ?? "").trim().toLowerCase();
|
|
1927
|
-
if (!workspaceKind) workspaceKind = String(metadata.workspace_kind ?? "").trim().toLowerCase();
|
|
1928
|
-
if (!workspaceId) workspaceId = String(metadata.workspace_id ?? "").trim().toLowerCase();
|
|
1929
|
-
if (workspaceKind === "shared" || workspaceId.startsWith("shared:")) visibility = "shared";
|
|
1930
|
-
else return true;
|
|
1931
|
-
}
|
|
1932
|
-
return visibility === "shared";
|
|
1933
|
-
}
|
|
1934
|
-
function projectAllowed(projectValue, filters) {
|
|
1935
|
-
const value = String(projectValue ?? "").trim();
|
|
1936
|
-
const valueBase = projectBasename(value);
|
|
1937
|
-
for (const blocked of filters.exclude) if (blocked === value || blocked === valueBase) return false;
|
|
1938
|
-
if (filters.include.length === 0) return true;
|
|
1939
|
-
for (const allowed of filters.include) if (allowed === value || allowed === valueBase) return true;
|
|
1940
|
-
return false;
|
|
1941
|
-
}
|
|
1942
1940
|
function filterOpsForPeer(store, peerDeviceId, ops) {
|
|
1943
1941
|
const filters = readPeerProjectFilters(store, peerDeviceId);
|
|
1944
1942
|
const allowPrivate = peerClaimedLocalActor(store, peerDeviceId);
|
|
@@ -1950,11 +1948,11 @@ function filterOpsForPeer(store, peerDeviceId, ops) {
|
|
|
1950
1948
|
continue;
|
|
1951
1949
|
}
|
|
1952
1950
|
const payload = parseOpPayload(op);
|
|
1953
|
-
if (!allowPrivate && !
|
|
1951
|
+
if (!allowPrivate && !syncVisibilityAllowed(payload)) {
|
|
1954
1952
|
skipped++;
|
|
1955
1953
|
continue;
|
|
1956
1954
|
}
|
|
1957
|
-
if (!
|
|
1955
|
+
if (!syncProjectAllowedByFilters(payload && typeof payload.project === "string" ? payload.project : null, filters)) {
|
|
1958
1956
|
skipped++;
|
|
1959
1957
|
continue;
|
|
1960
1958
|
}
|
|
@@ -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 =
|
|
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 (
|
|
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.
|
|
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({
|
|
2901
|
+
return c.json({
|
|
2902
|
+
error: "invalid json",
|
|
2903
|
+
status
|
|
2904
|
+
}, 400);
|
|
2860
2905
|
}
|
|
2861
|
-
const
|
|
2862
|
-
const
|
|
2863
|
-
if (!
|
|
2864
|
-
|
|
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:
|
|
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({
|
|
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({
|
|
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) => {
|