@powerhousedao/registry 6.0.0-dev.99 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/{dist/cdn-cache/@powerhousedao/vetra/6.0.0-dev.59/LICENSE → LICENSE} +1 -1
  2. package/README.md +177 -28
  3. package/dist/cli.d.mts +49 -0
  4. package/dist/cli.d.mts.map +1 -0
  5. package/dist/cli.mjs +1059 -0
  6. package/dist/cli.mjs.map +1 -0
  7. package/package.json +18 -13
  8. package/static/favicon.ico +0 -0
  9. package/static/logo.svg +11 -0
  10. package/dist/bundle.d.ts +0 -2
  11. package/dist/bundle.d.ts.map +0 -1
  12. package/dist/cdn-cache/@powerhousedao/vetra/6.0.0-dev.59/package.json +0 -114
  13. package/dist/src/cdn.d.ts +0 -12
  14. package/dist/src/cdn.d.ts.map +0 -1
  15. package/dist/src/cli.d.ts +0 -32
  16. package/dist/src/cli.d.ts.map +0 -1
  17. package/dist/src/cli.js +0 -161040
  18. package/dist/src/constants.d.ts +0 -4
  19. package/dist/src/constants.d.ts.map +0 -1
  20. package/dist/src/index.d.ts +0 -6
  21. package/dist/src/index.d.ts.map +0 -1
  22. package/dist/src/index.js +0 -74
  23. package/dist/src/middleware.d.ts +0 -6
  24. package/dist/src/middleware.d.ts.map +0 -1
  25. package/dist/src/packages.d.ts +0 -5
  26. package/dist/src/packages.d.ts.map +0 -1
  27. package/dist/src/run.d.ts +0 -3
  28. package/dist/src/run.d.ts.map +0 -1
  29. package/dist/src/types.d.ts +0 -69
  30. package/dist/src/types.d.ts.map +0 -1
  31. package/dist/src/verdaccio-config.d.ts +0 -3
  32. package/dist/src/verdaccio-config.d.ts.map +0 -1
  33. package/dist/storage/@powerhousedao/vetra/package.json +0 -154
  34. package/dist/tests/e2e.test.d.ts +0 -2
  35. package/dist/tests/e2e.test.d.ts.map +0 -1
  36. package/dist/tsconfig.tsbuildinfo +0 -1
  37. package/dist/vitest.config.d.ts +0 -3
  38. package/dist/vitest.config.d.ts.map +0 -1
