@femtomc/mu-server 26.2.53 → 26.2.55

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.
Files changed (2) hide show
  1. package/dist/server.js +101 -0
  2. package/package.json +6 -6
package/dist/server.js CHANGED
@@ -2,6 +2,7 @@ import { extname, join, resolve } from "node:path";
2
2
  import { currentRunId, EventLog, FsJsonlStore, getStorePaths, JsonlEventSink } from "@femtomc/mu-core/node";
3
3
  import { ForumStore } from "@femtomc/mu-forum";
4
4
  import { IssueStore } from "@femtomc/mu-issue";
5
+ import { getControlPlanePaths, IdentityStore, ROLE_SCOPES } from "@femtomc/mu-control-plane";
5
6
  import { eventRoutes } from "./api/events.js";
6
7
  import { forumRoutes } from "./api/forum.js";
7
8
  import { issueRoutes } from "./api/issues.js";
@@ -830,6 +831,106 @@ export function createServer(options = {}) {
830
831
  }
831
832
  return Response.json(activity, { headers });
832
833
  }
834
+ if (path === "/api/identities" || path === "/api/identities/link" || path === "/api/identities/unlink") {
835
+ const cpPaths = getControlPlanePaths(context.repoRoot);
836
+ const identityStore = new IdentityStore(cpPaths.identitiesPath);
837
+ await identityStore.load();
838
+ if (path === "/api/identities") {
839
+ if (request.method !== "GET") {
840
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
841
+ }
842
+ const includeInactive = url.searchParams.get("include_inactive")?.trim().toLowerCase() === "true";
843
+ const bindings = identityStore.listBindings({ includeInactive });
844
+ return Response.json({ count: bindings.length, bindings }, { headers });
845
+ }
846
+ if (path === "/api/identities/link") {
847
+ if (request.method !== "POST") {
848
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
849
+ }
850
+ let body;
851
+ try {
852
+ body = (await request.json());
853
+ }
854
+ catch {
855
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
856
+ }
857
+ const channel = typeof body.channel === "string" ? body.channel.trim() : "";
858
+ if (!channel || (channel !== "slack" && channel !== "discord" && channel !== "telegram")) {
859
+ return Response.json({ error: "channel is required (slack, discord, telegram)" }, { status: 400, headers });
860
+ }
861
+ const actorId = typeof body.actor_id === "string" ? body.actor_id.trim() : "";
862
+ if (!actorId) {
863
+ return Response.json({ error: "actor_id is required" }, { status: 400, headers });
864
+ }
865
+ const tenantId = typeof body.tenant_id === "string" ? body.tenant_id.trim() : "";
866
+ if (!tenantId) {
867
+ return Response.json({ error: "tenant_id is required" }, { status: 400, headers });
868
+ }
869
+ const roleKey = typeof body.role === "string" ? body.role.trim() : "operator";
870
+ const roleScopes = ROLE_SCOPES[roleKey];
871
+ if (!roleScopes) {
872
+ return Response.json({ error: `invalid role: ${roleKey} (operator, contributor, viewer)` }, { status: 400, headers });
873
+ }
874
+ const bindingId = typeof body.binding_id === "string" && body.binding_id.trim().length > 0
875
+ ? body.binding_id.trim()
876
+ : `bind-${crypto.randomUUID()}`;
877
+ const operatorId = typeof body.operator_id === "string" && body.operator_id.trim().length > 0
878
+ ? body.operator_id.trim()
879
+ : "default";
880
+ const decision = await identityStore.link({
881
+ bindingId,
882
+ operatorId,
883
+ channel: channel,
884
+ channelTenantId: tenantId,
885
+ channelActorId: actorId,
886
+ scopes: [...roleScopes],
887
+ });
888
+ switch (decision.kind) {
889
+ case "linked":
890
+ return Response.json({ ok: true, kind: "linked", binding: decision.binding }, { status: 201, headers });
891
+ case "binding_exists":
892
+ return Response.json({ ok: false, kind: "binding_exists", binding: decision.binding }, { status: 409, headers });
893
+ case "principal_already_linked":
894
+ return Response.json({ ok: false, kind: "principal_already_linked", binding: decision.binding }, { status: 409, headers });
895
+ }
896
+ }
897
+ if (path === "/api/identities/unlink") {
898
+ if (request.method !== "POST") {
899
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
900
+ }
901
+ let body;
902
+ try {
903
+ body = (await request.json());
904
+ }
905
+ catch {
906
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
907
+ }
908
+ const bindingId = typeof body.binding_id === "string" ? body.binding_id.trim() : "";
909
+ if (!bindingId) {
910
+ return Response.json({ error: "binding_id is required" }, { status: 400, headers });
911
+ }
912
+ const actorBindingId = typeof body.actor_binding_id === "string" ? body.actor_binding_id.trim() : "";
913
+ if (!actorBindingId) {
914
+ return Response.json({ error: "actor_binding_id is required" }, { status: 400, headers });
915
+ }
916
+ const reason = typeof body.reason === "string" ? body.reason.trim() : null;
917
+ const decision = await identityStore.unlinkSelf({
918
+ bindingId,
919
+ actorBindingId,
920
+ reason: reason || null,
921
+ });
922
+ switch (decision.kind) {
923
+ case "unlinked":
924
+ return Response.json({ ok: true, kind: "unlinked", binding: decision.binding }, { headers });
925
+ case "not_found":
926
+ return Response.json({ ok: false, kind: "not_found" }, { status: 404, headers });
927
+ case "invalid_actor":
928
+ return Response.json({ ok: false, kind: "invalid_actor" }, { status: 403, headers });
929
+ case "already_inactive":
930
+ return Response.json({ ok: false, kind: "already_inactive", binding: decision.binding }, { status: 409, headers });
931
+ }
932
+ }
933
+ }
833
934
  if (path.startsWith("/api/issues")) {
834
935
  const response = await issueRoutes(request, context);
835
936
  headers.forEach((value, key) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.53",
3
+ "version": "26.2.55",
4
4
  "description": "HTTP API server for mu status, work items, messaging setup, and web UI.",
5
5
  "keywords": [
6
6
  "mu",
@@ -31,10 +31,10 @@
31
31
  "start": "bun run dist/cli.js"
32
32
  },
33
33
  "dependencies": {
34
- "@femtomc/mu-agent": "26.2.53",
35
- "@femtomc/mu-control-plane": "26.2.53",
36
- "@femtomc/mu-core": "26.2.53",
37
- "@femtomc/mu-forum": "26.2.53",
38
- "@femtomc/mu-issue": "26.2.53"
34
+ "@femtomc/mu-agent": "26.2.54",
35
+ "@femtomc/mu-control-plane": "26.2.54",
36
+ "@femtomc/mu-core": "26.2.54",
37
+ "@femtomc/mu-forum": "26.2.54",
38
+ "@femtomc/mu-issue": "26.2.54"
39
39
  }
40
40
  }