@powerhousedao/registry 6.0.0-dev.105 → 6.0.0-dev.107
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/cdn-cache/@powerhousedao/vetra/6.0.0-dev.59/LICENSE → LICENSE} +1 -1
- package/README.md +100 -28
- package/dist/cli.d.mts +41 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +646 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +9 -7
- package/dist/bundle.d.ts +0 -2
- package/dist/bundle.d.ts.map +0 -1
- package/dist/cdn-cache/@powerhousedao/vetra/6.0.0-dev.59/package.json +0 -114
- package/dist/src/cdn.d.ts +0 -12
- package/dist/src/cdn.d.ts.map +0 -1
- package/dist/src/cli.d.ts +0 -32
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js +0 -161040
- package/dist/src/constants.d.ts +0 -4
- package/dist/src/constants.d.ts.map +0 -1
- package/dist/src/index.d.ts +0 -6
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -74
- package/dist/src/middleware.d.ts +0 -6
- package/dist/src/middleware.d.ts.map +0 -1
- package/dist/src/packages.d.ts +0 -5
- package/dist/src/packages.d.ts.map +0 -1
- package/dist/src/run.d.ts +0 -3
- package/dist/src/run.d.ts.map +0 -1
- package/dist/src/types.d.ts +0 -69
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/verdaccio-config.d.ts +0 -3
- package/dist/src/verdaccio-config.d.ts.map +0 -1
- package/dist/storage/@powerhousedao/vetra/package.json +0 -154
- package/dist/tests/e2e.test.d.ts +0 -2
- package/dist/tests/e2e.test.d.ts.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { binary, command, flag, number, option, optional, run, string } from "cmd-ts";
|
|
2
|
+
import express, { Router } from "express";
|
|
3
|
+
import { findUp } from "find-up";
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { runServer } from "verdaccio";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import { Readable } from "node:stream";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
10
|
+
import { extract } from "tar";
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/cdn.ts
|
|
13
|
+
var CdnCache = class {
|
|
14
|
+
#extractionLocks = /* @__PURE__ */ new Map();
|
|
15
|
+
constructor(registryUrl, cdnCachePath) {
|
|
16
|
+
this.registryUrl = registryUrl;
|
|
17
|
+
this.cdnCachePath = cdnCachePath;
|
|
18
|
+
}
|
|
19
|
+
async getFile(packageName, filePath) {
|
|
20
|
+
const version = this.getLatestCachedVersion(packageName) ?? await this.getLatestVersion(packageName);
|
|
21
|
+
if (!version) return null;
|
|
22
|
+
const versionDir = path.join(this.cdnCachePath, packageName, version);
|
|
23
|
+
const resolved = this.#resolveFile(versionDir, filePath);
|
|
24
|
+
if (resolved) return resolved;
|
|
25
|
+
await this.#extractWithLock(packageName, version);
|
|
26
|
+
return this.#resolveFile(versionDir, filePath);
|
|
27
|
+
}
|
|
28
|
+
#resolveFile(versionDir, filePath) {
|
|
29
|
+
const candidates = [
|
|
30
|
+
path.join(versionDir, filePath),
|
|
31
|
+
path.join(versionDir, "cdn", filePath),
|
|
32
|
+
path.join(versionDir, "dist", "cdn", filePath),
|
|
33
|
+
path.join(versionDir, "dist", filePath)
|
|
34
|
+
];
|
|
35
|
+
for (const candidate of candidates) if (this.isSafePath(candidate) && fs.existsSync(candidate)) return candidate;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
async #extractWithLock(packageName, version) {
|
|
39
|
+
const key = `${packageName}@${version}`;
|
|
40
|
+
const existing = this.#extractionLocks.get(key);
|
|
41
|
+
if (existing) return existing;
|
|
42
|
+
const promise = this.extractTarball(packageName, version).finally(() => {
|
|
43
|
+
this.#extractionLocks.delete(key);
|
|
44
|
+
});
|
|
45
|
+
this.#extractionLocks.set(key, promise);
|
|
46
|
+
return promise;
|
|
47
|
+
}
|
|
48
|
+
getLatestCachedVersion(packageName) {
|
|
49
|
+
const pkgDir = path.join(this.cdnCachePath, packageName);
|
|
50
|
+
try {
|
|
51
|
+
const versions = fs.readdirSync(pkgDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
52
|
+
if (versions.length === 0) return null;
|
|
53
|
+
versions.sort();
|
|
54
|
+
return versions[versions.length - 1];
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async getLatestVersion(packageName) {
|
|
60
|
+
try {
|
|
61
|
+
const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;
|
|
62
|
+
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
63
|
+
if (!res.ok) return null;
|
|
64
|
+
const distTags = (await res.json())["dist-tags"];
|
|
65
|
+
if (!distTags) return null;
|
|
66
|
+
return distTags.latest ?? Object.values(distTags)[0] ?? null;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async extractTarball(packageName, version) {
|
|
72
|
+
const shortName = packageName.startsWith("@") ? packageName.split("/")[1] : packageName;
|
|
73
|
+
const tarballUrl = `${this.registryUrl}/${encodeURIComponent(packageName)}/-/${shortName}-${version}.tgz`;
|
|
74
|
+
let res;
|
|
75
|
+
try {
|
|
76
|
+
res = await fetch(tarballUrl);
|
|
77
|
+
if (!res.ok || !res.body) return;
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const destDir = path.join(this.cdnCachePath, packageName, version);
|
|
82
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
83
|
+
const tmpFile = path.join(destDir, ".tmp-tarball.tgz");
|
|
84
|
+
try {
|
|
85
|
+
const fileStream = fs.createWriteStream(tmpFile);
|
|
86
|
+
await pipeline(Readable.fromWeb(res.body), fileStream);
|
|
87
|
+
await extract({
|
|
88
|
+
file: tmpFile,
|
|
89
|
+
cwd: destDir,
|
|
90
|
+
strip: 1
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
fs.rmSync(tmpFile, { force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
invalidate(packageName) {
|
|
97
|
+
const cacheDir = path.join(this.cdnCachePath, packageName);
|
|
98
|
+
if (!this.isSafePath(cacheDir)) return;
|
|
99
|
+
fs.rmSync(cacheDir, {
|
|
100
|
+
recursive: true,
|
|
101
|
+
force: true
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
isSafePath(filePath) {
|
|
105
|
+
const resolved = path.resolve(filePath);
|
|
106
|
+
const cacheRoot = path.resolve(this.cdnCachePath);
|
|
107
|
+
return resolved.startsWith(cacheRoot + path.sep) || resolved === cacheRoot;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/packages.ts
|
|
112
|
+
function readManifest(dir) {
|
|
113
|
+
const candidates = [
|
|
114
|
+
path.join(dir, "powerhouse.manifest.json"),
|
|
115
|
+
path.join(dir, "cdn", "powerhouse.manifest.json"),
|
|
116
|
+
path.join(dir, "dist", "powerhouse.manifest.json")
|
|
117
|
+
];
|
|
118
|
+
for (const manifestPath of candidates) try {
|
|
119
|
+
const raw = fs.readFileSync(manifestPath, "utf-8");
|
|
120
|
+
return JSON.parse(raw);
|
|
121
|
+
} catch {}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function getLatestVersionDir(pkgDir) {
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = fs.readdirSync(pkgDir, { withFileTypes: true });
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const versions = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
132
|
+
if (versions.length === 0) return null;
|
|
133
|
+
versions.sort();
|
|
134
|
+
return path.join(pkgDir, versions[versions.length - 1]);
|
|
135
|
+
}
|
|
136
|
+
function loadPackage(cdnCachePath, name) {
|
|
137
|
+
const pkgDir = path.join(cdnCachePath, name);
|
|
138
|
+
const manifest = readManifest(getLatestVersionDir(pkgDir) ?? pkgDir);
|
|
139
|
+
if (!manifest) console.error(`Failed to find manifest for "${name}" in "${cdnCachePath}".`);
|
|
140
|
+
return {
|
|
141
|
+
name: manifest?.name ?? name,
|
|
142
|
+
path: `/-/cdn/${name}`,
|
|
143
|
+
manifest
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function scanPackages(cdnCachePath) {
|
|
147
|
+
const absDir = path.resolve(cdnCachePath);
|
|
148
|
+
const packages = [];
|
|
149
|
+
let entries;
|
|
150
|
+
try {
|
|
151
|
+
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
152
|
+
} catch {
|
|
153
|
+
return packages;
|
|
154
|
+
}
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
if (!entry.isDirectory()) continue;
|
|
157
|
+
if (entry.name.startsWith("@")) {
|
|
158
|
+
const scopeDir = path.join(absDir, entry.name);
|
|
159
|
+
let scopedEntries;
|
|
160
|
+
try {
|
|
161
|
+
scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.log(error);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
for (const scopedEntry of scopedEntries) {
|
|
167
|
+
if (!scopedEntry.isDirectory()) continue;
|
|
168
|
+
const dirName = `${entry.name}/${scopedEntry.name}`;
|
|
169
|
+
const pkgDir = path.join(scopeDir, scopedEntry.name);
|
|
170
|
+
const manifest = readManifest(getLatestVersionDir(pkgDir) ?? pkgDir);
|
|
171
|
+
const name = manifest?.name ?? dirName;
|
|
172
|
+
packages.push({
|
|
173
|
+
name,
|
|
174
|
+
path: `/-/cdn/${dirName}`,
|
|
175
|
+
manifest
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
const pkgDir = path.join(absDir, entry.name);
|
|
180
|
+
const manifest = readManifest(getLatestVersionDir(pkgDir) ?? pkgDir);
|
|
181
|
+
const name = manifest?.name ?? entry.name;
|
|
182
|
+
packages.push({
|
|
183
|
+
name,
|
|
184
|
+
path: `/-/cdn/${entry.name}`,
|
|
185
|
+
manifest
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return packages;
|
|
190
|
+
}
|
|
191
|
+
function findPackagesByDocumentType(packagesDir, documentType) {
|
|
192
|
+
return scanPackages(packagesDir).filter((pkg) => {
|
|
193
|
+
if (!pkg.manifest?.documentModels) return false;
|
|
194
|
+
return pkg.manifest.documentModels.some((dm) => dm.id === documentType);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/middleware.ts
|
|
199
|
+
const MIME_TYPES = {
|
|
200
|
+
".js": "application/javascript",
|
|
201
|
+
".mjs": "application/javascript",
|
|
202
|
+
".css": "text/css",
|
|
203
|
+
".json": "application/json",
|
|
204
|
+
".wasm": "application/wasm",
|
|
205
|
+
".map": "application/json",
|
|
206
|
+
".html": "text/html",
|
|
207
|
+
".svg": "image/svg+xml"
|
|
208
|
+
};
|
|
209
|
+
function getContentType(filePath) {
|
|
210
|
+
return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
211
|
+
}
|
|
212
|
+
function createPowerhouseRouter(config, sse, webhooks) {
|
|
213
|
+
const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
|
|
214
|
+
const router = Router();
|
|
215
|
+
router.use((_req, res, next) => {
|
|
216
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
217
|
+
next();
|
|
218
|
+
});
|
|
219
|
+
router.get("/-/events", (_req, res) => {
|
|
220
|
+
sse.addClient(res);
|
|
221
|
+
});
|
|
222
|
+
router.get("/-/webhooks", (_req, res) => {
|
|
223
|
+
res.json(webhooks.getWebhooks());
|
|
224
|
+
});
|
|
225
|
+
router.post("/-/webhooks", express.json(), (req, res) => {
|
|
226
|
+
const { endpoint, headers } = req.body;
|
|
227
|
+
if (!endpoint) {
|
|
228
|
+
res.status(400).json({ error: "Missing required field: endpoint" });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
webhooks.addWebhook({
|
|
232
|
+
endpoint,
|
|
233
|
+
headers
|
|
234
|
+
});
|
|
235
|
+
res.status(201).json({
|
|
236
|
+
endpoint,
|
|
237
|
+
headers
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
router.delete("/-/webhooks", express.json(), (req, res) => {
|
|
241
|
+
const { endpoint } = req.body;
|
|
242
|
+
if (!endpoint) {
|
|
243
|
+
res.status(400).json({ error: "Missing required field: endpoint" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!webhooks.removeWebhook(endpoint)) {
|
|
247
|
+
res.status(404).json({ error: "Webhook not found" });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
res.status(204).end();
|
|
251
|
+
});
|
|
252
|
+
router.get("/packages", (req, res) => {
|
|
253
|
+
const packages = scanPackages(config.cdnCachePath);
|
|
254
|
+
const documentType = req.query.documentType;
|
|
255
|
+
if (documentType) {
|
|
256
|
+
const filtered = packages.filter((pkg) => pkg.manifest?.documentModels?.some((m) => m.id === documentType));
|
|
257
|
+
res.json(filtered);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
res.json(packages);
|
|
261
|
+
});
|
|
262
|
+
router.get("/packages/by-document-type", (req, res) => {
|
|
263
|
+
const documentType = req.query.type;
|
|
264
|
+
if (typeof documentType !== "string" || !documentType) {
|
|
265
|
+
res.status(400).json({ error: "Missing required query parameter: type" });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const packageNames = findPackagesByDocumentType(config.cdnCachePath, documentType).map((pkg) => pkg.name);
|
|
269
|
+
res.json(packageNames);
|
|
270
|
+
});
|
|
271
|
+
router.get("/packages/*name", (req, res) => {
|
|
272
|
+
const name = req.params.name.join("/");
|
|
273
|
+
const pkg = loadPackage(config.cdnCachePath, name);
|
|
274
|
+
if (!pkg) {
|
|
275
|
+
res.status(404).send("Package not found");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
res.json(pkg);
|
|
279
|
+
});
|
|
280
|
+
router.get("/-/cdn/*path", async (req, res) => {
|
|
281
|
+
const fullPath = req.params.path.join("/");
|
|
282
|
+
let packageName;
|
|
283
|
+
let filePath;
|
|
284
|
+
if (fullPath.startsWith("@")) {
|
|
285
|
+
const segments = fullPath.split("/");
|
|
286
|
+
if (segments.length < 2) {
|
|
287
|
+
res.status(400).send("Invalid package path");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
packageName = `${segments[0]}/${segments[1]}`;
|
|
291
|
+
filePath = segments.slice(2).join("/") || "index.js";
|
|
292
|
+
} else {
|
|
293
|
+
const segments = fullPath.split("/");
|
|
294
|
+
packageName = segments[0];
|
|
295
|
+
filePath = segments.slice(1).join("/") || "index.js";
|
|
296
|
+
}
|
|
297
|
+
const resolved = await cdn.getFile(packageName, filePath);
|
|
298
|
+
if (!resolved) {
|
|
299
|
+
res.status(404).send("File not found");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
res.setHeader("Content-Type", getContentType(filePath));
|
|
303
|
+
const content = fs.readFileSync(resolved);
|
|
304
|
+
res.send(content);
|
|
305
|
+
});
|
|
306
|
+
return router;
|
|
307
|
+
}
|
|
308
|
+
function createPublishHook(config, notifications) {
|
|
309
|
+
const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
|
|
310
|
+
return (req, res, next) => {
|
|
311
|
+
if (req.method !== "PUT") {
|
|
312
|
+
next();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const originalEnd = res.end.bind(res);
|
|
316
|
+
res.end = function(chunk, encoding, cb) {
|
|
317
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
318
|
+
const urlPath = req.path.replace(/^\//, "");
|
|
319
|
+
if (urlPath && !urlPath.startsWith("-/")) {
|
|
320
|
+
const packageName = decodeURIComponent(urlPath);
|
|
321
|
+
console.log(`[registry] Invalidating CDN cache for ${packageName}`);
|
|
322
|
+
cdn.invalidate(packageName);
|
|
323
|
+
cdn.getLatestVersion(packageName).then((version) => {
|
|
324
|
+
if (version) {
|
|
325
|
+
console.log(`[registry] Extracting ${packageName}@${version} to CDN cache`);
|
|
326
|
+
return cdn.extractTarball(packageName, version).then(() => version);
|
|
327
|
+
}
|
|
328
|
+
return null;
|
|
329
|
+
}).then((version) => {
|
|
330
|
+
notifications.notifyPublish({
|
|
331
|
+
packageName,
|
|
332
|
+
version
|
|
333
|
+
});
|
|
334
|
+
}).catch((err) => {
|
|
335
|
+
console.error(`[registry] Failed to extract ${packageName} to CDN cache:`, err);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return originalEnd(chunk, encoding, cb);
|
|
340
|
+
};
|
|
341
|
+
next();
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/notifications/manager.ts
|
|
346
|
+
var NotificationManager = class {
|
|
347
|
+
#channels;
|
|
348
|
+
constructor(channels) {
|
|
349
|
+
this.#channels = channels;
|
|
350
|
+
}
|
|
351
|
+
notifyPublish(event) {
|
|
352
|
+
for (const channel of this.#channels) channel.notifyPublish(event);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/notifications/sse.ts
|
|
357
|
+
var SSEChannel = class {
|
|
358
|
+
#clients = /* @__PURE__ */ new Set();
|
|
359
|
+
addClient(res) {
|
|
360
|
+
res.writeHead(200, {
|
|
361
|
+
"Content-Type": "text/event-stream",
|
|
362
|
+
"Cache-Control": "no-cache",
|
|
363
|
+
Connection: "keep-alive",
|
|
364
|
+
"Access-Control-Allow-Origin": "*"
|
|
365
|
+
});
|
|
366
|
+
res.write("event: connected\ndata: {}\n\n");
|
|
367
|
+
this.#clients.add(res);
|
|
368
|
+
res.on("close", () => {
|
|
369
|
+
this.#clients.delete(res);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
notifyPublish(event) {
|
|
373
|
+
const payload = `event: publish\ndata: ${JSON.stringify(event)}\n\n`;
|
|
374
|
+
for (const client of this.#clients) try {
|
|
375
|
+
client.write(payload);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error("[registry] SSE client write failed:", err);
|
|
378
|
+
this.#clients.delete(client);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region src/notifications/webhook.ts
|
|
384
|
+
const WEBHOOKS_FILE = "webhooks.json";
|
|
385
|
+
var WebhookChannel = class {
|
|
386
|
+
#predefined;
|
|
387
|
+
#dynamic;
|
|
388
|
+
#storagePath;
|
|
389
|
+
constructor(storagePath, config) {
|
|
390
|
+
this.#storagePath = storagePath;
|
|
391
|
+
this.#predefined = config?.webhooks ?? [];
|
|
392
|
+
this.#dynamic = this.#load();
|
|
393
|
+
}
|
|
394
|
+
getWebhooks() {
|
|
395
|
+
return [...this.#predefined, ...this.#dynamic];
|
|
396
|
+
}
|
|
397
|
+
addWebhook(webhook) {
|
|
398
|
+
if (this.getWebhooks().some((w) => w.endpoint === webhook.endpoint)) return;
|
|
399
|
+
this.#dynamic.push(webhook);
|
|
400
|
+
this.#save();
|
|
401
|
+
}
|
|
402
|
+
removeWebhook(endpoint) {
|
|
403
|
+
const before = this.#dynamic.length;
|
|
404
|
+
this.#dynamic = this.#dynamic.filter((w) => w.endpoint !== endpoint);
|
|
405
|
+
if (this.#dynamic.length === before) return false;
|
|
406
|
+
this.#save();
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
notifyPublish(event) {
|
|
410
|
+
for (const webhook of this.getWebhooks()) {
|
|
411
|
+
const headers = {
|
|
412
|
+
"Content-Type": "application/json",
|
|
413
|
+
...webhook.headers
|
|
414
|
+
};
|
|
415
|
+
fetch(webhook.endpoint, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
headers,
|
|
418
|
+
body: JSON.stringify(event)
|
|
419
|
+
}).catch((err) => {
|
|
420
|
+
console.error(`[registry] Webhook to ${webhook.endpoint} failed:`, err);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
#filePath() {
|
|
425
|
+
return path.join(this.#storagePath, WEBHOOKS_FILE);
|
|
426
|
+
}
|
|
427
|
+
#load() {
|
|
428
|
+
try {
|
|
429
|
+
const raw = fs.readFileSync(this.#filePath(), "utf-8");
|
|
430
|
+
return JSON.parse(raw);
|
|
431
|
+
} catch {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
#save() {
|
|
436
|
+
fs.mkdirSync(this.#storagePath, { recursive: true });
|
|
437
|
+
fs.writeFileSync(this.#filePath(), JSON.stringify(this.#dynamic, null, 2));
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/verdaccio-config.ts
|
|
442
|
+
function buildVerdaccioConfig(config) {
|
|
443
|
+
const htpasswdPath = path.join(config.storagePath, "htpasswd");
|
|
444
|
+
const base = {
|
|
445
|
+
storage: config.storagePath,
|
|
446
|
+
self_path: "./",
|
|
447
|
+
auth: { htpasswd: { file: htpasswdPath } },
|
|
448
|
+
uplinks: void 0,
|
|
449
|
+
packages: {
|
|
450
|
+
"@powerhousedao/*": {
|
|
451
|
+
access: "$all",
|
|
452
|
+
publish: "$authenticated",
|
|
453
|
+
unpublish: "$authenticated"
|
|
454
|
+
},
|
|
455
|
+
"**": {
|
|
456
|
+
access: "$all",
|
|
457
|
+
publish: "$authenticated",
|
|
458
|
+
unpublish: "$authenticated",
|
|
459
|
+
proxy: "npmjs"
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
web: {
|
|
463
|
+
enable: config.webEnabled !== false,
|
|
464
|
+
title: "Powerhouse Registry",
|
|
465
|
+
logo: "/app/static/logo.svg",
|
|
466
|
+
favicon: "/app/static/favicon.ico",
|
|
467
|
+
primary_color: "#38C780",
|
|
468
|
+
darkMode: true
|
|
469
|
+
},
|
|
470
|
+
server: { keepAliveTimeout: 60 },
|
|
471
|
+
log: {
|
|
472
|
+
type: "stdout",
|
|
473
|
+
format: "pretty",
|
|
474
|
+
level: "warn"
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
if (config.s3) base.store = { "aws-s3-storage": {
|
|
478
|
+
bucket: config.s3.bucket,
|
|
479
|
+
endpoint: config.s3.endpoint,
|
|
480
|
+
region: config.s3.region,
|
|
481
|
+
s3ForcePathStyle: config.s3.s3ForcePathStyle ?? true,
|
|
482
|
+
...config.s3.keyPrefix && { keyPrefix: config.s3.keyPrefix },
|
|
483
|
+
...config.s3.accessKeyId && { accessKeyId: config.s3.accessKeyId },
|
|
484
|
+
...config.s3.secretAccessKey && { secretAccessKey: config.s3.secretAccessKey }
|
|
485
|
+
} };
|
|
486
|
+
return base;
|
|
487
|
+
}
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/run.ts
|
|
490
|
+
async function resolveDir(dir) {
|
|
491
|
+
if (path.isAbsolute(dir)) {
|
|
492
|
+
await mkdir(dir, { recursive: true });
|
|
493
|
+
return dir;
|
|
494
|
+
}
|
|
495
|
+
const found = await findUp(dir, { type: "directory" });
|
|
496
|
+
if (!found) {
|
|
497
|
+
await mkdir(dir, { recursive: true });
|
|
498
|
+
return dir;
|
|
499
|
+
}
|
|
500
|
+
return found;
|
|
501
|
+
}
|
|
502
|
+
async function runRegistry(args) {
|
|
503
|
+
const { port, storageDir, cdnCacheDir, uplink, webEnabled, webhooks, s3AccessKeyId, s3Bucket, s3Endpoint, s3ForcePathStyle, s3KeyPrefix, s3Region, s3SecretAccessKey } = args;
|
|
504
|
+
const storagePath = await resolveDir(storageDir);
|
|
505
|
+
const cdnCachePath = await resolveDir(cdnCacheDir);
|
|
506
|
+
console.log({
|
|
507
|
+
storagePath,
|
|
508
|
+
cdnCachePath
|
|
509
|
+
});
|
|
510
|
+
const webhookConfigs = webhooks?.split(",").map((url) => url.trim()).filter(Boolean).map((endpoint) => ({ endpoint }));
|
|
511
|
+
const config = {
|
|
512
|
+
port,
|
|
513
|
+
storagePath,
|
|
514
|
+
cdnCachePath,
|
|
515
|
+
uplink,
|
|
516
|
+
webEnabled,
|
|
517
|
+
...webhookConfigs?.length && { notify: { webhooks: webhookConfigs } },
|
|
518
|
+
...s3Bucket && s3Endpoint && s3Region && { s3: {
|
|
519
|
+
bucket: s3Bucket,
|
|
520
|
+
endpoint: s3Endpoint,
|
|
521
|
+
region: s3Region,
|
|
522
|
+
accessKeyId: s3AccessKeyId,
|
|
523
|
+
secretAccessKey: s3SecretAccessKey,
|
|
524
|
+
keyPrefix: s3KeyPrefix,
|
|
525
|
+
s3ForcePathStyle
|
|
526
|
+
} }
|
|
527
|
+
};
|
|
528
|
+
await mkdir(storagePath, { recursive: true });
|
|
529
|
+
await mkdir(cdnCachePath, { recursive: true });
|
|
530
|
+
const verdaccioHandler = (await runServer(buildVerdaccioConfig(config))).listeners("request")[0];
|
|
531
|
+
const app = express();
|
|
532
|
+
const sseChannel = new SSEChannel();
|
|
533
|
+
const webhookChannel = new WebhookChannel(config.storagePath, config.notify);
|
|
534
|
+
const notifications = new NotificationManager([sseChannel, webhookChannel]);
|
|
535
|
+
app.use(createPowerhouseRouter(config, sseChannel, webhookChannel));
|
|
536
|
+
app.use(createPublishHook(config, notifications));
|
|
537
|
+
app.use((req, res) => verdaccioHandler(req, res));
|
|
538
|
+
return app.listen(port, () => {
|
|
539
|
+
console.log(`Powerhouse Registry running on http://localhost:${port}`);
|
|
540
|
+
console.log(` CDN: http://localhost:${port}/-/cdn/`);
|
|
541
|
+
console.log(` Packages: http://localhost:${port}/packages`);
|
|
542
|
+
console.log(` npm: http://localhost:${port}/`);
|
|
543
|
+
console.log(` Storage: ${storagePath}`);
|
|
544
|
+
console.log(` CDN cache: ${cdnCachePath}`);
|
|
545
|
+
if (config.s3) console.log(` S3: ${config.s3.endpoint}/${config.s3.bucket}`);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region cli.ts
|
|
550
|
+
const registryCommand = command({
|
|
551
|
+
name: "Package registry",
|
|
552
|
+
args: {
|
|
553
|
+
port: option({
|
|
554
|
+
long: "port",
|
|
555
|
+
type: number,
|
|
556
|
+
defaultValue: () => Number(process.env.PORT) || 8080,
|
|
557
|
+
defaultValueIsSerializable: true
|
|
558
|
+
}),
|
|
559
|
+
storageDir: option({
|
|
560
|
+
long: "storage-dir",
|
|
561
|
+
type: string,
|
|
562
|
+
defaultValue: () => process.env.REGISTRY_STORAGE || "./storage",
|
|
563
|
+
defaultValueIsSerializable: true
|
|
564
|
+
}),
|
|
565
|
+
cdnCacheDir: option({
|
|
566
|
+
long: "cdn-cache-dir",
|
|
567
|
+
type: string,
|
|
568
|
+
defaultValue: () => process.env.REGISTRY_CDN_CACHE || "./cdn-cache",
|
|
569
|
+
defaultValueIsSerializable: true
|
|
570
|
+
}),
|
|
571
|
+
uplink: option({
|
|
572
|
+
long: "uplink",
|
|
573
|
+
type: optional(string),
|
|
574
|
+
defaultValue: () => process.env.REGISTRY_UPLINK,
|
|
575
|
+
defaultValueIsSerializable: true
|
|
576
|
+
}),
|
|
577
|
+
s3Bucket: option({
|
|
578
|
+
long: "s3-bucket",
|
|
579
|
+
type: optional(string),
|
|
580
|
+
defaultValue: () => process.env.S3_BUCKET,
|
|
581
|
+
defaultValueIsSerializable: true
|
|
582
|
+
}),
|
|
583
|
+
s3Endpoint: option({
|
|
584
|
+
long: "s3-endpoint",
|
|
585
|
+
type: optional(string),
|
|
586
|
+
defaultValue: () => process.env.S3_ENDPOINT,
|
|
587
|
+
defaultValueIsSerializable: true
|
|
588
|
+
}),
|
|
589
|
+
s3Region: option({
|
|
590
|
+
long: "s3-region",
|
|
591
|
+
type: optional(string),
|
|
592
|
+
defaultValue: () => process.env.S3_REGION,
|
|
593
|
+
defaultValueIsSerializable: true
|
|
594
|
+
}),
|
|
595
|
+
s3AccessKeyId: option({
|
|
596
|
+
long: "s3-access-key-id",
|
|
597
|
+
type: optional(string),
|
|
598
|
+
defaultValue: () => process.env.S3_ACCESS_KEY_ID,
|
|
599
|
+
defaultValueIsSerializable: true
|
|
600
|
+
}),
|
|
601
|
+
s3SecretAccessKey: option({
|
|
602
|
+
long: "s3-secret-access-key",
|
|
603
|
+
type: optional(string),
|
|
604
|
+
defaultValue: () => process.env.S3_SECRET_ACCESS_KEY,
|
|
605
|
+
defaultValueIsSerializable: true
|
|
606
|
+
}),
|
|
607
|
+
s3KeyPrefix: option({
|
|
608
|
+
long: "s3-key-prefix",
|
|
609
|
+
type: optional(string),
|
|
610
|
+
defaultValue: () => process.env.S3_KEY_PREFIX,
|
|
611
|
+
defaultValueIsSerializable: true
|
|
612
|
+
}),
|
|
613
|
+
s3ForcePathStyle: flag({
|
|
614
|
+
long: "s3-force-path-style",
|
|
615
|
+
defaultValue: () => process.env.S3_FORCE_PATH_STYLE !== "false",
|
|
616
|
+
defaultValueIsSerializable: true
|
|
617
|
+
}),
|
|
618
|
+
webEnabled: flag({
|
|
619
|
+
long: "web-enabled",
|
|
620
|
+
defaultValue: () => process.env.REGISTRY_WEB !== "false",
|
|
621
|
+
defaultValueIsSerializable: true
|
|
622
|
+
}),
|
|
623
|
+
webhooks: option({
|
|
624
|
+
long: "webhook",
|
|
625
|
+
type: optional(string),
|
|
626
|
+
description: "Comma-separated webhook URLs to notify on publish",
|
|
627
|
+
defaultValue: () => process.env.REGISTRY_WEBHOOKS,
|
|
628
|
+
defaultValueIsSerializable: true
|
|
629
|
+
})
|
|
630
|
+
},
|
|
631
|
+
handler: async (args) => {
|
|
632
|
+
console.log(args);
|
|
633
|
+
try {
|
|
634
|
+
await runRegistry(args);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error("Failed to start registry:");
|
|
637
|
+
console.error(error);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
await run(binary(registryCommand), process.argv);
|
|
643
|
+
//#endregion
|
|
644
|
+
export { registryCommand };
|
|
645
|
+
|
|
646
|
+
//# sourceMappingURL=cli.mjs.map
|