package/dist/cli.mjs ADDED
@@ -0,0 +1,1059 @@
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 crypto, { randomBytes } from "node:crypto";
5
+ import { mkdir } from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { runServer } from "verdaccio";
8
+ import { signPayload } from "@verdaccio/signature";
9
+ import { verifyAuthBearerToken } from "@renown/sdk/node";
10
+ import fs from "node:fs";
11
+ import { Readable } from "node:stream";
12
+ import { pipeline } from "node:stream/promises";
13
+ import { extract } from "tar";
14
+ //#endregion
15
+ //#region src/auth/renown-middleware.ts
16
+ function audienceMatches(aud, expected) {
17
+ if (!aud) return false;
18
+ if (Array.isArray(aud)) return aud.includes(expected);
19
+ return aud === expected;
20
+ }
21
+ /**
22
+ * Translates a Renown-signed Bearer token (verifiable from the issuer's DID,
23
+ * stateless) into a verdaccio-format Bearer token (signed with verdaccio's
24
+ * own secret) that verdaccio's normal auth pipeline accepts.
25
+ *
26
+ * Falls through (calls `next()` without modifying auth) on any verification
27
+ * failure: malformed token, bad signature, expired, audience mismatch, or
28
+ * non-renown bearer token. This keeps the legacy htpasswd path usable during
29
+ * the migration grace period — verdaccio's own apiJWTmiddleware sees the
30
+ * original Authorization header and decides what to do with it.
31
+ */
32
+ function createRenownAuthMiddleware(opts) {
33
+ const expectedAud = opts.publicUrl;
34
+ return async (req, _res, next) => {
35
+ const header = req.headers.authorization;
36
+ if (!header?.startsWith("Bearer ")) return next();
37
+ const token = header.slice(7).trim();
38
+ if (!token) return next();
39
+ let verified;
40
+ try {
41
+ verified = await verifyAuthBearerToken(token, { audience: expectedAud });
42
+ } catch {
43
+ return next();
44
+ }
45
+ if (!verified) return next();
46
+ const payload = verified.payload;
47
+ if (!audienceMatches(payload?.aud, expectedAud)) return next();
48
+ const subject = verified.verifiableCredential.credentialSubject;
49
+ if (!subject?.address) return next();
50
+ const address = subject.address.toLowerCase();
51
+ const groups = ["$authenticated", "renown"];
52
+ let verdaccioJwt;
53
+ try {
54
+ verdaccioJwt = await signPayload({
55
+ name: address,
56
+ real_groups: groups,
57
+ groups
58
+ }, opts.verdaccioSecret, { expiresIn: "5m" });
59
+ } catch (err) {
60
+ console.error("[registry] failed to mint internal verdaccio token:", err);
61
+ return next();
62
+ }
63
+ req.headers.authorization = `Bearer ${verdaccioJwt}`;
64
+ req.renownUser = {
65
+ address,
66
+ did: payload?.iss,
67
+ chainId: subject.chainId,
68
+ networkId: subject.networkId
69
+ };
70
+ return next();
71
+ };
72
+ }
73
+ //#endregion
74
+ //#region src/semver.ts
75
+ /**
76
+ * Compare two semver version strings for sorting.
77
+ * Returns negative if a < b, positive if a > b, 0 if equal.
78
+ *
79
+ * Handles numeric component comparison (so "1.0.10" > "1.0.9")
80
+ * and prerelease ordering (release > prerelease).
81
+ */
82
+ function compareSemver(a, b) {
83
+ const [coreA, preA] = a.split("-", 2);
84
+ const [coreB, preB] = b.split("-", 2);
85
+ const partsA = coreA.split(".").map(Number);
86
+ const partsB = coreB.split(".").map(Number);
87
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
88
+ const na = partsA[i] ?? 0;
89
+ const nb = partsB[i] ?? 0;
90
+ if (na !== nb) return na - nb;
91
+ }
92
+ if (!preA && preB) return 1;
93
+ if (preA && !preB) return -1;
94
+ if (preA && preB) return preA < preB ? -1 : preA > preB ? 1 : 0;
95
+ return 0;
96
+ }
97
+ //#endregion
98
+ //#region src/cdn.ts
99
+ /**
100
+ * Parse a package specifier into name and version/tag.
101
+ * Supports:
102
+ * "@scope/pkg" -> { name: "@scope/pkg", tag: undefined }
103
+ * "@scope/pkg@dev" -> { name: "@scope/pkg", tag: "dev" }
104
+ * "@scope/pkg@1.0.0" -> { name: "@scope/pkg", tag: "1.0.0" }
105
+ * "pkg@latest" -> { name: "pkg", tag: "latest" }
106
+ */
107
+ function parsePackageSpec(spec) {
108
+ if (spec.startsWith("@")) {
109
+ const lastAt = spec.lastIndexOf("@");
110
+ if (lastAt > 0 && lastAt !== spec.indexOf("@")) return {
111
+ name: spec.slice(0, lastAt),
112
+ tag: spec.slice(lastAt + 1)
113
+ };
114
+ return {
115
+ name: spec,
116
+ tag: void 0
117
+ };
118
+ }
119
+ const atIndex = spec.indexOf("@");
120
+ if (atIndex > 0) return {
121
+ name: spec.slice(0, atIndex),
122
+ tag: spec.slice(atIndex + 1)
123
+ };
124
+ return {
125
+ name: spec,
126
+ tag: void 0
127
+ };
128
+ }
129
+ var CdnCache = class {
130
+ #extractionLocks = /* @__PURE__ */ new Map();
131
+ constructor(registryUrl, cdnCachePath) {
132
+ this.registryUrl = registryUrl;
133
+ this.cdnCachePath = cdnCachePath;
134
+ }
135
+ async getFileByVersion(packageName, version, filePath) {
136
+ const versionDir = path.join(this.cdnCachePath, packageName, version);
137
+ const resolved = this.#resolveFile(versionDir, filePath);
138
+ if (resolved) return resolved;
139
+ await this.#extractWithLock(packageName, version);
140
+ return this.#resolveFile(versionDir, filePath);
141
+ }
142
+ #resolveFile(versionDir, filePath) {
143
+ const candidates = [
144
+ path.join(versionDir, filePath),
145
+ path.join(versionDir, "cdn", filePath),
146
+ path.join(versionDir, "dist", "cdn", filePath),
147
+ path.join(versionDir, "dist", filePath)
148
+ ];
149
+ for (const candidate of candidates) if (this.isSafePath(candidate) && fs.existsSync(candidate)) return candidate;
150
+ return null;
151
+ }
152
+ async #extractWithLock(packageName, version) {
153
+ const key = `${packageName}@${version}`;
154
+ const existing = this.#extractionLocks.get(key);
155
+ if (existing) return existing;
156
+ const promise = this.extractTarball(packageName, version).finally(() => {
157
+ this.#extractionLocks.delete(key);
158
+ });
159
+ this.#extractionLocks.set(key, promise);
160
+ return promise;
161
+ }
162
+ getLatestCachedVersion(packageName) {
163
+ const pkgDir = path.join(this.cdnCachePath, packageName);
164
+ try {
165
+ const versions = fs.readdirSync(pkgDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
166
+ if (versions.length === 0) return null;
167
+ versions.sort(compareSemver);
168
+ return versions[versions.length - 1];
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+ /**
174
+ * Resolve a version for a package. If tag is a semver version that exists
175
+ * in the registry, return it directly. If tag is a dist-tag name (e.g.
176
+ * "dev", "latest"), resolve it to the concrete version. If no tag is
177
+ * provided, prefer "latest", then fall back to any available dist-tag.
178
+ */
179
+ async resolveVersion(packageName, tag) {
180
+ try {
181
+ const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;
182
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
183
+ if (!res.ok) return null;
184
+ const metadata = await res.json();
185
+ const distTags = metadata["dist-tags"];
186
+ const versions = metadata["versions"];
187
+ if (tag) {
188
+ if (versions && tag in versions) return tag;
189
+ if (distTags && tag in distTags) return distTags[tag];
190
+ return null;
191
+ }
192
+ if (!distTags) return null;
193
+ return distTags.latest ?? Object.values(distTags)[0] ?? null;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+ async extractTarball(packageName, version) {
199
+ const destDir = path.join(this.cdnCachePath, packageName, version);
200
+ if (fs.existsSync(path.join(destDir, "package.json"))) return;
201
+ const shortName = packageName.startsWith("@") ? packageName.split("/")[1] : packageName;
202
+ const tarballUrl = `${this.registryUrl}/${encodeURIComponent(packageName)}/-/${shortName}-${version}.tgz`;
203
+ let res;
204
+ try {
205
+ res = await fetch(tarballUrl);
206
+ if (!res.ok || !res.body) return;
207
+ } catch {
208
+ return;
209
+ }
210
+ fs.mkdirSync(destDir, { recursive: true });
211
+ const tmpFile = path.join(destDir, `.tmp-tarball-${crypto.randomUUID()}.tgz`);
212
+ try {
213
+ const fileStream = fs.createWriteStream(tmpFile);
214
+ await pipeline(Readable.fromWeb(res.body), fileStream);
215
+ await extract({
216
+ file: tmpFile,
217
+ cwd: destDir,
218
+ strip: 1
219
+ });
220
+ } finally {
221
+ fs.rmSync(tmpFile, { force: true });
222
+ }
223
+ }
224
+ invalidate(packageName) {
225
+ const cacheDir = path.join(this.cdnCachePath, packageName);
226
+ if (!this.isSafePath(cacheDir)) return;
227
+ fs.rmSync(cacheDir, {
228
+ recursive: true,
229
+ force: true
230
+ });
231
+ }
232
+ invalidateVersion(packageName, version) {
233
+ const versionDir = path.join(this.cdnCachePath, packageName, version);
234
+ if (!this.isSafePath(versionDir)) return;
235
+ fs.rmSync(versionDir, {
236
+ recursive: true,
237
+ force: true
238
+ });
239
+ const pkgDir = path.join(this.cdnCachePath, packageName);
240
+ try {
241
+ if (fs.readdirSync(pkgDir).length === 0) fs.rmdirSync(pkgDir);
242
+ } catch {}
243
+ }
244
+ /** Remove all cached version directories except the specified one. */
245
+ pruneOldVersions(packageName, keepVersion) {
246
+ const pkgDir = path.join(this.cdnCachePath, packageName);
247
+ try {
248
+ const entries = fs.readdirSync(pkgDir, { withFileTypes: true });
249
+ for (const entry of entries) if (entry.isDirectory() && entry.name !== keepVersion) {
250
+ const dir = path.join(pkgDir, entry.name);
251
+ if (this.isSafePath(dir)) fs.rmSync(dir, {
252
+ recursive: true,
253
+ force: true
254
+ });
255
+ }
256
+ } catch {}
257
+ }
258
+ isSafePath(filePath) {
259
+ const resolved = path.resolve(filePath);
260
+ const cacheRoot = path.resolve(this.cdnCachePath);
261
+ return resolved.startsWith(cacheRoot + path.sep) || resolved === cacheRoot;
262
+ }
263
+ };
264
+ //#endregion
265
+ //#region src/packages.ts
266
+ /**
267
+ * Read dist-tags, the full version list, and the local-publish flag for a
268
+ * package from verdaccio's on-disk storage (`{storagePath}/{name}/package.json`).
269
+ *
270
+ * `locallyPublished` is tri-state:
271
+ * - `true` → storage metadata has `_attachments` (tarball uploaded here).
272
+ * - `false` → storage metadata exists but `_attachments` is empty (proxy
273
+ * from the npm uplink only; no local publish at this registry).
274
+ * - `undefined` → metadata file wasn't readable. Happens with non-filesystem
275
+ * backends (S3, etc.) or if verdaccio stores metadata elsewhere.
276
+ * Callers should treat this as "unknown" and default to including
277
+ * the package, to avoid filtering the whole /packages list to an
278
+ * empty array on deployments where we can't observe _attachments.
279
+ */
280
+ function readPackageMetadata(storagePath, packageName) {
281
+ if (!storagePath) return { locallyPublished: void 0 };
282
+ try {
283
+ const metadataPath = path.join(storagePath, packageName, "package.json");
284
+ const raw = fs.readFileSync(metadataPath, "utf-8");
285
+ const parsed = JSON.parse(raw);
286
+ const distTags = parsed["dist-tags"];
287
+ const versions = (parsed.versions ? Object.keys(parsed.versions) : []).slice().sort(compareSemver);
288
+ const locallyPublished = !!parsed._attachments && Object.keys(parsed._attachments).length > 0;
289
+ return {
290
+ distTags: distTags && Object.keys(distTags).length > 0 ? distTags : void 0,
291
+ versions: versions.length > 0 ? versions : void 0,
292
+ locallyPublished
293
+ };
294
+ } catch {
295
+ return { locallyPublished: void 0 };
296
+ }
297
+ }
298
+ function readManifest(dir) {
299
+ const candidates = [
300
+ path.join(dir, "powerhouse.manifest.json"),
301
+ path.join(dir, "cdn", "powerhouse.manifest.json"),
302
+ path.join(dir, "dist", "powerhouse.manifest.json")
303
+ ];
304
+ for (const manifestPath of candidates) try {
305
+ const raw = fs.readFileSync(manifestPath, "utf-8");
306
+ return JSON.parse(raw);
307
+ } catch {}
308
+ return null;
309
+ }
310
+ function readPackageJsonVersion(dir) {
311
+ try {
312
+ const raw = fs.readFileSync(path.join(dir, "package.json"), "utf-8");
313
+ const pkg = JSON.parse(raw);
314
+ return typeof pkg.version === "string" ? pkg.version : void 0;
315
+ } catch {
316
+ return;
317
+ }
318
+ }
319
+ function getLatestVersionDir(pkgDir) {
320
+ let entries;
321
+ try {
322
+ entries = fs.readdirSync(pkgDir, { withFileTypes: true });
323
+ } catch {
324
+ return null;
325
+ }
326
+ const versions = entries.filter((e) => e.isDirectory()).map((e) => e.name);
327
+ if (versions.length === 0) return null;
328
+ versions.sort(compareSemver);
329
+ return path.join(pkgDir, versions[versions.length - 1]);
330
+ }
331
+ function loadPackage(cdnCachePath, name, version) {
332
+ const pkgDir = path.join(cdnCachePath, name);
333
+ const manifestDir = (version ? path.join(pkgDir, version) : getLatestVersionDir(pkgDir)) ?? pkgDir;
334
+ const manifest = readManifest(manifestDir);
335
+ if (!manifest) return null;
336
+ return {
337
+ name: manifest.name ?? name,
338
+ path: `/-/cdn/${name}`,
339
+ manifest,
340
+ documentTypes: getDocumentTypesFromManifest(manifest),
341
+ version: readPackageJsonVersion(manifestDir)
342
+ };
343
+ }
344
+ function getDocumentTypesFromManifest(manifest) {
345
+ if (!manifest) return [];
346
+ const documentTypes = [];
347
+ const { apps, documentModels, editors, subgraphs } = manifest;
348
+ if (apps?.length) documentTypes.push("powerhouse/document-drive");
349
+ documentTypes.push(...(documentModels ?? []).map((dm) => dm.id), ...(editors ?? []).flatMap((e) => e.documentTypes).filter((dt) => dt !== void 0), ...(subgraphs ?? []).flatMap((e) => e.documentTypes).filter((dt) => dt !== void 0));
350
+ return documentTypes;
351
+ }
352
+ function scanPackages(cdnCachePath, storagePath) {
353
+ const absDir = path.resolve(cdnCachePath);
354
+ const packages = [];
355
+ let entries;
356
+ try {
357
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
358
+ } catch {
359
+ return packages;
360
+ }
361
+ for (const entry of entries) {
362
+ if (!entry.isDirectory()) continue;
363
+ if (entry.name.startsWith("@")) {
364
+ const scopeDir = path.join(absDir, entry.name);
365
+ let scopedEntries;
366
+ try {
367
+ scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
368
+ } catch (error) {
369
+ console.log(error);
370
+ continue;
371
+ }
372
+ for (const scopedEntry of scopedEntries) {
373
+ if (!scopedEntry.isDirectory()) continue;
374
+ const dirName = `${entry.name}/${scopedEntry.name}`;
375
+ const pkgDir = path.join(scopeDir, scopedEntry.name);
376
+ const manifestDir = getLatestVersionDir(pkgDir) ?? pkgDir;
377
+ const manifest = readManifest(manifestDir);
378
+ const name = manifest?.name ?? dirName;
379
+ const { distTags, versions, locallyPublished } = readPackageMetadata(storagePath, name);
380
+ if (locallyPublished === false) continue;
381
+ packages.push({
382
+ name,
383
+ path: `/-/cdn/${dirName}`,
384
+ manifest,
385
+ documentTypes: getDocumentTypesFromManifest(manifest),
386
+ version: readPackageJsonVersion(manifestDir),
387
+ distTags,
388
+ versions
389
+ });
390
+ }
391
+ } else {
392
+ const pkgDir = path.join(absDir, entry.name);
393
+ const manifestDir = getLatestVersionDir(pkgDir) ?? pkgDir;
394
+ const manifest = readManifest(manifestDir);
395
+ const name = manifest?.name ?? entry.name;
396
+ const { distTags, versions, locallyPublished } = readPackageMetadata(storagePath, name);
397
+ if (locallyPublished === false) continue;
398
+ packages.push({
399
+ name,
400
+ path: `/-/cdn/${entry.name}`,
401
+ manifest,
402
+ documentTypes: getDocumentTypesFromManifest(manifest),
403
+ version: readPackageJsonVersion(manifestDir),
404
+ distTags,
405
+ versions
406
+ });
407
+ }
408
+ }
409
+ return packages;
410
+ }
411
+ function findPackagesByDocumentType(packagesDir, documentType) {
412
+ return scanPackages(packagesDir).filter((pkg) => {
413
+ if (!pkg.manifest?.documentModels) return false;
414
+ return pkg.manifest.documentModels.some((dm) => dm.id === documentType);
415
+ });
416
+ }
417
+ //#endregion
418
+ //#region src/middleware.ts
419
+ const MIME_TYPES = {
420
+ ".js": "application/javascript",
421
+ ".mjs": "application/javascript",
422
+ ".css": "text/css",
423
+ ".json": "application/json",
424
+ ".wasm": "application/wasm",
425
+ ".map": "application/json",
426
+ ".html": "text/html",
427
+ ".svg": "image/svg+xml"
428
+ };
429
+ function getContentType(filePath) {
430
+ return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
431
+ }
432
+ function createPowerhouseRouter(config, sse, webhooks) {
433
+ const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
434
+ const router = Router();
435
+ router.use((_req, res, next) => {
436
+ res.setHeader("Access-Control-Allow-Origin", "*");
437
+ next();
438
+ });
439
+ router.get("/-/events", (_req, res) => {
440
+ sse.addClient(res);
441
+ });
442
+ router.get("/-/webhooks", (_req, res) => {
443
+ res.json(webhooks.getWebhooks());
444
+ });
445
+ router.post("/-/webhooks", express.json(), (req, res) => {
446
+ const { endpoint, headers } = req.body;
447
+ if (!endpoint) {
448
+ res.status(400).json({ error: "Missing required field: endpoint" });
449
+ return;
450
+ }
451
+ webhooks.addWebhook({
452
+ endpoint,
453
+ headers
454
+ });
455
+ res.status(201).json({
456
+ endpoint,
457
+ headers
458
+ });
459
+ });
460
+ router.delete("/-/webhooks", express.json(), (req, res) => {
461
+ const { endpoint } = req.body;
462
+ if (!endpoint) {
463
+ res.status(400).json({ error: "Missing required field: endpoint" });
464
+ return;
465
+ }
466
+ if (!webhooks.removeWebhook(endpoint)) {
467
+ res.status(404).json({ error: "Webhook not found" });
468
+ return;
469
+ }
470
+ res.status(204).end();
471
+ });
472
+ const WARM_INTERVAL_MS = 3e4;
473
+ let warmInFlight = false;
474
+ let lastWarmAt = 0;
475
+ async function warmCdnCacheFromVerdaccio() {
476
+ if (warmInFlight) return;
477
+ if (Date.now() - lastWarmAt < WARM_INTERVAL_MS) return;
478
+ warmInFlight = true;
479
+ try {
480
+ const r = await fetch(`http://localhost:${config.port}/-/verdaccio/data/packages`);
481
+ if (!r.ok) {
482
+ console.error(`[registry] verdaccio package listing returned ${r.status}`);
483
+ return;
484
+ }
485
+ const known = await r.json();
486
+ const concurrency = 8;
487
+ let cursor = 0;
488
+ const workers = Array.from({ length: concurrency }).map(async () => {
489
+ while (cursor < known.length) {
490
+ const pkg = known[cursor++];
491
+ if (!pkg.version) continue;
492
+ try {
493
+ await cdn.extractTarball(pkg.name, pkg.version);
494
+ } catch (err) {
495
+ console.error(`[registry] failed to warm cache for ${pkg.name}@${pkg.version}:`, err);
496
+ }
497
+ }
498
+ });
499
+ await Promise.all(workers);
500
+ console.log(`[registry] /packages warm-up done (${known.length} pkgs)`);
501
+ } catch (err) {
502
+ console.error("[registry] /packages warm-up failed:", err);
503
+ } finally {
504
+ warmInFlight = false;
505
+ lastWarmAt = Date.now();
506
+ }
507
+ }
508
+ warmCdnCacheFromVerdaccio();
509
+ router.get("/packages", (req, res) => {
510
+ warmCdnCacheFromVerdaccio();
511
+ const packages = scanPackages(config.cdnCachePath, config.storagePath);
512
+ const documentType = req.query.documentType;
513
+ if (documentType) {
514
+ const filtered = packages.filter((pkg) => pkg.manifest?.documentModels?.some((m) => m.id === documentType));
515
+ res.json(filtered);
516
+ return;
517
+ }
518
+ res.json(packages);
519
+ });
520
+ router.get("/packages/by-document-type", (req, res) => {
521
+ const documentType = req.query.type;
522
+ if (typeof documentType !== "string" || !documentType) {
523
+ res.status(400).json({ error: "Missing required query parameter: type" });
524
+ return;
525
+ }
526
+ const packageNames = findPackagesByDocumentType(config.cdnCachePath, documentType).map((pkg) => pkg.name);
527
+ res.json(packageNames);
528
+ });
529
+ router.get("/packages/*", async (req, res) => {
530
+ const raw = req.params[0];
531
+ const { name, tag } = parsePackageSpec(raw);
532
+ const version = await cdn.resolveVersion(name, tag) ?? cdn.getLatestCachedVersion(name);
533
+ const pkg = loadPackage(config.cdnCachePath, name, version ?? void 0);
534
+ if (!pkg) {
535
+ res.status(404).send("Package not found");
536
+ return;
537
+ }
538
+ res.json(pkg);
539
+ });
540
+ router.get("/-/cdn/*", async (req, res) => {
541
+ const fullPath = req.params[0];
542
+ let packageSpec;
543
+ let filePath;
544
+ if (fullPath.startsWith("@")) {
545
+ const segments = fullPath.split("/");
546
+ if (segments.length < 2) {
547
+ res.status(400).send("Invalid package path");
548
+ return;
549
+ }
550
+ packageSpec = `${segments[0]}/${segments[1]}`;
551
+ filePath = segments.slice(2).join("/") || "index.js";
552
+ } else {
553
+ const segments = fullPath.split("/");
554
+ packageSpec = segments[0];
555
+ filePath = segments.slice(1).join("/") || "index.js";
556
+ }
557
+ const { name: packageName, tag } = parsePackageSpec(packageSpec);
558
+ const version = await cdn.resolveVersion(packageName, tag) ?? cdn.getLatestCachedVersion(packageName);
559
+ if (!version) {
560
+ res.status(404).send("File not found");
561
+ return;
562
+ }
563
+ const resolved = await cdn.getFileByVersion(packageName, version, filePath);
564
+ if (!resolved) {
565
+ res.status(404).send("File not found");
566
+ return;
567
+ }
568
+ res.setHeader("Content-Type", getContentType(filePath));
569
+ const content = fs.readFileSync(resolved);
570
+ res.send(content);
571
+ });
572
+ return router;
573
+ }
574
+ /**
575
+ * Parse verdaccio's unpublish URL shape:
576
+ * DELETE /<pkg>/-rev/<rev> → full package
577
+ * DELETE /<pkg>/-/<tarball-name>/-rev/<rev> → single version
578
+ * where <pkg> may be scoped (@scope%2Fname, encoded) or unscoped, and the
579
+ * tarball name is `<short-name>-<version>.tgz`.
580
+ */
581
+ function parseUnpublishRequest(reqPath) {
582
+ const revIdx = reqPath.indexOf("/-rev/");
583
+ if (revIdx <= 0) return null;
584
+ const beforeRev = reqPath.slice(1, revIdx);
585
+ const tarballIdx = beforeRev.indexOf("/-/");
586
+ if (tarballIdx === -1) return {
587
+ packageName: decodeURIComponent(beforeRev),
588
+ version: null
589
+ };
590
+ const packageName = decodeURIComponent(beforeRev.slice(0, tarballIdx));
591
+ const tarballName = beforeRev.slice(tarballIdx + 3);
592
+ if (!tarballName.endsWith(".tgz")) return null;
593
+ const prefix = `${packageName.startsWith("@") ? packageName.split("/")[1] : packageName}-`;
594
+ if (!tarballName.startsWith(prefix)) return null;
595
+ const version = tarballName.slice(prefix.length, -4);
596
+ if (!version) return null;
597
+ return {
598
+ packageName,
599
+ version
600
+ };
601
+ }
602
+ function createUnpublishHook(config, notifications) {
603
+ const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
604
+ return (req, res, next) => {
605
+ if (req.method !== "DELETE") {
606
+ next();
607
+ return;
608
+ }
609
+ const parsed = parseUnpublishRequest(req.path);
610
+ if (!parsed) {
611
+ next();
612
+ return;
613
+ }
614
+ const originalEnd = res.end.bind(res);
615
+ res.end = function(chunk, encoding, cb) {
616
+ if (res.statusCode >= 200 && res.statusCode < 300) try {
617
+ if (parsed.version) cdn.invalidateVersion(parsed.packageName, parsed.version);
618
+ else cdn.invalidate(parsed.packageName);
619
+ const renownUser = req.renownUser;
620
+ notifications.notifyUnpublish({
621
+ packageName: parsed.packageName,
622
+ version: parsed.version,
623
+ publishedBy: renownUser ? {
624
+ address: renownUser.address,
625
+ did: renownUser.did
626
+ } : void 0
627
+ });
628
+ } catch (err) {
629
+ console.error(`[registry] CDN purge failed for ${parsed.packageName}${parsed.version ? `@${parsed.version}` : ""}:`, err);
630
+ }
631
+ return originalEnd(chunk, encoding, cb);
632
+ };
633
+ next();
634
+ };
635
+ }
636
+ function createPublishHook(config, notifications) {
637
+ const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
638
+ return (req, res, next) => {
639
+ if (req.method !== "PUT" || req.path.includes("/-rev/")) {
640
+ next();
641
+ return;
642
+ }
643
+ const originalEnd = res.end.bind(res);
644
+ res.end = function(chunk, encoding, cb) {
645
+ const urlPath = req.path.replace(/^\//, "");
646
+ if (res.statusCode < 200 || res.statusCode >= 300 || !urlPath || urlPath.startsWith("-")) return originalEnd(chunk, encoding, cb);
647
+ const packageName = decodeURIComponent(urlPath);
648
+ const versionsObj = req.body.versions;
649
+ const versions = Object.keys(versionsObj);
650
+ const version = versions.at(0);
651
+ if (!version) {
652
+ console.error(`[registry] No version found for ${packageName}`);
653
+ return originalEnd(chunk, encoding, cb);
654
+ }
655
+ if (versions.length > 1) console.warn(`[registry] Multiple versions published for ${packageName}: ${JSON.stringify(versions)}`);
656
+ const renownUser = req.renownUser;
657
+ const publishedBy = renownUser ? {
658
+ address: renownUser.address,
659
+ did: renownUser.did
660
+ } : void 0;
661
+ cdn.extractTarball(packageName, version).then(() => {
662
+ notifications.notifyPublish({
663
+ packageName,
664
+ version,
665
+ publishedBy
666
+ });
667
+ }).catch((err) => {
668
+ console.error(`[registry] Failed to extract ${packageName} to CDN cache:`, err);
669
+ });
670
+ return originalEnd(chunk, encoding, cb);
671
+ };
672
+ next();
673
+ };
674
+ }
675
+ //#endregion
676
+ //#region src/notifications/manager.ts
677
+ var NotificationManager = class {
678
+ #channels;
679
+ constructor(channels) {
680
+ this.#channels = channels;
681
+ }
682
+ notifyPublish(event) {
683
+ for (const channel of this.#channels) channel.notifyPublish(event);
684
+ }
685
+ notifyUnpublish(event) {
686
+ for (const channel of this.#channels) channel.notifyUnpublish(event);
687
+ }
688
+ };
689
+ //#endregion
690
+ //#region src/notifications/sse.ts
691
+ var SSEChannel = class {
692
+ #clients = /* @__PURE__ */ new Set();
693
+ addClient(res) {
694
+ res.writeHead(200, {
695
+ "Content-Type": "text/event-stream",
696
+ "Cache-Control": "no-cache",
697
+ Connection: "keep-alive",
698
+ "Access-Control-Allow-Origin": "*"
699
+ });
700
+ res.write("event: connected\ndata: {}\n\n");
701
+ this.#clients.add(res);
702
+ res.on("close", () => {
703
+ this.#clients.delete(res);
704
+ });
705
+ }
706
+ notifyPublish(event) {
707
+ this.#broadcast("publish", event);
708
+ }
709
+ notifyUnpublish(event) {
710
+ this.#broadcast("unpublish", event);
711
+ }
712
+ #broadcast(eventName, event) {
713
+ const payload = `event: ${eventName}\ndata: ${JSON.stringify(event)}\n\n`;
714
+ for (const client of this.#clients) try {
715
+ client.write(payload);
716
+ } catch (err) {
717
+ console.error("[registry] SSE client write failed:", err);
718
+ this.#clients.delete(client);
719
+ }
720
+ }
721
+ };
722
+ //#endregion
723
+ //#region src/notifications/webhook.ts
724
+ const WEBHOOKS_FILE = "webhooks.json";
725
+ var WebhookChannel = class {
726
+ #predefined;
727
+ #dynamic;
728
+ #storagePath;
729
+ constructor(storagePath, config) {
730
+ this.#storagePath = storagePath;
731
+ this.#predefined = config?.webhooks ?? [];
732
+ this.#dynamic = this.#load();
733
+ }
734
+ getWebhooks() {
735
+ return [...this.#predefined, ...this.#dynamic];
736
+ }
737
+ addWebhook(webhook) {
738
+ if (this.getWebhooks().some((w) => w.endpoint === webhook.endpoint)) return;
739
+ this.#dynamic.push(webhook);
740
+ this.#save();
741
+ }
742
+ removeWebhook(endpoint) {
743
+ const before = this.#dynamic.length;
744
+ this.#dynamic = this.#dynamic.filter((w) => w.endpoint !== endpoint);
745
+ if (this.#dynamic.length === before) return false;
746
+ this.#save();
747
+ return true;
748
+ }
749
+ notifyPublish(event) {
750
+ this.#post({
751
+ type: "publish",
752
+ ...event
753
+ });
754
+ }
755
+ notifyUnpublish(event) {
756
+ this.#post({
757
+ type: "unpublish",
758
+ ...event
759
+ });
760
+ }
761
+ #post(body) {
762
+ for (const webhook of this.getWebhooks()) {
763
+ const headers = {
764
+ "Content-Type": "application/json",
765
+ ...webhook.headers
766
+ };
767
+ fetch(webhook.endpoint, {
768
+ method: "POST",
769
+ headers,
770
+ body: JSON.stringify(body)
771
+ }).catch((err) => {
772
+ console.error(`[registry] Webhook to ${webhook.endpoint} failed:`, err);
773
+ });
774
+ }
775
+ }
776
+ #filePath() {
777
+ return path.join(this.#storagePath, WEBHOOKS_FILE);
778
+ }
779
+ #load() {
780
+ try {
781
+ const raw = fs.readFileSync(this.#filePath(), "utf-8");
782
+ return JSON.parse(raw);
783
+ } catch {
784
+ return [];
785
+ }
786
+ }
787
+ #save() {
788
+ fs.mkdirSync(this.#storagePath, { recursive: true });
789
+ fs.writeFileSync(this.#filePath(), JSON.stringify(this.#dynamic, null, 2));
790
+ }
791
+ };
792
+ //#endregion
793
+ //#region src/verdaccio-config.ts
794
+ function buildVerdaccioConfig(config) {
795
+ const htpasswdPath = path.join(config.storagePath, "htpasswd");
796
+ const uplinkUrl = config.uplink ?? "https://registry.npmjs.org/";
797
+ const base = {
798
+ storage: config.storagePath,
799
+ self_path: "./",
800
+ ...config.verdaccioSecret ? { secret: config.verdaccioSecret } : {},
801
+ security: { api: { jwt: {
802
+ sign: { expiresIn: "5m" },
803
+ verify: {}
804
+ } } },
805
+ auth: { htpasswd: { file: htpasswdPath } },
806
+ uplinks: { npmjs: {
807
+ url: uplinkUrl,
808
+ maxage: "15m",
809
+ timeout: "30s",
810
+ cache: true
811
+ } },
812
+ packages: (() => {
813
+ const local = config.localPackagePatterns ?? [];
814
+ const localSet = new Set(local);
815
+ const access = {
816
+ access: "$all",
817
+ publish: "$authenticated",
818
+ unpublish: "$authenticated"
819
+ };
820
+ const entries = [];
821
+ for (const pattern of local) entries.push([pattern, { ...access }]);
822
+ if (!localSet.has("@powerhousedao/*")) entries.push(["@powerhousedao/*", {
823
+ ...access,
824
+ proxy: "npmjs"
825
+ }]);
826
+ if (!localSet.has("**")) entries.push(["**", {
827
+ ...access,
828
+ proxy: "npmjs"
829
+ }]);
830
+ return Object.fromEntries(entries);
831
+ })(),
832
+ web: {
833
+ enable: config.webEnabled !== false,
834
+ title: "Powerhouse Registry",
835
+ logo: "https://raw.githubusercontent.com/powerhouse-inc/powerhouse/main/packages/registry/static/logo.svg",
836
+ favicon: "/-/static/favicon.ico",
837
+ primary_color: "#38C780",
838
+ darkMode: true
839
+ },
840
+ server: { keepAliveTimeout: 60 },
841
+ log: {
842
+ type: "stdout",
843
+ format: "pretty",
844
+ level: "warn"
845
+ },
846
+ max_body_size: config.maxBodySize ?? "300mb"
847
+ };
848
+ if (config.s3) base.store = { "aws-s3-storage": {
849
+ bucket: config.s3.bucket,
850
+ endpoint: config.s3.endpoint,
851
+ region: config.s3.region,
852
+ s3ForcePathStyle: config.s3.s3ForcePathStyle ?? true,
853
+ ...config.s3.keyPrefix && { keyPrefix: config.s3.keyPrefix },
854
+ ...config.s3.accessKeyId && { accessKeyId: config.s3.accessKeyId },
855
+ ...config.s3.secretAccessKey && { secretAccessKey: config.s3.secretAccessKey }
856
+ } };
857
+ return base;
858
+ }
859
+ //#endregion
860
+ //#region src/run.ts
861
+ async function resolveDir(dir) {
862
+ if (path.isAbsolute(dir)) {
863
+ await mkdir(dir, { recursive: true });
864
+ return dir;
865
+ }
866
+ const found = await findUp(dir, { type: "directory" });
867
+ if (!found) {
868
+ await mkdir(dir, { recursive: true });
869
+ return dir;
870
+ }
871
+ return found;
872
+ }
873
+ async function runRegistry(args) {
874
+ const { port, storageDir, cdnCacheDir, uplink, webEnabled, webhooks, s3AccessKeyId, s3Bucket, s3Endpoint, s3ForcePathStyle, s3KeyPrefix, s3Region, s3SecretAccessKey, publicUrl, authRenown, verdaccioSecret: verdaccioSecretArg, localPackages } = args;
875
+ const storagePath = await resolveDir(storageDir);
876
+ const cdnCachePath = await resolveDir(cdnCacheDir);
877
+ const verdaccioSecret = verdaccioSecretArg ?? randomBytes(32).toString("hex");
878
+ const renownEnabled = authRenown === true && Boolean(publicUrl);
879
+ if (authRenown === true && !publicUrl) console.warn("[registry] auth-renown is enabled but --public-url / PH_REGISTRY_PUBLIC_URL is not set; Renown auth will be disabled.");
880
+ console.log({
881
+ storagePath,
882
+ cdnCachePath
883
+ });
884
+ const webhookConfigs = webhooks?.split(",").map((url) => url.trim()).filter(Boolean).map((endpoint) => ({ endpoint }));
885
+ const localPackagePatterns = localPackages?.split(",").map((p) => p.trim()).filter(Boolean);
886
+ const config = {
887
+ port,
888
+ storagePath,
889
+ cdnCachePath,
890
+ uplink,
891
+ webEnabled,
892
+ verdaccioSecret,
893
+ ...localPackagePatterns?.length ? { localPackagePatterns } : {},
894
+ ...renownEnabled && publicUrl ? { renown: { publicUrl } } : {},
895
+ ...webhookConfigs?.length && { notify: { webhooks: webhookConfigs } },
896
+ ...s3Bucket && s3Endpoint && s3Region && { s3: {
897
+ bucket: s3Bucket,
898
+ endpoint: s3Endpoint,
899
+ region: s3Region,
900
+ accessKeyId: s3AccessKeyId,
901
+ secretAccessKey: s3SecretAccessKey,
902
+ keyPrefix: s3KeyPrefix,
903
+ s3ForcePathStyle
904
+ } }
905
+ };
906
+ await mkdir(storagePath, { recursive: true });
907
+ await mkdir(cdnCachePath, { recursive: true });
908
+ const verdaccioHandler = (await runServer(buildVerdaccioConfig(config))).listeners("request")[0];
909
+ const app = express();
910
+ const sseChannel = new SSEChannel();
911
+ const webhookChannel = new WebhookChannel(config.storagePath, config.notify);
912
+ const notifications = new NotificationManager([sseChannel, webhookChannel]);
913
+ const staticDir = await findUp("static", { type: "directory" });
914
+ if (staticDir) app.use("/-/static", express.static(staticDir));
915
+ app.use(createPowerhouseRouter(config, sseChannel, webhookChannel));
916
+ if (config.renown) app.use(createRenownAuthMiddleware({
917
+ publicUrl: config.renown.publicUrl,
918
+ verdaccioSecret
919
+ }));
920
+ app.use(createPublishHook(config, notifications));
921
+ app.use(createUnpublishHook(config, notifications));
922
+ app.use((req, res) => verdaccioHandler(req, res));
923
+ return app.listen(port, () => {
924
+ console.log(`Powerhouse Registry running on http://localhost:${port}`);
925
+ console.log(` CDN: http://localhost:${port}/-/cdn/`);
926
+ console.log(` Packages: http://localhost:${port}/packages`);
927
+ console.log(` npm: http://localhost:${port}/`);
928
+ console.log(` Storage: ${storagePath}`);
929
+ console.log(` CDN cache: ${cdnCachePath}`);
930
+ if (config.s3) console.log(` S3: ${config.s3.endpoint}/${config.s3.bucket}`);
931
+ if (config.renown) console.log(` Renown auth: ${config.renown.publicUrl}`);
932
+ });
933
+ }
934
+ //#endregion
935
+ //#region cli.ts
936
+ const registryCommand = command({
937
+ name: "Package registry",
938
+ args: {
939
+ port: option({
940
+ long: "port",
941
+ type: number,
942
+ defaultValue: () => Number(process.env.PORT) || 8080,
943
+ defaultValueIsSerializable: true
944
+ }),
945
+ storageDir: option({
946
+ long: "storage-dir",
947
+ type: string,
948
+ defaultValue: () => process.env.REGISTRY_STORAGE || "./storage",
949
+ defaultValueIsSerializable: true
950
+ }),
951
+ cdnCacheDir: option({
952
+ long: "cdn-cache-dir",
953
+ type: string,
954
+ defaultValue: () => process.env.REGISTRY_CDN_CACHE || "./cdn-cache",
955
+ defaultValueIsSerializable: true
956
+ }),
957
+ uplink: option({
958
+ long: "uplink",
959
+ type: optional(string),
960
+ defaultValue: () => process.env.REGISTRY_UPLINK,
961
+ defaultValueIsSerializable: true
962
+ }),
963
+ s3Bucket: option({
964
+ long: "s3-bucket",
965
+ type: optional(string),
966
+ defaultValue: () => process.env.S3_BUCKET,
967
+ defaultValueIsSerializable: true
968
+ }),
969
+ s3Endpoint: option({
970
+ long: "s3-endpoint",
971
+ type: optional(string),
972
+ defaultValue: () => process.env.S3_ENDPOINT,
973
+ defaultValueIsSerializable: true
974
+ }),
975
+ s3Region: option({
976
+ long: "s3-region",
977
+ type: optional(string),
978
+ defaultValue: () => process.env.S3_REGION,
979
+ defaultValueIsSerializable: true
980
+ }),
981
+ s3AccessKeyId: option({
982
+ long: "s3-access-key-id",
983
+ type: optional(string),
984
+ defaultValue: () => process.env.S3_ACCESS_KEY_ID,
985
+ defaultValueIsSerializable: true
986
+ }),
987
+ s3SecretAccessKey: option({
988
+ long: "s3-secret-access-key",
989
+ type: optional(string),
990
+ defaultValue: () => process.env.S3_SECRET_ACCESS_KEY,
991
+ defaultValueIsSerializable: true
992
+ }),
993
+ s3KeyPrefix: option({
994
+ long: "s3-key-prefix",
995
+ type: optional(string),
996
+ defaultValue: () => process.env.S3_KEY_PREFIX,
997
+ defaultValueIsSerializable: true
998
+ }),
999
+ s3ForcePathStyle: flag({
1000
+ long: "s3-force-path-style",
1001
+ defaultValue: () => process.env.S3_FORCE_PATH_STYLE !== "false",
1002
+ defaultValueIsSerializable: true
1003
+ }),
1004
+ webEnabled: flag({
1005
+ long: "web-enabled",
1006
+ defaultValue: () => process.env.REGISTRY_WEB !== "false",
1007
+ defaultValueIsSerializable: true
1008
+ }),
1009
+ webhooks: option({
1010
+ long: "webhook",
1011
+ type: optional(string),
1012
+ description: "Comma-separated webhook URLs to notify on publish",
1013
+ defaultValue: () => process.env.REGISTRY_WEBHOOKS,
1014
+ defaultValueIsSerializable: true
1015
+ }),
1016
+ publicUrl: option({
1017
+ long: "public-url",
1018
+ type: optional(string),
1019
+ description: "Public origin of this registry (used as the JWT `aud` claim for Renown bearer tokens). Required when --auth-renown is true.",
1020
+ defaultValue: () => process.env.PH_REGISTRY_PUBLIC_URL,
1021
+ defaultValueIsSerializable: true
1022
+ }),
1023
+ authRenown: flag({
1024
+ long: "auth-renown",
1025
+ description: "Verify Renown-signed bearer tokens in front of verdaccio (stateless). Disabled when --public-url is unset.",
1026
+ defaultValue: () => process.env.PH_REGISTRY_AUTH_RENOWN === "true",
1027
+ defaultValueIsSerializable: true
1028
+ }),
1029
+ verdaccioSecret: option({
1030
+ long: "verdaccio-secret",
1031
+ type: optional(string),
1032
+ description: "Override verdaccio's internal JWT signing secret. Default: random per pod (fine — the swapped JWT never leaves this process).",
1033
+ defaultValue: () => process.env.PH_REGISTRY_VERDACCIO_SECRET,
1034
+ defaultValueIsSerializable: true
1035
+ }),
1036
+ localPackages: option({
1037
+ long: "local-packages",
1038
+ type: optional(string),
1039
+ description: "Comma-separated globs (e.g. '@powerhousedao/*,document-model,ph-cmd') served locally only — no npmjs uplink proxy. Lets you re-publish a workspace package whose version already exists on npmjs without bumping.",
1040
+ defaultValue: () => process.env.PH_REGISTRY_LOCAL_PACKAGES,
1041
+ defaultValueIsSerializable: true
1042
+ })
1043
+ },
1044
+ handler: async (args) => {
1045
+ console.log(args);
1046
+ try {
1047
+ await runRegistry(args);
1048
+ } catch (error) {
1049
+ console.error("Failed to start registry:");
1050
+ console.error(error);
1051
+ process.exit(1);
1052
+ }
1053
+ }
1054
+ });
1055
+ await run(binary(registryCommand), process.argv);
1056
+ //#endregion
1057
+ export { registryCommand };
1058
+
1059
+ //# sourceMappingURL=cli.mjs.map