@stolyarovmn/dsh-ui-registry-aggregator 0.4.13
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/LICENSE +21 -0
- package/README.md +225 -0
- package/cordis.patch.yml +6 -0
- package/icon.svg +16 -0
- package/lib/client.js +1254 -0
- package/lib/core.js +1241 -0
- package/lib/index.js +214 -0
- package/package.json +92 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { browseSources, FILTER_TAGS, githubPluginDetails, githubRepositoryStats, healthSources, npmPluginDetails, SOURCE_TYPES } from "./core.js";
|
|
3
|
+
|
|
4
|
+
export const NAMESPACE = "registry-aggregator";
|
|
5
|
+
export const name = "registry-aggregator";
|
|
6
|
+
export const RPC_CHANNEL = "/api";
|
|
7
|
+
export const RPC_PREFIX = "plugin-sources";
|
|
8
|
+
|
|
9
|
+
const SourceTypeSchema = z.union(SOURCE_TYPES.map((type) => z.const(type)));
|
|
10
|
+
const SourceAuthSchema = z.object({
|
|
11
|
+
sourceId: z.string().required().comment("Source id allowed to use this credential."),
|
|
12
|
+
tokenEnv: z.string().required().comment("Host environment variable containing its bearer token."),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const PluginSourceSchema = z.object({
|
|
16
|
+
id: z.string().required().comment("Stable source id."),
|
|
17
|
+
name: z.string().default("").comment("Display name."),
|
|
18
|
+
type: SourceTypeSchema.required().comment("Adapter type."),
|
|
19
|
+
url: z.string().comment("Optional catalog or API URL."),
|
|
20
|
+
enabled: z.boolean().default(true),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_SOURCES = [
|
|
24
|
+
{ id: "npm", name: "npm", type: "npm", enabled: true },
|
|
25
|
+
{ id: "github", name: "GitHub", type: "github", enabled: true },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const PluginSourcesSchema = z.object({
|
|
29
|
+
sources: z.array(PluginSourceSchema).default(DEFAULT_SOURCES).volatile(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const Config = z.object({
|
|
33
|
+
sources: z.array(PluginSourceSchema).default(DEFAULT_SOURCES).volatile(),
|
|
34
|
+
sourceDefaultsVersion: z.natural().default(0).volatile(),
|
|
35
|
+
timeoutMs: z.natural().min(250).max(60_000).default(10_000),
|
|
36
|
+
maxResponseBytes: z.natural().min(1_024).max(10 * 1024 * 1024).default(2 * 1024 * 1024),
|
|
37
|
+
maxPlugins: z.natural().min(1).max(5_000).default(2_000),
|
|
38
|
+
maxSources: z.natural().min(1).max(100).default(20),
|
|
39
|
+
maxTotalPlugins: z.natural().min(1).max(10_000).default(5_000),
|
|
40
|
+
maxRpcBytes: z.natural().min(16_384).max(20 * 1024 * 1024).default(4 * 1024 * 1024),
|
|
41
|
+
concurrency: z.natural().min(1).max(16).default(4),
|
|
42
|
+
privateSourceIds: z.array(z.string()).default([]).comment("Host-controlled source ids allowed to reach private networks."),
|
|
43
|
+
auth: z.array(SourceAuthSchema).default([]).comment("Host-controlled source-to-token-environment allowlist."),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function failure(code, error, details = {}) {
|
|
47
|
+
return { ok: false, error: { code, message: String(error?.message ?? error), details } };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sourcesFromConfig(config) {
|
|
51
|
+
const value = config?.sources;
|
|
52
|
+
const sources = typeof value?.get === "function" ? value.get() : value;
|
|
53
|
+
return Array.isArray(sources) ? sources : [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function browseRequestFrom(payload) {
|
|
57
|
+
if (payload === undefined || payload === null) return { query: "", page: 1, pageSize: 20, sorts: [], stableOnly: false, freshnessDays: 0, tag: "", dshMetadata: "any" };
|
|
58
|
+
if (typeof payload !== "object" || Array.isArray(payload)) throw new TypeError("payload must be an object");
|
|
59
|
+
const query = payload.query === undefined ? "" : payload.query;
|
|
60
|
+
if (typeof query !== "string") throw new TypeError("query must be a string");
|
|
61
|
+
if (query.length > 200) throw new RangeError("query is too long");
|
|
62
|
+
const page = payload.page === undefined ? 1 : payload.page;
|
|
63
|
+
if (!Number.isInteger(page) || page < 1 || page > 10_000) throw new RangeError("page must be a positive integer");
|
|
64
|
+
const pageSize = payload.pageSize === undefined ? 20 : payload.pageSize;
|
|
65
|
+
if (![20, 50, 100].includes(pageSize)) throw new RangeError("pageSize must be 20, 50, or 100");
|
|
66
|
+
const healthOnly = payload.healthOnly === undefined ? false : payload.healthOnly;
|
|
67
|
+
if (typeof healthOnly !== "boolean") throw new TypeError("healthOnly must be a boolean");
|
|
68
|
+
const refreshRevision = payload.refreshRevision === undefined ? 0 : payload.refreshRevision;
|
|
69
|
+
if (!Number.isInteger(refreshRevision) || refreshRevision < 0 || refreshRevision > 1_000_000_000) throw new RangeError("refreshRevision must be a non-negative integer");
|
|
70
|
+
|
|
71
|
+
const sorts = payload.sorts === undefined ? [] : payload.sorts;
|
|
72
|
+
if (!Array.isArray(sorts) || sorts.length > 4) throw new RangeError("sorts must contain at most four criteria");
|
|
73
|
+
const seenSorts = new Set();
|
|
74
|
+
for (const criterion of sorts) {
|
|
75
|
+
if (!criterion || typeof criterion !== "object" || Array.isArray(criterion)) throw new TypeError("each sort criterion must be an object");
|
|
76
|
+
if (!["stars", "downloads", "freshness", "name"].includes(criterion.key)) throw new RangeError("invalid sort key");
|
|
77
|
+
if (!["asc", "desc"].includes(criterion.direction)) throw new RangeError("invalid sort direction");
|
|
78
|
+
if (seenSorts.has(criterion.key)) throw new RangeError("sort keys must be unique");
|
|
79
|
+
seenSorts.add(criterion.key);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The old single-sort field is accepted for 0.3.x clients during upgrade.
|
|
83
|
+
const sort = payload.sort;
|
|
84
|
+
if (sort !== undefined && !["relevance", "stars", "downloads", "freshness", "stars-downloads", "downloads-freshness", "name"].includes(sort)) throw new RangeError("invalid sort");
|
|
85
|
+
const stableOnly = payload.stableOnly === undefined ? false : payload.stableOnly;
|
|
86
|
+
if (typeof stableOnly !== "boolean") throw new TypeError("stableOnly must be a boolean");
|
|
87
|
+
const freshnessDays = payload.freshnessDays === undefined ? 0 : payload.freshnessDays;
|
|
88
|
+
if (![0, 30, 90, 365].includes(freshnessDays)) throw new RangeError("freshnessDays must be 0, 30, 90, or 365");
|
|
89
|
+
const tag = payload.tag === undefined ? "" : payload.tag;
|
|
90
|
+
if (typeof tag !== "string" || (tag && !FILTER_TAGS.includes(tag))) throw new RangeError("invalid tag");
|
|
91
|
+
const dshMetadata = payload.dshMetadata === undefined ? "any" : payload.dshMetadata;
|
|
92
|
+
if (!["any", "declared", "unknown"].includes(dshMetadata)) throw new RangeError("invalid dshMetadata");
|
|
93
|
+
return { query: query.trim(), page, pageSize, healthOnly, refreshRevision, sorts, ...(sort === undefined ? {} : { sort }), stableOnly, freshnessDays, tag, dshMetadata };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function detailsRequestFrom(payload) {
|
|
97
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new TypeError("payload must be an object");
|
|
98
|
+
if (typeof payload.package === "string" && payload.package.trim()) {
|
|
99
|
+
if (typeof payload.version !== "string" || !payload.version.trim()) throw new TypeError("version must be a string");
|
|
100
|
+
if (payload.package.length > 214 || payload.version.length > 100) throw new RangeError("package details request is too long");
|
|
101
|
+
return { kind: "npm", packageName: payload.package.trim(), version: payload.version.trim() };
|
|
102
|
+
}
|
|
103
|
+
if (typeof payload.repository === "string" && payload.repository.trim()) {
|
|
104
|
+
if (payload.repository.length > 2_048) throw new RangeError("repository details request is too long");
|
|
105
|
+
return { kind: "github", repository: payload.repository.trim() };
|
|
106
|
+
}
|
|
107
|
+
throw new TypeError("package or repository must be provided");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function starsRequestFrom(payload) {
|
|
111
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new TypeError("payload must be an object");
|
|
112
|
+
if (!Array.isArray(payload.repositories)) throw new TypeError("repositories must be an array");
|
|
113
|
+
if (payload.repositories.length > 20) throw new RangeError("at most 20 repositories can be requested");
|
|
114
|
+
const repositories = payload.repositories.map((value) => {
|
|
115
|
+
if (typeof value !== "string" || !value.trim()) throw new TypeError("repository must be a non-empty string");
|
|
116
|
+
if (value.length > 2_048) throw new RangeError("repository value is too long");
|
|
117
|
+
return value.trim();
|
|
118
|
+
});
|
|
119
|
+
return { repositories };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function limitRpcValue(value, maxBytes) {
|
|
123
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
124
|
+
if (bytes > maxBytes) throw new Error(`catalog RPC response exceeds ${maxBytes} bytes`);
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function apply(ctx, config = {}) {
|
|
129
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
130
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber), "registry-aggregator: hide generic settings page");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
ctx.inject(["connection"], (rpcCtx) => {
|
|
134
|
+
const auth = new Map((config.auth ?? []).map((row) => [row.sourceId, row.tokenEnv]));
|
|
135
|
+
const privateSourceIds = new Set(config.privateSourceIds ?? []);
|
|
136
|
+
const maxRpcBytes = config.maxRpcBytes ?? 4 * 1024 * 1024;
|
|
137
|
+
const options = {
|
|
138
|
+
timeoutMs: config.timeoutMs ?? 10_000,
|
|
139
|
+
maxResponseBytes: config.maxResponseBytes ?? 2 * 1024 * 1024,
|
|
140
|
+
maxPlugins: config.maxPlugins ?? 2_000,
|
|
141
|
+
maxSources: config.maxSources ?? 20,
|
|
142
|
+
maxTotalPlugins: config.maxTotalPlugins ?? 5_000,
|
|
143
|
+
concurrency: config.concurrency ?? 4,
|
|
144
|
+
resolveAllowPrivateNetwork: (source) => privateSourceIds.has(source.id),
|
|
145
|
+
resolveAuthToken(source) {
|
|
146
|
+
const tokenEnv = auth.get(source.id);
|
|
147
|
+
if (!tokenEnv) return undefined;
|
|
148
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tokenEnv)) throw new Error(`invalid token environment variable configured for source ${source.id}`);
|
|
149
|
+
const token = process.env[tokenEnv];
|
|
150
|
+
if (!token) throw new Error(`configured credential for source ${source.id} is unavailable`);
|
|
151
|
+
return token;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
async function dispatch(endpoint, payload, signal) {
|
|
156
|
+
try {
|
|
157
|
+
const sources = sourcesFromConfig(config);
|
|
158
|
+
const requestOptions = { ...options, signal };
|
|
159
|
+
if (endpoint === "browse") {
|
|
160
|
+
const request = browseRequestFrom(payload);
|
|
161
|
+
if (request.healthOnly) {
|
|
162
|
+
const value = { sources: await healthSources(sources, requestOptions), generatedAt: new Date().toISOString() };
|
|
163
|
+
return { ok: true, value: limitRpcValue(value, maxRpcBytes) };
|
|
164
|
+
}
|
|
165
|
+
const value = await browseSources(sources, request.query, { ...requestOptions, ...request });
|
|
166
|
+
return { ok: true, value: limitRpcValue(value, maxRpcBytes) };
|
|
167
|
+
}
|
|
168
|
+
if (endpoint === "health") {
|
|
169
|
+
const value = { sources: await healthSources(sources, requestOptions), generatedAt: new Date().toISOString() };
|
|
170
|
+
return { ok: true, value: limitRpcValue(value, maxRpcBytes) };
|
|
171
|
+
}
|
|
172
|
+
if (endpoint === "details") {
|
|
173
|
+
const request = detailsRequestFrom(payload);
|
|
174
|
+
const value = request.kind === "npm"
|
|
175
|
+
? await npmPluginDetails(request.packageName, request.version, requestOptions)
|
|
176
|
+
: await githubPluginDetails(request.repository, requestOptions);
|
|
177
|
+
return { ok: true, value: limitRpcValue(value, maxRpcBytes) };
|
|
178
|
+
}
|
|
179
|
+
if (endpoint === "stars") {
|
|
180
|
+
const request = starsRequestFrom(payload);
|
|
181
|
+
const value = await githubRepositoryStats(request.repositories, requestOptions);
|
|
182
|
+
return { ok: true, value: limitRpcValue(value, maxRpcBytes) };
|
|
183
|
+
}
|
|
184
|
+
return failure("plugin-sources/not-found", `unknown endpoint ${endpoint}`, { endpoint });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return failure("plugin-sources/invalid-request", error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function route(endpoint) {
|
|
191
|
+
return {
|
|
192
|
+
path: `${RPC_CHANNEL}/${RPC_PREFIX}/${endpoint}`,
|
|
193
|
+
methods: ["POST"],
|
|
194
|
+
requestBody: "buffered",
|
|
195
|
+
async fetch(request) {
|
|
196
|
+
let envelope;
|
|
197
|
+
try { envelope = await request.json(); }
|
|
198
|
+
catch { return new Response("body is not JSON", { status: 400 }); }
|
|
199
|
+
if (!envelope || typeof envelope !== "object" || envelope.type !== "client-request"
|
|
200
|
+
|| typeof envelope.rpcId !== "string" || envelope.method !== `${RPC_PREFIX}/${endpoint}`) {
|
|
201
|
+
return new Response("invalid client-request message", { status: 400 });
|
|
202
|
+
}
|
|
203
|
+
const result = await dispatch(endpoint, envelope.payload, request.signal);
|
|
204
|
+
return Response.json({ type: "server-response", rpcId: envelope.rpcId, result });
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
rpcCtx.connection.fetch.register(route("health"));
|
|
209
|
+
rpcCtx.connection.fetch.register(route("browse"));
|
|
210
|
+
rpcCtx.connection.fetch.register(route("details"));
|
|
211
|
+
rpcCtx.connection.fetch.register(route("stars"));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stolyarovmn/dsh-ui-registry-aggregator",
|
|
3
|
+
"description": "Federated plugin registry aggregator for DeepSeek Harness: combine trusted sources, search one index, and install through the native Plugin Manager.",
|
|
4
|
+
"version": "0.4.13",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Stolyarovmn",
|
|
7
|
+
"url": "https://github.com/Stolyarovmn"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "lib/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./lib/index.js",
|
|
13
|
+
"./client": "./lib/client.js",
|
|
14
|
+
"./core": "./lib/core.js",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/*.test.js",
|
|
19
|
+
"test:pack": "npm pack --dry-run --json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"lib/*.js",
|
|
23
|
+
"cordis.patch.yml",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"icon.svg"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"dsh",
|
|
30
|
+
"deepseek-harness",
|
|
31
|
+
"dsh-plugin",
|
|
32
|
+
"plugin",
|
|
33
|
+
"registry",
|
|
34
|
+
"aggregator",
|
|
35
|
+
"registry-aggregator",
|
|
36
|
+
"plugin-registry",
|
|
37
|
+
"plugin-sources",
|
|
38
|
+
"federated-search"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@deepseek-ai/dsh-client-connection": "0.1.7-rc.1",
|
|
42
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
43
|
+
"ipaddr.js": "^2.2.0",
|
|
44
|
+
"undici": "^7.16.0",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.1"
|
|
46
|
+
},
|
|
47
|
+
"dsh": {
|
|
48
|
+
"bundle": {
|
|
49
|
+
"patch": "./cordis.patch.yml"
|
|
50
|
+
},
|
|
51
|
+
"client": {
|
|
52
|
+
"platform": "web",
|
|
53
|
+
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-connection",
|
|
55
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
56
|
+
"@deepseek-ai/dsh-client-locale",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
"catalog": {
|
|
63
|
+
"category": "ui",
|
|
64
|
+
"summary": {
|
|
65
|
+
"en": "Registry Aggregator for DSH 0.1.7+: combine trusted plugin sources, search them as one index, and install through the native Plugin Manager.",
|
|
66
|
+
"zh": "DeepSeek Harness 0.1.7+ 插件注册源聚合器:聚合可信来源、统一搜索,并通过原生 Plugin Manager 安装。"
|
|
67
|
+
},
|
|
68
|
+
"capabilities": [
|
|
69
|
+
"slots",
|
|
70
|
+
"settings",
|
|
71
|
+
"network",
|
|
72
|
+
"plugin-manager"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"license": "MIT",
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"@deepseek-ai/dsh": ">=0.1.7-rc.1 <0.2.0"
|
|
82
|
+
},
|
|
83
|
+
"icon": "./icon.svg",
|
|
84
|
+
"repository": {
|
|
85
|
+
"type": "git",
|
|
86
|
+
"url": "git+https://github.com/Stolyarovmn/dsh-ui-registry-aggregator.git"
|
|
87
|
+
},
|
|
88
|
+
"homepage": "https://github.com/Stolyarovmn/dsh-ui-registry-aggregator#readme",
|
|
89
|
+
"bugs": {
|
|
90
|
+
"url": "https://github.com/Stolyarovmn/dsh-ui-registry-aggregator/issues"
|
|
91
|
+
}
|
|
92
|
+
}
|