@x12i/memorix-service 3.6.0 → 3.6.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.
@@ -661,437 +661,123 @@ export async function registerMetadataRoutes(app, platform) {
661
661
  note,
662
662
  };
663
663
  });
664
- app.get("/api/metadata/packages/:kind/:id/associations", async (req) => {
665
- requireMagit(platform);
666
- const kind = parseKind(req.params.kind);
667
- const id = String(req.params.id ?? "").trim();
664
+ // Magit 2.14 package channels & release sets (Mode A only; fail closed if unset)
665
+ // Static paths registered before /:kind/:id/* so Fastify never treats them as params.
666
+ app.get("/api/metadata/packages/capabilities", async () => {
667
+ const magit = requireMagit(platform);
668
+ const caps = magit.packageCatalogCapabilities();
669
+ return {
670
+ ...caps,
671
+ note: "Safe capability flags for Packages UI. Never includes tokens or store selector.",
672
+ };
673
+ });
674
+ app.post("/api/metadata/packages/channels/promote", async (req) => {
675
+ const magit = requireMagit(platform);
676
+ const body = req.body;
677
+ const kind = parseKind(body.kind);
678
+ const id = body.id?.trim();
679
+ const channel = body.channel?.trim();
680
+ const version = body.version?.trim();
668
681
  if (!id)
669
682
  throw new ServiceError("VALIDATION", "id required", 400);
670
- const packs = installedPacks();
671
- const q = req.query;
672
- const agentId = resolveOwnerAgent(packs, kind, id, q.agentId);
673
- const pack = packs[agentId];
674
- if (!pack) {
675
- throw new ServiceError("NOT_FOUND", `No installed pack for agent ${agentId}`, 404);
676
- }
677
- return {
683
+ if (!channel)
684
+ throw new ServiceError("VALIDATION", "channel required", 400);
685
+ if (!version)
686
+ throw new ServiceError("VALIDATION", "version required", 400);
687
+ const result = await magit.setPackageChannel({
678
688
  kind,
679
689
  id,
680
- agentId,
681
- associations: associationsFromPack(pack),
690
+ channel,
691
+ version,
692
+ message: body.message?.trim() || undefined,
693
+ expectedVersion: body.expectedVersion,
694
+ });
695
+ return {
696
+ ...result,
697
+ note: "Channel pointer updated. Packages do not move; promote does not install into organizations.",
682
698
  };
683
699
  });
684
- // FR-CPS-3/5 — flat catalog listing. Org-free: kind + id only, no agentId
685
- // resolution needed (a catalog package isn't owned by any installed pack).
686
- app.get("/api/metadata/packages/:kind/:id/versions", async (req) => {
700
+ app.get("/api/metadata/packages/:kind/:id/channels", async (req) => {
687
701
  const magit = requireMagit(platform);
688
702
  const kind = parseKind(req.params.kind);
689
703
  const id = String(req.params.id ?? "").trim();
690
704
  if (!id)
691
705
  throw new ServiceError("VALIDATION", "id required", 400);
692
- const q = req.query;
693
- const maxCount = q.maxCount ? Number(q.maxCount) : 50;
694
- const versions = await magit.listPackages({
695
- kind,
696
- id,
697
- maxCount: Number.isFinite(maxCount) ? maxCount : 50,
698
- });
706
+ const channels = await magit.listPackageChannels({ kind, id });
699
707
  return {
700
708
  kind,
701
709
  id,
702
- // `commit` aliases `version` for existing HTTP callers written against
703
- // the pre-cutover org-scoped history shape (e.g. Studio's
704
- // PackVersionsPage.tsx keys/renders list rows by `.commit`). `version`
705
- // is the FR-CPS-4/5 field name and the one new callers should use.
706
- versions: versions.map((v) => ({ ...v, commit: v.version })),
707
- note: "Versions from the central Magit package catalog.",
710
+ channels,
711
+ note: "Known channel heads for this package (shared catalog).",
708
712
  };
709
713
  });
710
- // FR-CPS-5 — fetch a package version's payload directly (§7 of the FR).
711
- // Org header is ignored: catalog reads are org-free.
712
- app.get("/api/metadata/packages/:kind/:id/versions/:version", async (req) => {
714
+ app.get("/api/metadata/packages/:kind/:id/channels/:channel", async (req) => {
713
715
  const magit = requireMagit(platform);
714
716
  const kind = parseKind(req.params.kind);
715
717
  const id = String(req.params.id ?? "").trim();
718
+ const channel = String(req.params.channel ?? "").trim();
716
719
  if (!id)
717
720
  throw new ServiceError("VALIDATION", "id required", 400);
718
- const version = String(req.params.version ?? "").trim();
719
- if (!version)
720
- throw new ServiceError("VALIDATION", "version required", 400);
721
- const fetched = await magit.fetchPackage({ kind, id, version });
721
+ if (!channel)
722
+ throw new ServiceError("VALIDATION", "channel required", 400);
723
+ const result = await magit.resolvePackageChannel({ kind, id, channel });
722
724
  return {
723
- kind,
724
- id,
725
- version,
726
- checksum: fetched.checksum,
727
- payload: fetched.payload,
728
- associations: fetched.associations,
725
+ ...result,
726
+ note: "Resolved after Magit sync (fresh channel head).",
729
727
  };
730
728
  });
731
- app.post("/api/metadata/packages/pack", async (req) => {
729
+ app.get("/api/metadata/packages/:kind/:id/channels/:channel/history", async (req) => {
732
730
  const magit = requireMagit(platform);
733
- const body = req.body;
734
- const kind = parseKind(body.kind);
735
- const id = body.id?.trim();
731
+ const kind = parseKind(req.params.kind);
732
+ const id = String(req.params.id ?? "").trim();
733
+ const channel = String(req.params.channel ?? "").trim();
736
734
  if (!id)
737
735
  throw new ServiceError("VALIDATION", "id required", 400);
738
- const message = body.message?.trim();
739
- if (!message)
740
- throw new ServiceError("VALIDATION", "message required", 400);
741
- const packs = installedPacks();
742
- let agentId;
743
- try {
744
- agentId = resolveOwnerAgent(packs, kind, id, body.agentId);
745
- }
746
- catch (e) {
747
- throw new ServiceError("NOT_FOUND", e instanceof Error ? e.message : String(e), 404);
748
- }
749
- const live = packs[agentId];
750
- if (!live) {
751
- throw new ServiceError("NOT_FOUND", `No installed pack for agent ${agentId}`, 404);
752
- }
753
- const mode = kind === "agent" ? (body.mode === "narrow" ? "narrow" : "full") : undefined;
754
- const payload = buildPackagePayload(live, {
736
+ if (!channel)
737
+ throw new ServiceError("VALIDATION", "channel required", 400);
738
+ const history = await magit.packageChannelHistory({ kind, id, channel });
739
+ return {
755
740
  kind,
756
741
  id,
757
- mode,
758
- includeConnectorIds: body.includeConnectorIds,
759
- includeServiceIds: body.includeServiceIds,
760
- });
761
- const validation = validateMetadataPack(payload);
762
- if (!validation.ok) {
763
- return { packed: false, result: validation };
742
+ channel,
743
+ history,
744
+ note: "Package channel history — promote a previous version to roll back the pointer.",
745
+ };
746
+ });
747
+ app.post("/api/metadata/packages/releases/pack", async (req) => {
748
+ const magit = requireMagit(platform);
749
+ const body = req.body;
750
+ const members = (body.members ?? []).map((m) => ({
751
+ kind: parseKind(m.kind),
752
+ id: String(m.id ?? "").trim(),
753
+ version: String(m.version ?? "").trim(),
754
+ }));
755
+ if (members.length === 0) {
756
+ throw new ServiceError("VALIDATION", "members must list at least one { kind, id, version }", 400);
764
757
  }
765
- const associations = kind === "agent" && mode
766
- ? releaseAssociations(live, mode, body.includeConnectorIds, body.includeServiceIds)
767
- : undefined;
768
- const saved = await magit.packPackage({
769
- kind,
770
- id,
771
- message,
772
- pack: payload,
773
- mode,
774
- associations,
775
- });
776
- // Full agent pack also snapshots selected connector/service packages.
777
- // `commit` aliases `version` for the same backward-compat reason as the
778
- // versions-list route above.
779
- const bundled = [];
780
- if (kind === "agent" && mode === "full" && associations) {
781
- for (const connectorId of associations.connectorIds) {
782
- const slice = buildPackagePayload(live, { kind: "connector", id: connectorId });
783
- const ok = validateMetadataPack(slice);
784
- if (!ok.ok)
785
- continue;
786
- const b = await magit.packPackage({
787
- kind: "connector",
788
- id: connectorId,
789
- message: `bundled with agent ${id}: ${message}`,
790
- pack: slice,
791
- });
792
- bundled.push({ kind: "connector", id: connectorId, version: b.version, commit: b.version });
793
- }
794
- for (const serviceId of associations.serviceIds) {
795
- const slice = buildPackagePayload(live, { kind: "service", id: serviceId });
796
- const ok = validateMetadataPack(slice);
797
- if (!ok.ok)
798
- continue;
799
- const b = await magit.packPackage({
800
- kind: "service",
801
- id: serviceId,
802
- message: `bundled with agent ${id}: ${message}`,
803
- pack: slice,
804
- });
805
- bundled.push({ kind: "service", id: serviceId, version: b.version, commit: b.version });
758
+ for (const m of members) {
759
+ if (!m.id || !m.version) {
760
+ throw new ServiceError("VALIDATION", "each member needs kind, id, and version", 400);
806
761
  }
807
762
  }
808
- const note = saved.published === false
809
- ? "Published locally but not to the shared catalog (published=false)."
810
- : saved.published === true
811
- ? "Published to the shared package catalog."
812
- : "Published to the package catalog.";
763
+ const result = await magit.packRelease({
764
+ members,
765
+ message: body.message?.trim() || undefined,
766
+ });
813
767
  return {
814
- packed: true,
815
- kind,
816
- id,
817
- agentId,
818
- mode: mode ?? null,
819
- associations,
820
- bundled,
821
- version: saved.version,
822
- checksum: saved.checksum,
823
- reused: saved.reused,
824
- ...(saved.published !== undefined ? { published: saved.published } : {}),
825
- ...(saved.storeId ? { storeId: saved.storeId } : {}),
826
- ...(saved.packageTypeId ? { packageTypeId: saved.packageTypeId } : {}),
827
- note,
768
+ ...result,
769
+ note: "Immutable release set (content-addressed). Promote via release channel. Install separately into a target organization.",
828
770
  };
829
771
  });
830
- app.post("/api/metadata/packages/restore", async (req) => {
831
- const scope = resolveScope(req);
772
+ app.post("/api/metadata/packages/releases/channels/promote", async (req) => {
832
773
  const magit = requireMagit(platform);
833
774
  const body = req.body;
834
- if (!body?.confirm) {
835
- throw new ServiceError("CONFIRMATION_REQUIRED", "explicit confirm:true required for package restore", 400);
836
- }
837
- if (!scope.orgId?.trim()) {
838
- throw new ServiceError("VALIDATION", "x-memorix-org-id required — org is the restore target, not package identity", 400);
839
- }
840
- const kind = parseKind(body.kind);
841
- const id = body.id?.trim();
842
- if (!id)
843
- throw new ServiceError("VALIDATION", "id required", 400);
844
- const version = (body.version ?? body.commit)?.trim();
845
- if (!version)
846
- throw new ServiceError("VALIDATION", "version required", 400);
847
- const packs = installedPacks();
848
- let agentId;
849
- try {
850
- agentId = resolveOwnerAgent(packs, kind, id, body.agentId);
851
- }
852
- catch {
853
- agentId = kind === "agent" ? id : body.agentId?.trim() || "";
854
- }
855
- if (!agentId) {
856
- throw new ServiceError("VALIDATION", "agentId required to restore connector/service into an agent pack", 400);
857
- }
858
- const sliceAgentId = catalogAgentId(kind, id, agentId);
859
- const fetched = await magit.fetchPackage({ kind, id, version });
860
- const loadedPack = packagePayloadToPack(sliceAgentId, fetched.payload);
861
- let installPack = loadedPack;
862
- // Connector/service restore merges slice into current live agent pack.
863
- if (kind === "connector" || kind === "service") {
864
- const live = packs[agentId] ?? emptyPackForRestore(agentId);
865
- if (kind === "connector") {
866
- const keepRefs = new Set((loadedPack.sources ?? [])
867
- .map((s) => s && typeof s === "object" && "connectorRef" in s
868
- ? String(s.connectorRef ?? "")
869
- : "")
870
- .filter(Boolean));
871
- const otherSources = (live.sources ?? []).filter((s) => {
872
- const ref = s && typeof s === "object" && "connectorRef" in s
873
- ? String(s.connectorRef ?? "")
874
- : "";
875
- return !keepRefs.has(ref);
876
- });
877
- installPack = {
878
- ...live,
879
- agentId,
880
- sources: [...otherSources, ...(loadedPack.sources ?? [])],
881
- };
882
- }
883
- else {
884
- const svcId = id;
885
- const otherServices = (live.services ?? []).filter((s) => !(s && typeof s === "object" && "id" in s && String(s.id) === svcId));
886
- installPack = {
887
- ...live,
888
- agentId,
889
- services: [...otherServices, ...(loadedPack.services ?? [])],
890
- };
891
- }
892
- }
893
- installPack = stripOrgRuntimeSourcesFromPack(installPack);
894
- const validation = validateMetadataPack(installPack);
895
- if (!validation.ok) {
896
- throw new ServiceError("VALIDATION", "pack at version failed validation", 400, { issues: validation.issues });
897
- }
898
- const message = body.message?.trim() ||
899
- `Restore ${kind}:${id} @ ${version.slice(0, 12)} into org ${scope.orgId}`;
900
- const result = platform.metadata.install(installPack, {
901
- reinstall: true,
902
- magitCommit: version,
903
- magitRelease: undefined,
904
- source: "rollback",
905
- message,
906
- environment: "live",
907
- });
908
- if (!result.ok) {
909
- throw new ServiceError("VALIDATION", result.issues.map((i) => i.message).join("; ") || "restore install failed", 400);
910
- }
911
- return {
912
- restored: true,
913
- kind,
914
- id,
915
- agentId,
916
- version,
917
- checksum: fetched.checksum,
918
- targetOrgId: scope.orgId,
919
- result,
920
- note: "Installed from the shared package catalog into this organization. Organization is the install target only.",
921
- };
922
- });
923
- app.post("/api/metadata/packages/diff", async (req) => {
924
- const magit = requireMagit(platform);
925
- const body = req.body;
926
- const kind = parseKind(body.kind);
927
- const id = body.id?.trim();
928
- if (!id)
929
- throw new ServiceError("VALIDATION", "id required", 400);
930
- const from = body.from?.trim();
931
- const to = body.to?.trim();
932
- if (!from || !to) {
933
- throw new ServiceError("VALIDATION", "from and to required", 400);
934
- }
935
- const packs = installedPacks();
936
- let agentId;
937
- try {
938
- agentId = resolveOwnerAgent(packs, kind, id, body.agentId);
939
- }
940
- catch {
941
- agentId = kind === "agent" ? id : body.agentId?.trim() || id;
942
- }
943
- const sliceAgentId = catalogAgentId(kind, id, agentId);
944
- // "installed" sentinel → live / installed pack slice for typed-store compare.
945
- // Flat Mode B still maps installed → "live" commit/mongo side via MagitPackService.
946
- const fromRef = from === "installed" ? "live" : from;
947
- const toRef = to === "installed" ? "live" : to;
948
- const needsInstalled = fromRef === "live" ||
949
- toRef === "live" ||
950
- from === "installed" ||
951
- to === "installed";
952
- let installedPack = null;
953
- if (needsInstalled) {
954
- const live = packs[agentId];
955
- if (live) {
956
- installedPack = buildPackagePayload(live, {
957
- kind,
958
- id,
959
- mode: kind === "agent" ? "narrow" : undefined,
960
- });
961
- }
962
- }
963
- const diff = await magit.diffPackages(sliceAgentId, {
964
- from: fromRef,
965
- to: toRef,
966
- kind,
967
- id,
968
- installedPack,
969
- });
970
- return {
971
- ...diff,
972
- kind,
973
- id,
974
- agentId,
975
- catalogAgentId: sliceAgentId,
976
- note: "Compare package versions or version vs installed. Not env→env copy.",
977
- };
978
- });
979
- // Magit 2.14 — package channels & release sets (Mode A only; fail closed if unset)
980
- app.get("/api/metadata/packages/capabilities", async () => {
981
- const magit = requireMagit(platform);
982
- const caps = magit.packageCatalogCapabilities();
983
- return {
984
- ...caps,
985
- note: "Safe capability flags for Packages UI. Never includes tokens or store selector.",
986
- };
987
- });
988
- app.post("/api/metadata/packages/channels/promote", async (req) => {
989
- const magit = requireMagit(platform);
990
- const body = req.body;
991
- const kind = parseKind(body.kind);
992
- const id = body.id?.trim();
993
- const channel = body.channel?.trim();
994
- const version = body.version?.trim();
995
- if (!id)
996
- throw new ServiceError("VALIDATION", "id required", 400);
997
- if (!channel)
998
- throw new ServiceError("VALIDATION", "channel required", 400);
999
- if (!version)
1000
- throw new ServiceError("VALIDATION", "version required", 400);
1001
- const result = await magit.setPackageChannel({
1002
- kind,
1003
- id,
1004
- channel,
1005
- version,
1006
- message: body.message?.trim() || undefined,
1007
- expectedVersion: body.expectedVersion,
1008
- });
1009
- return {
1010
- ...result,
1011
- note: "Channel pointer updated. Packages do not move; promote does not install into organizations.",
1012
- };
1013
- });
1014
- app.get("/api/metadata/packages/:kind/:id/channels", async (req) => {
1015
- const magit = requireMagit(platform);
1016
- const kind = parseKind(req.params.kind);
1017
- const id = String(req.params.id ?? "").trim();
1018
- if (!id)
1019
- throw new ServiceError("VALIDATION", "id required", 400);
1020
- const channels = await magit.listPackageChannels({ kind, id });
1021
- return {
1022
- kind,
1023
- id,
1024
- channels,
1025
- note: "Known channel heads for this package (shared catalog).",
1026
- };
1027
- });
1028
- app.get("/api/metadata/packages/:kind/:id/channels/:channel", async (req) => {
1029
- const magit = requireMagit(platform);
1030
- const kind = parseKind(req.params.kind);
1031
- const id = String(req.params.id ?? "").trim();
1032
- const channel = String(req.params.channel ?? "").trim();
1033
- if (!id)
1034
- throw new ServiceError("VALIDATION", "id required", 400);
1035
- if (!channel)
1036
- throw new ServiceError("VALIDATION", "channel required", 400);
1037
- const result = await magit.resolvePackageChannel({ kind, id, channel });
1038
- return {
1039
- ...result,
1040
- note: "Resolved after Magit sync (fresh channel head).",
1041
- };
1042
- });
1043
- app.get("/api/metadata/packages/:kind/:id/channels/:channel/history", async (req) => {
1044
- const magit = requireMagit(platform);
1045
- const kind = parseKind(req.params.kind);
1046
- const id = String(req.params.id ?? "").trim();
1047
- const channel = String(req.params.channel ?? "").trim();
1048
- if (!id)
1049
- throw new ServiceError("VALIDATION", "id required", 400);
1050
- if (!channel)
1051
- throw new ServiceError("VALIDATION", "channel required", 400);
1052
- const history = await magit.packageChannelHistory({ kind, id, channel });
1053
- return {
1054
- kind,
1055
- id,
1056
- channel,
1057
- history,
1058
- note: "Package channel history — promote a previous version to roll back the pointer.",
1059
- };
1060
- });
1061
- app.post("/api/metadata/packages/releases/pack", async (req) => {
1062
- const magit = requireMagit(platform);
1063
- const body = req.body;
1064
- const members = (body.members ?? []).map((m) => ({
1065
- kind: parseKind(m.kind),
1066
- id: String(m.id ?? "").trim(),
1067
- version: String(m.version ?? "").trim(),
1068
- }));
1069
- if (members.length === 0) {
1070
- throw new ServiceError("VALIDATION", "members must list at least one { kind, id, version }", 400);
1071
- }
1072
- for (const m of members) {
1073
- if (!m.id || !m.version) {
1074
- throw new ServiceError("VALIDATION", "each member needs kind, id, and version", 400);
1075
- }
1076
- }
1077
- const result = await magit.packRelease({
1078
- members,
1079
- message: body.message?.trim() || undefined,
1080
- });
1081
- return {
1082
- ...result,
1083
- note: "Immutable release set (content-addressed). Promote via release channel. Install separately into a target organization.",
1084
- };
1085
- });
1086
- app.post("/api/metadata/packages/releases/channels/promote", async (req) => {
1087
- const magit = requireMagit(platform);
1088
- const body = req.body;
1089
- const channel = body.channel?.trim();
1090
- const releaseId = body.releaseId?.trim();
1091
- if (!channel)
1092
- throw new ServiceError("VALIDATION", "channel required", 400);
1093
- if (!releaseId) {
1094
- throw new ServiceError("VALIDATION", "releaseId required", 400);
775
+ const channel = body.channel?.trim();
776
+ const releaseId = body.releaseId?.trim();
777
+ if (!channel)
778
+ throw new ServiceError("VALIDATION", "channel required", 400);
779
+ if (!releaseId) {
780
+ throw new ServiceError("VALIDATION", "releaseId required", 400);
1095
781
  }
1096
782
  const result = await magit.setReleaseChannel({
1097
783
  channel,
@@ -1135,6 +821,18 @@ export async function registerMetadataRoutes(app, platform) {
1135
821
  note: "Rollback list: promote a previous releaseId to move the channel back.",
1136
822
  };
1137
823
  });
824
+ app.get("/api/metadata/packages/releases/by-id/:releaseId", async (req) => {
825
+ const magit = requireMagit(platform);
826
+ const releaseId = String(req.params.releaseId ?? "").trim();
827
+ if (!releaseId) {
828
+ throw new ServiceError("VALIDATION", "releaseId required", 400);
829
+ }
830
+ const release = await magit.getRelease({ releaseId });
831
+ return {
832
+ ...release,
833
+ note: "Immutable release set from Magit readRelease (after store sync).",
834
+ };
835
+ });
1138
836
  /**
1139
837
  * Atomic install of a release set into the target org.
1140
838
  * Body: { confirm:true, channel? | releaseId?, message? }
@@ -1161,18 +859,8 @@ export async function registerMetadataRoutes(app, platform) {
1161
859
  }
1162
860
  else if (body.releaseId?.trim()) {
1163
861
  releaseId = body.releaseId.trim();
1164
- // Resolve via channel history is not available for bare releaseId
1165
- // pack response members must be re-fetched through channel or re-cut.
1166
- // Prefer channel path; for releaseId-only, resolve from listReleaseChannels.
1167
- const listed = await magit.listReleaseChannels();
1168
- const match = listed.find((c) => c.releaseId === releaseId);
1169
- if (match) {
1170
- members = match.release.members;
1171
- channel = match.channel;
1172
- }
1173
- else {
1174
- throw new ServiceError("VALIDATION", "releaseId not found on any release channel; pass channel to install from a channel head", 400);
1175
- }
862
+ const release = await magit.getRelease({ releaseId });
863
+ members = release.members;
1176
864
  }
1177
865
  else {
1178
866
  throw new ServiceError("VALIDATION", "channel or releaseId required", 400);
@@ -1300,6 +988,321 @@ export async function registerMetadataRoutes(app, platform) {
1300
988
  note: "Atomic release install into the target organization. On failure, prior packs are restored.",
1301
989
  };
1302
990
  });
991
+ app.get("/api/metadata/packages/:kind/:id/associations", async (req) => {
992
+ requireMagit(platform);
993
+ const kind = parseKind(req.params.kind);
994
+ const id = String(req.params.id ?? "").trim();
995
+ if (!id)
996
+ throw new ServiceError("VALIDATION", "id required", 400);
997
+ const packs = installedPacks();
998
+ const q = req.query;
999
+ const agentId = resolveOwnerAgent(packs, kind, id, q.agentId);
1000
+ const pack = packs[agentId];
1001
+ if (!pack) {
1002
+ throw new ServiceError("NOT_FOUND", `No installed pack for agent ${agentId}`, 404);
1003
+ }
1004
+ return {
1005
+ kind,
1006
+ id,
1007
+ agentId,
1008
+ associations: associationsFromPack(pack),
1009
+ };
1010
+ });
1011
+ // FR-CPS-3/5 — flat catalog listing. Org-free: kind + id only, no agentId
1012
+ // resolution needed (a catalog package isn't owned by any installed pack).
1013
+ app.get("/api/metadata/packages/:kind/:id/versions", async (req) => {
1014
+ const magit = requireMagit(platform);
1015
+ const kind = parseKind(req.params.kind);
1016
+ const id = String(req.params.id ?? "").trim();
1017
+ if (!id)
1018
+ throw new ServiceError("VALIDATION", "id required", 400);
1019
+ const q = req.query;
1020
+ const maxCount = q.maxCount ? Number(q.maxCount) : 50;
1021
+ const versions = await magit.listPackages({
1022
+ kind,
1023
+ id,
1024
+ maxCount: Number.isFinite(maxCount) ? maxCount : 50,
1025
+ });
1026
+ return {
1027
+ kind,
1028
+ id,
1029
+ // `commit` aliases `version` for existing HTTP callers written against
1030
+ // the pre-cutover org-scoped history shape (e.g. Studio's
1031
+ // PackVersionsPage.tsx keys/renders list rows by `.commit`). `version`
1032
+ // is the FR-CPS-4/5 field name and the one new callers should use.
1033
+ versions: versions.map((v) => ({ ...v, commit: v.version })),
1034
+ note: "Versions from the central Magit package catalog.",
1035
+ };
1036
+ });
1037
+ // FR-CPS-5 — fetch a package version's payload directly (§7 of the FR).
1038
+ // Org header is ignored: catalog reads are org-free.
1039
+ app.get("/api/metadata/packages/:kind/:id/versions/:version", async (req) => {
1040
+ const magit = requireMagit(platform);
1041
+ const kind = parseKind(req.params.kind);
1042
+ const id = String(req.params.id ?? "").trim();
1043
+ if (!id)
1044
+ throw new ServiceError("VALIDATION", "id required", 400);
1045
+ const version = String(req.params.version ?? "").trim();
1046
+ if (!version)
1047
+ throw new ServiceError("VALIDATION", "version required", 400);
1048
+ const fetched = await magit.fetchPackage({ kind, id, version });
1049
+ return {
1050
+ kind,
1051
+ id,
1052
+ version,
1053
+ checksum: fetched.checksum,
1054
+ payload: fetched.payload,
1055
+ associations: fetched.associations,
1056
+ };
1057
+ });
1058
+ app.post("/api/metadata/packages/pack", async (req) => {
1059
+ const magit = requireMagit(platform);
1060
+ const body = req.body;
1061
+ const kind = parseKind(body.kind);
1062
+ const id = body.id?.trim();
1063
+ if (!id)
1064
+ throw new ServiceError("VALIDATION", "id required", 400);
1065
+ const message = body.message?.trim();
1066
+ if (!message)
1067
+ throw new ServiceError("VALIDATION", "message required", 400);
1068
+ const packs = installedPacks();
1069
+ let agentId;
1070
+ try {
1071
+ agentId = resolveOwnerAgent(packs, kind, id, body.agentId);
1072
+ }
1073
+ catch (e) {
1074
+ throw new ServiceError("NOT_FOUND", e instanceof Error ? e.message : String(e), 404);
1075
+ }
1076
+ const live = packs[agentId];
1077
+ if (!live) {
1078
+ throw new ServiceError("NOT_FOUND", `No installed pack for agent ${agentId}`, 404);
1079
+ }
1080
+ const mode = kind === "agent" ? (body.mode === "narrow" ? "narrow" : "full") : undefined;
1081
+ const payload = buildPackagePayload(live, {
1082
+ kind,
1083
+ id,
1084
+ mode,
1085
+ includeConnectorIds: body.includeConnectorIds,
1086
+ includeServiceIds: body.includeServiceIds,
1087
+ });
1088
+ const validation = validateMetadataPack(payload);
1089
+ if (!validation.ok) {
1090
+ return { packed: false, result: validation };
1091
+ }
1092
+ const associations = kind === "agent" && mode
1093
+ ? releaseAssociations(live, mode, body.includeConnectorIds, body.includeServiceIds)
1094
+ : undefined;
1095
+ const saved = await magit.packPackage({
1096
+ kind,
1097
+ id,
1098
+ message,
1099
+ pack: payload,
1100
+ mode,
1101
+ associations,
1102
+ });
1103
+ // Full agent pack also snapshots selected connector/service packages.
1104
+ // `commit` aliases `version` for the same backward-compat reason as the
1105
+ // versions-list route above.
1106
+ const bundled = [];
1107
+ if (kind === "agent" && mode === "full" && associations) {
1108
+ for (const connectorId of associations.connectorIds) {
1109
+ const slice = buildPackagePayload(live, { kind: "connector", id: connectorId });
1110
+ const ok = validateMetadataPack(slice);
1111
+ if (!ok.ok)
1112
+ continue;
1113
+ const b = await magit.packPackage({
1114
+ kind: "connector",
1115
+ id: connectorId,
1116
+ message: `bundled with agent ${id}: ${message}`,
1117
+ pack: slice,
1118
+ });
1119
+ bundled.push({ kind: "connector", id: connectorId, version: b.version, commit: b.version });
1120
+ }
1121
+ for (const serviceId of associations.serviceIds) {
1122
+ const slice = buildPackagePayload(live, { kind: "service", id: serviceId });
1123
+ const ok = validateMetadataPack(slice);
1124
+ if (!ok.ok)
1125
+ continue;
1126
+ const b = await magit.packPackage({
1127
+ kind: "service",
1128
+ id: serviceId,
1129
+ message: `bundled with agent ${id}: ${message}`,
1130
+ pack: slice,
1131
+ });
1132
+ bundled.push({ kind: "service", id: serviceId, version: b.version, commit: b.version });
1133
+ }
1134
+ }
1135
+ const note = saved.published === false
1136
+ ? "Published locally but not to the shared catalog (published=false)."
1137
+ : saved.published === true
1138
+ ? "Published to the shared package catalog."
1139
+ : "Published to the package catalog.";
1140
+ return {
1141
+ packed: true,
1142
+ kind,
1143
+ id,
1144
+ agentId,
1145
+ mode: mode ?? null,
1146
+ associations,
1147
+ bundled,
1148
+ version: saved.version,
1149
+ checksum: saved.checksum,
1150
+ reused: saved.reused,
1151
+ ...(saved.published !== undefined ? { published: saved.published } : {}),
1152
+ ...(saved.storeId ? { storeId: saved.storeId } : {}),
1153
+ ...(saved.packageTypeId ? { packageTypeId: saved.packageTypeId } : {}),
1154
+ note,
1155
+ };
1156
+ });
1157
+ app.post("/api/metadata/packages/restore", async (req) => {
1158
+ const scope = resolveScope(req);
1159
+ const magit = requireMagit(platform);
1160
+ const body = req.body;
1161
+ if (!body?.confirm) {
1162
+ throw new ServiceError("CONFIRMATION_REQUIRED", "explicit confirm:true required for package restore", 400);
1163
+ }
1164
+ if (!scope.orgId?.trim()) {
1165
+ throw new ServiceError("VALIDATION", "x-memorix-org-id required — org is the restore target, not package identity", 400);
1166
+ }
1167
+ const kind = parseKind(body.kind);
1168
+ const id = body.id?.trim();
1169
+ if (!id)
1170
+ throw new ServiceError("VALIDATION", "id required", 400);
1171
+ const version = (body.version ?? body.commit)?.trim();
1172
+ if (!version)
1173
+ throw new ServiceError("VALIDATION", "version required", 400);
1174
+ const packs = installedPacks();
1175
+ let agentId;
1176
+ try {
1177
+ agentId = resolveOwnerAgent(packs, kind, id, body.agentId);
1178
+ }
1179
+ catch {
1180
+ agentId = kind === "agent" ? id : body.agentId?.trim() || "";
1181
+ }
1182
+ if (!agentId) {
1183
+ throw new ServiceError("VALIDATION", "agentId required to restore connector/service into an agent pack", 400);
1184
+ }
1185
+ const sliceAgentId = catalogAgentId(kind, id, agentId);
1186
+ const fetched = await magit.fetchPackage({ kind, id, version });
1187
+ const loadedPack = packagePayloadToPack(sliceAgentId, fetched.payload);
1188
+ let installPack = loadedPack;
1189
+ // Connector/service restore merges slice into current live agent pack.
1190
+ if (kind === "connector" || kind === "service") {
1191
+ const live = packs[agentId] ?? emptyPackForRestore(agentId);
1192
+ if (kind === "connector") {
1193
+ const keepRefs = new Set((loadedPack.sources ?? [])
1194
+ .map((s) => s && typeof s === "object" && "connectorRef" in s
1195
+ ? String(s.connectorRef ?? "")
1196
+ : "")
1197
+ .filter(Boolean));
1198
+ const otherSources = (live.sources ?? []).filter((s) => {
1199
+ const ref = s && typeof s === "object" && "connectorRef" in s
1200
+ ? String(s.connectorRef ?? "")
1201
+ : "";
1202
+ return !keepRefs.has(ref);
1203
+ });
1204
+ installPack = {
1205
+ ...live,
1206
+ agentId,
1207
+ sources: [...otherSources, ...(loadedPack.sources ?? [])],
1208
+ };
1209
+ }
1210
+ else {
1211
+ const svcId = id;
1212
+ const otherServices = (live.services ?? []).filter((s) => !(s && typeof s === "object" && "id" in s && String(s.id) === svcId));
1213
+ installPack = {
1214
+ ...live,
1215
+ agentId,
1216
+ services: [...otherServices, ...(loadedPack.services ?? [])],
1217
+ };
1218
+ }
1219
+ }
1220
+ installPack = stripOrgRuntimeSourcesFromPack(installPack);
1221
+ const validation = validateMetadataPack(installPack);
1222
+ if (!validation.ok) {
1223
+ throw new ServiceError("VALIDATION", "pack at version failed validation", 400, { issues: validation.issues });
1224
+ }
1225
+ const message = body.message?.trim() ||
1226
+ `Restore ${kind}:${id} @ ${version.slice(0, 12)} into org ${scope.orgId}`;
1227
+ const result = platform.metadata.install(installPack, {
1228
+ reinstall: true,
1229
+ magitCommit: version,
1230
+ magitRelease: undefined,
1231
+ source: "rollback",
1232
+ message,
1233
+ environment: "live",
1234
+ });
1235
+ if (!result.ok) {
1236
+ throw new ServiceError("VALIDATION", result.issues.map((i) => i.message).join("; ") || "restore install failed", 400);
1237
+ }
1238
+ return {
1239
+ restored: true,
1240
+ kind,
1241
+ id,
1242
+ agentId,
1243
+ version,
1244
+ checksum: fetched.checksum,
1245
+ targetOrgId: scope.orgId,
1246
+ result,
1247
+ note: "Installed from the shared package catalog into this organization. Organization is the install target only.",
1248
+ };
1249
+ });
1250
+ app.post("/api/metadata/packages/diff", async (req) => {
1251
+ const magit = requireMagit(platform);
1252
+ const body = req.body;
1253
+ const kind = parseKind(body.kind);
1254
+ const id = body.id?.trim();
1255
+ if (!id)
1256
+ throw new ServiceError("VALIDATION", "id required", 400);
1257
+ const from = body.from?.trim();
1258
+ const to = body.to?.trim();
1259
+ if (!from || !to) {
1260
+ throw new ServiceError("VALIDATION", "from and to required", 400);
1261
+ }
1262
+ const packs = installedPacks();
1263
+ let agentId;
1264
+ try {
1265
+ agentId = resolveOwnerAgent(packs, kind, id, body.agentId);
1266
+ }
1267
+ catch {
1268
+ agentId = kind === "agent" ? id : body.agentId?.trim() || id;
1269
+ }
1270
+ const sliceAgentId = catalogAgentId(kind, id, agentId);
1271
+ // "installed" sentinel → live / installed pack slice for typed-store compare.
1272
+ // Flat Mode B still maps installed → "live" commit/mongo side via MagitPackService.
1273
+ const fromRef = from === "installed" ? "live" : from;
1274
+ const toRef = to === "installed" ? "live" : to;
1275
+ const needsInstalled = fromRef === "live" ||
1276
+ toRef === "live" ||
1277
+ from === "installed" ||
1278
+ to === "installed";
1279
+ let installedPack = null;
1280
+ if (needsInstalled) {
1281
+ const live = packs[agentId];
1282
+ if (live) {
1283
+ installedPack = buildPackagePayload(live, {
1284
+ kind,
1285
+ id,
1286
+ mode: kind === "agent" ? "narrow" : undefined,
1287
+ });
1288
+ }
1289
+ }
1290
+ const diff = await magit.diffPackages(sliceAgentId, {
1291
+ from: fromRef,
1292
+ to: toRef,
1293
+ kind,
1294
+ id,
1295
+ installedPack,
1296
+ });
1297
+ return {
1298
+ ...diff,
1299
+ kind,
1300
+ id,
1301
+ agentId,
1302
+ catalogAgentId: sliceAgentId,
1303
+ note: "Compare package versions or version vs installed. Not env→env copy.",
1304
+ };
1305
+ });
1303
1306
  }
1304
1307
  function emptyPackForRestore(agentId) {
1305
1308
  return {