@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 +594 -51
- 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 +4160 -3398
- package/static/index.html +330 -1
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
|
|
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,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 && !
|
|
1958
|
+
if (!allowPrivate && !syncVisibilityAllowed(payload)) {
|
|
1954
1959
|
skipped++;
|
|
1955
1960
|
continue;
|
|
1956
1961
|
}
|
|
1957
|
-
if (!
|
|
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 =
|
|
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 (
|
|
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.
|
|
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({
|
|
2909
|
+
return c.json({
|
|
2910
|
+
error: "invalid json",
|
|
2911
|
+
status
|
|
2912
|
+
}, 400);
|
|
2860
2913
|
}
|
|
2861
|
-
const
|
|
2862
|
-
const
|
|
2863
|
-
if (!
|
|
2864
|
-
|
|
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:
|
|
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({
|
|
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({
|
|
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) => {
|