@rudderhq/server 0.7.7 → 0.7.9
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/routes/chats.stream-routes.d.ts.map +1 -1
- package/dist/routes/chats.stream-routes.js +2 -0
- package/dist/routes/chats.stream-routes.js.map +1 -1
- package/dist/routes/rudder-plugins.d.ts.map +1 -1
- package/dist/routes/rudder-plugins.js +48 -1
- package/dist/routes/rudder-plugins.js.map +1 -1
- package/dist/services/chat-family-groups.d.ts +6 -0
- package/dist/services/chat-family-groups.d.ts.map +1 -1
- package/dist/services/chat-family-groups.js +13 -0
- package/dist/services/chat-family-groups.js.map +1 -1
- package/dist/services/chats.create.d.ts +2 -0
- package/dist/services/chats.create.d.ts.map +1 -1
- package/dist/services/chats.create.js +9 -0
- package/dist/services/chats.create.js.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.d.ts +1 -0
- package/dist/services/knowledge-portability/organization-skills.d.ts.map +1 -1
- package/dist/services/knowledge-portability/organization-skills.js +4 -3
- package/dist/services/knowledge-portability/organization-skills.js.map +1 -1
- package/dist/services/messenger.d.ts +2 -2
- package/dist/services/rudder-plugin-agent-bindings.d.ts +16 -0
- package/dist/services/rudder-plugin-agent-bindings.d.ts.map +1 -0
- package/dist/services/rudder-plugin-agent-bindings.js +24 -0
- package/dist/services/rudder-plugin-agent-bindings.js.map +1 -0
- package/dist/services/rudder-plugin-catalog.d.ts +109 -0
- package/dist/services/rudder-plugin-catalog.d.ts.map +1 -0
- package/dist/services/rudder-plugin-catalog.js +954 -0
- package/dist/services/rudder-plugin-catalog.js.map +1 -0
- package/dist/services/rudder-plugin-catalog.test.d.ts +2 -0
- package/dist/services/rudder-plugin-catalog.test.d.ts.map +1 -0
- package/dist/services/rudder-plugin-catalog.test.js +321 -0
- package/dist/services/rudder-plugin-catalog.test.js.map +1 -0
- package/dist/services/rudder-plugins.d.ts +6 -1
- package/dist/services/rudder-plugins.d.ts.map +1 -1
- package/dist/services/rudder-plugins.js +37 -23
- package/dist/services/rudder-plugins.js.map +1 -1
- package/package.json +14 -14
- package/ui-dist/assets/{LibraryLiveSurface-dimGX-1o.js → LibraryLiveSurface-C0jsVm76.js} +1 -1
- package/ui-dist/assets/{abnfDiagram-VRR7QNED-PqSZLPMO.js → abnfDiagram-VRR7QNED-U1mPal0M.js} +1 -1
- package/ui-dist/assets/{addon-fit-DvAjF4-B.js → addon-fit-B18ohVaY.js} +1 -1
- package/ui-dist/assets/{arc-CKbjeZb4.js → arc-CYOWHL_W.js} +1 -1
- package/ui-dist/assets/{architectureDiagram-ZJ3FMSHR-C0kmB3IZ.js → architectureDiagram-ZJ3FMSHR-CmiN5Erc.js} +1 -1
- package/ui-dist/assets/{blockDiagram-677ZJIJ3-DRdC8Krw.js → blockDiagram-677ZJIJ3-Chl4uqNv.js} +1 -1
- package/ui-dist/assets/{c4Diagram-LMCZKHZV-BYUYByQE.js → c4Diagram-LMCZKHZV-DSUkRSKu.js} +1 -1
- package/ui-dist/assets/channel-Dw8qUeMo.js +1 -0
- package/ui-dist/assets/{chunk-2Q5K7J3B-BruVb1Jv.js → chunk-2Q5K7J3B-DTJMcqWI.js} +1 -1
- package/ui-dist/assets/{chunk-32BRIVSS-B4Tqtajh.js → chunk-32BRIVSS-zdmMVLnX.js} +1 -1
- package/ui-dist/assets/{chunk-5VM5RSS4-DvLmrz3t.js → chunk-5VM5RSS4-D7CHEd8F.js} +1 -1
- package/ui-dist/assets/{chunk-EX3LRPZG-CPCqJcdR.js → chunk-EX3LRPZG-BSlg__Tm.js} +1 -1
- package/ui-dist/assets/{chunk-JWPE2WC7-aQoz24bX.js → chunk-JWPE2WC7-DZ-jdJNV.js} +1 -1
- package/ui-dist/assets/{chunk-MOJQB5TN-D2Xs5cdO.js → chunk-MOJQB5TN-BBqkM1ra.js} +1 -1
- package/ui-dist/assets/{chunk-RYQCIY6F-B8FR7hgH.js → chunk-RYQCIY6F-DcEtbyB7.js} +1 -1
- package/ui-dist/assets/{chunk-V7JOEXUC-COmt1x8T.js → chunk-V7JOEXUC-DzXDmCLX.js} +1 -1
- package/ui-dist/assets/{chunk-VR4S4FIN-B7VviPnW.js → chunk-VR4S4FIN-CY5XcPhT.js} +1 -1
- package/ui-dist/assets/{chunk-XXDRQBXY-CrloaQaR.js → chunk-XXDRQBXY-kROQNOzd.js} +1 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-BP_GJixo.js +1 -0
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-BP_GJixo.js +1 -0
- package/ui-dist/assets/{cose-bilkent-JH36ORCC-DhGo8Yae.js → cose-bilkent-JH36ORCC-CrnVyxOw.js} +1 -1
- package/ui-dist/assets/{cynefin-VYW2F7L2-BI63hGZo.js → cynefin-VYW2F7L2-LqXNJWXt.js} +1 -1
- package/ui-dist/assets/{cynefinDiagram-TSTJHNR4-Ce9SCLky.js → cynefinDiagram-TSTJHNR4-CuIowjom.js} +1 -1
- package/ui-dist/assets/{dagre-VKFMJZFB-CtVLdTeC.js → dagre-VKFMJZFB-_JHXcILM.js} +1 -1
- package/ui-dist/assets/{diagram-FQU43EPY-Bdqqdblj.js → diagram-FQU43EPY-BsWNb9wJ.js} +1 -1
- package/ui-dist/assets/{diagram-G47NLZAW-DRJWhYIE.js → diagram-G47NLZAW-NRzY78z7.js} +1 -1
- package/ui-dist/assets/{diagram-NH7WQ7WH-WaGfCgx2.js → diagram-NH7WQ7WH-D83JoKpS.js} +1 -1
- package/ui-dist/assets/{diagram-OA4YK3LP-CM_w6FCr.js → diagram-OA4YK3LP-CiQTkxEF.js} +1 -1
- package/ui-dist/assets/{diagram-WEI45ONY-D7S-Ph2j.js → diagram-WEI45ONY-zNOFqhj1.js} +1 -1
- package/ui-dist/assets/{ebnfDiagram-CCIWWBDH-FvIkWLWt.js → ebnfDiagram-CCIWWBDH-l9X92AzX.js} +1 -1
- package/ui-dist/assets/{erDiagram-Q63AITRT-BzmyFLxq.js → erDiagram-Q63AITRT-CZ2xCVfq.js} +1 -1
- package/ui-dist/assets/{flowDiagram-23GEKE2U-BfTbxwld.js → flowDiagram-23GEKE2U-DsqDeyPl.js} +1 -1
- package/ui-dist/assets/{ganttDiagram-NO4QXBWP-B0hlM15q.js → ganttDiagram-NO4QXBWP-ZovCvOAJ.js} +1 -1
- package/ui-dist/assets/{gitGraphDiagram-IHSO6WYX-B1rxK64G.js → gitGraphDiagram-IHSO6WYX-CqBEHQAs.js} +1 -1
- package/ui-dist/assets/{graph-Bkly5M3T.js → graph-ClsHEbyV.js} +1 -1
- package/ui-dist/assets/{index-CIcAMJ5k.js → index-B1A7LikO.js} +1 -1
- package/ui-dist/assets/{index-D4V5IHfv.js → index-BCGk5KQX.js} +1 -1
- package/ui-dist/assets/{index-BYrAbBx0.js → index-BDk7B-iN.js} +1 -1
- package/ui-dist/assets/{index-Bh1Th3ou.js → index-BXtyqqSu.js} +549 -549
- package/ui-dist/assets/{index-DFW_WSsQ.js → index-BeOqvf7L.js} +1 -1
- package/ui-dist/assets/{index-CGgpr-5b.js → index-Cgo8Oik4.js} +1 -1
- package/ui-dist/assets/{index-6oB_Zvdr.js → index-CkFvrO-z.js} +1 -1
- package/ui-dist/assets/{index-3DvAbpYk.js → index-Cv5Jhfkz.js} +1 -1
- package/ui-dist/assets/{index-DSdE--iw.js → index-Cv6MlpIo.js} +1 -1
- package/ui-dist/assets/{index-ZFFXSX52.js → index-CxDxuCus.js} +1 -1
- package/ui-dist/assets/{index-DMbiJK10.js → index-Dd7fnO2Z.js} +1 -1
- package/ui-dist/assets/{index-BpCd6R_K.js → index-DfoLd51a.js} +1 -1
- package/ui-dist/assets/{index-CkKcsZns.js → index-FPebsItp.js} +1 -1
- package/ui-dist/assets/{index-B0gY4i_0.js → index-FgEEryl0.js} +1 -1
- package/ui-dist/assets/index-WqUafwTi.css +32 -0
- package/ui-dist/assets/{index-CKpO6u4R.js → index-kq-WskyQ.js} +1 -1
- package/ui-dist/assets/{infoDiagram-FWYZ7A6U-C3TzEIOT.js → infoDiagram-FWYZ7A6U-DZJsMi0N.js} +1 -1
- package/ui-dist/assets/{ishikawaDiagram-FXEZZL3T-CxtYc9Ud.js → ishikawaDiagram-FXEZZL3T-7VX262KJ.js} +1 -1
- package/ui-dist/assets/{journeyDiagram-5HDEW3XC-CzsOseTQ.js → journeyDiagram-5HDEW3XC-W9pqd9nR.js} +1 -1
- package/ui-dist/assets/{kanban-definition-HUTT4EX6-BmijkVvF.js → kanban-definition-HUTT4EX6-eSHDQWV5.js} +1 -1
- package/ui-dist/assets/{layout-DU_cSdAD.js → layout-BxwzAdvW.js} +1 -1
- package/ui-dist/assets/{linear-D9ziGRIB.js → linear-D6gDQD1r.js} +1 -1
- package/ui-dist/assets/{map-CHIthIWG.js → map-Dl8Tlr4E.js} +1 -1
- package/ui-dist/assets/{mermaid.core-BxQWu5kU.js → mermaid.core-DqGHAs5q.js} +4 -4
- package/ui-dist/assets/{mindmap-definition-LN4V7U3C-Cn98DSTk.js → mindmap-definition-LN4V7U3C-DlkWbxIe.js} +1 -1
- package/ui-dist/assets/{pegDiagram-2B236MQR-DDt1-ES-.js → pegDiagram-2B236MQR-Cc49Opte.js} +1 -1
- package/ui-dist/assets/{pieDiagram-ENE6RG2P-DDgkj81R.js → pieDiagram-ENE6RG2P-Cso_iAzG.js} +1 -1
- package/ui-dist/assets/{quadrantDiagram-ABIIQ3AL-DzvkWdhu.js → quadrantDiagram-ABIIQ3AL-FKJGDnHG.js} +1 -1
- package/ui-dist/assets/{railroadDiagram-RFXS5EU6-CgKuhJ-N.js → railroadDiagram-RFXS5EU6-BqBQCfZ_.js} +1 -1
- package/ui-dist/assets/{requirementDiagram-TGXJPOKE-PcWFr79h.js → requirementDiagram-TGXJPOKE-CSkS2SPX.js} +1 -1
- package/ui-dist/assets/{sankeyDiagram-HTMAVEWB-DgJImEaz.js → sankeyDiagram-HTMAVEWB-CgnZO3lC.js} +1 -1
- package/ui-dist/assets/{sequenceDiagram-DBY2YBRQ-CpTvjpA2.js → sequenceDiagram-DBY2YBRQ-DHtt0uuq.js} +1 -1
- package/ui-dist/assets/{sizeCapture-X5ZJPWSS-BE4O-poN.js → sizeCapture-X5ZJPWSS-CApvjjgj.js} +1 -1
- package/ui-dist/assets/{stateDiagram-2N3HPSRC-ChczC0lb.js → stateDiagram-2N3HPSRC-DvJNOzm2.js} +1 -1
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-B_eJRf5W.js +1 -0
- package/ui-dist/assets/{swimlanes-5IMT3BWC-CTea_mvr.js → swimlanes-5IMT3BWC-DfQGr1Ph.js} +2 -2
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-BrIPYwzL.js +8 -0
- package/ui-dist/assets/{timeline-definition-FHXFAJF6-B-4xcxaN.js → timeline-definition-FHXFAJF6-BfEt2pSH.js} +1 -1
- package/ui-dist/assets/{vennDiagram-L72KCM5P-C7Od7Tzd.js → vennDiagram-L72KCM5P-BwnjAS5O.js} +1 -1
- package/ui-dist/assets/{wardleyDiagram-EHGQE667-I1TbpmZU.js → wardleyDiagram-EHGQE667-I4H5G4bu.js} +1 -1
- package/ui-dist/assets/{xterm-B5snYVfv.js → xterm-Bx_bzS86.js} +1 -1
- package/ui-dist/assets/{xychartDiagram-FW5EYKEG-CSsj6ncv.js → xychartDiagram-FW5EYKEG-c9wafyO4.js} +1 -1
- package/ui-dist/index.html +2 -2
- package/ui-dist/assets/channel-CpubPHe8.js +0 -1
- package/ui-dist/assets/classDiagram-OUVF2IWQ-CYX62kJD.js +0 -1
- package/ui-dist/assets/classDiagram-v2-EOCWNBFH-CYX62kJD.js +0 -1
- package/ui-dist/assets/index-DBw_BpPt.css +0 -32
- package/ui-dist/assets/stateDiagram-v2-6OUMAXLB-BSSRIm6i.js +0 -1
- package/ui-dist/assets/swimlanesDiagram-G3AALYLV-u3fzizSe.js +0 -8
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
import { installedPlugins, pluginImportReports, pluginPackages, pluginSources, } from "@rudderhq/db";
|
|
2
|
+
import { and, eq, ne } from "drizzle-orm";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { notFound, unprocessable } from "../errors.js";
|
|
6
|
+
import { resolveRudderInstanceRoot } from "../home-paths.js";
|
|
7
|
+
import { rudderPluginService } from "./rudder-plugins.js";
|
|
8
|
+
const DEFAULT_CATALOG_URL = "https://raw.githubusercontent.com/Undertone0809/rudder-plugins/main/catalog.json";
|
|
9
|
+
const MAX_FILES = 500;
|
|
10
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
const MAX_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
12
|
+
const FETCH_CONCURRENCY = 12;
|
|
13
|
+
const MAX_REDIRECTS = 3;
|
|
14
|
+
const CATALOG_DEGRADED_VISIBILITY_MS = 30_000;
|
|
15
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "__pycache__"]);
|
|
16
|
+
const PRIORITY_PREFIXES = [
|
|
17
|
+
"",
|
|
18
|
+
"skills/",
|
|
19
|
+
"skills/.curated/",
|
|
20
|
+
"skills/.experimental/",
|
|
21
|
+
"skills/.system/",
|
|
22
|
+
".agents/skills/",
|
|
23
|
+
".claude/skills/",
|
|
24
|
+
".cline/skills/",
|
|
25
|
+
".codebuddy/skills/",
|
|
26
|
+
".codex/skills/",
|
|
27
|
+
".commandcode/skills/",
|
|
28
|
+
".continue/skills/",
|
|
29
|
+
".github/skills/",
|
|
30
|
+
".goose/skills/",
|
|
31
|
+
".grok/skills/",
|
|
32
|
+
".iflow/skills/",
|
|
33
|
+
".junie/skills/",
|
|
34
|
+
".kilocode/skills/",
|
|
35
|
+
".kimchi/skills/",
|
|
36
|
+
".kiro/skills/",
|
|
37
|
+
".minimax/skills/",
|
|
38
|
+
".mux/skills/",
|
|
39
|
+
".neovate/skills/",
|
|
40
|
+
".opencode/skills/",
|
|
41
|
+
".openhands/skills/",
|
|
42
|
+
".pi/skills/",
|
|
43
|
+
".qoder/skills/",
|
|
44
|
+
".roo/skills/",
|
|
45
|
+
".trae/skills/",
|
|
46
|
+
".windsurf/skills/",
|
|
47
|
+
".zcode/skills/",
|
|
48
|
+
".zencoder/skills/",
|
|
49
|
+
];
|
|
50
|
+
export function createCatalogFreshnessLease(durationMs = CATALOG_DEGRADED_VISIBILITY_MS, now = Date.now) {
|
|
51
|
+
let staleUntil = 0;
|
|
52
|
+
return {
|
|
53
|
+
observe(freshness) {
|
|
54
|
+
const observedAt = now();
|
|
55
|
+
if (freshness === "stale") {
|
|
56
|
+
staleUntil = Math.max(staleUntil, observedAt + durationMs);
|
|
57
|
+
return "stale";
|
|
58
|
+
}
|
|
59
|
+
return observedAt < staleUntil ? "stale" : "fresh";
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export async function fetchPluginCatalogResource(fetcher, url, init = {}, allowedHosts) {
|
|
64
|
+
return fetchBounded(fetcher, url, init, allowedHosts);
|
|
65
|
+
}
|
|
66
|
+
function asRecord(value) {
|
|
67
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
68
|
+
? value
|
|
69
|
+
: {};
|
|
70
|
+
}
|
|
71
|
+
function requiredString(value, label, max = 1_200) {
|
|
72
|
+
if (typeof value !== "string" || !value.trim() || value.length > max) {
|
|
73
|
+
throw unprocessable(`Plugin catalog ${label} is invalid`);
|
|
74
|
+
}
|
|
75
|
+
return value.trim();
|
|
76
|
+
}
|
|
77
|
+
function safeRelativePath(value, label, allowEmpty = false) {
|
|
78
|
+
if (allowEmpty && value === "")
|
|
79
|
+
return "";
|
|
80
|
+
const raw = requiredString(value, label, 1_024).replace(/\\/g, "/").replace(/^\.\//, "");
|
|
81
|
+
if (raw.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
82
|
+
throw unprocessable(`Plugin catalog ${label} contains an unsafe path`);
|
|
83
|
+
}
|
|
84
|
+
const normalized = path.posix.normalize(raw).replace(/\/$/, "");
|
|
85
|
+
if (!normalized
|
|
86
|
+
|| normalized === "."
|
|
87
|
+
|| normalized.startsWith("../")
|
|
88
|
+
|| normalized.includes("/../")
|
|
89
|
+
|| path.posix.isAbsolute(normalized)
|
|
90
|
+
|| normalized.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
91
|
+
throw unprocessable(`Plugin catalog ${label} contains an unsafe path`);
|
|
92
|
+
}
|
|
93
|
+
return normalized;
|
|
94
|
+
}
|
|
95
|
+
function httpsUrl(value, label, githubOnly = false) {
|
|
96
|
+
const raw = requiredString(value, label, 500);
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = new URL(raw);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw unprocessable(`Plugin catalog ${label} is not a valid URL`);
|
|
103
|
+
}
|
|
104
|
+
if (parsed.protocol !== "https:" || (githubOnly && parsed.hostname !== "github.com")) {
|
|
105
|
+
throw unprocessable(`Plugin catalog ${label} must use HTTPS${githubOnly ? " on github.com" : ""}`);
|
|
106
|
+
}
|
|
107
|
+
return parsed.toString().replace(/\/$/, "");
|
|
108
|
+
}
|
|
109
|
+
function validateCatalog(value) {
|
|
110
|
+
const record = asRecord(value);
|
|
111
|
+
if (record.schemaVersion !== 1 || !Array.isArray(record.plugins)) {
|
|
112
|
+
throw unprocessable("Plugin catalog has an unsupported schema");
|
|
113
|
+
}
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
const plugins = record.plugins.map((raw, index) => {
|
|
116
|
+
const entry = asRecord(raw);
|
|
117
|
+
const slug = requiredString(entry.slug, `plugins[${index}].slug`, 80);
|
|
118
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug) || seen.has(slug)) {
|
|
119
|
+
throw unprocessable(`Plugin catalog contains an invalid or duplicate slug: ${slug}`);
|
|
120
|
+
}
|
|
121
|
+
seen.add(slug);
|
|
122
|
+
if (entry.sourceKind !== "codex_plugin" && entry.sourceKind !== "skills_add") {
|
|
123
|
+
throw unprocessable(`Plugin catalog ${slug} has an invalid source kind`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
slug,
|
|
127
|
+
displayName: requiredString(entry.displayName, `${slug}.displayName`, 80),
|
|
128
|
+
developer: requiredString(entry.developer, `${slug}.developer`, 80),
|
|
129
|
+
category: requiredString(entry.category, `${slug}.category`, 80),
|
|
130
|
+
shortDescription: requiredString(entry.shortDescription, `${slug}.shortDescription`, 180),
|
|
131
|
+
sourceKind: entry.sourceKind,
|
|
132
|
+
sourcePath: safeRelativePath(entry.sourcePath, `${slug}.sourcePath`),
|
|
133
|
+
iconPath: safeRelativePath(entry.iconPath, `${slug}.iconPath`),
|
|
134
|
+
iconDarkPath: safeRelativePath(entry.iconDarkPath, `${slug}.iconDarkPath`),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
return { schemaVersion: 1, updatedAt: requiredString(record.updatedAt, "updatedAt", 80), plugins };
|
|
138
|
+
}
|
|
139
|
+
function validateDescriptor(value, expected) {
|
|
140
|
+
const record = asRecord(value);
|
|
141
|
+
const source = asRecord(record.source);
|
|
142
|
+
const license = asRecord(record.license);
|
|
143
|
+
const assets = asRecord(record.assets);
|
|
144
|
+
if (record.schemaVersion !== 1 || record.slug !== expected.slug || record.kind !== expected.sourceKind) {
|
|
145
|
+
throw unprocessable(`Plugin source descriptor does not match catalog entry ${expected.slug}`);
|
|
146
|
+
}
|
|
147
|
+
if (source.versionStrategy !== "latest_stable_release_or_head") {
|
|
148
|
+
throw unprocessable(`Plugin source ${expected.slug} has an unsupported version strategy`);
|
|
149
|
+
}
|
|
150
|
+
const sourceValue = requiredString(source.skillsAddSource, `${expected.slug}.source.skillsAddSource`, 200);
|
|
151
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(sourceValue)) {
|
|
152
|
+
throw unprocessable(`Plugin source ${expected.slug} has an invalid skills add source`);
|
|
153
|
+
}
|
|
154
|
+
const origin = assets.origin;
|
|
155
|
+
if (origin !== "rudder_generic" && origin !== "upstream_redistributable") {
|
|
156
|
+
throw unprocessable(`Plugin source ${expected.slug} has invalid asset provenance`);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
slug: expected.slug,
|
|
161
|
+
kind: expected.sourceKind,
|
|
162
|
+
displayName: requiredString(record.displayName, `${expected.slug}.displayName`, 80),
|
|
163
|
+
developer: requiredString(record.developer, `${expected.slug}.developer`, 80),
|
|
164
|
+
category: requiredString(record.category, `${expected.slug}.category`, 80),
|
|
165
|
+
shortDescription: requiredString(record.shortDescription, `${expected.slug}.shortDescription`, 180),
|
|
166
|
+
longDescription: requiredString(record.longDescription, `${expected.slug}.longDescription`),
|
|
167
|
+
capabilities: Array.isArray(record.capabilities)
|
|
168
|
+
? record.capabilities.map((item, index) => requiredString(item, `${expected.slug}.capabilities[${index}]`, 80))
|
|
169
|
+
: [],
|
|
170
|
+
websiteUrl: httpsUrl(record.websiteUrl, `${expected.slug}.websiteUrl`),
|
|
171
|
+
privacyPolicyUrl: httpsUrl(record.privacyPolicyUrl, `${expected.slug}.privacyPolicyUrl`),
|
|
172
|
+
termsOfServiceUrl: httpsUrl(record.termsOfServiceUrl, `${expected.slug}.termsOfServiceUrl`),
|
|
173
|
+
license: {
|
|
174
|
+
spdx: requiredString(license.spdx, `${expected.slug}.license.spdx`, 80),
|
|
175
|
+
sourceUrl: httpsUrl(license.sourceUrl, `${expected.slug}.license.sourceUrl`),
|
|
176
|
+
note: requiredString(license.note, `${expected.slug}.license.note`, 400),
|
|
177
|
+
},
|
|
178
|
+
source: {
|
|
179
|
+
repositoryUrl: httpsUrl(source.repositoryUrl, `${expected.slug}.source.repositoryUrl`, true),
|
|
180
|
+
skillsAddSource: sourceValue,
|
|
181
|
+
subdirectory: source.subdirectory === "" ? "" : safeRelativePath(source.subdirectory, `${expected.slug}.source.subdirectory`),
|
|
182
|
+
versionStrategy: "latest_stable_release_or_head",
|
|
183
|
+
},
|
|
184
|
+
assets: {
|
|
185
|
+
icon: safeRelativePath(assets.icon, `${expected.slug}.assets.icon`),
|
|
186
|
+
iconDark: safeRelativePath(assets.iconDark, `${expected.slug}.assets.iconDark`),
|
|
187
|
+
origin,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function parseSemver(value) {
|
|
192
|
+
if (typeof value !== "string" || value.includes("-"))
|
|
193
|
+
return null;
|
|
194
|
+
const match = value.match(/^v?(\d+)\.(\d+)\.(\d+)(?:\+.*)?$/);
|
|
195
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
196
|
+
}
|
|
197
|
+
function compareRelease(left, right) {
|
|
198
|
+
const a = parseSemver(left.tag_name);
|
|
199
|
+
const b = parseSemver(right.tag_name);
|
|
200
|
+
for (let index = 0; index < 3; index += 1) {
|
|
201
|
+
if (a[index] !== b[index])
|
|
202
|
+
return b[index] - a[index];
|
|
203
|
+
}
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
function parseGitHubRepository(repositoryUrl) {
|
|
207
|
+
const parsed = new URL(repositoryUrl);
|
|
208
|
+
const segments = parsed.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "").split("/");
|
|
209
|
+
if (parsed.hostname !== "github.com" || segments.length !== 2) {
|
|
210
|
+
throw unprocessable("Plugin source must identify one public GitHub repository");
|
|
211
|
+
}
|
|
212
|
+
return { owner: segments[0], repo: segments[1] };
|
|
213
|
+
}
|
|
214
|
+
export function parseSkillsAddSource(source, explicitSubdirectory) {
|
|
215
|
+
const trimmed = source.trim();
|
|
216
|
+
let owner;
|
|
217
|
+
let repo;
|
|
218
|
+
let ref = null;
|
|
219
|
+
let sourceSubdirectory = "";
|
|
220
|
+
const shorthand = trimmed.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:@([^/]+))?(?:\/(.+))?$/);
|
|
221
|
+
if (shorthand && !trimmed.includes(":")) {
|
|
222
|
+
owner = shorthand[1];
|
|
223
|
+
repo = shorthand[2];
|
|
224
|
+
if ([owner, repo].some((segment) => segment === "." || segment === "..")) {
|
|
225
|
+
throw unprocessable("Source must be a public GitHub owner/repository or HTTPS repository URL");
|
|
226
|
+
}
|
|
227
|
+
ref = shorthand[3] ? decodeURIComponent(shorthand[3]) : null;
|
|
228
|
+
sourceSubdirectory = shorthand[4] ?? "";
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
let parsed;
|
|
232
|
+
try {
|
|
233
|
+
parsed = new URL(trimmed);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
throw unprocessable("Source must be a public GitHub owner/repository or HTTPS repository URL");
|
|
237
|
+
}
|
|
238
|
+
if (parsed.protocol !== "https:" || parsed.hostname !== "github.com") {
|
|
239
|
+
throw unprocessable("Source must use HTTPS on github.com");
|
|
240
|
+
}
|
|
241
|
+
const segments = parsed.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "").split("/");
|
|
242
|
+
if (segments.length < 2)
|
|
243
|
+
throw unprocessable("Source must identify one GitHub owner and repository");
|
|
244
|
+
owner = segments[0];
|
|
245
|
+
repo = segments[1];
|
|
246
|
+
if (segments[2] === "tree" && segments[3]) {
|
|
247
|
+
ref = decodeURIComponent(segments[3]);
|
|
248
|
+
sourceSubdirectory = segments.slice(4).join("/");
|
|
249
|
+
}
|
|
250
|
+
else if (segments.length > 2) {
|
|
251
|
+
sourceSubdirectory = segments.slice(2).join("/");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const subdirectory = explicitSubdirectory !== undefined
|
|
255
|
+
? explicitSubdirectory.trim()
|
|
256
|
+
: sourceSubdirectory;
|
|
257
|
+
return {
|
|
258
|
+
repositoryUrl: `https://github.com/${owner}/${repo}`,
|
|
259
|
+
source: `${owner}/${repo}`,
|
|
260
|
+
owner,
|
|
261
|
+
repo,
|
|
262
|
+
ref,
|
|
263
|
+
subdirectory: subdirectory ? safeRelativePath(subdirectory, "source.subdirectory") : "",
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
export function catalogSourceMatches(left, right) {
|
|
267
|
+
const normalizeRepository = (value) => value.replace(/\.git$/, "").replace(/\/$/, "").toLocaleLowerCase("en-US");
|
|
268
|
+
const normalizeSubdirectory = (value) => value.replace(/^\.\//, "").replace(/\/$/, "");
|
|
269
|
+
return normalizeRepository(left.repositoryUrl) === normalizeRepository(right.repositoryUrl)
|
|
270
|
+
&& normalizeSubdirectory(left.subdirectory) === normalizeSubdirectory(right.subdirectory);
|
|
271
|
+
}
|
|
272
|
+
export function discoverSkillsAddPaths(tree, subdirectory = "") {
|
|
273
|
+
const prefix = subdirectory ? `${subdirectory.replace(/\/$/, "")}/` : "";
|
|
274
|
+
const all = tree
|
|
275
|
+
.filter((entry) => entry.type === "blob" && entry.path.toLocaleLowerCase("en-US").endsWith("skill.md"))
|
|
276
|
+
.map((entry) => entry.path)
|
|
277
|
+
.filter((entryPath) => !prefix || entryPath === `${prefix}SKILL.md` || entryPath.startsWith(prefix));
|
|
278
|
+
if (all.length === 0)
|
|
279
|
+
return [];
|
|
280
|
+
const found = [];
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
const lower = new Set(all.map((entry) => entry.toLocaleLowerCase("en-US")));
|
|
283
|
+
for (const priority of PRIORITY_PREFIXES) {
|
|
284
|
+
const fullPrefix = `${prefix}${priority}`;
|
|
285
|
+
const isContainer = priority !== "";
|
|
286
|
+
for (const skillPath of all) {
|
|
287
|
+
if (!skillPath.startsWith(fullPrefix))
|
|
288
|
+
continue;
|
|
289
|
+
const rest = skillPath.slice(fullPrefix.length);
|
|
290
|
+
const parts = rest.split("/");
|
|
291
|
+
const direct = rest.toLocaleLowerCase("en-US") === "skill.md"
|
|
292
|
+
|| (parts.length === 2 && parts[1].toLocaleLowerCase("en-US") === "skill.md");
|
|
293
|
+
const skillDirs = parts.slice(0, -1);
|
|
294
|
+
const hasAncestor = skillDirs.slice(0, -1).some((_, index) => {
|
|
295
|
+
const ancestor = skillDirs.slice(0, index + 1).join("/");
|
|
296
|
+
return lower.has(`${fullPrefix}${ancestor}/SKILL.md`.toLocaleLowerCase("en-US"));
|
|
297
|
+
});
|
|
298
|
+
const nested = isContainer
|
|
299
|
+
&& parts.length >= 3
|
|
300
|
+
&& parts.length <= 4
|
|
301
|
+
&& parts.at(-1).toLocaleLowerCase("en-US") === "skill.md"
|
|
302
|
+
&& skillDirs.every((part) => !SKIP_DIRS.has(part.toLocaleLowerCase("en-US")))
|
|
303
|
+
&& !hasAncestor;
|
|
304
|
+
if ((direct || nested) && !seen.has(skillPath)) {
|
|
305
|
+
found.push(skillPath);
|
|
306
|
+
seen.add(skillPath);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (found.length > 0)
|
|
311
|
+
return found.sort();
|
|
312
|
+
return all
|
|
313
|
+
.filter((skillPath) => {
|
|
314
|
+
const relativeParts = skillPath.slice(prefix.length).split("/");
|
|
315
|
+
const directories = relativeParts.slice(0, -1);
|
|
316
|
+
return relativeParts.length <= 6
|
|
317
|
+
&& directories.every((part) => !SKIP_DIRS.has(part.toLocaleLowerCase("en-US")));
|
|
318
|
+
})
|
|
319
|
+
.sort();
|
|
320
|
+
}
|
|
321
|
+
async function fetchBounded(fetcher, url, init = {}, allowedHosts) {
|
|
322
|
+
let current = new URL(url);
|
|
323
|
+
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
|
|
324
|
+
if (current.protocol !== "https:" || (allowedHosts && !allowedHosts.has(current.hostname))) {
|
|
325
|
+
throw unprocessable("Plugin source redirected outside the allowed HTTPS hosts");
|
|
326
|
+
}
|
|
327
|
+
const response = await fetcher(current, { ...init, redirect: "manual" });
|
|
328
|
+
if (![301, 302, 303, 307, 308].includes(response.status))
|
|
329
|
+
return response;
|
|
330
|
+
const location = response.headers.get("location");
|
|
331
|
+
if (!location || redirects === MAX_REDIRECTS)
|
|
332
|
+
throw unprocessable("Plugin source exceeded the redirect limit");
|
|
333
|
+
current = new URL(location, current);
|
|
334
|
+
}
|
|
335
|
+
throw unprocessable("Plugin source exceeded the redirect limit");
|
|
336
|
+
}
|
|
337
|
+
async function responseJson(response, label) {
|
|
338
|
+
if (!response.ok)
|
|
339
|
+
throw unprocessable(`${label} returned HTTP ${response.status}`);
|
|
340
|
+
try {
|
|
341
|
+
return await response.json();
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
throw unprocessable(`${label} did not return valid JSON`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function githubJson(fetcher, apiPath) {
|
|
348
|
+
const response = await fetchBounded(fetcher, `https://api.github.com${apiPath}`, {
|
|
349
|
+
headers: {
|
|
350
|
+
accept: "application/vnd.github+json",
|
|
351
|
+
"user-agent": "Rudder-Plugin-Hub",
|
|
352
|
+
"x-github-api-version": "2022-11-28",
|
|
353
|
+
},
|
|
354
|
+
}, new Set(["api.github.com"]));
|
|
355
|
+
return responseJson(response, "GitHub API");
|
|
356
|
+
}
|
|
357
|
+
export async function resolveGitHubVersion(fetcher, input) {
|
|
358
|
+
const { owner, repo } = parseGitHubRepository(input.repositoryUrl);
|
|
359
|
+
const repository = asRecord(await githubJson(fetcher, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`));
|
|
360
|
+
if (repository.private === true)
|
|
361
|
+
throw unprocessable("Curated and URL Plugin sources must be public");
|
|
362
|
+
let strategy;
|
|
363
|
+
let version;
|
|
364
|
+
let ref;
|
|
365
|
+
if (input.ref) {
|
|
366
|
+
strategy = "explicit_ref";
|
|
367
|
+
version = input.ref;
|
|
368
|
+
ref = input.ref;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
const rawReleases = await githubJson(fetcher, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases?per_page=30`);
|
|
372
|
+
const releases = Array.isArray(rawReleases)
|
|
373
|
+
? rawReleases.map(asRecord).filter((release) => release.draft !== true && release.prerelease !== true && parseSemver(release.tag_name))
|
|
374
|
+
: [];
|
|
375
|
+
const stable = releases.sort(compareRelease)[0];
|
|
376
|
+
if (stable) {
|
|
377
|
+
strategy = "stable_release";
|
|
378
|
+
version = requiredString(stable.tag_name, "GitHub release tag", 100);
|
|
379
|
+
ref = version;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
strategy = "default_branch_head";
|
|
383
|
+
ref = requiredString(repository.default_branch, "GitHub default branch", 200);
|
|
384
|
+
version = ref;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const commit = asRecord(await githubJson(fetcher, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${encodeURIComponent(ref)}`));
|
|
388
|
+
const commitSha = requiredString(commit.sha, "GitHub commit SHA", 40);
|
|
389
|
+
if (!/^[a-f0-9]{40}$/i.test(commitSha))
|
|
390
|
+
throw unprocessable("GitHub did not resolve a full immutable commit SHA");
|
|
391
|
+
return {
|
|
392
|
+
repositoryUrl: input.repositoryUrl,
|
|
393
|
+
source: input.source,
|
|
394
|
+
subdirectory: input.subdirectory,
|
|
395
|
+
strategy,
|
|
396
|
+
version: strategy === "default_branch_head" ? commitSha.slice(0, 12) : version.replace(/^v/, ""),
|
|
397
|
+
commitSha: commitSha.toLocaleLowerCase("en-US"),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function safePackageEntries(tree, subdirectory) {
|
|
401
|
+
const prefix = subdirectory ? `${subdirectory.replace(/\/$/, "")}/` : "";
|
|
402
|
+
const entries = tree
|
|
403
|
+
.filter((entry) => entry.type === "blob" && (!prefix || entry.path.startsWith(prefix)))
|
|
404
|
+
.map((entry) => ({ ...entry, relativePath: prefix ? entry.path.slice(prefix.length) : entry.path }))
|
|
405
|
+
.filter((entry) => entry.relativePath.length > 0);
|
|
406
|
+
if (entries.length === 0)
|
|
407
|
+
throw unprocessable("Plugin source directory is empty");
|
|
408
|
+
if (entries.length > MAX_FILES)
|
|
409
|
+
throw unprocessable("Plugin package exceeds the 500-file V1 limit");
|
|
410
|
+
let total = 0;
|
|
411
|
+
const folded = new Set();
|
|
412
|
+
for (const entry of entries) {
|
|
413
|
+
const relativePath = safeRelativePath(entry.relativePath, "package file");
|
|
414
|
+
const lower = relativePath.toLocaleLowerCase("en-US");
|
|
415
|
+
if (folded.has(lower))
|
|
416
|
+
throw unprocessable(`Plugin contains a duplicate or case-colliding path: ${relativePath}`);
|
|
417
|
+
folded.add(lower);
|
|
418
|
+
const size = entry.size ?? 0;
|
|
419
|
+
if (size > MAX_FILE_BYTES)
|
|
420
|
+
throw unprocessable(`Plugin file exceeds 2 MiB: ${relativePath}`);
|
|
421
|
+
total += size;
|
|
422
|
+
if (total > MAX_TOTAL_BYTES)
|
|
423
|
+
throw unprocessable("Plugin package exceeds the 10 MiB V1 limit");
|
|
424
|
+
}
|
|
425
|
+
return entries;
|
|
426
|
+
}
|
|
427
|
+
async function fetchGitHubFiles(fetcher, resolution) {
|
|
428
|
+
const { owner, repo } = parseGitHubRepository(resolution.repositoryUrl);
|
|
429
|
+
const treeValue = asRecord(await githubJson(fetcher, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/git/trees/${resolution.commitSha}?recursive=1`));
|
|
430
|
+
if (treeValue.truncated === true || !Array.isArray(treeValue.tree))
|
|
431
|
+
throw unprocessable("GitHub package tree is unavailable or truncated");
|
|
432
|
+
const tree = treeValue.tree.map((raw) => {
|
|
433
|
+
const entry = asRecord(raw);
|
|
434
|
+
const type = entry.type === "blob" ? "blob" : entry.type === "tree" ? "tree" : null;
|
|
435
|
+
if (!type)
|
|
436
|
+
return null;
|
|
437
|
+
return {
|
|
438
|
+
path: requiredString(entry.path, "GitHub tree path", 1_024),
|
|
439
|
+
type,
|
|
440
|
+
sha: requiredString(entry.sha, "GitHub tree SHA", 80),
|
|
441
|
+
...(typeof entry.size === "number" ? { size: entry.size } : {}),
|
|
442
|
+
};
|
|
443
|
+
}).filter((entry) => Boolean(entry));
|
|
444
|
+
const selected = safePackageEntries(tree, resolution.subdirectory);
|
|
445
|
+
const files = new Array(selected.length);
|
|
446
|
+
let next = 0;
|
|
447
|
+
async function worker() {
|
|
448
|
+
while (next < selected.length) {
|
|
449
|
+
const index = next;
|
|
450
|
+
next += 1;
|
|
451
|
+
const entry = selected[index];
|
|
452
|
+
const response = await fetchBounded(fetcher, `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${resolution.commitSha}/${entry.path.split("/").map(encodeURIComponent).join("/")}`, { headers: { "user-agent": "Rudder-Plugin-Hub" } }, new Set(["raw.githubusercontent.com"]));
|
|
453
|
+
if (!response.ok)
|
|
454
|
+
throw unprocessable(`Plugin file fetch failed with HTTP ${response.status}: ${entry.relativePath}`);
|
|
455
|
+
const content = Buffer.from(await response.arrayBuffer());
|
|
456
|
+
if (content.byteLength > MAX_FILE_BYTES)
|
|
457
|
+
throw unprocessable(`Plugin file exceeds 2 MiB: ${entry.relativePath}`);
|
|
458
|
+
files[index] = { path: entry.relativePath, content: content.toString("base64"), encoding: "base64" };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, selected.length) }, () => worker()));
|
|
462
|
+
return { tree, files };
|
|
463
|
+
}
|
|
464
|
+
function normalizedSemver(value) {
|
|
465
|
+
return parseSemver(value) ? value.replace(/^v/, "") : `0.0.0-${value.replace(/[^0-9A-Za-z-]+/g, "-").slice(0, 32) || "snapshot"}`;
|
|
466
|
+
}
|
|
467
|
+
export function synthesizeSkillsPlugin(descriptor, resolution, tree, files) {
|
|
468
|
+
const sourcePrefix = resolution.subdirectory ? `${resolution.subdirectory}/` : "";
|
|
469
|
+
const discovered = discoverSkillsAddPaths(tree, resolution.subdirectory)
|
|
470
|
+
.map((skillPath) => sourcePrefix ? skillPath.slice(sourcePrefix.length) : skillPath);
|
|
471
|
+
if (discovered.length === 0)
|
|
472
|
+
throw unprocessable("No compatible Skills were discovered in this source");
|
|
473
|
+
const roots = discovered.map((skillPath) => path.posix.dirname(skillPath));
|
|
474
|
+
const discoveredSkillPaths = new Set(discovered);
|
|
475
|
+
const matchesRoot = (filePath, root) => root === "."
|
|
476
|
+
? filePath !== ".codex-plugin/plugin.json"
|
|
477
|
+
&& !filePath.startsWith(".codex-plugin/")
|
|
478
|
+
&& (filePath === "SKILL.md"
|
|
479
|
+
|| path.posix.basename(filePath).toLocaleLowerCase("en-US") !== "skill.md"
|
|
480
|
+
|| discoveredSkillPaths.has(filePath))
|
|
481
|
+
: filePath === `${root}/SKILL.md`
|
|
482
|
+
|| (filePath.startsWith(`${root}/`)
|
|
483
|
+
&& (path.posix.basename(filePath).toLocaleLowerCase("en-US") !== "skill.md"
|
|
484
|
+
|| discoveredSkillPaths.has(filePath)));
|
|
485
|
+
const selected = files.filter((file) => roots.some((root) => matchesRoot(file.path, root)));
|
|
486
|
+
const rootsByTarget = new Map();
|
|
487
|
+
const mapped = selected.map((file) => {
|
|
488
|
+
const root = roots.find((candidate) => matchesRoot(file.path, candidate));
|
|
489
|
+
const skillDir = root === "." ? descriptor.slug : path.posix.basename(root);
|
|
490
|
+
let targetRoot = `skills/${skillDir}`;
|
|
491
|
+
const existing = rootsByTarget.get(targetRoot);
|
|
492
|
+
if (existing && existing !== root)
|
|
493
|
+
targetRoot = `skills/${descriptor.slug}-${skillDir}`;
|
|
494
|
+
rootsByTarget.set(targetRoot, root);
|
|
495
|
+
const relative = root === "." ? file.path : file.path.slice(root.length + 1);
|
|
496
|
+
return { ...file, path: `${targetRoot}/${relative}` };
|
|
497
|
+
});
|
|
498
|
+
const manifest = {
|
|
499
|
+
name: descriptor.slug,
|
|
500
|
+
version: normalizedSemver(resolution.version),
|
|
501
|
+
description: descriptor.shortDescription,
|
|
502
|
+
author: { name: descriptor.developer, url: descriptor.websiteUrl },
|
|
503
|
+
homepage: descriptor.websiteUrl,
|
|
504
|
+
repository: descriptor.source.repositoryUrl,
|
|
505
|
+
license: descriptor.license.spdx,
|
|
506
|
+
skills: "./skills/",
|
|
507
|
+
interface: {
|
|
508
|
+
displayName: descriptor.displayName,
|
|
509
|
+
shortDescription: descriptor.shortDescription,
|
|
510
|
+
longDescription: descriptor.longDescription,
|
|
511
|
+
developerName: descriptor.developer,
|
|
512
|
+
category: descriptor.category,
|
|
513
|
+
capabilities: descriptor.capabilities,
|
|
514
|
+
websiteURL: descriptor.websiteUrl,
|
|
515
|
+
privacyPolicyURL: descriptor.privacyPolicyUrl,
|
|
516
|
+
termsOfServiceURL: descriptor.termsOfServiceUrl,
|
|
517
|
+
},
|
|
518
|
+
rudder: {
|
|
519
|
+
generatedFrom: "skills_add",
|
|
520
|
+
source: descriptor.source.skillsAddSource,
|
|
521
|
+
commitSha: resolution.commitSha,
|
|
522
|
+
discoveredSkills: discovered,
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
return [
|
|
526
|
+
{ path: ".codex-plugin/plugin.json", content: JSON.stringify(manifest), encoding: "utf8" },
|
|
527
|
+
...mapped,
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
function groupComponents(components) {
|
|
531
|
+
return {
|
|
532
|
+
skills: components.filter((component) => component.type === "skill"),
|
|
533
|
+
mcps: components.filter((component) => component.type === "mcp"),
|
|
534
|
+
apps: components.filter((component) => component.type === "app"),
|
|
535
|
+
unsupported: components.filter((component) => component.type === "unsupported"),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
export function rudderPluginCatalogService(db, mcpOptions, options = {}) {
|
|
539
|
+
const plugins = rudderPluginService(db, mcpOptions);
|
|
540
|
+
const fetcher = options.fetch ?? fetch;
|
|
541
|
+
const catalogUrl = options.catalogUrl ?? process.env.RUDDER_PLUGIN_CATALOG_URL?.trim() ?? DEFAULT_CATALOG_URL;
|
|
542
|
+
const catalogHost = new URL(catalogUrl).hostname;
|
|
543
|
+
const catalogHosts = new Set([catalogHost]);
|
|
544
|
+
const cachePath = options.cachePath ?? path.resolve(resolveRudderInstanceRoot(), "cache", "plugin-catalog", "catalog.json");
|
|
545
|
+
let memory = null;
|
|
546
|
+
const resolutionCache = new Map();
|
|
547
|
+
const freshnessLease = createCatalogFreshnessLease();
|
|
548
|
+
async function readCache() {
|
|
549
|
+
if (memory)
|
|
550
|
+
return memory;
|
|
551
|
+
try {
|
|
552
|
+
const value = JSON.parse(await fs.readFile(cachePath, "utf8"));
|
|
553
|
+
memory = { ...value, document: validateCatalog(value.document) };
|
|
554
|
+
return memory;
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async function writeCache(cache) {
|
|
561
|
+
memory = cache;
|
|
562
|
+
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
|
563
|
+
const temporary = `${cachePath}.${process.pid}.tmp`;
|
|
564
|
+
await fs.writeFile(temporary, `${JSON.stringify(cache, null, 2)}\n`, { mode: 0o600 });
|
|
565
|
+
await fs.rename(temporary, cachePath);
|
|
566
|
+
}
|
|
567
|
+
async function loadCatalog() {
|
|
568
|
+
const previous = await readCache();
|
|
569
|
+
try {
|
|
570
|
+
const response = await fetchBounded(fetcher, catalogUrl, {
|
|
571
|
+
headers: {
|
|
572
|
+
accept: "application/json",
|
|
573
|
+
"user-agent": "Rudder-Plugin-Hub",
|
|
574
|
+
...(previous?.etag ? { "if-none-match": previous.etag } : {}),
|
|
575
|
+
},
|
|
576
|
+
}, catalogHosts);
|
|
577
|
+
if (response.status === 304 && previous) {
|
|
578
|
+
return { cache: previous, freshness: freshnessLease.observe("fresh") };
|
|
579
|
+
}
|
|
580
|
+
const document = validateCatalog(await responseJson(response, "Plugin catalog"));
|
|
581
|
+
const cache = { etag: response.headers.get("etag"), fetchedAt: new Date().toISOString(), document };
|
|
582
|
+
await writeCache(cache);
|
|
583
|
+
return { cache, freshness: freshnessLease.observe("fresh") };
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
if (previous)
|
|
587
|
+
return { cache: previous, freshness: freshnessLease.observe("stale") };
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function catalogAssetUrl(relativePath) {
|
|
592
|
+
return new URL(relativePath, catalogUrl).toString();
|
|
593
|
+
}
|
|
594
|
+
async function loadDescriptor(slug) {
|
|
595
|
+
const loaded = await loadCatalog();
|
|
596
|
+
const entry = loaded.cache.document.plugins.find((candidate) => candidate.slug === slug);
|
|
597
|
+
if (!entry)
|
|
598
|
+
throw notFound("Plugin catalog entry not found");
|
|
599
|
+
const response = await fetchBounded(fetcher, catalogAssetUrl(entry.sourcePath), {
|
|
600
|
+
headers: { accept: "application/json", "user-agent": "Rudder-Plugin-Hub" },
|
|
601
|
+
}, catalogHosts);
|
|
602
|
+
const descriptor = validateDescriptor(await responseJson(response, "Plugin source descriptor"), entry);
|
|
603
|
+
return { entry, descriptor, freshness: loaded.freshness };
|
|
604
|
+
}
|
|
605
|
+
async function catalogDescriptorForSource(source) {
|
|
606
|
+
try {
|
|
607
|
+
const loaded = await loadCatalog();
|
|
608
|
+
for (const entry of loaded.cache.document.plugins) {
|
|
609
|
+
try {
|
|
610
|
+
const response = await fetchBounded(fetcher, catalogAssetUrl(entry.sourcePath), {
|
|
611
|
+
headers: { accept: "application/json", "user-agent": "Rudder-Plugin-Hub" },
|
|
612
|
+
}, catalogHosts);
|
|
613
|
+
const descriptor = validateDescriptor(await responseJson(response, "Plugin source descriptor"), entry);
|
|
614
|
+
if (catalogSourceMatches(source, descriptor.source))
|
|
615
|
+
return { slug: entry.slug, descriptor };
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
// An unavailable unrelated curated descriptor must not block an
|
|
619
|
+
// otherwise valid explicit URL Import.
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// URL Import remains available when the optional curated catalog is down.
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
async function resolveDescriptor(descriptor) {
|
|
629
|
+
const key = `${descriptor.source.repositoryUrl}#${descriptor.source.subdirectory}`;
|
|
630
|
+
const cached = resolutionCache.get(key);
|
|
631
|
+
if (cached && cached.expiresAt > Date.now())
|
|
632
|
+
return cached.value;
|
|
633
|
+
const value = await resolveGitHubVersion(fetcher, {
|
|
634
|
+
repositoryUrl: descriptor.source.repositoryUrl,
|
|
635
|
+
source: descriptor.source.skillsAddSource,
|
|
636
|
+
subdirectory: descriptor.source.subdirectory,
|
|
637
|
+
});
|
|
638
|
+
resolutionCache.set(key, { expiresAt: Date.now() + 5 * 60_000, value });
|
|
639
|
+
return value;
|
|
640
|
+
}
|
|
641
|
+
async function installationState(orgId) {
|
|
642
|
+
const rows = await db.select({
|
|
643
|
+
installation: installedPlugins,
|
|
644
|
+
pkg: pluginPackages,
|
|
645
|
+
source: pluginSources,
|
|
646
|
+
}).from(installedPlugins)
|
|
647
|
+
.innerJoin(pluginPackages, eq(installedPlugins.packageId, pluginPackages.id))
|
|
648
|
+
.leftJoin(pluginSources, eq(installedPlugins.sourceId, pluginSources.id))
|
|
649
|
+
.where(and(eq(installedPlugins.orgId, orgId), ne(installedPlugins.lifecycleState, "uninstalled")));
|
|
650
|
+
return rows;
|
|
651
|
+
}
|
|
652
|
+
async function catalog(orgId) {
|
|
653
|
+
const loaded = await loadCatalog();
|
|
654
|
+
const installed = await installationState(orgId);
|
|
655
|
+
const entries = await Promise.all(loaded.cache.document.plugins.map(async (entry) => {
|
|
656
|
+
const current = installed.find((row) => asRecord(row.source?.metadata).catalogSlug === entry.slug);
|
|
657
|
+
const metadata = asRecord(current?.source?.metadata);
|
|
658
|
+
const installedSourceSha = typeof metadata.commitSha === "string" ? metadata.commitSha : null;
|
|
659
|
+
let latestVersion = null;
|
|
660
|
+
let latestSourceSha = null;
|
|
661
|
+
if (current) {
|
|
662
|
+
try {
|
|
663
|
+
const { descriptor } = await loadDescriptor(entry.slug);
|
|
664
|
+
const resolution = await resolveDescriptor(descriptor);
|
|
665
|
+
latestVersion = resolution.version;
|
|
666
|
+
latestSourceSha = resolution.commitSha;
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// Catalog discovery remains usable with installed state when the
|
|
670
|
+
// lightweight upstream version check is temporarily unavailable.
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
slug: entry.slug,
|
|
675
|
+
displayName: entry.displayName,
|
|
676
|
+
developer: entry.developer,
|
|
677
|
+
category: entry.category,
|
|
678
|
+
shortDescription: entry.shortDescription,
|
|
679
|
+
sourceKind: entry.sourceKind,
|
|
680
|
+
iconUrl: `/api/plugins/catalog/${encodeURIComponent(entry.slug)}/icon`,
|
|
681
|
+
installedPluginId: current?.installation.id ?? null,
|
|
682
|
+
installedVersion: current?.pkg.version ?? null,
|
|
683
|
+
installedSourceSha,
|
|
684
|
+
latestVersion,
|
|
685
|
+
latestSourceSha,
|
|
686
|
+
updateAvailable: Boolean(installedSourceSha && latestSourceSha && installedSourceSha !== latestSourceSha),
|
|
687
|
+
};
|
|
688
|
+
}));
|
|
689
|
+
return { entries, freshness: loaded.freshness, updatedAt: loaded.cache.document.updatedAt };
|
|
690
|
+
}
|
|
691
|
+
async function installedDetail(orgId, descriptor, resolution, catalogSlug, iconUrl) {
|
|
692
|
+
const rows = await installationState(orgId);
|
|
693
|
+
const current = rows.find((row) => {
|
|
694
|
+
const metadata = asRecord(row.source?.metadata);
|
|
695
|
+
return metadata.catalogSlug === catalogSlug && metadata.commitSha === resolution.commitSha;
|
|
696
|
+
});
|
|
697
|
+
if (!current)
|
|
698
|
+
return null;
|
|
699
|
+
const compatibility = asRecord(current.pkg.compatibility);
|
|
700
|
+
const components = Array.isArray(compatibility.components)
|
|
701
|
+
? compatibility.components
|
|
702
|
+
: [];
|
|
703
|
+
const warnings = Array.isArray(compatibility.warnings)
|
|
704
|
+
? compatibility.warnings.filter((value) => typeof value === "string")
|
|
705
|
+
: [];
|
|
706
|
+
return {
|
|
707
|
+
slug: descriptor.slug,
|
|
708
|
+
displayName: descriptor.displayName,
|
|
709
|
+
developer: descriptor.developer,
|
|
710
|
+
category: descriptor.category,
|
|
711
|
+
shortDescription: descriptor.shortDescription,
|
|
712
|
+
longDescription: descriptor.longDescription,
|
|
713
|
+
capabilities: descriptor.capabilities,
|
|
714
|
+
websiteUrl: descriptor.websiteUrl,
|
|
715
|
+
privacyPolicyUrl: descriptor.privacyPolicyUrl,
|
|
716
|
+
termsOfServiceUrl: descriptor.termsOfServiceUrl,
|
|
717
|
+
license: descriptor.license,
|
|
718
|
+
sourceKind: descriptor.kind,
|
|
719
|
+
iconUrl,
|
|
720
|
+
previewId: null,
|
|
721
|
+
packageId: current.pkg.id,
|
|
722
|
+
action: "installed",
|
|
723
|
+
installedPluginId: current.installation.id,
|
|
724
|
+
resolution,
|
|
725
|
+
components,
|
|
726
|
+
groups: groupComponents(components),
|
|
727
|
+
warnings,
|
|
728
|
+
capabilityDiff: null,
|
|
729
|
+
skillConflicts: [],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
async function preparePreview(orgId, descriptor, resolution, catalogSlug, iconUrl) {
|
|
733
|
+
const downloaded = await fetchGitHubFiles(fetcher, resolution);
|
|
734
|
+
const files = descriptor.kind === "skills_add"
|
|
735
|
+
? synthesizeSkillsPlugin(descriptor, resolution, downloaded.tree, downloaded.files)
|
|
736
|
+
: downloaded.files;
|
|
737
|
+
const report = await plugins.inspect(orgId, {
|
|
738
|
+
sourceType: "local_upload",
|
|
739
|
+
sourceLabel: `${descriptor.displayName} @ ${resolution.commitSha.slice(0, 12)}`,
|
|
740
|
+
files: files.map((file) => ({ ...file, encoding: file.encoding ?? "utf8" })),
|
|
741
|
+
}, {
|
|
742
|
+
type: "git",
|
|
743
|
+
label: `${descriptor.displayName} @ ${resolution.commitSha.slice(0, 12)}`,
|
|
744
|
+
locator: `${resolution.repositoryUrl}${resolution.subdirectory ? `/tree/${resolution.commitSha}/${resolution.subdirectory}` : `#${resolution.commitSha}`}`,
|
|
745
|
+
metadata: {
|
|
746
|
+
catalogSlug,
|
|
747
|
+
sourceKind: descriptor.kind,
|
|
748
|
+
skillsAddSource: descriptor.source.skillsAddSource,
|
|
749
|
+
repository: resolution.repositoryUrl,
|
|
750
|
+
subdirectory: resolution.subdirectory,
|
|
751
|
+
resolutionStrategy: resolution.strategy,
|
|
752
|
+
resolvedVersion: resolution.version,
|
|
753
|
+
commitSha: resolution.commitSha,
|
|
754
|
+
immutable: true,
|
|
755
|
+
category: descriptor.category,
|
|
756
|
+
descriptor: {
|
|
757
|
+
slug: descriptor.slug,
|
|
758
|
+
displayName: descriptor.displayName,
|
|
759
|
+
developer: descriptor.developer,
|
|
760
|
+
category: descriptor.category,
|
|
761
|
+
shortDescription: descriptor.shortDescription,
|
|
762
|
+
longDescription: descriptor.longDescription,
|
|
763
|
+
capabilities: descriptor.capabilities,
|
|
764
|
+
websiteUrl: descriptor.websiteUrl,
|
|
765
|
+
privacyPolicyUrl: descriptor.privacyPolicyUrl,
|
|
766
|
+
termsOfServiceUrl: descriptor.termsOfServiceUrl,
|
|
767
|
+
license: descriptor.license,
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
if (!report.packageId || report.errors.length > 0) {
|
|
772
|
+
throw unprocessable(report.errors.join(" ") || "Plugin Preview could not be created");
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
slug: descriptor.slug,
|
|
776
|
+
displayName: descriptor.displayName,
|
|
777
|
+
developer: descriptor.developer,
|
|
778
|
+
category: descriptor.category,
|
|
779
|
+
shortDescription: descriptor.shortDescription,
|
|
780
|
+
longDescription: descriptor.longDescription,
|
|
781
|
+
capabilities: descriptor.capabilities,
|
|
782
|
+
websiteUrl: descriptor.websiteUrl,
|
|
783
|
+
privacyPolicyUrl: descriptor.privacyPolicyUrl,
|
|
784
|
+
termsOfServiceUrl: descriptor.termsOfServiceUrl,
|
|
785
|
+
license: descriptor.license,
|
|
786
|
+
sourceKind: descriptor.kind,
|
|
787
|
+
iconUrl,
|
|
788
|
+
previewId: report.id,
|
|
789
|
+
packageId: report.packageId,
|
|
790
|
+
action: report.operation,
|
|
791
|
+
installedPluginId: report.installedPluginId,
|
|
792
|
+
resolution,
|
|
793
|
+
components: report.components,
|
|
794
|
+
groups: groupComponents(report.components),
|
|
795
|
+
warnings: report.warnings,
|
|
796
|
+
capabilityDiff: report.capabilityDiff,
|
|
797
|
+
skillConflicts: report.skillConflicts,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
async function previewCatalog(orgId, slug) {
|
|
801
|
+
const { descriptor } = await loadDescriptor(slug);
|
|
802
|
+
const resolution = await resolveDescriptor(descriptor);
|
|
803
|
+
const current = await installedDetail(orgId, descriptor, resolution, slug, `/api/plugins/catalog/${encodeURIComponent(slug)}/icon`);
|
|
804
|
+
if (current)
|
|
805
|
+
return current;
|
|
806
|
+
return preparePreview(orgId, descriptor, resolution, slug, `/api/plugins/catalog/${encodeURIComponent(slug)}/icon`);
|
|
807
|
+
}
|
|
808
|
+
async function previewSource(orgId, sourceInput, explicitSubdirectory) {
|
|
809
|
+
const parsed = parseSkillsAddSource(sourceInput, explicitSubdirectory);
|
|
810
|
+
const resolution = await resolveGitHubVersion(fetcher, parsed);
|
|
811
|
+
const catalogMatch = await catalogDescriptorForSource(parsed);
|
|
812
|
+
const slug = parsed.repo.toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 64);
|
|
813
|
+
const descriptor = catalogMatch?.descriptor ?? {
|
|
814
|
+
schemaVersion: 1,
|
|
815
|
+
slug: slug || "imported-skills",
|
|
816
|
+
kind: "skills_add",
|
|
817
|
+
displayName: parsed.repo.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()),
|
|
818
|
+
developer: parsed.owner,
|
|
819
|
+
category: "Community",
|
|
820
|
+
shortDescription: `Skills discovered from ${parsed.source}.`,
|
|
821
|
+
longDescription: `A Skills-only Plugin preview generated deterministically from the public ${parsed.source} repository.`,
|
|
822
|
+
capabilities: ["Read", "Write"],
|
|
823
|
+
websiteUrl: parsed.repositoryUrl,
|
|
824
|
+
privacyPolicyUrl: "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement",
|
|
825
|
+
termsOfServiceUrl: "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service",
|
|
826
|
+
license: {
|
|
827
|
+
spdx: "NOASSERTION",
|
|
828
|
+
sourceUrl: parsed.repositoryUrl,
|
|
829
|
+
note: "Check the upstream repository license before installing this uncataloged source.",
|
|
830
|
+
},
|
|
831
|
+
source: {
|
|
832
|
+
repositoryUrl: parsed.repositoryUrl,
|
|
833
|
+
skillsAddSource: parsed.source,
|
|
834
|
+
subdirectory: parsed.subdirectory,
|
|
835
|
+
versionStrategy: "latest_stable_release_or_head",
|
|
836
|
+
},
|
|
837
|
+
assets: { icon: "", iconDark: "", origin: "rudder_generic" },
|
|
838
|
+
};
|
|
839
|
+
return preparePreview(orgId, descriptor, resolution, catalogMatch?.slug ?? null, catalogMatch ? `/api/plugins/catalog/${encodeURIComponent(catalogMatch.slug)}/icon` : "");
|
|
840
|
+
}
|
|
841
|
+
async function previewDetail(orgId, previewId) {
|
|
842
|
+
const stored = await db.select({
|
|
843
|
+
report: pluginImportReports,
|
|
844
|
+
pkg: pluginPackages,
|
|
845
|
+
source: pluginSources,
|
|
846
|
+
}).from(pluginImportReports)
|
|
847
|
+
.innerJoin(pluginPackages, eq(pluginImportReports.packageId, pluginPackages.id))
|
|
848
|
+
.innerJoin(pluginSources, eq(pluginImportReports.sourceId, pluginSources.id))
|
|
849
|
+
.where(and(eq(pluginImportReports.orgId, orgId), eq(pluginImportReports.id, previewId)))
|
|
850
|
+
.then((rows) => rows[0] ?? null);
|
|
851
|
+
if (!stored)
|
|
852
|
+
throw notFound("Plugin Preview not found");
|
|
853
|
+
const metadata = asRecord(stored.source.metadata);
|
|
854
|
+
const descriptor = asRecord(metadata.descriptor);
|
|
855
|
+
const normalized = asRecord(stored.pkg.normalizedManifest);
|
|
856
|
+
const report = asRecord(stored.report.report);
|
|
857
|
+
const sourceKind = metadata.sourceKind;
|
|
858
|
+
if ((sourceKind !== "codex_plugin" && sourceKind !== "skills_add") || metadata.immutable !== true) {
|
|
859
|
+
throw notFound("Plugin Preview detail not found");
|
|
860
|
+
}
|
|
861
|
+
const strategy = metadata.resolutionStrategy;
|
|
862
|
+
if (strategy !== "stable_release" && strategy !== "default_branch_head" && strategy !== "explicit_ref") {
|
|
863
|
+
throw unprocessable("Stored Plugin Preview has an invalid resolution strategy");
|
|
864
|
+
}
|
|
865
|
+
const commitSha = requiredString(metadata.commitSha, "Preview commit SHA", 40);
|
|
866
|
+
if (!/^[0-9a-f]{40}$/.test(commitSha)) {
|
|
867
|
+
throw unprocessable("Stored Plugin Preview is missing a full immutable commit SHA");
|
|
868
|
+
}
|
|
869
|
+
const components = Array.isArray(report.components)
|
|
870
|
+
? report.components
|
|
871
|
+
: [];
|
|
872
|
+
const warnings = Array.isArray(report.warnings)
|
|
873
|
+
? report.warnings.filter((value) => typeof value === "string")
|
|
874
|
+
: [];
|
|
875
|
+
const capabilityDiff = report.capabilityDiff && typeof report.capabilityDiff === "object"
|
|
876
|
+
? report.capabilityDiff
|
|
877
|
+
: null;
|
|
878
|
+
const skillConflicts = Array.isArray(report.skillConflicts)
|
|
879
|
+
? report.skillConflicts
|
|
880
|
+
: [];
|
|
881
|
+
const catalogSlug = typeof metadata.catalogSlug === "string" && metadata.catalogSlug
|
|
882
|
+
? metadata.catalogSlug
|
|
883
|
+
: null;
|
|
884
|
+
const installation = await installationState(orgId).then((rows) => rows.find((row) => (row.pkg.id === stored.pkg.id || row.installation.id === report.installedPluginId)) ?? null);
|
|
885
|
+
const exactPackageInstalled = installation?.pkg.id === stored.pkg.id;
|
|
886
|
+
const action = exactPackageInstalled
|
|
887
|
+
? "installed"
|
|
888
|
+
: stored.report.status === "review_required" && installation && report.operation === "update"
|
|
889
|
+
? "update"
|
|
890
|
+
: "install";
|
|
891
|
+
const license = asRecord(descriptor.license);
|
|
892
|
+
const resolution = {
|
|
893
|
+
repositoryUrl: httpsUrl(metadata.repository, "Preview repository", true),
|
|
894
|
+
source: requiredString(metadata.skillsAddSource, "Preview source", 200),
|
|
895
|
+
subdirectory: metadata.subdirectory === ""
|
|
896
|
+
? ""
|
|
897
|
+
: safeRelativePath(metadata.subdirectory, "Preview subdirectory"),
|
|
898
|
+
strategy,
|
|
899
|
+
version: requiredString(metadata.resolvedVersion, "Preview version", 100),
|
|
900
|
+
commitSha,
|
|
901
|
+
};
|
|
902
|
+
return {
|
|
903
|
+
slug: catalogSlug ?? requiredString(descriptor.slug ?? stored.pkg.name, "Preview slug", 64),
|
|
904
|
+
displayName: requiredString(descriptor.displayName ?? normalized.displayName, "Preview display name", 80),
|
|
905
|
+
developer: requiredString(descriptor.developer ?? normalized.publisher, "Preview developer", 80),
|
|
906
|
+
category: requiredString(descriptor.category ?? metadata.category ?? normalized.category, "Preview category", 80),
|
|
907
|
+
shortDescription: requiredString(descriptor.shortDescription ?? normalized.shortDescription, "Preview short description", 180),
|
|
908
|
+
longDescription: requiredString(descriptor.longDescription, "Preview long description"),
|
|
909
|
+
capabilities: Array.isArray(descriptor.capabilities)
|
|
910
|
+
? descriptor.capabilities.filter((value) => typeof value === "string")
|
|
911
|
+
: [],
|
|
912
|
+
websiteUrl: httpsUrl(descriptor.websiteUrl, "Preview website URL"),
|
|
913
|
+
privacyPolicyUrl: httpsUrl(descriptor.privacyPolicyUrl, "Preview privacy policy URL"),
|
|
914
|
+
termsOfServiceUrl: httpsUrl(descriptor.termsOfServiceUrl, "Preview terms URL"),
|
|
915
|
+
license: {
|
|
916
|
+
spdx: requiredString(license.spdx, "Preview license", 80),
|
|
917
|
+
sourceUrl: httpsUrl(license.sourceUrl, "Preview license URL"),
|
|
918
|
+
note: requiredString(license.note, "Preview license note", 400),
|
|
919
|
+
},
|
|
920
|
+
sourceKind,
|
|
921
|
+
iconUrl: catalogSlug ? `/api/plugins/catalog/${encodeURIComponent(catalogSlug)}/icon` : "",
|
|
922
|
+
previewId: stored.report.id,
|
|
923
|
+
packageId: stored.pkg.id,
|
|
924
|
+
action,
|
|
925
|
+
installedPluginId: action === "installed" || action === "update"
|
|
926
|
+
? installation?.installation.id ?? null
|
|
927
|
+
: null,
|
|
928
|
+
resolution,
|
|
929
|
+
components,
|
|
930
|
+
groups: groupComponents(components),
|
|
931
|
+
warnings,
|
|
932
|
+
capabilityDiff,
|
|
933
|
+
skillConflicts,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
async function icon(slug, dark) {
|
|
937
|
+
const loaded = await loadCatalog();
|
|
938
|
+
const entry = loaded.cache.document.plugins.find((candidate) => candidate.slug === slug);
|
|
939
|
+
if (!entry)
|
|
940
|
+
throw notFound("Plugin catalog icon not found");
|
|
941
|
+
const response = await fetchBounded(fetcher, catalogAssetUrl(dark ? entry.iconDarkPath : entry.iconPath), {
|
|
942
|
+
headers: { accept: "image/png", "user-agent": "Rudder-Plugin-Hub" },
|
|
943
|
+
}, catalogHosts);
|
|
944
|
+
if (!response.ok)
|
|
945
|
+
throw notFound("Plugin catalog icon not found");
|
|
946
|
+
const content = Buffer.from(await response.arrayBuffer());
|
|
947
|
+
if (content.byteLength === 0 || content.byteLength > 512 * 1024 || !content.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
948
|
+
throw unprocessable("Plugin catalog icon is not a bounded PNG");
|
|
949
|
+
}
|
|
950
|
+
return { content, etag: response.headers.get("etag"), freshness: loaded.freshness };
|
|
951
|
+
}
|
|
952
|
+
return { catalog, icon, previewCatalog, previewSource, previewDetail };
|
|
953
|
+
}
|
|
954
|
+
//# sourceMappingURL=rudder-plugin-catalog.js.map
|