@powerhousedao/registry 6.2.2-dev.4 → 6.2.2-dev.40

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/cli.d.mts CHANGED
@@ -21,6 +21,7 @@ declare const registryCommand: Partial<cmd_ts_dist_cjs_argparser_js0.Register> &
21
21
  webhooks: string | undefined;
22
22
  publicUrl: string | undefined;
23
23
  authRenown: boolean;
24
+ renownUrl: string | undefined;
24
25
  verdaccioSecret: string | undefined;
25
26
  localPackages: string | undefined;
26
27
  databaseUrl: string | undefined;
@@ -42,6 +43,7 @@ declare const registryCommand: Partial<cmd_ts_dist_cjs_argparser_js0.Register> &
42
43
  webhooks: string | undefined;
43
44
  publicUrl: string | undefined;
44
45
  authRenown: boolean;
46
+ renownUrl: string | undefined;
45
47
  verdaccioSecret: string | undefined;
46
48
  localPackages: string | undefined;
47
49
  databaseUrl: string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.mts","names":[],"sources":["../cli.ts"],"mappings":";;;;;cAiBa,eAAA,EAAe,OAAA,CAkJ1B,6BAAA,CAlJ0B,QAAA;iBAAA,6BAAA,CAAA,YAAA"}
1
+ {"version":3,"file":"cli.d.mts","names":[],"sources":["../cli.ts"],"mappings":";;;;;cAiBa,eAAA,EAAe,OAAA,CAkK1B,6BAAA,CAlK0B,QAAA;iBAAA,6BAAA,CAAA,YAAA"}
package/dist/cli.mjs CHANGED
@@ -1,12 +1,11 @@
1
1
  import { binary, command, flag, number, option, optional, run, string } from "cmd-ts";
2
2
  import express, { Router } from "express";
3
3
  import { findUp } from "find-up";
4
- import crypto, { randomBytes } from "node:crypto";
4
+ import crypto, { randomBytes, randomUUID } from "node:crypto";
5
5
  import { mkdir } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { runServer } from "verdaccio";
8
- import { signPayload } from "@verdaccio/signature";
9
- import { verifyAuthBearerToken } from "@renown/sdk/node";
8
+ import { Pool } from "pg";
10
9
  import fs from "node:fs";
11
10
  import { pipeline } from "node:stream/promises";
12
11
  import { Readable } from "node:stream";
@@ -14,65 +13,98 @@ import { extract } from "tar";
14
13
  import { slimManifest } from "@powerhousedao/shared/registry";
15
14
  import { fileURLToPath } from "node:url";
16
15
  //#endregion
17
- //#region src/auth/renown-middleware.ts
18
- function audienceMatches(aud, expected) {
19
- if (!aud) return false;
20
- if (Array.isArray(aud)) return aud.includes(expected);
21
- return aud === expected;
16
+ //#region src/auth/pg-store.ts
17
+ /** Build a real Postgres pool from a connection string. */
18
+ function createPgPool(databaseUrl) {
19
+ return new Pool({ connectionString: databaseUrl });
22
20
  }
23
21
  /**
24
- * Translates a Renown-signed Bearer token (verifiable from the issuer's DID,
25
- * stateless) into a verdaccio-format Bearer token (signed with verdaccio's
26
- * own secret) that verdaccio's normal auth pipeline accepts.
22
+ * Postgres-backed AuthStore. Two small tables:
23
+ * - registry_users(username PK, password_hash, created_at)
24
+ * - registry_package_owners(package_name PK, owners text[], claimed_at)
27
25
  *
28
- * Falls through (calls `next()` without modifying auth) on any verification
29
- * failure: malformed token, bad signature, expired, audience mismatch, or
30
- * non-renown bearer token. This keeps the legacy htpasswd path usable during
31
- * the migration grace period — verdaccio's own apiJWTmiddleware sees the
32
- * original Authorization header and decides what to do with it.
26
+ * Ownership claim is race-free: `INSERT ... ON CONFLICT DO NOTHING` means the
27
+ * first publisher wins atomically; the follow-up read returns the actual
28
+ * owners so a losing racer is denied.
29
+ *
30
+ * Takes an already-built `pg.Pool` (tests inject a pg-mem pool cast to Pool).
33
31
  */
34
- function createRenownAuthMiddleware(opts) {
35
- const expectedAud = opts.publicUrl;
36
- return async (req, _res, next) => {
37
- const header = req.headers.authorization;
38
- if (!header?.startsWith("Bearer ")) return next();
39
- const token = header.slice(7).trim();
40
- if (!token) return next();
41
- let verified;
42
- try {
43
- verified = await verifyAuthBearerToken(token, { audience: expectedAud });
44
- } catch {
45
- return next();
46
- }
47
- if (!verified) return next();
48
- const payload = verified.payload;
49
- if (!audienceMatches(payload?.aud, expectedAud)) return next();
50
- const subject = verified.verifiableCredential.credentialSubject;
51
- if (!subject?.address) return next();
52
- const address = subject.address.toLowerCase();
53
- const groups = ["$authenticated", "renown"];
54
- let verdaccioJwt;
55
- try {
56
- verdaccioJwt = await signPayload({
57
- name: address,
58
- real_groups: groups,
59
- groups
60
- }, opts.verdaccioSecret, { expiresIn: "5m" });
61
- } catch (err) {
62
- console.error("[registry] failed to mint internal verdaccio token:", err);
63
- return next();
32
+ function createPgStore(pool) {
33
+ let initialized = null;
34
+ return {
35
+ init() {
36
+ initialized ??= (async () => {
37
+ await pool.query(`
38
+ CREATE TABLE IF NOT EXISTS registry_users (
39
+ username text PRIMARY KEY,
40
+ password_hash text NOT NULL,
41
+ created_at timestamptz NOT NULL DEFAULT now()
42
+ )`);
43
+ await pool.query(`
44
+ CREATE TABLE IF NOT EXISTS registry_package_owners (
45
+ package_name text PRIMARY KEY,
46
+ owners text[] NOT NULL,
47
+ claimed_at timestamptz NOT NULL DEFAULT now()
48
+ )`);
49
+ })();
50
+ return initialized;
51
+ },
52
+ async getUser(username) {
53
+ const row = (await pool.query("SELECT password_hash FROM registry_users WHERE username = $1", [username])).rows[0];
54
+ return row ? { passwordHash: row.password_hash } : null;
55
+ },
56
+ async createUser(username, passwordHash) {
57
+ try {
58
+ await pool.query("INSERT INTO registry_users (username, password_hash) VALUES ($1, $2)", [username, passwordHash]);
59
+ return true;
60
+ } catch (err) {
61
+ if (err.code === "23505") return false;
62
+ throw err;
63
+ }
64
+ },
65
+ async getOwners(pkg) {
66
+ return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? null;
67
+ },
68
+ async getOwnersFor(pkgs) {
69
+ if (pkgs.length === 0) return {};
70
+ const placeholders = pkgs.map((_, i) => `$${i + 1}`).join(",");
71
+ const res = await pool.query(`SELECT package_name, owners FROM registry_package_owners WHERE package_name IN (${placeholders})`, pkgs);
72
+ const out = {};
73
+ for (const row of res.rows) out[row.package_name] = row.owners;
74
+ return out;
75
+ },
76
+ async claimOwner(pkg, username) {
77
+ await pool.query(`INSERT INTO registry_package_owners (package_name, owners)
78
+ VALUES ($1, ARRAY[$2]::text[])
79
+ ON CONFLICT (package_name) DO NOTHING`, [pkg, username]);
80
+ return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? [];
81
+ },
82
+ close() {
83
+ return pool.end();
64
84
  }
65
- req.headers.authorization = `Bearer ${verdaccioJwt}`;
66
- req.renownUser = {
67
- address,
68
- did: payload?.iss,
69
- chainId: subject.chainId,
70
- networkId: subject.networkId
71
- };
72
- return next();
73
85
  };
74
86
  }
75
87
  //#endregion
88
+ //#region src/auth/store-handoff.ts
89
+ const REGISTRY_KEY = Symbol.for("@powerhousedao/registry:auth-store-handoff");
90
+ function registry() {
91
+ const g = globalThis;
92
+ return g[REGISTRY_KEY] ??= /* @__PURE__ */ new Map();
93
+ }
94
+ /** Stash a store instance and return a token to carry through plugin config. */
95
+ function stashAuthStore(store) {
96
+ const token = randomUUID();
97
+ registry().set(token, {
98
+ store,
99
+ loaded: false
100
+ });
101
+ return token;
102
+ }
103
+ /** True once the plugin has loaded the store for this token. */
104
+ function wasStoreLoaded(token) {
105
+ return registry().get(token)?.loaded ?? false;
106
+ }
107
+ //#endregion
76
108
  //#region src/semver.ts
77
109
  /**
78
110
  * Compare two semver version strings for sorting.
@@ -246,6 +278,37 @@ var CdnCache = class {
246
278
  if (fs.readdirSync(pkgDir).length === 0) fs.rmdirSync(pkgDir);
247
279
  } catch {}
248
280
  }
281
+ async reconcileWithRegistry(packageName) {
282
+ const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;
283
+ let versions;
284
+ try {
285
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
286
+ if (!res.ok) return [];
287
+ const meta = await res.json();
288
+ versions = Object.keys(meta.versions ?? {});
289
+ } catch {
290
+ return [];
291
+ }
292
+ if (versions.length === 0) return [];
293
+ return this.reconcileVersions(packageName, versions);
294
+ }
295
+ reconcileVersions(packageName, keepVersions) {
296
+ const pkgDir = path.join(this.cdnCachePath, packageName);
297
+ const keep = new Set(keepVersions);
298
+ const removed = [];
299
+ let entries;
300
+ try {
301
+ entries = fs.readdirSync(pkgDir, { withFileTypes: true });
302
+ } catch {
303
+ return removed;
304
+ }
305
+ for (const entry of entries) {
306
+ if (!entry.isDirectory() || keep.has(entry.name)) continue;
307
+ this.invalidateVersion(packageName, entry.name);
308
+ removed.push(entry.name);
309
+ }
310
+ return removed;
311
+ }
249
312
  /** Remove all cached version directories except the specified one. */
250
313
  pruneOldVersions(packageName, keepVersion) {
251
314
  const pkgDir = path.join(this.cdnCachePath, packageName);
@@ -341,17 +404,40 @@ function getLatestVersionDir(pkgDir) {
341
404
  versions.sort(compareSemver);
342
405
  return path.join(pkgDir, versions[versions.length - 1]);
343
406
  }
344
- function loadPackage(cdnCachePath, name, version) {
407
+ function loadPackage(cdnCachePath, name, version, storagePath) {
345
408
  const pkgDir = path.join(cdnCachePath, name);
346
409
  const manifestDir = (version ? path.join(pkgDir, version) : getLatestVersionDir(pkgDir)) ?? pkgDir;
347
410
  const manifest = readManifest(manifestDir);
348
411
  if (!manifest) return null;
412
+ const resolvedName = manifest.name || name;
413
+ const { distTags, versions } = readPackageMetadata(storagePath, resolvedName);
349
414
  return {
350
- name: manifest.name || name,
415
+ name: resolvedName,
351
416
  path: `/-/cdn/${name}`,
352
417
  manifest,
353
418
  documentTypes: getDocumentTypesFromManifest(manifest),
354
- version: readPackageJsonVersion(manifestDir)
419
+ version: readPackageJsonVersion(manifestDir),
420
+ distTags,
421
+ versions
422
+ };
423
+ }
424
+ /**
425
+ * Project a full {@link PackageInfo} down to the trimmed shape the paginated
426
+ * package-listing UI renders per row. Version metadata and documentTypes are
427
+ * intentionally dropped — the client fetches them on demand.
428
+ */
429
+ function toPackageListItem(pkg) {
430
+ const publisher = pkg.manifest?.publisher;
431
+ return {
432
+ name: pkg.name,
433
+ path: pkg.path,
434
+ version: pkg.version,
435
+ description: pkg.manifest?.description ?? void 0,
436
+ category: pkg.manifest?.category ?? void 0,
437
+ publisher: publisher ? {
438
+ name: publisher.name,
439
+ url: publisher.url
440
+ } : void 0
355
441
  };
356
442
  }
357
443
  function getDocumentTypesFromManifest(manifest) {
@@ -480,6 +566,20 @@ function createWarmer(config, cdn) {
480
566
  }
481
567
  //#endregion
482
568
  //#region src/middleware.ts
569
+ const DEFAULT_PAGE_SIZE = 30;
570
+ const MAX_PAGE_SIZE = 50;
571
+ /** Parse+clamp a `limit` query param to 1..MAX_PAGE_SIZE (default 30). */
572
+ function clampPageSize(raw) {
573
+ const n = Number.parseInt(String(raw), 10);
574
+ if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_SIZE;
575
+ return Math.min(n, MAX_PAGE_SIZE);
576
+ }
577
+ /** Parse a non-negative `offset` query param (default 0). */
578
+ function parseOffset(raw) {
579
+ const n = Number.parseInt(String(raw), 10);
580
+ if (!Number.isFinite(n) || n < 0) return 0;
581
+ return n;
582
+ }
483
583
  const MIME_TYPES = {
484
584
  ".js": "application/javascript",
485
585
  ".mjs": "application/javascript",
@@ -493,6 +593,15 @@ const MIME_TYPES = {
493
593
  function getContentType(filePath) {
494
594
  return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
495
595
  }
596
+ function publisherFromRequest(req) {
597
+ const name = req.remote_user?.name;
598
+ if (!name) return void 0;
599
+ if (name.startsWith("did:pkh:")) return {
600
+ address: (name.split(":").pop() ?? name).toLowerCase(),
601
+ did: name
602
+ };
603
+ return { address: name };
604
+ }
496
605
  /**
497
606
  * Resolve a package version. Exact versions skip the network call. Upstream
498
607
  * errors fall back to the latest cached version; genuine not-found falls back
@@ -532,9 +641,23 @@ function etagMatches(header, etag) {
532
641
  return tag === "*" || opaqueTag(tag) === target;
533
642
  });
534
643
  }
535
- function createPowerhouseRouter(config, sse, webhooks) {
644
+ function createPowerhouseRouter(config, sse, webhooks, ownerStore) {
536
645
  const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
537
646
  const router = Router();
647
+ const withOwners = async (pkgs) => {
648
+ if (!ownerStore || pkgs.length === 0) return pkgs;
649
+ try {
650
+ await ownerStore.init();
651
+ const map = await ownerStore.getOwnersFor(pkgs.map((p) => p.name));
652
+ return pkgs.map((p) => p.name in map ? {
653
+ ...p,
654
+ owners: map[p.name]
655
+ } : p);
656
+ } catch (err) {
657
+ console.error("[registry] owner lookup failed:", err);
658
+ return pkgs;
659
+ }
660
+ };
538
661
  router.use((_req, res, next) => {
539
662
  res.setHeader("Access-Control-Allow-Origin", "*");
540
663
  next();
@@ -574,16 +697,32 @@ function createPowerhouseRouter(config, sse, webhooks) {
574
697
  });
575
698
  const warm = createWarmer(config, cdn);
576
699
  warm();
577
- router.get("/packages", (req, res) => {
700
+ router.get("/packages", async (req, res) => {
578
701
  warm();
579
702
  const packages = scanPackages(config.cdnCachePath, config.storagePath);
580
703
  const documentType = req.query.documentType;
581
704
  if (documentType) {
582
705
  const filtered = packages.filter((pkg) => pkg.manifest?.documentModels?.some((m) => m.id === documentType));
583
- res.json(filtered);
706
+ res.json(await withOwners(filtered));
707
+ return;
708
+ }
709
+ if (req.query.limit === void 0) {
710
+ res.json(await withOwners(packages));
584
711
  return;
585
712
  }
586
- res.json(packages);
713
+ const limit = clampPageSize(req.query.limit);
714
+ const offset = parseOffset(req.query.offset);
715
+ const search = typeof req.query.search === "string" ? req.query.search.trim().toLowerCase() : "";
716
+ const sorted = (search ? packages.filter((pkg) => pkg.name.toLowerCase().includes(search)) : packages).slice().sort((a, b) => a.name.localeCompare(b.name));
717
+ const total = sorted.length;
718
+ const items = sorted.slice(offset, offset + limit).map(toPackageListItem);
719
+ res.json({
720
+ items,
721
+ total,
722
+ limit,
723
+ offset,
724
+ hasMore: offset + limit < total
725
+ });
587
726
  });
588
727
  router.get("/packages/by-document-type", (req, res) => {
589
728
  const documentType = req.query.type;
@@ -603,12 +742,12 @@ function createPowerhouseRouter(config, sse, webhooks) {
603
742
  return;
604
743
  }
605
744
  const version = resolution.kind === "ok" ? resolution.version : void 0;
606
- const pkg = loadPackage(config.cdnCachePath, name, version);
745
+ const pkg = loadPackage(config.cdnCachePath, name, version, config.storagePath);
607
746
  if (!pkg) {
608
747
  res.status(404).send("Package not found");
609
748
  return;
610
749
  }
611
- res.json(pkg);
750
+ res.json((await withOwners([pkg]))[0]);
612
751
  });
613
752
  router.get("/-/cdn/*", async (req, res) => {
614
753
  const fullPath = req.params[0];
@@ -694,9 +833,41 @@ function parseUnpublishRequest(reqPath) {
694
833
  version
695
834
  };
696
835
  }
836
+ function parseManifestRewrite(reqPath) {
837
+ const revIdx = reqPath.indexOf("/-rev/");
838
+ if (revIdx <= 0) return null;
839
+ const beforeRev = reqPath.slice(1, revIdx);
840
+ if (beforeRev.includes("/-/")) return null;
841
+ return { packageName: decodeURIComponent(beforeRev) };
842
+ }
697
843
  function createUnpublishHook(config, notifications) {
698
844
  const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
845
+ const handleManifestRewrite = (req, res) => {
846
+ const rewrite = parseManifestRewrite(req.path);
847
+ if (!rewrite) return;
848
+ const originalEnd = res.end.bind(res);
849
+ res.end = function(chunk, encoding, cb) {
850
+ if (res.statusCode >= 200 && res.statusCode < 300) {
851
+ const publishedBy = publisherFromRequest(req);
852
+ cdn.reconcileWithRegistry(rewrite.packageName).then((removed) => {
853
+ for (const version of removed) notifications.notifyUnpublish({
854
+ packageName: rewrite.packageName,
855
+ version,
856
+ publishedBy
857
+ });
858
+ }).catch((err) => {
859
+ console.error(`[registry] CDN reconcile failed for ${rewrite.packageName}:`, err);
860
+ });
861
+ }
862
+ return originalEnd(chunk, encoding, cb);
863
+ };
864
+ };
699
865
  return (req, res, next) => {
866
+ if (req.method === "PUT") {
867
+ handleManifestRewrite(req, res);
868
+ next();
869
+ return;
870
+ }
700
871
  if (req.method !== "DELETE") {
701
872
  next();
702
873
  return;
@@ -711,14 +882,10 @@ function createUnpublishHook(config, notifications) {
711
882
  if (res.statusCode >= 200 && res.statusCode < 300) try {
712
883
  if (parsed.version) cdn.invalidateVersion(parsed.packageName, parsed.version);
713
884
  else cdn.invalidate(parsed.packageName);
714
- const renownUser = req.renownUser;
715
885
  notifications.notifyUnpublish({
716
886
  packageName: parsed.packageName,
717
887
  version: parsed.version,
718
- publishedBy: renownUser ? {
719
- address: renownUser.address,
720
- did: renownUser.did
721
- } : void 0
888
+ publishedBy: publisherFromRequest(req)
722
889
  });
723
890
  } catch (err) {
724
891
  console.error(`[registry] CDN purge failed for ${parsed.packageName}${parsed.version ? `@${parsed.version}` : ""}:`, err);
@@ -748,11 +915,7 @@ function createPublishHook(config, notifications) {
748
915
  return originalEnd(chunk, encoding, cb);
749
916
  }
750
917
  if (versions.length > 1) console.warn(`[registry] Multiple versions published for ${packageName}: ${JSON.stringify(versions)}`);
751
- const renownUser = req.renownUser;
752
- const publishedBy = renownUser ? {
753
- address: renownUser.address,
754
- did: renownUser.did
755
- } : void 0;
918
+ const publishedBy = publisherFromRequest(req);
756
919
  cdn.extractTarball(packageName, version).then(() => {
757
920
  notifications.notifyPublish({
758
921
  packageName,
@@ -890,10 +1053,15 @@ function buildVerdaccioConfig(config) {
890
1053
  const htpasswdPath = path.join(config.storagePath, "htpasswd");
891
1054
  const uplinkUrl = config.uplink ?? "https://registry.npmjs.org/";
892
1055
  const usePgAuth = Boolean(config.databaseUrl || config.authStore);
1056
+ const storeToken = config.authStoreToken ?? (config.authStore ? stashAuthStore(config.authStore) : void 0);
893
1057
  const pluginsDir = config.pluginsDir ?? path.join(path.dirname(fileURLToPath(import.meta.url)), "plugins");
894
1058
  const auth = usePgAuth ? { "registry-auth": {
895
1059
  ...config.databaseUrl ? { databaseUrl: config.databaseUrl } : {},
896
- ...config.authStore ? { store: config.authStore } : {}
1060
+ ...storeToken ? { storeToken } : {},
1061
+ ...config.renown ? {
1062
+ publicUrl: config.renown.publicUrl,
1063
+ ...config.renown.renownUrl ? { renownUrl: config.renown.renownUrl } : {}
1064
+ } : {}
897
1065
  } } : { htpasswd: { file: htpasswdPath } };
898
1066
  const base = {
899
1067
  storage: config.storagePath,
@@ -973,18 +1141,21 @@ async function resolveDir(dir) {
973
1141
  return found;
974
1142
  }
975
1143
  async function runRegistry(args) {
976
- const { port, storageDir, cdnCacheDir, uplink, uplinkMaxage, webEnabled, webhooks, s3AccessKeyId, s3Bucket, s3Endpoint, s3ForcePathStyle, s3KeyPrefix, s3Region, s3SecretAccessKey, publicUrl, authRenown, verdaccioSecret: verdaccioSecretArg, localPackages, databaseUrl, pluginsDir, authStore } = args;
1144
+ const { port, storageDir, cdnCacheDir, uplink, uplinkMaxage, webEnabled, webhooks, s3AccessKeyId, s3Bucket, s3Endpoint, s3ForcePathStyle, s3KeyPrefix, s3Region, s3SecretAccessKey, publicUrl, authRenown, renownUrl, verdaccioSecret: verdaccioSecretArg, localPackages, databaseUrl, pluginsDir, authStore } = args;
977
1145
  const storagePath = await resolveDir(storageDir);
978
1146
  const cdnCachePath = await resolveDir(cdnCacheDir);
979
1147
  const verdaccioSecret = verdaccioSecretArg ?? randomBytes(32).toString("hex");
980
1148
  const renownEnabled = authRenown === true && Boolean(publicUrl);
981
1149
  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.");
1150
+ if (renownEnabled && !databaseUrl && !authStore) console.warn("[registry] Renown auth requires a database (--database-url) for the auth plugin; renown will be inactive.");
982
1151
  console.log({
983
1152
  storagePath,
984
1153
  cdnCachePath
985
1154
  });
986
1155
  const webhookConfigs = webhooks?.split(",").map((url) => url.trim()).filter(Boolean).map((endpoint) => ({ endpoint }));
987
1156
  const localPackagePatterns = localPackages?.split(",").map((p) => p.trim()).filter(Boolean);
1157
+ const sharedAuthStore = authStore ?? (databaseUrl ? createPgStore(createPgPool(databaseUrl)) : void 0);
1158
+ const authStoreToken = sharedAuthStore ? stashAuthStore(sharedAuthStore) : void 0;
988
1159
  const config = {
989
1160
  port,
990
1161
  storagePath,
@@ -994,7 +1165,10 @@ async function runRegistry(args) {
994
1165
  webEnabled,
995
1166
  verdaccioSecret,
996
1167
  ...localPackagePatterns?.length ? { localPackagePatterns } : {},
997
- ...renownEnabled && publicUrl ? { renown: { publicUrl } } : {},
1168
+ ...renownEnabled && publicUrl ? { renown: {
1169
+ publicUrl,
1170
+ ...renownUrl ? { renownUrl } : {}
1171
+ } } : {},
998
1172
  ...webhookConfigs?.length && { notify: { webhooks: webhookConfigs } },
999
1173
  ...s3Bucket && s3Endpoint && s3Region && { s3: {
1000
1174
  bucket: s3Bucket,
@@ -1007,23 +1181,25 @@ async function runRegistry(args) {
1007
1181
  } },
1008
1182
  ...databaseUrl ? { databaseUrl } : {},
1009
1183
  ...pluginsDir ? { pluginsDir } : {},
1010
- ...authStore ? { authStore } : {}
1184
+ ...sharedAuthStore ? { authStore: sharedAuthStore } : {},
1185
+ ...authStoreToken ? { authStoreToken } : {}
1011
1186
  };
1012
1187
  if (config.databaseUrl || config.authStore) console.log("[registry] Postgres-backed auth plugin active (persistent accounts + package ownership)");
1013
1188
  await mkdir(storagePath, { recursive: true });
1014
1189
  await mkdir(cdnCachePath, { recursive: true });
1015
- const verdaccioHandler = (await runServer(buildVerdaccioConfig(config))).listeners("request")[0];
1190
+ const verdaccioServer = await runServer(buildVerdaccioConfig(config));
1191
+ if (authStoreToken && !wasStoreLoaded(authStoreToken)) {
1192
+ verdaccioServer.close();
1193
+ throw new Error("registry-auth plugin failed to load despite a configured database/auth store; refusing to start without auth and package-ownership enforcement.");
1194
+ }
1195
+ const verdaccioHandler = verdaccioServer.listeners("request")[0];
1016
1196
  const app = express();
1017
1197
  const sseChannel = new SSEChannel();
1018
1198
  const webhookChannel = new WebhookChannel(config.storagePath, config.notify);
1019
1199
  const notifications = new NotificationManager([sseChannel, webhookChannel]);
1020
1200
  const staticDir = await findUp("static", { type: "directory" });
1021
1201
  if (staticDir) app.use("/-/static", express.static(staticDir));
1022
- app.use(createPowerhouseRouter(config, sseChannel, webhookChannel));
1023
- if (config.renown) app.use(createRenownAuthMiddleware({
1024
- publicUrl: config.renown.publicUrl,
1025
- verdaccioSecret
1026
- }));
1202
+ app.use(createPowerhouseRouter(config, sseChannel, webhookChannel, sharedAuthStore));
1027
1203
  app.use(createPublishHook(config, notifications));
1028
1204
  app.use(createUnpublishHook(config, notifications));
1029
1205
  app.use((req, res) => verdaccioHandler(req, res));
@@ -1140,6 +1316,13 @@ const registryCommand = command({
1140
1316
  defaultValue: () => process.env.PH_REGISTRY_AUTH_RENOWN === "true",
1141
1317
  defaultValueIsSerializable: true
1142
1318
  }),
1319
+ renownUrl: option({
1320
+ long: "renown-url",
1321
+ type: optional(string),
1322
+ description: "Renown service base URL for credential verification. Defaults to https://www.renown.id.",
1323
+ defaultValue: () => process.env.PH_REGISTRY_RENOWN_URL,
1324
+ defaultValueIsSerializable: true
1325
+ }),
1143
1326
  verdaccioSecret: option({
1144
1327
  long: "verdaccio-secret",
1145
1328
  type: optional(string),
@@ -1163,7 +1346,13 @@ const registryCommand = command({
1163
1346
  })
1164
1347
  },
1165
1348
  handler: async (args) => {
1166
- console.log(args);
1349
+ const redact = (v) => v ? "[redacted]" : void 0;
1350
+ console.log({
1351
+ ...args,
1352
+ databaseUrl: redact(args.databaseUrl),
1353
+ verdaccioSecret: redact(args.verdaccioSecret),
1354
+ s3SecretAccessKey: redact(args.s3SecretAccessKey)
1355
+ });
1167
1356
  try {
1168
1357
  await runRegistry(args);
1169
1358
  } catch (error) {
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":["#resolveFile","#extractWithLock","#extractionLocks","#channels","#clients","#broadcast","#storagePath","#predefined","#dynamic","#load","#save","#post","#filePath"],"sources":["../src/constants.ts","../src/auth/renown-middleware.ts","../src/semver.ts","../src/cdn.ts","../src/packages.ts","../src/warmup.ts","../src/middleware.ts","../src/notifications/manager.ts","../src/notifications/sse.ts","../src/notifications/webhook.ts","../src/verdaccio-config.ts","../src/run.ts","../cli.ts"],"sourcesContent":["export const DEFAULT_PORT = 8080;\nexport const DEFAULT_STORAGE_DIR_NAME = \"./storage\" as const;\nexport const DEFAULT_REGISTRY_CDN_CACHE_DIR_NAME = \"./cdn-cache\" as const;\n","import type { NextFunction, Request, Response } from \"express\";\nimport { signPayload } from \"@verdaccio/signature\";\nimport { verifyAuthBearerToken } from \"@renown/sdk/node\";\n\nexport interface RenownAuthOptions {\n /** This registry's expected `aud` claim — typically its public origin\n * (e.g. `https://registry.dev.vetra.io`). Tokens with a different audience\n * fall through as unauthenticated, so they cannot be replayed against a\n * different registry. */\n publicUrl: string;\n /** Verdaccio's top-level JWT signing secret. The middleware mints an\n * in-process verdaccio-format JWT signed with this secret so verdaccio's\n * built-in `apiJWTmiddleware` accepts the request. The token never leaves\n * this pod, so a per-pod random secret is fine. */\n verdaccioSecret: string;\n}\n\nexport interface RenownUser {\n address: string;\n did?: string;\n chainId?: number;\n networkId?: string;\n}\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n renownUser?: RenownUser;\n }\n }\n}\n\ninterface JwtPayloadShape {\n aud?: string | string[];\n iss?: string;\n exp?: number;\n}\n\nfunction audienceMatches(\n aud: string | string[] | undefined,\n expected: string,\n): boolean {\n if (!aud) return false;\n if (Array.isArray(aud)) return aud.includes(expected);\n return aud === expected;\n}\n\n/**\n * Translates a Renown-signed Bearer token (verifiable from the issuer's DID,\n * stateless) into a verdaccio-format Bearer token (signed with verdaccio's\n * own secret) that verdaccio's normal auth pipeline accepts.\n *\n * Falls through (calls `next()` without modifying auth) on any verification\n * failure: malformed token, bad signature, expired, audience mismatch, or\n * non-renown bearer token. This keeps the legacy htpasswd path usable during\n * the migration grace period — verdaccio's own apiJWTmiddleware sees the\n * original Authorization header and decides what to do with it.\n */\nexport function createRenownAuthMiddleware(opts: RenownAuthOptions) {\n const expectedAud = opts.publicUrl;\n\n return async (req: Request, _res: Response, next: NextFunction) => {\n const header = req.headers.authorization;\n if (!header?.startsWith(\"Bearer \")) {\n return next();\n }\n\n const token = header.slice(\"Bearer \".length).trim();\n if (!token) {\n return next();\n }\n\n let verified: Awaited<ReturnType<typeof verifyAuthBearerToken>>;\n try {\n // Pass `audience` so did-jwt validates the `aud` claim itself. Tokens\n // minted by ph-cli for this registry carry aud=publicUrl; if we don't\n // tell did-jwt what audience to accept, it throws\n // `invalid_config: JWT audience is required but your app address has\n // not been configured` and we silently fall through.\n verified = await verifyAuthBearerToken(token, { audience: expectedAud });\n } catch {\n return next();\n }\n if (!verified) {\n return next();\n }\n\n // Defence-in-depth: did-jwt's `audience` option already enforced this,\n // but verify the claim again in case verifyAuthBearerToken's behavior\n // ever changes (e.g. silently passing without checking).\n const payload = verified.payload as JwtPayloadShape | undefined;\n if (!audienceMatches(payload?.aud, expectedAud)) {\n return next();\n }\n\n const subject = verified.verifiableCredential.credentialSubject;\n if (!subject?.address) {\n return next();\n }\n\n const address = subject.address.toLowerCase();\n const groups = [\"$authenticated\", \"renown\"];\n\n let verdaccioJwt: string;\n try {\n verdaccioJwt = await signPayload(\n { name: address, real_groups: groups, groups } as any,\n opts.verdaccioSecret,\n { expiresIn: \"5m\" },\n );\n } catch (err) {\n console.error(\"[registry] failed to mint internal verdaccio token:\", err);\n return next();\n }\n\n req.headers.authorization = `Bearer ${verdaccioJwt}`;\n req.renownUser = {\n address,\n did: payload?.iss,\n chainId: subject.chainId,\n networkId: subject.networkId,\n };\n return next();\n };\n}\n","/**\n * Compare two semver version strings for sorting.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n *\n * Handles numeric component comparison (so \"1.0.10\" > \"1.0.9\")\n * and prerelease ordering (release > prerelease).\n */\nexport function compareSemver(a: string, b: string): number {\n const [coreA, preA] = a.split(\"-\", 2);\n const [coreB, preB] = b.split(\"-\", 2);\n\n const partsA = coreA.split(\".\").map(Number);\n const partsB = coreB.split(\".\").map(Number);\n\n for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {\n const na = partsA[i] ?? 0;\n const nb = partsB[i] ?? 0;\n if (na !== nb) return na - nb;\n }\n\n // Equal core versions — release (no prerelease) sorts after prerelease\n if (!preA && preB) return 1;\n if (preA && !preB) return -1;\n if (preA && preB) return preA < preB ? -1 : preA > preB ? 1 : 0;\n\n return 0;\n}\n","import crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { extract } from \"tar\";\nimport { compareSemver } from \"./semver.js\";\n\n/**\n * Parse a package specifier into name and version/tag.\n * Supports:\n * \"@scope/pkg\" -> { name: \"@scope/pkg\", tag: undefined }\n * \"@scope/pkg@dev\" -> { name: \"@scope/pkg\", tag: \"dev\" }\n * \"@scope/pkg@1.0.0\" -> { name: \"@scope/pkg\", tag: \"1.0.0\" }\n * \"pkg@latest\" -> { name: \"pkg\", tag: \"latest\" }\n */\nexport function parsePackageSpec(spec: string): {\n name: string;\n tag: string | undefined;\n} {\n // For scoped packages (@scope/name@tag), split on the last @\n // For unscoped packages (name@tag), split on the first @\n if (spec.startsWith(\"@\")) {\n // Scoped: find the @ after the scope/name portion\n const lastAt = spec.lastIndexOf(\"@\");\n if (lastAt > 0 && lastAt !== spec.indexOf(\"@\")) {\n return { name: spec.slice(0, lastAt), tag: spec.slice(lastAt + 1) };\n }\n return { name: spec, tag: undefined };\n }\n const atIndex = spec.indexOf(\"@\");\n if (atIndex > 0) {\n return { name: spec.slice(0, atIndex), tag: spec.slice(atIndex + 1) };\n }\n return { name: spec, tag: undefined };\n}\n\n/** True when tag is a concrete semver, false for dist-tag names or undefined. */\nexport function isExactVersion(tag?: string): boolean {\n // Strict semver charset: the version flows into the ETag header, so\n // arbitrary characters after the prerelease/build separator must not match.\n return (\n !!tag &&\n /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/.test(tag)\n );\n}\n\nexport class CdnCache {\n #extractionLocks = new Map<string, Promise<void>>();\n\n constructor(\n private registryUrl: string,\n private cdnCachePath: string,\n ) {}\n\n async getFileByVersion(\n packageName: string,\n version: string,\n filePath: string,\n ): Promise<string | null> {\n const versionDir = path.join(this.cdnCachePath, packageName, version);\n\n // Check all possible paths before attempting extraction\n const resolved = this.#resolveFile(versionDir, filePath);\n if (resolved) return resolved;\n\n // File not found in any location — extract tarball and try again\n await this.#extractWithLock(packageName, version);\n\n return this.#resolveFile(versionDir, filePath);\n }\n\n #resolveFile(versionDir: string, filePath: string): string | null {\n // Check direct path first, then fall back to cdn/ and dist/cdn/ subdirectories\n // (npm tarballs contain files under dist/, bun bundles go to cdn/)\n const candidates = [\n path.join(versionDir, filePath),\n path.join(versionDir, \"cdn\", filePath),\n path.join(versionDir, \"dist\", \"cdn\", filePath),\n path.join(versionDir, \"dist\", filePath),\n ];\n\n for (const candidate of candidates) {\n if (this.isSafePath(candidate) && fs.existsSync(candidate))\n return candidate;\n }\n\n return null;\n }\n\n async #extractWithLock(packageName: string, version: string): Promise<void> {\n const key = `${packageName}@${version}`;\n const existing = this.#extractionLocks.get(key);\n if (existing) return existing;\n\n const promise = this.extractTarball(packageName, version).finally(() => {\n this.#extractionLocks.delete(key);\n });\n this.#extractionLocks.set(key, promise);\n return promise;\n }\n\n getLatestCachedVersion(packageName: string): string | null {\n const pkgDir = path.join(this.cdnCachePath, packageName);\n try {\n const entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n const versions = entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name);\n if (versions.length === 0) return null;\n versions.sort(compareSemver);\n return versions[versions.length - 1];\n } catch {\n return null;\n }\n }\n\n /**\n * Resolve a version for a package. If tag is a semver version that exists\n * in the registry, return it directly. If tag is a dist-tag name (e.g.\n * \"dev\", \"latest\"), resolve it to the concrete version. If no tag is\n * provided, prefer \"latest\", then fall back to any available dist-tag.\n * Returns null only for genuine not-found (404, or absent tag/version).\n * Throws on network errors and non-OK responses other than 404.\n */\n async resolveVersion(\n packageName: string,\n tag?: string,\n ): Promise<string | null> {\n const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;\n const res = await fetch(url, {\n headers: { Accept: \"application/json\" },\n });\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `Upstream metadata for ${packageName} returned ${res.status}`,\n );\n }\n const metadata = (await res.json()) as Record<string, unknown>;\n const distTags = metadata[\"dist-tags\"] as\n | Record<string, string>\n | undefined;\n const versions = metadata[\"versions\"] as\n | Record<string, unknown>\n | undefined;\n\n if (tag) {\n // If the tag matches an exact version in the registry, use it directly\n if (versions && tag in versions) return tag;\n // Otherwise treat it as a dist-tag name\n if (distTags && tag in distTags) return distTags[tag];\n // Tag not found\n return null;\n }\n\n if (!distTags) return null;\n // No tag specified: prefer \"latest\", fall back to any available tag\n return distTags.latest ?? Object.values(distTags)[0] ?? null;\n }\n\n async extractTarball(packageName: string, version: string): Promise<void> {\n const destDir = path.join(this.cdnCachePath, packageName, version);\n\n // Idempotence guard. extractTarball is hot-path-called from\n // warmCdnCacheFromVerdaccio on every /packages request (which the\n // deployment's readiness probe hits every 5s). Without this skip the\n // tarball is re-fetched (multi-MB over S3) and re-extracted on each\n // call, pinning CPU at multi-vCPU per pod and triggering HPA spirals.\n // We treat the presence of package.json as the marker for \"already\n // extracted\" — it's the first file npm tarballs put under the version\n // directory and removing it (e.g. by invalidate*) requires the rest to\n // go too.\n if (fs.existsSync(path.join(destDir, \"package.json\"))) return;\n\n const shortName = packageName.startsWith(\"@\")\n ? packageName.split(\"/\")[1]\n : packageName;\n const tarballUrl = `${this.registryUrl}/${encodeURIComponent(packageName)}/-/${shortName}-${version}.tgz`;\n\n let res: Response;\n try {\n res = await fetch(tarballUrl);\n if (!res.ok || !res.body) return;\n } catch {\n return;\n }\n\n fs.mkdirSync(destDir, { recursive: true });\n\n const tmpFile = path.join(\n destDir,\n `.tmp-tarball-${crypto.randomUUID()}.tgz`,\n );\n try {\n const fileStream = fs.createWriteStream(tmpFile);\n await pipeline(Readable.fromWeb(res.body as never), fileStream);\n await extract({ file: tmpFile, cwd: destDir, strip: 1 });\n } finally {\n fs.rmSync(tmpFile, { force: true });\n }\n }\n\n invalidate(packageName: string): void {\n const cacheDir = path.join(this.cdnCachePath, packageName);\n if (!this.isSafePath(cacheDir)) return;\n fs.rmSync(cacheDir, { recursive: true, force: true });\n }\n\n invalidateVersion(packageName: string, version: string): void {\n const versionDir = path.join(this.cdnCachePath, packageName, version);\n if (!this.isSafePath(versionDir)) return;\n fs.rmSync(versionDir, { recursive: true, force: true });\n // If the package dir is now empty, remove it too so the scanner doesn't\n // keep returning a ghost entry with no versions.\n const pkgDir = path.join(this.cdnCachePath, packageName);\n try {\n if (fs.readdirSync(pkgDir).length === 0) {\n fs.rmdirSync(pkgDir);\n }\n } catch {\n // ignore — dir may not exist\n }\n }\n\n /** Remove all cached version directories except the specified one. */\n pruneOldVersions(packageName: string, keepVersion: string): void {\n const pkgDir = path.join(this.cdnCachePath, packageName);\n try {\n const entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && entry.name !== keepVersion) {\n const dir = path.join(pkgDir, entry.name);\n if (this.isSafePath(dir)) {\n fs.rmSync(dir, { recursive: true, force: true });\n }\n }\n }\n } catch {\n // ignore — directory may not exist yet\n }\n }\n\n private isSafePath(filePath: string): boolean {\n const resolved = path.resolve(filePath);\n const cacheRoot = path.resolve(this.cdnCachePath);\n return resolved.startsWith(cacheRoot + path.sep) || resolved === cacheRoot;\n }\n}\n","import type { Manifest } from \"@powerhousedao/shared\";\nimport { slimManifest } from \"@powerhousedao/shared/registry\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { compareSemver } from \"./semver.js\";\nimport type { PackageInfo } from \"./types.js\";\n\n/**\n * Read dist-tags, the full version list, and the local-publish flag for a\n * package from verdaccio's on-disk storage (`{storagePath}/{name}/package.json`).\n *\n * `locallyPublished` is tri-state:\n * - `true` → storage metadata has `_attachments` (tarball uploaded here).\n * - `false` → storage metadata exists but `_attachments` is empty (proxy\n * from the npm uplink only; no local publish at this registry).\n * - `undefined` → metadata file wasn't readable. Happens with non-filesystem\n * backends (S3, etc.) or if verdaccio stores metadata elsewhere.\n * Callers should treat this as \"unknown\" and default to including\n * the package, to avoid filtering the whole /packages list to an\n * empty array on deployments where we can't observe _attachments.\n */\nfunction readPackageMetadata(\n storagePath: string | undefined,\n packageName: string,\n): {\n distTags?: Record<string, string>;\n versions?: string[];\n locallyPublished: boolean | undefined;\n} {\n if (!storagePath) return { locallyPublished: undefined };\n try {\n const metadataPath = path.join(storagePath, packageName, \"package.json\");\n const raw = fs.readFileSync(metadataPath, \"utf-8\");\n const parsed = JSON.parse(raw) as {\n \"dist-tags\"?: Record<string, string>;\n versions?: Record<string, unknown>;\n _attachments?: Record<string, unknown>;\n };\n const distTags = parsed[\"dist-tags\"];\n const rawVersions = parsed.versions ? Object.keys(parsed.versions) : [];\n const versions = rawVersions.slice().sort(compareSemver);\n const locallyPublished =\n !!parsed._attachments && Object.keys(parsed._attachments).length > 0;\n return {\n distTags:\n distTags && Object.keys(distTags).length > 0 ? distTags : undefined,\n versions: versions.length > 0 ? versions : undefined,\n locallyPublished,\n };\n } catch {\n return { locallyPublished: undefined };\n }\n}\n\n/**\n * Locally-published check for a package, from verdaccio storage metadata.\n * Returns the same tri-state as `readPackageMetadata.locallyPublished`:\n * `true` (has `_attachments`), `false` (proxy-only), `undefined` (unreadable).\n */\nexport function isLocallyPublished(\n storagePath: string | undefined,\n packageName: string,\n): boolean | undefined {\n return readPackageMetadata(storagePath, packageName).locallyPublished;\n}\n\nfunction readManifest(dir: string): Manifest | null {\n const candidates = [\n path.join(dir, \"powerhouse.manifest.json\"),\n path.join(dir, \"cdn\", \"powerhouse.manifest.json\"),\n path.join(dir, \"dist\", \"powerhouse.manifest.json\"),\n ];\n for (const manifestPath of candidates) {\n try {\n const raw = fs.readFileSync(manifestPath, \"utf-8\");\n // Manifests are publisher-supplied JSON; slim to the known summary\n // fields so one oversized publish can't bloat every /packages\n // listing (a single 7.8 MB `features` blob once pushed the response\n // past clients' localStorage quota). The raw file stays available\n // through the CDN path.\n return slimManifest(JSON.parse(raw) as Manifest);\n } catch {\n // try next candidate\n }\n }\n return null;\n}\n\nfunction readPackageJsonVersion(dir: string): string | undefined {\n try {\n const raw = fs.readFileSync(path.join(dir, \"package.json\"), \"utf-8\");\n const pkg = JSON.parse(raw) as { version?: unknown };\n return typeof pkg.version === \"string\" ? pkg.version : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction getLatestVersionDir(pkgDir: string): string | null {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n } catch {\n return null;\n }\n const versions = entries.filter((e) => e.isDirectory()).map((e) => e.name);\n if (versions.length === 0) return null;\n versions.sort(compareSemver);\n return path.join(pkgDir, versions[versions.length - 1]);\n}\n\nexport function loadPackage(\n cdnCachePath: string,\n name: string,\n version?: string,\n): PackageInfo | null {\n const pkgDir = path.join(cdnCachePath, name);\n const versionDir = version\n ? path.join(pkgDir, version)\n : getLatestVersionDir(pkgDir);\n const manifestDir = versionDir ?? pkgDir;\n const manifest = readManifest(manifestDir);\n\n if (!manifest) {\n return null;\n }\n return {\n name: manifest.name || name,\n path: `/-/cdn/${name}`,\n manifest,\n documentTypes: getDocumentTypesFromManifest(manifest),\n version: readPackageJsonVersion(manifestDir),\n };\n}\n\nfunction getDocumentTypesFromManifest(manifest: Manifest | undefined | null) {\n if (!manifest) return [];\n\n const documentTypes: string[] = [];\n const { apps, documentModels, editors, subgraphs } = manifest;\n\n if (apps?.length) {\n documentTypes.push(\"powerhouse/document-drive\");\n }\n documentTypes.push(\n ...(documentModels ?? []).map((dm) => dm.id),\n ...(editors ?? [])\n .flatMap((e) => e.documentTypes)\n .filter((dt) => dt !== undefined),\n ...(subgraphs ?? [])\n .flatMap((e) => e.documentTypes)\n .filter((dt) => dt !== undefined),\n );\n\n return documentTypes;\n}\n\nexport function scanPackages(\n cdnCachePath: string,\n storagePath?: string,\n): PackageInfo[] {\n const absDir = path.resolve(cdnCachePath);\n const packages: PackageInfo[] = [];\n\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(absDir, { withFileTypes: true });\n } catch {\n return packages;\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n if (entry.name.startsWith(\"@\")) {\n const scopeDir = path.join(absDir, entry.name);\n let scopedEntries: fs.Dirent[];\n try {\n scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });\n } catch (error) {\n console.log(error);\n continue;\n }\n for (const scopedEntry of scopedEntries) {\n if (!scopedEntry.isDirectory()) continue;\n const dirName = `${entry.name}/${scopedEntry.name}`;\n const pkgDir = path.join(scopeDir, scopedEntry.name);\n const versionDir = getLatestVersionDir(pkgDir);\n const manifestDir = versionDir ?? pkgDir;\n const manifest = readManifest(manifestDir);\n // `||` (not `??`): slimManifest normalizes a missing manifest name to\n // \"\" — fall back to the directory name in that case too.\n const name = manifest?.name || dirName;\n const { distTags, versions, locallyPublished } = readPackageMetadata(\n storagePath,\n name,\n );\n // Drop npm-uplink passthroughs from the default listing. Only\n // skip when we can affirmatively tell the package is a proxy\n // (no `_attachments` in filesystem-backed storage). When the flag\n // is `undefined` (no storagePath, or non-filesystem backend where\n // we can't read verdaccio's metadata) we include the entry — the\n // alternative would be filtering everything to `[]` on S3 deploys.\n if (locallyPublished === false) continue;\n packages.push({\n name,\n path: `/-/cdn/${dirName}`,\n manifest,\n documentTypes: getDocumentTypesFromManifest(manifest),\n version: readPackageJsonVersion(manifestDir),\n distTags,\n versions,\n });\n }\n } else {\n const pkgDir = path.join(absDir, entry.name);\n const versionDir = getLatestVersionDir(pkgDir);\n const manifestDir = versionDir ?? pkgDir;\n const manifest = readManifest(manifestDir);\n const name = manifest?.name || entry.name;\n const { distTags, versions, locallyPublished } = readPackageMetadata(\n storagePath,\n name,\n );\n if (locallyPublished === false) continue;\n packages.push({\n name,\n path: `/-/cdn/${entry.name}`,\n manifest,\n documentTypes: getDocumentTypesFromManifest(manifest),\n version: readPackageJsonVersion(manifestDir),\n distTags,\n versions,\n });\n }\n }\n\n return packages;\n}\n\nexport function findPackagesByDocumentType(\n packagesDir: string,\n documentType: string,\n): PackageInfo[] {\n const allPackages = scanPackages(packagesDir);\n\n return allPackages.filter((pkg) => {\n if (!pkg.manifest?.documentModels) {\n return false;\n }\n return pkg.manifest.documentModels.some((dm) => dm.id === documentType);\n });\n}\n","import type { CdnCache } from \"./cdn.js\";\nimport { isLocallyPublished } from \"./packages.js\";\nimport type { RegistryConfig } from \"./types.js\";\n\n// The verdaccio listing already yields local-only, latest-per-name entries;\n// we re-filter by `_attachments` and dedupe as a guard against backend changes.\nconst WARM_INTERVAL_MS = 30_000;\nconst WARM_CONCURRENCY = 8;\n\ninterface VerdaccioPackage {\n name: string;\n version?: string;\n}\n\n/**\n * Build a throttled warmer that extracts locally-published package tarballs\n * into the CDN cache. 30s minimum interval plus an in-flight guard prevent\n * redundant fan-out from readiness-probe traffic across pods.\n */\nexport function createWarmer(\n config: RegistryConfig,\n cdn: CdnCache,\n): () => Promise<void> {\n let warmInFlight = false;\n let lastWarmAt = 0;\n\n return async function warm(): Promise<void> {\n if (warmInFlight) return;\n if (Date.now() - lastWarmAt < WARM_INTERVAL_MS) return;\n warmInFlight = true;\n try {\n const r = await fetch(\n `http://localhost:${config.port}/-/verdaccio/data/packages`,\n );\n if (!r.ok) {\n console.error(\n `[registry] verdaccio package listing returned ${r.status}`,\n );\n return;\n }\n const listed = (await r.json()) as VerdaccioPackage[];\n\n // Latest version per name (the listing yields one entry per package;\n // dedupe defensively), scoped to locally-published packages only.\n const latestByName = new Map<string, string>();\n for (const pkg of listed) {\n if (!pkg.version) continue;\n if (isLocallyPublished(config.storagePath, pkg.name) === false)\n continue;\n latestByName.set(pkg.name, pkg.version);\n }\n\n const targets = [...latestByName.entries()];\n let cursor = 0;\n const workers = Array.from({ length: WARM_CONCURRENCY }).map(async () => {\n while (cursor < targets.length) {\n const [name, version] = targets[cursor++];\n try {\n await cdn.extractTarball(name, version);\n } catch (err) {\n console.error(\n `[registry] failed to warm cache for ${name}@${version}:`,\n err,\n );\n }\n }\n });\n await Promise.all(workers);\n console.log(`[registry] /packages warm-up done (${targets.length} pkgs)`);\n // Throttle only after a successful cycle so failures (e.g. registry\n // not listening yet during startup) retry on the next call.\n lastWarmAt = Date.now();\n } catch (err) {\n console.error(\"[registry] /packages warm-up failed:\", err);\n } finally {\n warmInFlight = false;\n }\n };\n}\n","import express, {\n Router,\n type NextFunction,\n type Request,\n type Response,\n} from \"express\";\nimport crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport { CdnCache, isExactVersion, parsePackageSpec } from \"./cdn.js\";\nimport type { SSEChannel } from \"./notifications/sse.js\";\nimport type { NotificationChannel } from \"./notifications/types.js\";\nimport type { WebhookChannel } from \"./notifications/webhook.js\";\nimport {\n findPackagesByDocumentType,\n loadPackage,\n scanPackages,\n} from \"./packages.js\";\nimport type { RegistryConfig } from \"./types.js\";\nimport { createWarmer } from \"./warmup.js\";\n\nconst MIME_TYPES: Record<string, string> = {\n \".js\": \"application/javascript\",\n \".mjs\": \"application/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".wasm\": \"application/wasm\",\n \".map\": \"application/json\",\n \".html\": \"text/html\",\n \".svg\": \"image/svg+xml\",\n};\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase();\n return MIME_TYPES[ext] ?? \"application/octet-stream\";\n}\n\ntype VersionResolution =\n | { kind: \"ok\"; version: string }\n | { kind: \"not-found\" }\n | { kind: \"upstream-error\" };\n\n/**\n * Resolve a package version. Exact versions skip the network call. Upstream\n * errors fall back to the latest cached version; genuine not-found falls back\n * too, then reports not-found.\n */\nasync function resolvePackageVersion(\n cdn: CdnCache,\n packageName: string,\n tag: string | undefined,\n): Promise<VersionResolution> {\n if (tag && isExactVersion(tag)) return { kind: \"ok\", version: tag };\n\n try {\n const resolved =\n (await cdn.resolveVersion(packageName, tag)) ??\n cdn.getLatestCachedVersion(packageName);\n if (!resolved) return { kind: \"not-found\" };\n return { kind: \"ok\", version: resolved };\n } catch {\n const cached = cdn.getLatestCachedVersion(packageName);\n if (!cached) return { kind: \"upstream-error\" };\n return { kind: \"ok\", version: cached };\n }\n}\n\n/** Strip the weak-validator prefix: weak comparison is correct for GET/HEAD. */\nfunction opaqueTag(tag: string): string {\n return tag.startsWith(\"W/\") ? tag.slice(2) : tag;\n}\n\n/** RFC 9110 If-None-Match: a comma-separated list of entity-tags or \"*\". */\nfunction etagMatches(\n header: string | string[] | undefined,\n etag: string,\n): boolean {\n if (!header) return false;\n const target = opaqueTag(etag);\n const value = Array.isArray(header) ? header.join(\",\") : header;\n return value.split(\",\").some((candidate) => {\n const tag = candidate.trim();\n return tag === \"*\" || opaqueTag(tag) === target;\n });\n}\n\nexport function createPowerhouseRouter(\n config: RegistryConfig,\n sse: SSEChannel,\n webhooks: WebhookChannel,\n): Router {\n const cdn = new CdnCache(\n `http://localhost:${config.port}`,\n config.cdnCachePath,\n );\n const router = Router();\n\n // CORS on every response\n router.use((_req: Request, res: Response, next: NextFunction) => {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n next();\n });\n\n // SSE endpoint for publish notifications\n router.get(\"/-/events\", (_req: Request, res: Response) => {\n sse.addClient(res);\n });\n\n // Webhook management\n router.get(\"/-/webhooks\", (_req: Request, res: Response) => {\n res.json(webhooks.getWebhooks());\n });\n\n router.post(\"/-/webhooks\", express.json(), (req: Request, res: Response) => {\n const { endpoint, headers } = req.body as {\n endpoint?: string;\n headers?: Record<string, string>;\n };\n if (!endpoint) {\n res.status(400).json({ error: \"Missing required field: endpoint\" });\n return;\n }\n webhooks.addWebhook({ endpoint, headers });\n res.status(201).json({ endpoint, headers });\n });\n\n router.delete(\n \"/-/webhooks\",\n express.json(),\n (req: Request, res: Response) => {\n const { endpoint } = req.body as { endpoint?: string };\n if (!endpoint) {\n res.status(400).json({ error: \"Missing required field: endpoint\" });\n return;\n }\n const removed = webhooks.removeWebhook(endpoint);\n if (!removed) {\n res.status(404).json({ error: \"Webhook not found\" });\n return;\n }\n res.status(204).end();\n },\n );\n\n const warm = createWarmer(config, cdn);\n\n // Kick off an initial warm so /packages is useful soon after pod start\n // even if no clients hit it. Fire-and-forget — must not block the listener.\n void warm();\n\n // Package listing API.\n // Returns whatever's currently in the local cdn-cache (instant response —\n // important: this endpoint is wired to the deployment's readiness probe,\n // so it must not synchronously fetch or extract). Each call also nudges\n // a background warm-up so newly-published packages appear in the listing\n // without operator intervention.\n router.get(\"/packages\", (req: Request, res: Response) => {\n void warm();\n const packages = scanPackages(config.cdnCachePath, config.storagePath);\n const documentType = req.query.documentType as string | undefined;\n if (documentType) {\n const filtered = packages.filter((pkg) =>\n pkg.manifest?.documentModels?.some((m) => m.id === documentType),\n );\n res.json(filtered);\n return;\n }\n res.json(packages);\n });\n\n // Find packages by document type - returns array of package names\n router.get(\"/packages/by-document-type\", (req: Request, res: Response) => {\n const documentType = req.query.type;\n\n if (typeof documentType !== \"string\" || !documentType) {\n res.status(400).json({ error: \"Missing required query parameter: type\" });\n return;\n }\n\n const packages = findPackagesByDocumentType(\n config.cdnCachePath,\n documentType,\n );\n const packageNames = packages.map((pkg) => pkg.name);\n res.json(packageNames);\n });\n\n // Single package info\n router.get(\"/packages/*\", async (req: Request, res: Response) => {\n const raw = (req.params as Record<string, string>)[0];\n const { name, tag } = parsePackageSpec(raw);\n const resolution = await resolvePackageVersion(cdn, name, tag);\n if (resolution.kind === \"upstream-error\") {\n res.status(503).send(\"Upstream registry unavailable\");\n return;\n }\n const version = resolution.kind === \"ok\" ? resolution.version : undefined;\n const pkg = loadPackage(config.cdnCachePath, name, version);\n if (!pkg) {\n res.status(404).send(\"Package not found\");\n return;\n }\n res.json(pkg);\n });\n\n // CDN file serving\n router.get(\"/-/cdn/*\", async (req: Request, res: Response) => {\n const fullPath = (req.params as Record<string, string>)[0];\n\n // Parse scoped or unscoped package specifier from the path\n let packageSpec: string;\n let filePath: string;\n\n if (fullPath.startsWith(\"@\")) {\n // Scoped: @scope/pkg@1.0.0/file.js -> packageSpec = @scope/pkg@1.0.0, filePath = file.js\n const segments = fullPath.split(\"/\");\n if (segments.length < 2) {\n res.status(400).send(\"Invalid package path\");\n return;\n }\n packageSpec = `${segments[0]}/${segments[1]}`;\n filePath = segments.slice(2).join(\"/\") || \"index.js\";\n } else {\n // Unscoped: pkg@1.0.0/file.js -> packageSpec = pkg@1.0.0, filePath = file.js\n const segments = fullPath.split(\"/\");\n packageSpec = segments[0];\n filePath = segments.slice(1).join(\"/\") || \"index.js\";\n }\n\n const { name: packageName, tag } = parsePackageSpec(packageSpec);\n const pinned = isExactVersion(tag);\n const resolution = await resolvePackageVersion(cdn, packageName, tag);\n if (resolution.kind === \"upstream-error\") {\n res.status(503).send(\"Upstream registry unavailable\");\n return;\n }\n if (resolution.kind === \"not-found\") {\n res.status(404).send(\"File not found\");\n return;\n }\n const version = resolution.version;\n\n const resolved = await cdn.getFileByVersion(packageName, version, filePath);\n if (!resolved) {\n // Pinned requests skip the metadata lookup above, so a miss here may be\n // an upstream failure rather than a genuine 404 — probe to distinguish,\n // otherwise the CDN would cache a 404 while upstream is merely down.\n if (pinned) {\n try {\n await cdn.resolveVersion(packageName, tag);\n } catch {\n res.status(503).send(\"Upstream registry unavailable\");\n return;\n }\n }\n res.status(404).send(\"File not found\");\n return;\n }\n\n // Cache based on the request shape: pinned requests are immutable, moving\n // ones (dist-tag / untagged) must revalidate frequently.\n res.setHeader(\n \"Cache-Control\",\n pinned\n ? \"public, max-age=31536000, immutable\"\n : \"public, max-age=60, must-revalidate\",\n );\n\n // Hash the file path: it comes from the URL and may contain characters\n // that are invalid in header values.\n const fileHash = crypto\n .createHash(\"sha1\")\n .update(filePath)\n .digest(\"hex\")\n .slice(0, 16);\n const etag = `W/\"${version}-${fileHash}\"`;\n res.setHeader(\"ETag\", etag);\n if (etagMatches(req.headers[\"if-none-match\"], etag)) {\n res.status(304).end();\n return;\n }\n\n res.setHeader(\"Content-Type\", getContentType(filePath));\n try {\n await pipeline(fs.createReadStream(resolved), res);\n } catch {\n // Stream failure (I/O error, client abort) after headers may already\n // be sent — destroy the socket so the request doesn't hang.\n res.destroy();\n }\n });\n\n return router;\n}\n\n/**\n * Parse verdaccio's unpublish URL shape:\n * DELETE /<pkg>/-rev/<rev> → full package\n * DELETE /<pkg>/-/<tarball-name>/-rev/<rev> → single version\n * where <pkg> may be scoped (@scope%2Fname, encoded) or unscoped, and the\n * tarball name is `<short-name>-<version>.tgz`.\n */\nexport function parseUnpublishRequest(\n reqPath: string,\n): { packageName: string; version: string | null } | null {\n const revIdx = reqPath.indexOf(\"/-rev/\");\n if (revIdx <= 0) return null;\n const beforeRev = reqPath.slice(1, revIdx); // strip leading slash\n\n const tarballMarker = \"/-/\";\n const tarballIdx = beforeRev.indexOf(tarballMarker);\n if (tarballIdx === -1) {\n // Full package: beforeRev is the package name (possibly URL-encoded scope)\n const packageName = decodeURIComponent(beforeRev);\n return { packageName, version: null };\n }\n\n const packageName = decodeURIComponent(beforeRev.slice(0, tarballIdx));\n const tarballName = beforeRev.slice(tarballIdx + tarballMarker.length);\n if (!tarballName.endsWith(\".tgz\")) return null;\n const shortName = packageName.startsWith(\"@\")\n ? packageName.split(\"/\")[1]\n : packageName;\n const prefix = `${shortName}-`;\n if (!tarballName.startsWith(prefix)) return null;\n const version = tarballName.slice(prefix.length, -\".tgz\".length);\n if (!version) return null;\n return { packageName, version };\n}\n\nexport function createUnpublishHook(\n config: RegistryConfig,\n notifications: NotificationChannel,\n) {\n const cdn = new CdnCache(\n `http://localhost:${config.port}`,\n config.cdnCachePath,\n );\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (req.method !== \"DELETE\") {\n next();\n return;\n }\n\n const parsed = parseUnpublishRequest(req.path);\n if (!parsed) {\n next();\n return;\n }\n\n const originalEnd = res.end.bind(res);\n res.end = function (\n this: Response,\n chunk?: unknown,\n encoding?: unknown,\n cb?: () => void,\n ) {\n if (res.statusCode >= 200 && res.statusCode < 300) {\n try {\n if (parsed.version) {\n cdn.invalidateVersion(parsed.packageName, parsed.version);\n } else {\n cdn.invalidate(parsed.packageName);\n }\n const renownUser = req.renownUser;\n notifications.notifyUnpublish({\n packageName: parsed.packageName,\n version: parsed.version,\n publishedBy: renownUser\n ? { address: renownUser.address, did: renownUser.did }\n : undefined,\n });\n } catch (err) {\n console.error(\n `[registry] CDN purge failed for ${parsed.packageName}${parsed.version ? `@${parsed.version}` : \"\"}:`,\n err,\n );\n }\n }\n return originalEnd(chunk, encoding as BufferEncoding, cb);\n };\n\n next();\n };\n}\n\nexport function createPublishHook(\n config: RegistryConfig,\n notifications: NotificationChannel,\n) {\n const cdn = new CdnCache(\n `http://localhost:${config.port}`,\n config.cdnCachePath,\n );\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Only intercept PUT requests to npm publish endpoints.\n // Skip PUTs to `/<pkg>/-rev/<rev>` — those are npm's manifest-rewrite\n // step during single-version unpublish, not a new publish.\n if (req.method !== \"PUT\" || req.path.includes(\"/-rev/\")) {\n next();\n return;\n }\n\n const originalEnd = res.end.bind(res);\n res.end = function (\n this: Response,\n chunk?: unknown,\n encoding?: unknown,\n cb?: () => void,\n ) {\n const urlPath = req.path.replace(/^\\//, \"\");\n if (\n res.statusCode < 200 ||\n res.statusCode >= 300 ||\n !urlPath ||\n urlPath.startsWith(\"-\")\n ) {\n return originalEnd(chunk, encoding as BufferEncoding, cb);\n }\n const packageName = decodeURIComponent(urlPath);\n const versionsObj = (req.body as { versions: Record<string, unknown> })\n .versions;\n const versions = Object.keys(versionsObj);\n const version = versions.at(0);\n if (!version) {\n console.error(`[registry] No version found for ${packageName}`);\n return originalEnd(chunk, encoding as BufferEncoding, cb);\n }\n if (versions.length > 1) {\n console.warn(\n `[registry] Multiple versions published for ${packageName}: ${JSON.stringify(versions)}`,\n );\n }\n\n const renownUser = req.renownUser;\n const publishedBy = renownUser\n ? { address: renownUser.address, did: renownUser.did }\n : undefined;\n cdn\n .extractTarball(packageName, version)\n .then(() => {\n notifications.notifyPublish({ packageName, version, publishedBy });\n })\n .catch((err) => {\n console.error(\n `[registry] Failed to extract ${packageName} to CDN cache:`,\n err,\n );\n });\n\n return originalEnd(chunk, encoding as BufferEncoding, cb);\n };\n\n next();\n };\n}\n","import type {\n NotificationChannel,\n PublishEvent,\n UnpublishEvent,\n} from \"./types.js\";\n\nexport class NotificationManager implements NotificationChannel {\n #channels: NotificationChannel[];\n\n constructor(channels: NotificationChannel[]) {\n this.#channels = channels;\n }\n\n notifyPublish(event: PublishEvent): void {\n for (const channel of this.#channels) {\n channel.notifyPublish(event);\n }\n }\n\n notifyUnpublish(event: UnpublishEvent): void {\n for (const channel of this.#channels) {\n channel.notifyUnpublish(event);\n }\n }\n}\n","import type { Response } from \"express\";\nimport type {\n NotificationChannel,\n PublishEvent,\n UnpublishEvent,\n} from \"./types.js\";\n\nexport class SSEChannel implements NotificationChannel {\n #clients = new Set<Response>();\n\n addClient(res: Response): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n res.write(\"event: connected\\ndata: {}\\n\\n\");\n\n this.#clients.add(res);\n res.on(\"close\", () => {\n this.#clients.delete(res);\n });\n }\n\n notifyPublish(event: PublishEvent): void {\n this.#broadcast(\"publish\", event);\n }\n\n notifyUnpublish(event: UnpublishEvent): void {\n this.#broadcast(\"unpublish\", event);\n }\n\n #broadcast(eventName: string, event: PublishEvent | UnpublishEvent): void {\n const payload = `event: ${eventName}\\ndata: ${JSON.stringify(event)}\\n\\n`;\n for (const client of this.#clients) {\n try {\n client.write(payload);\n } catch (err) {\n console.error(\"[registry] SSE client write failed:\", err);\n this.#clients.delete(client);\n }\n }\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { NotifyConfig, WebhookConfig } from \"../types.js\";\nimport type {\n NotificationChannel,\n PublishEvent,\n UnpublishEvent,\n} from \"./types.js\";\n\nconst WEBHOOKS_FILE = \"webhooks.json\";\n\nexport class WebhookChannel implements NotificationChannel {\n #predefined: WebhookConfig[];\n #dynamic: WebhookConfig[];\n #storagePath: string;\n\n constructor(storagePath: string, config?: NotifyConfig) {\n this.#storagePath = storagePath;\n this.#predefined = config?.webhooks ?? [];\n this.#dynamic = this.#load();\n }\n\n getWebhooks(): WebhookConfig[] {\n return [...this.#predefined, ...this.#dynamic];\n }\n\n addWebhook(webhook: WebhookConfig): void {\n const exists = this.getWebhooks().some(\n (w) => w.endpoint === webhook.endpoint,\n );\n if (exists) return;\n this.#dynamic.push(webhook);\n this.#save();\n }\n\n removeWebhook(endpoint: string): boolean {\n const before = this.#dynamic.length;\n this.#dynamic = this.#dynamic.filter((w) => w.endpoint !== endpoint);\n if (this.#dynamic.length === before) return false;\n this.#save();\n return true;\n }\n\n notifyPublish(event: PublishEvent): void {\n this.#post({ type: \"publish\", ...event });\n }\n\n notifyUnpublish(event: UnpublishEvent): void {\n this.#post({ type: \"unpublish\", ...event });\n }\n\n #post(body: Record<string, unknown>): void {\n for (const webhook of this.getWebhooks()) {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...webhook.headers,\n };\n\n fetch(webhook.endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n }).catch((err: unknown) => {\n console.error(`[registry] Webhook to ${webhook.endpoint} failed:`, err);\n });\n }\n }\n\n #filePath(): string {\n return path.join(this.#storagePath, WEBHOOKS_FILE);\n }\n\n #load(): WebhookConfig[] {\n try {\n const raw = fs.readFileSync(this.#filePath(), \"utf-8\");\n return JSON.parse(raw) as WebhookConfig[];\n } catch {\n return [];\n }\n }\n\n #save(): void {\n fs.mkdirSync(this.#storagePath, { recursive: true });\n fs.writeFileSync(this.#filePath(), JSON.stringify(this.#dynamic, null, 2));\n }\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { RegistryConfig } from \"./types.js\";\n\nexport function buildVerdaccioConfig(config: RegistryConfig) {\n const htpasswdPath = path.join(config.storagePath, \"htpasswd\");\n\n const uplinkUrl = config.uplink ?? \"https://registry.npmjs.org/\";\n\n // With a database configured, use the Postgres-backed auth plugin\n // (persistent accounts + npm-style package ownership). Verdaccio loads it\n // via require() from the dist/plugins dir. Without a DB (local dev / tests),\n // keep the built-in htpasswd path.\n const usePgAuth = Boolean(config.databaseUrl || config.authStore);\n const pluginsDir =\n config.pluginsDir ??\n path.join(path.dirname(fileURLToPath(import.meta.url)), \"plugins\");\n const auth = usePgAuth\n ? {\n \"registry-auth\": {\n ...(config.databaseUrl ? { databaseUrl: config.databaseUrl } : {}),\n ...(config.authStore ? { store: config.authStore } : {}),\n },\n }\n : { htpasswd: { file: htpasswdPath } };\n\n const base: Record<string, unknown> = {\n storage: config.storagePath,\n self_path: \"./\",\n // Top-level secret used by verdaccio to sign / verify its API JWTs.\n // The renown middleware mints a verdaccio-format JWT with the same\n // secret so verdaccio's apiJWTmiddleware accepts the swapped token.\n ...(config.verdaccioSecret ? { secret: config.verdaccioSecret } : {}),\n // Force JWT mode for the npm API. Without this verdaccio falls back to\n // its legacy aes-encrypted token format, which signPayload won't produce.\n security: {\n api: {\n jwt: {\n sign: { expiresIn: \"90d\" },\n verify: {},\n },\n },\n },\n auth,\n ...(usePgAuth ? { plugins: pluginsDir } : {}),\n uplinks: {\n npmjs: {\n url: uplinkUrl,\n // Defaults to verdaccio's own default of 2m. The previous 15m\n // hardcoded value made publish-to-install dev loops painful —\n // when a newly-published version landed on npmjs, our registry\n // kept handing out the pre-publish packument for up to 15min.\n // Operators that want heavier upstream caching for production\n // can opt in via --uplink-maxage / PH_REGISTRY_UPLINK_MAXAGE.\n maxage: config.uplinkMaxage ?? \"2m\",\n timeout: \"30s\",\n cache: true,\n },\n },\n // Verdaccio matches packages config top-to-bottom (first match wins),\n // so local-only globs must come first. We also skip emitting the\n // default proxied entries when a glob with the same key is in the\n // local-only list — otherwise the value would be overwritten but the\n // iteration position would still be the default's earlier slot.\n packages: (() => {\n const local = config.localPackagePatterns ?? [];\n const localSet = new Set(local);\n const access = {\n access: \"$all\",\n publish: \"$authenticated\",\n unpublish: \"$authenticated\",\n };\n const entries: [string, Record<string, unknown>][] = [];\n // Locals first — no proxy means verdaccio resolves them from local\n // storage only, so re-publishing a version that exists on npmjs\n // doesn't 409.\n for (const pattern of local) {\n entries.push([pattern, { ...access }]);\n }\n // Defaults follow, skipping any glob the caller already overrode.\n if (!localSet.has(\"@powerhousedao/*\")) {\n entries.push([\"@powerhousedao/*\", { ...access, proxy: \"npmjs\" }]);\n }\n if (!localSet.has(\"**\")) {\n entries.push([\"**\", { ...access, proxy: \"npmjs\" }]);\n }\n return Object.fromEntries(entries);\n })(),\n web: {\n enable: config.webEnabled !== false,\n title: \"Powerhouse Registry\",\n logo: \"https://raw.githubusercontent.com/powerhouse-inc/powerhouse/main/packages/registry/static/logo.svg\",\n favicon: \"/-/static/favicon.ico\",\n primary_color: \"#38C780\",\n darkMode: true,\n },\n server: {\n keepAliveTimeout: 60,\n },\n log: {\n type: \"stdout\",\n format: \"pretty\",\n level: \"warn\",\n },\n max_body_size: config.maxBodySize ?? \"300mb\",\n };\n\n if (config.s3) {\n base.store = {\n \"aws-s3-storage\": {\n bucket: config.s3.bucket,\n endpoint: config.s3.endpoint,\n region: config.s3.region,\n s3ForcePathStyle: config.s3.s3ForcePathStyle ?? true,\n ...(config.s3.keyPrefix && { keyPrefix: config.s3.keyPrefix }),\n ...(config.s3.accessKeyId && { accessKeyId: config.s3.accessKeyId }),\n ...(config.s3.secretAccessKey && {\n secretAccessKey: config.s3.secretAccessKey,\n }),\n },\n };\n }\n\n return base;\n}\n","import express from \"express\";\nimport { findUp } from \"find-up\";\nimport { randomBytes } from \"node:crypto\";\nimport { mkdir } from \"node:fs/promises\";\nimport type { Server } from \"node:http\";\nimport path from \"node:path\";\nimport { runServer } from \"verdaccio\";\nimport { createRenownAuthMiddleware } from \"./auth/renown-middleware.js\";\nimport {\n createPowerhouseRouter,\n createPublishHook,\n createUnpublishHook,\n} from \"./middleware.js\";\nimport { NotificationManager } from \"./notifications/manager.js\";\nimport { SSEChannel } from \"./notifications/sse.js\";\nimport { WebhookChannel } from \"./notifications/webhook.js\";\nimport type { RegistryCommandArgs, RegistryConfig } from \"./types.js\";\nimport { buildVerdaccioConfig } from \"./verdaccio-config.js\";\n\nasync function resolveDir(dir: string): Promise<string> {\n if (path.isAbsolute(dir)) {\n await mkdir(dir, { recursive: true });\n return dir;\n }\n const found = await findUp(dir, { type: \"directory\" });\n if (!found) {\n await mkdir(dir, { recursive: true });\n return dir;\n }\n return found;\n}\n\nexport async function runRegistry(args: RegistryCommandArgs) {\n const {\n port,\n storageDir,\n cdnCacheDir,\n uplink,\n uplinkMaxage,\n webEnabled,\n webhooks,\n s3AccessKeyId,\n s3Bucket,\n s3Endpoint,\n s3ForcePathStyle,\n s3KeyPrefix,\n s3Region,\n s3SecretAccessKey,\n publicUrl,\n authRenown,\n verdaccioSecret: verdaccioSecretArg,\n localPackages,\n databaseUrl,\n pluginsDir,\n authStore,\n } = args;\n const storagePath = await resolveDir(storageDir);\n const cdnCachePath = await resolveDir(cdnCacheDir);\n\n // Per-pod random verdaccio JWT secret. The verdaccio-format token we mint\n // in the renown middleware never leaves the pod (it's swapped into the\n // request before verdaccio sees it), so a per-pod secret is sufficient.\n // An override is exposed for tests / multi-pod behaviors that depend on\n // shared verdaccio JWTs.\n const verdaccioSecret = verdaccioSecretArg ?? randomBytes(32).toString(\"hex\");\n\n // Renown auth turns on when the operator both opts in (`--auth-renown`,\n // default true via the CLI flag) and has set --public-url for the audience\n // claim. Tests / programmatic users that don't pass either keep the legacy\n // unsigned/htpasswd path with no warning.\n const renownEnabled = authRenown === true && Boolean(publicUrl);\n if (authRenown === true && !publicUrl) {\n console.warn(\n \"[registry] auth-renown is enabled but --public-url / PH_REGISTRY_PUBLIC_URL is not set; Renown auth will be disabled.\",\n );\n }\n\n console.log({\n storagePath,\n cdnCachePath,\n });\n\n const webhookConfigs = webhooks\n ?.split(\",\")\n .map((url) => url.trim())\n .filter(Boolean)\n .map((endpoint) => ({ endpoint }));\n\n const localPackagePatterns = localPackages\n ?.split(\",\")\n .map((p) => p.trim())\n .filter(Boolean);\n\n const config: RegistryConfig = {\n port,\n storagePath,\n cdnCachePath,\n uplink,\n uplinkMaxage,\n webEnabled,\n verdaccioSecret,\n ...(localPackagePatterns?.length ? { localPackagePatterns } : {}),\n ...(renownEnabled && publicUrl ? { renown: { publicUrl } } : {}),\n ...(webhookConfigs?.length && {\n notify: { webhooks: webhookConfigs },\n }),\n ...(s3Bucket &&\n s3Endpoint &&\n s3Region && {\n s3: {\n bucket: s3Bucket,\n endpoint: s3Endpoint,\n region: s3Region,\n accessKeyId: s3AccessKeyId,\n secretAccessKey: s3SecretAccessKey,\n keyPrefix: s3KeyPrefix,\n s3ForcePathStyle,\n },\n }),\n ...(databaseUrl ? { databaseUrl } : {}),\n ...(pluginsDir ? { pluginsDir } : {}),\n ...(authStore ? { authStore } : {}),\n };\n\n if (config.databaseUrl || config.authStore) {\n console.log(\n \"[registry] Postgres-backed auth plugin active (persistent accounts + package ownership)\",\n );\n }\n // Ensure directories exist (for relative paths resolved via findUp)\n await mkdir(storagePath, { recursive: true });\n await mkdir(cdnCachePath, { recursive: true });\n\n const verdaccioConfig = buildVerdaccioConfig(config);\n\n // verdaccio's runServer returns Promise<any> (upstream type limitation)\n const verdaccioServer = (await runServer(verdaccioConfig)) as Server;\n const verdaccioHandler = verdaccioServer.listeners(\"request\")[0] as (\n ...args: unknown[]\n ) => void;\n\n const app = express();\n\n const sseChannel = new SSEChannel();\n const webhookChannel = new WebhookChannel(config.storagePath, config.notify);\n const notifications = new NotificationManager([sseChannel, webhookChannel]);\n\n // Serve static assets (logo, etc.)\n const staticDir = await findUp(\"static\", { type: \"directory\" });\n if (staticDir) {\n app.use(\"/-/static\", express.static(staticDir));\n }\n\n // Our routes take priority over Verdaccio\n app.use(createPowerhouseRouter(config, sseChannel, webhookChannel));\n\n // Renown bearer-token auth runs before the publish/unpublish hooks so they\n // see `req.renownUser`, and before verdaccio so the swapped Authorization\n // header reaches verdaccio's apiJWTmiddleware.\n if (config.renown) {\n app.use(\n createRenownAuthMiddleware({\n publicUrl: config.renown.publicUrl,\n verdaccioSecret,\n }),\n );\n }\n\n app.use(createPublishHook(config, notifications));\n app.use(createUnpublishHook(config, notifications));\n\n // Verdaccio handles everything else (npm protocol, web UI, auth)\n app.use((req, res) => verdaccioHandler(req, res));\n\n const server = app.listen(port, () => {\n console.log(`Powerhouse Registry running on http://localhost:${port}`);\n console.log(` CDN: http://localhost:${port}/-/cdn/`);\n console.log(` Packages: http://localhost:${port}/packages`);\n console.log(` npm: http://localhost:${port}/`);\n console.log(` Storage: ${storagePath}`);\n console.log(` CDN cache: ${cdnCachePath}`);\n if (config.s3) {\n console.log(` S3: ${config.s3.endpoint}/${config.s3.bucket}`);\n }\n if (config.renown) {\n console.log(` Renown auth: ${config.renown.publicUrl}`);\n }\n });\n\n return server;\n}\n","import {\n binary,\n command,\n flag,\n number,\n option,\n optional,\n run,\n string,\n} from \"cmd-ts\";\nimport {\n DEFAULT_PORT,\n DEFAULT_REGISTRY_CDN_CACHE_DIR_NAME,\n DEFAULT_STORAGE_DIR_NAME,\n} from \"./src/constants.js\";\nimport { runRegistry } from \"./src/run.js\";\n\nexport const registryCommand = command({\n name: \"Package registry\",\n args: {\n port: option({\n long: \"port\",\n type: number,\n defaultValue: () => Number(process.env.PORT) || DEFAULT_PORT,\n defaultValueIsSerializable: true,\n }),\n storageDir: option({\n long: \"storage-dir\",\n type: string,\n defaultValue: () =>\n process.env.REGISTRY_STORAGE || DEFAULT_STORAGE_DIR_NAME,\n defaultValueIsSerializable: true,\n }),\n cdnCacheDir: option({\n long: \"cdn-cache-dir\",\n type: string,\n defaultValue: () =>\n process.env.REGISTRY_CDN_CACHE || DEFAULT_REGISTRY_CDN_CACHE_DIR_NAME,\n defaultValueIsSerializable: true,\n }),\n uplink: option({\n long: \"uplink\",\n type: optional(string),\n defaultValue: () => process.env.REGISTRY_UPLINK,\n defaultValueIsSerializable: true,\n }),\n uplinkMaxage: option({\n long: \"uplink-maxage\",\n type: optional(string),\n description:\n \"How long verdaccio caches npmjs uplink metadata before refetching. \" +\n \"Accepts verdaccio time strings (e.g. '30s', '2m', '1h'). \" +\n \"Default '2m' matches verdaccio upstream — shortens the publish-to-\" +\n \"install propagation window in dev. Bump for production deployments \" +\n \"that want to reduce npmjs load.\",\n defaultValue: () => process.env.PH_REGISTRY_UPLINK_MAXAGE,\n defaultValueIsSerializable: true,\n }),\n s3Bucket: option({\n long: \"s3-bucket\",\n type: optional(string),\n defaultValue: () => process.env.S3_BUCKET,\n defaultValueIsSerializable: true,\n }),\n s3Endpoint: option({\n long: \"s3-endpoint\",\n type: optional(string),\n defaultValue: () => process.env.S3_ENDPOINT,\n defaultValueIsSerializable: true,\n }),\n s3Region: option({\n long: \"s3-region\",\n type: optional(string),\n defaultValue: () => process.env.S3_REGION,\n defaultValueIsSerializable: true,\n }),\n s3AccessKeyId: option({\n long: \"s3-access-key-id\",\n type: optional(string),\n defaultValue: () => process.env.S3_ACCESS_KEY_ID,\n defaultValueIsSerializable: true,\n }),\n s3SecretAccessKey: option({\n long: \"s3-secret-access-key\",\n type: optional(string),\n defaultValue: () => process.env.S3_SECRET_ACCESS_KEY,\n defaultValueIsSerializable: true,\n }),\n s3KeyPrefix: option({\n long: \"s3-key-prefix\",\n type: optional(string),\n defaultValue: () => process.env.S3_KEY_PREFIX,\n defaultValueIsSerializable: true,\n }),\n s3ForcePathStyle: flag({\n long: \"s3-force-path-style\",\n defaultValue: () => process.env.S3_FORCE_PATH_STYLE !== \"false\",\n defaultValueIsSerializable: true,\n }),\n webEnabled: flag({\n long: \"web-enabled\",\n defaultValue: () => process.env.REGISTRY_WEB !== \"false\",\n defaultValueIsSerializable: true,\n }),\n webhooks: option({\n long: \"webhook\",\n type: optional(string),\n description: \"Comma-separated webhook URLs to notify on publish\",\n defaultValue: () => process.env.REGISTRY_WEBHOOKS,\n defaultValueIsSerializable: true,\n }),\n publicUrl: option({\n long: \"public-url\",\n type: optional(string),\n description:\n \"Public origin of this registry (used as the JWT `aud` claim for Renown bearer tokens). Required when --auth-renown is true.\",\n defaultValue: () => process.env.PH_REGISTRY_PUBLIC_URL,\n defaultValueIsSerializable: true,\n }),\n authRenown: flag({\n long: \"auth-renown\",\n description:\n \"Verify Renown-signed bearer tokens in front of verdaccio (stateless). Disabled when --public-url is unset.\",\n defaultValue: () => process.env.PH_REGISTRY_AUTH_RENOWN === \"true\",\n defaultValueIsSerializable: true,\n }),\n verdaccioSecret: option({\n long: \"verdaccio-secret\",\n type: optional(string),\n description:\n \"Override verdaccio's internal JWT signing secret. Default: random per pod (fine — the swapped JWT never leaves this process).\",\n defaultValue: () => process.env.PH_REGISTRY_VERDACCIO_SECRET,\n defaultValueIsSerializable: true,\n }),\n localPackages: option({\n long: \"local-packages\",\n type: optional(string),\n description:\n \"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.\",\n defaultValue: () => process.env.PH_REGISTRY_LOCAL_PACKAGES,\n defaultValueIsSerializable: true,\n }),\n databaseUrl: option({\n long: \"database-url\",\n type: optional(string),\n description:\n \"Postgres connection string. When set, the registry uses the DB-backed auth plugin (persistent accounts + npm-style package ownership) instead of the built-in htpasswd.\",\n defaultValue: () =>\n process.env.PH_REGISTRY_DATABASE_URL ?? process.env.DATABASE_URL,\n defaultValueIsSerializable: true,\n }),\n },\n handler: async (args) => {\n console.log(args);\n\n try {\n await runRegistry(args);\n } catch (error) {\n console.error(\"Failed to start registry:\");\n console.error(error);\n process.exit(1);\n }\n },\n});\n\nconst registryCli = binary(registryCommand);\n\nawait run(registryCli, process.argv);\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":["#resolveFile","#extractWithLock","#extractionLocks","#channels","#clients","#broadcast","#storagePath","#predefined","#dynamic","#load","#save","#post","#filePath"],"sources":["../src/constants.ts","../src/auth/pg-store.ts","../src/auth/store-handoff.ts","../src/semver.ts","../src/cdn.ts","../src/packages.ts","../src/warmup.ts","../src/middleware.ts","../src/notifications/manager.ts","../src/notifications/sse.ts","../src/notifications/webhook.ts","../src/verdaccio-config.ts","../src/run.ts","../cli.ts"],"sourcesContent":["export const DEFAULT_PORT = 8080;\nexport const DEFAULT_STORAGE_DIR_NAME = \"./storage\" as const;\nexport const DEFAULT_REGISTRY_CDN_CACHE_DIR_NAME = \"./cdn-cache\" as const;\n","import { Pool } from \"pg\";\nimport type { AuthStore, UserRecord } from \"./auth-store.js\";\n\n/** Build a real Postgres pool from a connection string. */\nexport function createPgPool(databaseUrl: string): Pool {\n return new Pool({ connectionString: databaseUrl });\n}\n\n/**\n * Postgres-backed AuthStore. Two small tables:\n * - registry_users(username PK, password_hash, created_at)\n * - registry_package_owners(package_name PK, owners text[], claimed_at)\n *\n * Ownership claim is race-free: `INSERT ... ON CONFLICT DO NOTHING` means the\n * first publisher wins atomically; the follow-up read returns the actual\n * owners so a losing racer is denied.\n *\n * Takes an already-built `pg.Pool` (tests inject a pg-mem pool cast to Pool).\n */\nexport function createPgStore(pool: Pool): AuthStore {\n let initialized: Promise<void> | null = null;\n\n return {\n init(): Promise<void> {\n initialized ??= (async () => {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS registry_users (\n username text PRIMARY KEY,\n password_hash text NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now()\n )`);\n await pool.query(`\n CREATE TABLE IF NOT EXISTS registry_package_owners (\n package_name text PRIMARY KEY,\n owners text[] NOT NULL,\n claimed_at timestamptz NOT NULL DEFAULT now()\n )`);\n })();\n return initialized;\n },\n\n async getUser(username: string): Promise<UserRecord | null> {\n const res = await pool.query<{ password_hash: string }>(\n \"SELECT password_hash FROM registry_users WHERE username = $1\",\n [username],\n );\n const row = res.rows[0] as { password_hash: string } | undefined;\n return row ? { passwordHash: row.password_hash } : null;\n },\n\n async createUser(username: string, passwordHash: string): Promise<boolean> {\n // Atomic: the PRIMARY KEY enforces uniqueness. A duplicate raises a\n // unique-violation (SQLSTATE 23505) — catch it as \"already registered\".\n try {\n await pool.query(\n \"INSERT INTO registry_users (username, password_hash) VALUES ($1, $2)\",\n [username, passwordHash],\n );\n return true;\n } catch (err) {\n if ((err as { code?: string }).code === \"23505\") return false;\n throw err;\n }\n },\n\n async getOwners(pkg: string): Promise<string[] | null> {\n const res = await pool.query<{ owners: string[] }>(\n \"SELECT owners FROM registry_package_owners WHERE package_name = $1\",\n [pkg],\n );\n return res.rows[0]?.owners ?? null;\n },\n\n async getOwnersFor(pkgs: string[]): Promise<Record<string, string[]>> {\n if (pkgs.length === 0) return {};\n // IN (...) with per-value placeholders — portable across pg and pg-mem,\n // unlike `= ANY($1)` array binding.\n const placeholders = pkgs.map((_, i) => `$${i + 1}`).join(\",\");\n const res = await pool.query<{ package_name: string; owners: string[] }>(\n `SELECT package_name, owners FROM registry_package_owners WHERE package_name IN (${placeholders})`,\n pkgs,\n );\n const out: Record<string, string[]> = {};\n for (const row of res.rows) out[row.package_name] = row.owners;\n return out;\n },\n\n async claimOwner(pkg: string, username: string): Promise<string[]> {\n // First publisher wins atomically; then read the actual owners.\n await pool.query(\n `INSERT INTO registry_package_owners (package_name, owners)\n VALUES ($1, ARRAY[$2]::text[])\n ON CONFLICT (package_name) DO NOTHING`,\n [pkg, username],\n );\n const res = await pool.query<{ owners: string[] }>(\n \"SELECT owners FROM registry_package_owners WHERE package_name = $1\",\n [pkg],\n );\n return res.rows[0]?.owners ?? [];\n },\n\n close(): Promise<void> {\n return pool.end();\n },\n };\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { AuthStore } from \"./auth-store.js\";\n\n// Carry a live AuthStore to the plugin as a token (verdaccio merges the app\n// config's `store` block into plugin configs), tracking whether it loaded.\nconst REGISTRY_KEY = Symbol.for(\"@powerhousedao/registry:auth-store-handoff\");\n\ninterface Entry {\n store: AuthStore;\n loaded: boolean;\n}\n\nfunction registry(): Map<string, Entry> {\n const g = globalThis as { [REGISTRY_KEY]?: Map<string, Entry> };\n return (g[REGISTRY_KEY] ??= new Map<string, Entry>());\n}\n\n/** Stash a store instance and return a token to carry through plugin config. */\nexport function stashAuthStore(store: AuthStore): string {\n const token = randomUUID();\n registry().set(token, { store, loaded: false });\n return token;\n}\n\n/** Resolve a stashed store by token (undefined if unknown). */\nexport function takeAuthStore(token: string): AuthStore | undefined {\n return registry().get(token)?.store;\n}\n\n/** Record that the plugin fully constructed with this token's store. */\nexport function markStoreLoaded(token: string): void {\n const entry = registry().get(token);\n if (entry) entry.loaded = true;\n}\n\n/** True once the plugin has loaded the store for this token. */\nexport function wasStoreLoaded(token: string): boolean {\n return registry().get(token)?.loaded ?? false;\n}\n","/**\n * Compare two semver version strings for sorting.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n *\n * Handles numeric component comparison (so \"1.0.10\" > \"1.0.9\")\n * and prerelease ordering (release > prerelease).\n */\nexport function compareSemver(a: string, b: string): number {\n const [coreA, preA] = a.split(\"-\", 2);\n const [coreB, preB] = b.split(\"-\", 2);\n\n const partsA = coreA.split(\".\").map(Number);\n const partsB = coreB.split(\".\").map(Number);\n\n for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {\n const na = partsA[i] ?? 0;\n const nb = partsB[i] ?? 0;\n if (na !== nb) return na - nb;\n }\n\n // Equal core versions — release (no prerelease) sorts after prerelease\n if (!preA && preB) return 1;\n if (preA && !preB) return -1;\n if (preA && preB) return preA < preB ? -1 : preA > preB ? 1 : 0;\n\n return 0;\n}\n","import crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { extract } from \"tar\";\nimport { compareSemver } from \"./semver.js\";\n\n/**\n * Parse a package specifier into name and version/tag.\n * Supports:\n * \"@scope/pkg\" -> { name: \"@scope/pkg\", tag: undefined }\n * \"@scope/pkg@dev\" -> { name: \"@scope/pkg\", tag: \"dev\" }\n * \"@scope/pkg@1.0.0\" -> { name: \"@scope/pkg\", tag: \"1.0.0\" }\n * \"pkg@latest\" -> { name: \"pkg\", tag: \"latest\" }\n */\nexport function parsePackageSpec(spec: string): {\n name: string;\n tag: string | undefined;\n} {\n // For scoped packages (@scope/name@tag), split on the last @\n // For unscoped packages (name@tag), split on the first @\n if (spec.startsWith(\"@\")) {\n // Scoped: find the @ after the scope/name portion\n const lastAt = spec.lastIndexOf(\"@\");\n if (lastAt > 0 && lastAt !== spec.indexOf(\"@\")) {\n return { name: spec.slice(0, lastAt), tag: spec.slice(lastAt + 1) };\n }\n return { name: spec, tag: undefined };\n }\n const atIndex = spec.indexOf(\"@\");\n if (atIndex > 0) {\n return { name: spec.slice(0, atIndex), tag: spec.slice(atIndex + 1) };\n }\n return { name: spec, tag: undefined };\n}\n\n/** True when tag is a concrete semver, false for dist-tag names or undefined. */\nexport function isExactVersion(tag?: string): boolean {\n // Strict semver charset: the version flows into the ETag header, so\n // arbitrary characters after the prerelease/build separator must not match.\n return (\n !!tag &&\n /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/.test(tag)\n );\n}\n\nexport class CdnCache {\n #extractionLocks = new Map<string, Promise<void>>();\n\n constructor(\n private registryUrl: string,\n private cdnCachePath: string,\n ) {}\n\n async getFileByVersion(\n packageName: string,\n version: string,\n filePath: string,\n ): Promise<string | null> {\n const versionDir = path.join(this.cdnCachePath, packageName, version);\n\n // Check all possible paths before attempting extraction\n const resolved = this.#resolveFile(versionDir, filePath);\n if (resolved) return resolved;\n\n // File not found in any location — extract tarball and try again\n await this.#extractWithLock(packageName, version);\n\n return this.#resolveFile(versionDir, filePath);\n }\n\n #resolveFile(versionDir: string, filePath: string): string | null {\n // Check direct path first, then fall back to cdn/ and dist/cdn/ subdirectories\n // (npm tarballs contain files under dist/, bun bundles go to cdn/)\n const candidates = [\n path.join(versionDir, filePath),\n path.join(versionDir, \"cdn\", filePath),\n path.join(versionDir, \"dist\", \"cdn\", filePath),\n path.join(versionDir, \"dist\", filePath),\n ];\n\n for (const candidate of candidates) {\n if (this.isSafePath(candidate) && fs.existsSync(candidate))\n return candidate;\n }\n\n return null;\n }\n\n async #extractWithLock(packageName: string, version: string): Promise<void> {\n const key = `${packageName}@${version}`;\n const existing = this.#extractionLocks.get(key);\n if (existing) return existing;\n\n const promise = this.extractTarball(packageName, version).finally(() => {\n this.#extractionLocks.delete(key);\n });\n this.#extractionLocks.set(key, promise);\n return promise;\n }\n\n getLatestCachedVersion(packageName: string): string | null {\n const pkgDir = path.join(this.cdnCachePath, packageName);\n try {\n const entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n const versions = entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name);\n if (versions.length === 0) return null;\n versions.sort(compareSemver);\n return versions[versions.length - 1];\n } catch {\n return null;\n }\n }\n\n /**\n * Resolve a version for a package. If tag is a semver version that exists\n * in the registry, return it directly. If tag is a dist-tag name (e.g.\n * \"dev\", \"latest\"), resolve it to the concrete version. If no tag is\n * provided, prefer \"latest\", then fall back to any available dist-tag.\n * Returns null only for genuine not-found (404, or absent tag/version).\n * Throws on network errors and non-OK responses other than 404.\n */\n async resolveVersion(\n packageName: string,\n tag?: string,\n ): Promise<string | null> {\n const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;\n const res = await fetch(url, {\n headers: { Accept: \"application/json\" },\n });\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `Upstream metadata for ${packageName} returned ${res.status}`,\n );\n }\n const metadata = (await res.json()) as Record<string, unknown>;\n const distTags = metadata[\"dist-tags\"] as\n | Record<string, string>\n | undefined;\n const versions = metadata[\"versions\"] as\n | Record<string, unknown>\n | undefined;\n\n if (tag) {\n // If the tag matches an exact version in the registry, use it directly\n if (versions && tag in versions) return tag;\n // Otherwise treat it as a dist-tag name\n if (distTags && tag in distTags) return distTags[tag];\n // Tag not found\n return null;\n }\n\n if (!distTags) return null;\n // No tag specified: prefer \"latest\", fall back to any available tag\n return distTags.latest ?? Object.values(distTags)[0] ?? null;\n }\n\n async extractTarball(packageName: string, version: string): Promise<void> {\n const destDir = path.join(this.cdnCachePath, packageName, version);\n\n // Idempotence guard. extractTarball is hot-path-called from\n // warmCdnCacheFromVerdaccio on every /packages request (which the\n // deployment's readiness probe hits every 5s). Without this skip the\n // tarball is re-fetched (multi-MB over S3) and re-extracted on each\n // call, pinning CPU at multi-vCPU per pod and triggering HPA spirals.\n // We treat the presence of package.json as the marker for \"already\n // extracted\" — it's the first file npm tarballs put under the version\n // directory and removing it (e.g. by invalidate*) requires the rest to\n // go too.\n if (fs.existsSync(path.join(destDir, \"package.json\"))) return;\n\n const shortName = packageName.startsWith(\"@\")\n ? packageName.split(\"/\")[1]\n : packageName;\n const tarballUrl = `${this.registryUrl}/${encodeURIComponent(packageName)}/-/${shortName}-${version}.tgz`;\n\n let res: Response;\n try {\n res = await fetch(tarballUrl);\n if (!res.ok || !res.body) return;\n } catch {\n return;\n }\n\n fs.mkdirSync(destDir, { recursive: true });\n\n const tmpFile = path.join(\n destDir,\n `.tmp-tarball-${crypto.randomUUID()}.tgz`,\n );\n try {\n const fileStream = fs.createWriteStream(tmpFile);\n await pipeline(Readable.fromWeb(res.body as never), fileStream);\n await extract({ file: tmpFile, cwd: destDir, strip: 1 });\n } finally {\n fs.rmSync(tmpFile, { force: true });\n }\n }\n\n invalidate(packageName: string): void {\n const cacheDir = path.join(this.cdnCachePath, packageName);\n if (!this.isSafePath(cacheDir)) return;\n fs.rmSync(cacheDir, { recursive: true, force: true });\n }\n\n invalidateVersion(packageName: string, version: string): void {\n const versionDir = path.join(this.cdnCachePath, packageName, version);\n if (!this.isSafePath(versionDir)) return;\n fs.rmSync(versionDir, { recursive: true, force: true });\n // If the package dir is now empty, remove it too so the scanner doesn't\n // keep returning a ghost entry with no versions.\n const pkgDir = path.join(this.cdnCachePath, packageName);\n try {\n if (fs.readdirSync(pkgDir).length === 0) {\n fs.rmdirSync(pkgDir);\n }\n } catch {\n // ignore — dir may not exist\n }\n }\n\n // Reconcile the cache against the registry's current version set; returns\n // removed versions. Empty/failed listings are a no-op (never mass-purge).\n async reconcileWithRegistry(packageName: string): Promise<string[]> {\n const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;\n let versions: string[];\n try {\n const res = await fetch(url, { headers: { Accept: \"application/json\" } });\n if (!res.ok) return [];\n const meta = (await res.json()) as { versions?: Record<string, unknown> };\n versions = Object.keys(meta.versions ?? {});\n } catch {\n return [];\n }\n if (versions.length === 0) return [];\n return this.reconcileVersions(packageName, versions);\n }\n\n // Drop cached version dirs absent from `keepVersions`; returns the removed\n // ones. Mirrors single-version unpublish (a manifest rewrite, no DELETE).\n reconcileVersions(\n packageName: string,\n keepVersions: Iterable<string>,\n ): string[] {\n const pkgDir = path.join(this.cdnCachePath, packageName);\n const keep = new Set(keepVersions);\n const removed: string[] = [];\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n } catch {\n return removed;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || keep.has(entry.name)) continue;\n this.invalidateVersion(packageName, entry.name);\n removed.push(entry.name);\n }\n return removed;\n }\n\n /** Remove all cached version directories except the specified one. */\n pruneOldVersions(packageName: string, keepVersion: string): void {\n const pkgDir = path.join(this.cdnCachePath, packageName);\n try {\n const entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && entry.name !== keepVersion) {\n const dir = path.join(pkgDir, entry.name);\n if (this.isSafePath(dir)) {\n fs.rmSync(dir, { recursive: true, force: true });\n }\n }\n }\n } catch {\n // ignore — directory may not exist yet\n }\n }\n\n private isSafePath(filePath: string): boolean {\n const resolved = path.resolve(filePath);\n const cacheRoot = path.resolve(this.cdnCachePath);\n return resolved.startsWith(cacheRoot + path.sep) || resolved === cacheRoot;\n }\n}\n","import type { Manifest } from \"@powerhousedao/shared\";\nimport { slimManifest } from \"@powerhousedao/shared/registry\";\nimport type { PackageListItem } from \"@powerhousedao/shared/registry\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { compareSemver } from \"./semver.js\";\nimport type { PackageInfo } from \"./types.js\";\n\n/**\n * Read dist-tags, the full version list, and the local-publish flag for a\n * package from verdaccio's on-disk storage (`{storagePath}/{name}/package.json`).\n *\n * `locallyPublished` is tri-state:\n * - `true` → storage metadata has `_attachments` (tarball uploaded here).\n * - `false` → storage metadata exists but `_attachments` is empty (proxy\n * from the npm uplink only; no local publish at this registry).\n * - `undefined` → metadata file wasn't readable. Happens with non-filesystem\n * backends (S3, etc.) or if verdaccio stores metadata elsewhere.\n * Callers should treat this as \"unknown\" and default to including\n * the package, to avoid filtering the whole /packages list to an\n * empty array on deployments where we can't observe _attachments.\n */\nfunction readPackageMetadata(\n storagePath: string | undefined,\n packageName: string,\n): {\n distTags?: Record<string, string>;\n versions?: string[];\n locallyPublished: boolean | undefined;\n} {\n if (!storagePath) return { locallyPublished: undefined };\n try {\n const metadataPath = path.join(storagePath, packageName, \"package.json\");\n const raw = fs.readFileSync(metadataPath, \"utf-8\");\n const parsed = JSON.parse(raw) as {\n \"dist-tags\"?: Record<string, string>;\n versions?: Record<string, unknown>;\n _attachments?: Record<string, unknown>;\n };\n const distTags = parsed[\"dist-tags\"];\n const rawVersions = parsed.versions ? Object.keys(parsed.versions) : [];\n const versions = rawVersions.slice().sort(compareSemver);\n const locallyPublished =\n !!parsed._attachments && Object.keys(parsed._attachments).length > 0;\n return {\n distTags:\n distTags && Object.keys(distTags).length > 0 ? distTags : undefined,\n versions: versions.length > 0 ? versions : undefined,\n locallyPublished,\n };\n } catch {\n return { locallyPublished: undefined };\n }\n}\n\n/**\n * Locally-published check for a package, from verdaccio storage metadata.\n * Returns the same tri-state as `readPackageMetadata.locallyPublished`:\n * `true` (has `_attachments`), `false` (proxy-only), `undefined` (unreadable).\n */\nexport function isLocallyPublished(\n storagePath: string | undefined,\n packageName: string,\n): boolean | undefined {\n return readPackageMetadata(storagePath, packageName).locallyPublished;\n}\n\nfunction readManifest(dir: string): Manifest | null {\n const candidates = [\n path.join(dir, \"powerhouse.manifest.json\"),\n path.join(dir, \"cdn\", \"powerhouse.manifest.json\"),\n path.join(dir, \"dist\", \"powerhouse.manifest.json\"),\n ];\n for (const manifestPath of candidates) {\n try {\n const raw = fs.readFileSync(manifestPath, \"utf-8\");\n // Manifests are publisher-supplied JSON; slim to the known summary\n // fields so one oversized publish can't bloat every /packages\n // listing (a single 7.8 MB `features` blob once pushed the response\n // past clients' localStorage quota). The raw file stays available\n // through the CDN path.\n return slimManifest(JSON.parse(raw) as Manifest);\n } catch {\n // try next candidate\n }\n }\n return null;\n}\n\nfunction readPackageJsonVersion(dir: string): string | undefined {\n try {\n const raw = fs.readFileSync(path.join(dir, \"package.json\"), \"utf-8\");\n const pkg = JSON.parse(raw) as { version?: unknown };\n return typeof pkg.version === \"string\" ? pkg.version : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction getLatestVersionDir(pkgDir: string): string | null {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n } catch {\n return null;\n }\n const versions = entries.filter((e) => e.isDirectory()).map((e) => e.name);\n if (versions.length === 0) return null;\n versions.sort(compareSemver);\n return path.join(pkgDir, versions[versions.length - 1]);\n}\n\nexport function loadPackage(\n cdnCachePath: string,\n name: string,\n version?: string,\n storagePath?: string,\n): PackageInfo | null {\n const pkgDir = path.join(cdnCachePath, name);\n const versionDir = version\n ? path.join(pkgDir, version)\n : getLatestVersionDir(pkgDir);\n const manifestDir = versionDir ?? pkgDir;\n const manifest = readManifest(manifestDir);\n\n if (!manifest) {\n return null;\n }\n const resolvedName = manifest.name || name;\n // Include dist-tags/versions so the paginated UI can lazy-load version\n // metadata for a single package (the list DTO omits them). Falls back to\n // undefined when storage metadata isn't available.\n const { distTags, versions } = readPackageMetadata(storagePath, resolvedName);\n return {\n name: resolvedName,\n path: `/-/cdn/${name}`,\n manifest,\n documentTypes: getDocumentTypesFromManifest(manifest),\n version: readPackageJsonVersion(manifestDir),\n distTags,\n versions,\n };\n}\n\n/**\n * Project a full {@link PackageInfo} down to the trimmed shape the paginated\n * package-listing UI renders per row. Version metadata and documentTypes are\n * intentionally dropped — the client fetches them on demand.\n */\nexport function toPackageListItem(pkg: PackageInfo): PackageListItem {\n const publisher = pkg.manifest?.publisher;\n return {\n name: pkg.name,\n path: pkg.path,\n version: pkg.version,\n description: pkg.manifest?.description ?? undefined,\n category: pkg.manifest?.category ?? undefined,\n publisher: publisher\n ? { name: publisher.name, url: publisher.url }\n : undefined,\n };\n}\n\nfunction getDocumentTypesFromManifest(manifest: Manifest | undefined | null) {\n if (!manifest) return [];\n\n const documentTypes: string[] = [];\n const { apps, documentModels, editors, subgraphs } = manifest;\n\n if (apps?.length) {\n documentTypes.push(\"powerhouse/document-drive\");\n }\n documentTypes.push(\n ...(documentModels ?? []).map((dm) => dm.id),\n ...(editors ?? [])\n .flatMap((e) => e.documentTypes)\n .filter((dt) => dt !== undefined),\n ...(subgraphs ?? [])\n .flatMap((e) => e.documentTypes)\n .filter((dt) => dt !== undefined),\n );\n\n return documentTypes;\n}\n\nexport function scanPackages(\n cdnCachePath: string,\n storagePath?: string,\n): PackageInfo[] {\n const absDir = path.resolve(cdnCachePath);\n const packages: PackageInfo[] = [];\n\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(absDir, { withFileTypes: true });\n } catch {\n return packages;\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n if (entry.name.startsWith(\"@\")) {\n const scopeDir = path.join(absDir, entry.name);\n let scopedEntries: fs.Dirent[];\n try {\n scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });\n } catch (error) {\n console.log(error);\n continue;\n }\n for (const scopedEntry of scopedEntries) {\n if (!scopedEntry.isDirectory()) continue;\n const dirName = `${entry.name}/${scopedEntry.name}`;\n const pkgDir = path.join(scopeDir, scopedEntry.name);\n const versionDir = getLatestVersionDir(pkgDir);\n const manifestDir = versionDir ?? pkgDir;\n const manifest = readManifest(manifestDir);\n // `||` (not `??`): slimManifest normalizes a missing manifest name to\n // \"\" — fall back to the directory name in that case too.\n const name = manifest?.name || dirName;\n const { distTags, versions, locallyPublished } = readPackageMetadata(\n storagePath,\n name,\n );\n // Drop npm-uplink passthroughs from the default listing. Only\n // skip when we can affirmatively tell the package is a proxy\n // (no `_attachments` in filesystem-backed storage). When the flag\n // is `undefined` (no storagePath, or non-filesystem backend where\n // we can't read verdaccio's metadata) we include the entry — the\n // alternative would be filtering everything to `[]` on S3 deploys.\n if (locallyPublished === false) continue;\n packages.push({\n name,\n path: `/-/cdn/${dirName}`,\n manifest,\n documentTypes: getDocumentTypesFromManifest(manifest),\n version: readPackageJsonVersion(manifestDir),\n distTags,\n versions,\n });\n }\n } else {\n const pkgDir = path.join(absDir, entry.name);\n const versionDir = getLatestVersionDir(pkgDir);\n const manifestDir = versionDir ?? pkgDir;\n const manifest = readManifest(manifestDir);\n const name = manifest?.name || entry.name;\n const { distTags, versions, locallyPublished } = readPackageMetadata(\n storagePath,\n name,\n );\n if (locallyPublished === false) continue;\n packages.push({\n name,\n path: `/-/cdn/${entry.name}`,\n manifest,\n documentTypes: getDocumentTypesFromManifest(manifest),\n version: readPackageJsonVersion(manifestDir),\n distTags,\n versions,\n });\n }\n }\n\n return packages;\n}\n\nexport function findPackagesByDocumentType(\n packagesDir: string,\n documentType: string,\n): PackageInfo[] {\n const allPackages = scanPackages(packagesDir);\n\n return allPackages.filter((pkg) => {\n if (!pkg.manifest?.documentModels) {\n return false;\n }\n return pkg.manifest.documentModels.some((dm) => dm.id === documentType);\n });\n}\n","import type { CdnCache } from \"./cdn.js\";\nimport { isLocallyPublished } from \"./packages.js\";\nimport type { RegistryConfig } from \"./types.js\";\n\n// The verdaccio listing already yields local-only, latest-per-name entries;\n// we re-filter by `_attachments` and dedupe as a guard against backend changes.\nconst WARM_INTERVAL_MS = 30_000;\nconst WARM_CONCURRENCY = 8;\n\ninterface VerdaccioPackage {\n name: string;\n version?: string;\n}\n\n/**\n * Build a throttled warmer that extracts locally-published package tarballs\n * into the CDN cache. 30s minimum interval plus an in-flight guard prevent\n * redundant fan-out from readiness-probe traffic across pods.\n */\nexport function createWarmer(\n config: RegistryConfig,\n cdn: CdnCache,\n): () => Promise<void> {\n let warmInFlight = false;\n let lastWarmAt = 0;\n\n return async function warm(): Promise<void> {\n if (warmInFlight) return;\n if (Date.now() - lastWarmAt < WARM_INTERVAL_MS) return;\n warmInFlight = true;\n try {\n const r = await fetch(\n `http://localhost:${config.port}/-/verdaccio/data/packages`,\n );\n if (!r.ok) {\n console.error(\n `[registry] verdaccio package listing returned ${r.status}`,\n );\n return;\n }\n const listed = (await r.json()) as VerdaccioPackage[];\n\n // Latest version per name (the listing yields one entry per package;\n // dedupe defensively), scoped to locally-published packages only.\n const latestByName = new Map<string, string>();\n for (const pkg of listed) {\n if (!pkg.version) continue;\n if (isLocallyPublished(config.storagePath, pkg.name) === false)\n continue;\n latestByName.set(pkg.name, pkg.version);\n }\n\n const targets = [...latestByName.entries()];\n let cursor = 0;\n const workers = Array.from({ length: WARM_CONCURRENCY }).map(async () => {\n while (cursor < targets.length) {\n const [name, version] = targets[cursor++];\n try {\n await cdn.extractTarball(name, version);\n } catch (err) {\n console.error(\n `[registry] failed to warm cache for ${name}@${version}:`,\n err,\n );\n }\n }\n });\n await Promise.all(workers);\n console.log(`[registry] /packages warm-up done (${targets.length} pkgs)`);\n // Throttle only after a successful cycle so failures (e.g. registry\n // not listening yet during startup) retry on the next call.\n lastWarmAt = Date.now();\n } catch (err) {\n console.error(\"[registry] /packages warm-up failed:\", err);\n } finally {\n warmInFlight = false;\n }\n };\n}\n","import express, {\n Router,\n type NextFunction,\n type Request,\n type Response,\n} from \"express\";\nimport crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { AuthStore } from \"./auth/auth-store.js\";\nimport { CdnCache, isExactVersion, parsePackageSpec } from \"./cdn.js\";\nimport type { PackageInfo } from \"./types.js\";\nimport type { SSEChannel } from \"./notifications/sse.js\";\nimport type { NotificationChannel } from \"./notifications/types.js\";\nimport type { WebhookChannel } from \"./notifications/webhook.js\";\nimport {\n findPackagesByDocumentType,\n loadPackage,\n scanPackages,\n toPackageListItem,\n} from \"./packages.js\";\nimport type { RegistryConfig } from \"./types.js\";\nimport { createWarmer } from \"./warmup.js\";\n\n// Paginated /packages mode: default and max page sizes.\nconst DEFAULT_PAGE_SIZE = 30;\nconst MAX_PAGE_SIZE = 50;\n\n/** Parse+clamp a `limit` query param to 1..MAX_PAGE_SIZE (default 30). */\nfunction clampPageSize(raw: unknown): number {\n const n = Number.parseInt(String(raw), 10);\n if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_SIZE;\n return Math.min(n, MAX_PAGE_SIZE);\n}\n\n/** Parse a non-negative `offset` query param (default 0). */\nfunction parseOffset(raw: unknown): number {\n const n = Number.parseInt(String(raw), 10);\n if (!Number.isFinite(n) || n < 0) return 0;\n return n;\n}\n\nconst MIME_TYPES: Record<string, string> = {\n \".js\": \"application/javascript\",\n \".mjs\": \"application/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".wasm\": \"application/wasm\",\n \".map\": \"application/json\",\n \".html\": \"text/html\",\n \".svg\": \"image/svg+xml\",\n};\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase();\n return MIME_TYPES[ext] ?? \"application/octet-stream\";\n}\n\n// Publisher identity from verdaccio's remote_user (the renown middleware sets\n// its name to the owner pkh DID). undefined when anonymous.\nfunction publisherFromRequest(\n req: Request,\n): { address: string; did?: string } | undefined {\n const name = (req as { remote_user?: { name?: string } }).remote_user?.name;\n if (!name) return undefined;\n if (name.startsWith(\"did:pkh:\")) {\n const address = (name.split(\":\").pop() ?? name).toLowerCase();\n return { address, did: name };\n }\n return { address: name };\n}\n\ntype VersionResolution =\n | { kind: \"ok\"; version: string }\n | { kind: \"not-found\" }\n | { kind: \"upstream-error\" };\n\n/**\n * Resolve a package version. Exact versions skip the network call. Upstream\n * errors fall back to the latest cached version; genuine not-found falls back\n * too, then reports not-found.\n */\nasync function resolvePackageVersion(\n cdn: CdnCache,\n packageName: string,\n tag: string | undefined,\n): Promise<VersionResolution> {\n if (tag && isExactVersion(tag)) return { kind: \"ok\", version: tag };\n\n try {\n const resolved =\n (await cdn.resolveVersion(packageName, tag)) ??\n cdn.getLatestCachedVersion(packageName);\n if (!resolved) return { kind: \"not-found\" };\n return { kind: \"ok\", version: resolved };\n } catch {\n const cached = cdn.getLatestCachedVersion(packageName);\n if (!cached) return { kind: \"upstream-error\" };\n return { kind: \"ok\", version: cached };\n }\n}\n\n/** Strip the weak-validator prefix: weak comparison is correct for GET/HEAD. */\nfunction opaqueTag(tag: string): string {\n return tag.startsWith(\"W/\") ? tag.slice(2) : tag;\n}\n\n/** RFC 9110 If-None-Match: a comma-separated list of entity-tags or \"*\". */\nfunction etagMatches(\n header: string | string[] | undefined,\n etag: string,\n): boolean {\n if (!header) return false;\n const target = opaqueTag(etag);\n const value = Array.isArray(header) ? header.join(\",\") : header;\n return value.split(\",\").some((candidate) => {\n const tag = candidate.trim();\n return tag === \"*\" || opaqueTag(tag) === target;\n });\n}\n\nexport function createPowerhouseRouter(\n config: RegistryConfig,\n sse: SSEChannel,\n webhooks: WebhookChannel,\n ownerStore?: AuthStore,\n): Router {\n const cdn = new CdnCache(\n `http://localhost:${config.port}`,\n config.cdnCachePath,\n );\n const router = Router();\n\n // Attach package owners from the auth store. Best-effort: a missing store or\n // DB error degrades to no `owners` (the /packages route feeds readiness).\n const withOwners = async (pkgs: PackageInfo[]): Promise<PackageInfo[]> => {\n if (!ownerStore || pkgs.length === 0) return pkgs;\n try {\n await ownerStore.init();\n const map = await ownerStore.getOwnersFor(pkgs.map((p) => p.name));\n return pkgs.map((p) =>\n p.name in map ? { ...p, owners: map[p.name] } : p,\n );\n } catch (err) {\n console.error(\"[registry] owner lookup failed:\", err);\n return pkgs;\n }\n };\n\n // CORS on every response\n router.use((_req: Request, res: Response, next: NextFunction) => {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n next();\n });\n\n // SSE endpoint for publish notifications\n router.get(\"/-/events\", (_req: Request, res: Response) => {\n sse.addClient(res);\n });\n\n // Webhook management\n router.get(\"/-/webhooks\", (_req: Request, res: Response) => {\n res.json(webhooks.getWebhooks());\n });\n\n router.post(\"/-/webhooks\", express.json(), (req: Request, res: Response) => {\n const { endpoint, headers } = req.body as {\n endpoint?: string;\n headers?: Record<string, string>;\n };\n if (!endpoint) {\n res.status(400).json({ error: \"Missing required field: endpoint\" });\n return;\n }\n webhooks.addWebhook({ endpoint, headers });\n res.status(201).json({ endpoint, headers });\n });\n\n router.delete(\n \"/-/webhooks\",\n express.json(),\n (req: Request, res: Response) => {\n const { endpoint } = req.body as { endpoint?: string };\n if (!endpoint) {\n res.status(400).json({ error: \"Missing required field: endpoint\" });\n return;\n }\n const removed = webhooks.removeWebhook(endpoint);\n if (!removed) {\n res.status(404).json({ error: \"Webhook not found\" });\n return;\n }\n res.status(204).end();\n },\n );\n\n const warm = createWarmer(config, cdn);\n\n // Kick off an initial warm so /packages is useful soon after pod start\n // even if no clients hit it. Fire-and-forget — must not block the listener.\n void warm();\n\n // Package listing API.\n // Returns whatever's currently in the local cdn-cache (instant response —\n // important: this endpoint is wired to the deployment's readiness probe,\n // so it must not synchronously fetch or extract). Each call also nudges\n // a background warm-up so newly-published packages appear in the listing\n // without operator intervention.\n router.get(\"/packages\", async (req: Request, res: Response) => {\n void warm();\n const packages = scanPackages(config.cdnCachePath, config.storagePath);\n\n // Legacy mode 1 (unchanged): filter by document type, return the full\n // PackageInfo array. Used by MissingPackageModal / package discovery.\n const documentType = req.query.documentType as string | undefined;\n if (documentType) {\n const filtered = packages.filter((pkg) =>\n pkg.manifest?.documentModels?.some((m) => m.id === documentType),\n );\n res.json(await withOwners(filtered));\n return;\n }\n\n // Legacy mode 2 (unchanged): no `limit` → full PackageInfo array in\n // directory order. Keeps every existing consumer working.\n if (req.query.limit === undefined) {\n res.json(await withOwners(packages));\n return;\n }\n\n // Paginated mode: `?limit=&offset=&search=` → trimmed-item envelope.\n // Sort by name so offset paging is stable (scanPackages is dir order).\n const limit = clampPageSize(req.query.limit);\n const offset = parseOffset(req.query.offset);\n const search =\n typeof req.query.search === \"string\"\n ? req.query.search.trim().toLowerCase()\n : \"\";\n\n // Match on package name only (not description).\n const filtered = search\n ? packages.filter((pkg) => pkg.name.toLowerCase().includes(search))\n : packages;\n const sorted = filtered\n .slice()\n .sort((a, b) => a.name.localeCompare(b.name));\n\n const total = sorted.length;\n const items = sorted.slice(offset, offset + limit).map(toPackageListItem);\n res.json({\n items,\n total,\n limit,\n offset,\n hasMore: offset + limit < total,\n });\n });\n\n // Find packages by document type - returns array of package names\n router.get(\"/packages/by-document-type\", (req: Request, res: Response) => {\n const documentType = req.query.type;\n\n if (typeof documentType !== \"string\" || !documentType) {\n res.status(400).json({ error: \"Missing required query parameter: type\" });\n return;\n }\n\n const packages = findPackagesByDocumentType(\n config.cdnCachePath,\n documentType,\n );\n const packageNames = packages.map((pkg) => pkg.name);\n res.json(packageNames);\n });\n\n // Single package info\n router.get(\"/packages/*\", async (req: Request, res: Response) => {\n const raw = (req.params as Record<string, string>)[0];\n const { name, tag } = parsePackageSpec(raw);\n const resolution = await resolvePackageVersion(cdn, name, tag);\n if (resolution.kind === \"upstream-error\") {\n res.status(503).send(\"Upstream registry unavailable\");\n return;\n }\n const version = resolution.kind === \"ok\" ? resolution.version : undefined;\n const pkg = loadPackage(\n config.cdnCachePath,\n name,\n version,\n config.storagePath,\n );\n if (!pkg) {\n res.status(404).send(\"Package not found\");\n return;\n }\n res.json((await withOwners([pkg]))[0]);\n });\n\n // CDN file serving\n router.get(\"/-/cdn/*\", async (req: Request, res: Response) => {\n const fullPath = (req.params as Record<string, string>)[0];\n\n // Parse scoped or unscoped package specifier from the path\n let packageSpec: string;\n let filePath: string;\n\n if (fullPath.startsWith(\"@\")) {\n // Scoped: @scope/pkg@1.0.0/file.js -> packageSpec = @scope/pkg@1.0.0, filePath = file.js\n const segments = fullPath.split(\"/\");\n if (segments.length < 2) {\n res.status(400).send(\"Invalid package path\");\n return;\n }\n packageSpec = `${segments[0]}/${segments[1]}`;\n filePath = segments.slice(2).join(\"/\") || \"index.js\";\n } else {\n // Unscoped: pkg@1.0.0/file.js -> packageSpec = pkg@1.0.0, filePath = file.js\n const segments = fullPath.split(\"/\");\n packageSpec = segments[0];\n filePath = segments.slice(1).join(\"/\") || \"index.js\";\n }\n\n const { name: packageName, tag } = parsePackageSpec(packageSpec);\n const pinned = isExactVersion(tag);\n const resolution = await resolvePackageVersion(cdn, packageName, tag);\n if (resolution.kind === \"upstream-error\") {\n res.status(503).send(\"Upstream registry unavailable\");\n return;\n }\n if (resolution.kind === \"not-found\") {\n res.status(404).send(\"File not found\");\n return;\n }\n const version = resolution.version;\n\n const resolved = await cdn.getFileByVersion(packageName, version, filePath);\n if (!resolved) {\n // Pinned requests skip the metadata lookup above, so a miss here may be\n // an upstream failure rather than a genuine 404 — probe to distinguish,\n // otherwise the CDN would cache a 404 while upstream is merely down.\n if (pinned) {\n try {\n await cdn.resolveVersion(packageName, tag);\n } catch {\n res.status(503).send(\"Upstream registry unavailable\");\n return;\n }\n }\n res.status(404).send(\"File not found\");\n return;\n }\n\n // Cache based on the request shape: pinned requests are immutable, moving\n // ones (dist-tag / untagged) must revalidate frequently.\n res.setHeader(\n \"Cache-Control\",\n pinned\n ? \"public, max-age=31536000, immutable\"\n : \"public, max-age=60, must-revalidate\",\n );\n\n // Hash the file path: it comes from the URL and may contain characters\n // that are invalid in header values.\n const fileHash = crypto\n .createHash(\"sha1\")\n .update(filePath)\n .digest(\"hex\")\n .slice(0, 16);\n const etag = `W/\"${version}-${fileHash}\"`;\n res.setHeader(\"ETag\", etag);\n if (etagMatches(req.headers[\"if-none-match\"], etag)) {\n res.status(304).end();\n return;\n }\n\n res.setHeader(\"Content-Type\", getContentType(filePath));\n try {\n await pipeline(fs.createReadStream(resolved), res);\n } catch {\n // Stream failure (I/O error, client abort) after headers may already\n // be sent — destroy the socket so the request doesn't hang.\n res.destroy();\n }\n });\n\n return router;\n}\n\n/**\n * Parse verdaccio's unpublish URL shape:\n * DELETE /<pkg>/-rev/<rev> → full package\n * DELETE /<pkg>/-/<tarball-name>/-rev/<rev> → single version\n * where <pkg> may be scoped (@scope%2Fname, encoded) or unscoped, and the\n * tarball name is `<short-name>-<version>.tgz`.\n */\nexport function parseUnpublishRequest(\n reqPath: string,\n): { packageName: string; version: string | null } | null {\n const revIdx = reqPath.indexOf(\"/-rev/\");\n if (revIdx <= 0) return null;\n const beforeRev = reqPath.slice(1, revIdx); // strip leading slash\n\n const tarballMarker = \"/-/\";\n const tarballIdx = beforeRev.indexOf(tarballMarker);\n if (tarballIdx === -1) {\n // Full package: beforeRev is the package name (possibly URL-encoded scope)\n const packageName = decodeURIComponent(beforeRev);\n return { packageName, version: null };\n }\n\n const packageName = decodeURIComponent(beforeRev.slice(0, tarballIdx));\n const tarballName = beforeRev.slice(tarballIdx + tarballMarker.length);\n if (!tarballName.endsWith(\".tgz\")) return null;\n const shortName = packageName.startsWith(\"@\")\n ? packageName.split(\"/\")[1]\n : packageName;\n const prefix = `${shortName}-`;\n if (!tarballName.startsWith(prefix)) return null;\n const version = tarballName.slice(prefix.length, -\".tgz\".length);\n if (!version) return null;\n return { packageName, version };\n}\n\n// PUT /<pkg>/-rev/<rev> is npm's manifest rewrite (single-version unpublish,\n// deprecate). Exclude the tarball-DELETE shape, which also carries /-rev/.\nexport function parseManifestRewrite(\n reqPath: string,\n): { packageName: string } | null {\n const revIdx = reqPath.indexOf(\"/-rev/\");\n if (revIdx <= 0) return null;\n const beforeRev = reqPath.slice(1, revIdx);\n if (beforeRev.includes(\"/-/\")) return null;\n return { packageName: decodeURIComponent(beforeRev) };\n}\n\nexport function createUnpublishHook(\n config: RegistryConfig,\n notifications: NotificationChannel,\n) {\n const cdn = new CdnCache(\n `http://localhost:${config.port}`,\n config.cdnCachePath,\n );\n\n // Reconcile the CDN cache after verdaccio rewrites a manifest to drop a\n // version. Re-fetch survivors — req.body isn't reliable on this route.\n const handleManifestRewrite = (req: Request, res: Response) => {\n const rewrite = parseManifestRewrite(req.path);\n if (!rewrite) return;\n\n const originalEnd = res.end.bind(res);\n res.end = function (\n this: Response,\n chunk?: unknown,\n encoding?: unknown,\n cb?: () => void,\n ) {\n if (res.statusCode >= 200 && res.statusCode < 300) {\n const publishedBy = publisherFromRequest(req);\n cdn\n .reconcileWithRegistry(rewrite.packageName)\n .then((removed) => {\n for (const version of removed) {\n notifications.notifyUnpublish({\n packageName: rewrite.packageName,\n version,\n publishedBy,\n });\n }\n })\n .catch((err) => {\n console.error(\n `[registry] CDN reconcile failed for ${rewrite.packageName}:`,\n err,\n );\n });\n }\n return originalEnd(chunk, encoding as BufferEncoding, cb);\n };\n };\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (req.method === \"PUT\") {\n handleManifestRewrite(req, res);\n next();\n return;\n }\n if (req.method !== \"DELETE\") {\n next();\n return;\n }\n\n const parsed = parseUnpublishRequest(req.path);\n if (!parsed) {\n next();\n return;\n }\n\n const originalEnd = res.end.bind(res);\n res.end = function (\n this: Response,\n chunk?: unknown,\n encoding?: unknown,\n cb?: () => void,\n ) {\n if (res.statusCode >= 200 && res.statusCode < 300) {\n try {\n if (parsed.version) {\n cdn.invalidateVersion(parsed.packageName, parsed.version);\n } else {\n cdn.invalidate(parsed.packageName);\n }\n notifications.notifyUnpublish({\n packageName: parsed.packageName,\n version: parsed.version,\n publishedBy: publisherFromRequest(req),\n });\n } catch (err) {\n console.error(\n `[registry] CDN purge failed for ${parsed.packageName}${parsed.version ? `@${parsed.version}` : \"\"}:`,\n err,\n );\n }\n }\n return originalEnd(chunk, encoding as BufferEncoding, cb);\n };\n\n next();\n };\n}\n\nexport function createPublishHook(\n config: RegistryConfig,\n notifications: NotificationChannel,\n) {\n const cdn = new CdnCache(\n `http://localhost:${config.port}`,\n config.cdnCachePath,\n );\n\n return (req: Request, res: Response, next: NextFunction) => {\n // Only intercept PUT requests to npm publish endpoints.\n // Skip PUTs to `/<pkg>/-rev/<rev>` — those are npm's manifest-rewrite\n // step during single-version unpublish, not a new publish.\n if (req.method !== \"PUT\" || req.path.includes(\"/-rev/\")) {\n next();\n return;\n }\n\n const originalEnd = res.end.bind(res);\n res.end = function (\n this: Response,\n chunk?: unknown,\n encoding?: unknown,\n cb?: () => void,\n ) {\n const urlPath = req.path.replace(/^\\//, \"\");\n if (\n res.statusCode < 200 ||\n res.statusCode >= 300 ||\n !urlPath ||\n urlPath.startsWith(\"-\")\n ) {\n return originalEnd(chunk, encoding as BufferEncoding, cb);\n }\n const packageName = decodeURIComponent(urlPath);\n const versionsObj = (req.body as { versions: Record<string, unknown> })\n .versions;\n const versions = Object.keys(versionsObj);\n const version = versions.at(0);\n if (!version) {\n console.error(`[registry] No version found for ${packageName}`);\n return originalEnd(chunk, encoding as BufferEncoding, cb);\n }\n if (versions.length > 1) {\n console.warn(\n `[registry] Multiple versions published for ${packageName}: ${JSON.stringify(versions)}`,\n );\n }\n\n const publishedBy = publisherFromRequest(req);\n cdn\n .extractTarball(packageName, version)\n .then(() => {\n notifications.notifyPublish({ packageName, version, publishedBy });\n })\n .catch((err) => {\n console.error(\n `[registry] Failed to extract ${packageName} to CDN cache:`,\n err,\n );\n });\n\n return originalEnd(chunk, encoding as BufferEncoding, cb);\n };\n\n next();\n };\n}\n","import type {\n NotificationChannel,\n PublishEvent,\n UnpublishEvent,\n} from \"./types.js\";\n\nexport class NotificationManager implements NotificationChannel {\n #channels: NotificationChannel[];\n\n constructor(channels: NotificationChannel[]) {\n this.#channels = channels;\n }\n\n notifyPublish(event: PublishEvent): void {\n for (const channel of this.#channels) {\n channel.notifyPublish(event);\n }\n }\n\n notifyUnpublish(event: UnpublishEvent): void {\n for (const channel of this.#channels) {\n channel.notifyUnpublish(event);\n }\n }\n}\n","import type { Response } from \"express\";\nimport type {\n NotificationChannel,\n PublishEvent,\n UnpublishEvent,\n} from \"./types.js\";\n\nexport class SSEChannel implements NotificationChannel {\n #clients = new Set<Response>();\n\n addClient(res: Response): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n res.write(\"event: connected\\ndata: {}\\n\\n\");\n\n this.#clients.add(res);\n res.on(\"close\", () => {\n this.#clients.delete(res);\n });\n }\n\n notifyPublish(event: PublishEvent): void {\n this.#broadcast(\"publish\", event);\n }\n\n notifyUnpublish(event: UnpublishEvent): void {\n this.#broadcast(\"unpublish\", event);\n }\n\n #broadcast(eventName: string, event: PublishEvent | UnpublishEvent): void {\n const payload = `event: ${eventName}\\ndata: ${JSON.stringify(event)}\\n\\n`;\n for (const client of this.#clients) {\n try {\n client.write(payload);\n } catch (err) {\n console.error(\"[registry] SSE client write failed:\", err);\n this.#clients.delete(client);\n }\n }\n }\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { NotifyConfig, WebhookConfig } from \"../types.js\";\nimport type {\n NotificationChannel,\n PublishEvent,\n UnpublishEvent,\n} from \"./types.js\";\n\nconst WEBHOOKS_FILE = \"webhooks.json\";\n\nexport class WebhookChannel implements NotificationChannel {\n #predefined: WebhookConfig[];\n #dynamic: WebhookConfig[];\n #storagePath: string;\n\n constructor(storagePath: string, config?: NotifyConfig) {\n this.#storagePath = storagePath;\n this.#predefined = config?.webhooks ?? [];\n this.#dynamic = this.#load();\n }\n\n getWebhooks(): WebhookConfig[] {\n return [...this.#predefined, ...this.#dynamic];\n }\n\n addWebhook(webhook: WebhookConfig): void {\n const exists = this.getWebhooks().some(\n (w) => w.endpoint === webhook.endpoint,\n );\n if (exists) return;\n this.#dynamic.push(webhook);\n this.#save();\n }\n\n removeWebhook(endpoint: string): boolean {\n const before = this.#dynamic.length;\n this.#dynamic = this.#dynamic.filter((w) => w.endpoint !== endpoint);\n if (this.#dynamic.length === before) return false;\n this.#save();\n return true;\n }\n\n notifyPublish(event: PublishEvent): void {\n this.#post({ type: \"publish\", ...event });\n }\n\n notifyUnpublish(event: UnpublishEvent): void {\n this.#post({ type: \"unpublish\", ...event });\n }\n\n #post(body: Record<string, unknown>): void {\n for (const webhook of this.getWebhooks()) {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n ...webhook.headers,\n };\n\n fetch(webhook.endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n }).catch((err: unknown) => {\n console.error(`[registry] Webhook to ${webhook.endpoint} failed:`, err);\n });\n }\n }\n\n #filePath(): string {\n return path.join(this.#storagePath, WEBHOOKS_FILE);\n }\n\n #load(): WebhookConfig[] {\n try {\n const raw = fs.readFileSync(this.#filePath(), \"utf-8\");\n return JSON.parse(raw) as WebhookConfig[];\n } catch {\n return [];\n }\n }\n\n #save(): void {\n fs.mkdirSync(this.#storagePath, { recursive: true });\n fs.writeFileSync(this.#filePath(), JSON.stringify(this.#dynamic, null, 2));\n }\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { AuthStore } from \"./auth/auth-store.js\";\nimport { stashAuthStore } from \"./auth/store-handoff.js\";\nimport type { RegistryConfig } from \"./types.js\";\n\nexport function buildVerdaccioConfig(config: RegistryConfig) {\n const htpasswdPath = path.join(config.storagePath, \"htpasswd\");\n\n const uplinkUrl = config.uplink ?? \"https://registry.npmjs.org/\";\n\n // With a database configured, use the Postgres-backed auth plugin\n // (persistent accounts + npm-style package ownership). Verdaccio loads it\n // via require() from the dist/plugins dir. Without a DB (local dev / tests),\n // keep the built-in htpasswd path.\n const usePgAuth = Boolean(config.databaseUrl || config.authStore);\n // Prefer a runtime-provided token (so the launcher owns it for load\n // detection); otherwise stash the injected store here (direct callers/tests).\n const storeToken =\n config.authStoreToken ??\n (config.authStore\n ? stashAuthStore(config.authStore as AuthStore)\n : undefined);\n const pluginsDir =\n config.pluginsDir ??\n path.join(path.dirname(fileURLToPath(import.meta.url)), \"plugins\");\n const auth = usePgAuth\n ? {\n \"registry-auth\": {\n ...(config.databaseUrl ? { databaseUrl: config.databaseUrl } : {}),\n ...(storeToken ? { storeToken } : {}),\n ...(config.renown\n ? {\n publicUrl: config.renown.publicUrl,\n ...(config.renown.renownUrl\n ? { renownUrl: config.renown.renownUrl }\n : {}),\n }\n : {}),\n },\n }\n : { htpasswd: { file: htpasswdPath } };\n\n const base: Record<string, unknown> = {\n storage: config.storagePath,\n self_path: \"./\",\n // Top-level secret used by verdaccio to sign / verify its API JWTs.\n // The renown middleware mints a verdaccio-format JWT with the same\n // secret so verdaccio's apiJWTmiddleware accepts the swapped token.\n ...(config.verdaccioSecret ? { secret: config.verdaccioSecret } : {}),\n // Force JWT mode for the npm API. Without this verdaccio falls back to\n // its legacy aes-encrypted token format, which signPayload won't produce.\n security: {\n api: {\n jwt: {\n sign: { expiresIn: \"90d\" },\n verify: {},\n },\n },\n },\n auth,\n ...(usePgAuth ? { plugins: pluginsDir } : {}),\n uplinks: {\n npmjs: {\n url: uplinkUrl,\n // Defaults to verdaccio's own default of 2m. The previous 15m\n // hardcoded value made publish-to-install dev loops painful —\n // when a newly-published version landed on npmjs, our registry\n // kept handing out the pre-publish packument for up to 15min.\n // Operators that want heavier upstream caching for production\n // can opt in via --uplink-maxage / PH_REGISTRY_UPLINK_MAXAGE.\n maxage: config.uplinkMaxage ?? \"2m\",\n timeout: \"30s\",\n cache: true,\n },\n },\n // Verdaccio matches packages config top-to-bottom (first match wins),\n // so local-only globs must come first. We also skip emitting the\n // default proxied entries when a glob with the same key is in the\n // local-only list — otherwise the value would be overwritten but the\n // iteration position would still be the default's earlier slot.\n packages: (() => {\n const local = config.localPackagePatterns ?? [];\n const localSet = new Set(local);\n const access = {\n access: \"$all\",\n publish: \"$authenticated\",\n unpublish: \"$authenticated\",\n };\n const entries: [string, Record<string, unknown>][] = [];\n // Locals first — no proxy means verdaccio resolves them from local\n // storage only, so re-publishing a version that exists on npmjs\n // doesn't 409.\n for (const pattern of local) {\n entries.push([pattern, { ...access }]);\n }\n // Defaults follow, skipping any glob the caller already overrode.\n if (!localSet.has(\"@powerhousedao/*\")) {\n entries.push([\"@powerhousedao/*\", { ...access, proxy: \"npmjs\" }]);\n }\n if (!localSet.has(\"**\")) {\n entries.push([\"**\", { ...access, proxy: \"npmjs\" }]);\n }\n return Object.fromEntries(entries);\n })(),\n web: {\n enable: config.webEnabled !== false,\n title: \"Powerhouse Registry\",\n logo: \"https://raw.githubusercontent.com/powerhouse-inc/powerhouse/main/packages/registry/static/logo.svg\",\n favicon: \"/-/static/favicon.ico\",\n primary_color: \"#38C780\",\n darkMode: true,\n },\n server: {\n keepAliveTimeout: 60,\n },\n log: {\n type: \"stdout\",\n format: \"pretty\",\n level: \"warn\",\n },\n max_body_size: config.maxBodySize ?? \"300mb\",\n };\n\n if (config.s3) {\n base.store = {\n \"aws-s3-storage\": {\n bucket: config.s3.bucket,\n endpoint: config.s3.endpoint,\n region: config.s3.region,\n s3ForcePathStyle: config.s3.s3ForcePathStyle ?? true,\n ...(config.s3.keyPrefix && { keyPrefix: config.s3.keyPrefix }),\n ...(config.s3.accessKeyId && { accessKeyId: config.s3.accessKeyId }),\n ...(config.s3.secretAccessKey && {\n secretAccessKey: config.s3.secretAccessKey,\n }),\n },\n };\n }\n\n return base;\n}\n","import express from \"express\";\nimport { findUp } from \"find-up\";\nimport { randomBytes } from \"node:crypto\";\nimport { mkdir } from \"node:fs/promises\";\nimport type { Server } from \"node:http\";\nimport path from \"node:path\";\nimport { runServer } from \"verdaccio\";\nimport type { AuthStore } from \"./auth/auth-store.js\";\nimport { createPgPool, createPgStore } from \"./auth/pg-store.js\";\nimport { stashAuthStore, wasStoreLoaded } from \"./auth/store-handoff.js\";\nimport {\n createPowerhouseRouter,\n createPublishHook,\n createUnpublishHook,\n} from \"./middleware.js\";\nimport { NotificationManager } from \"./notifications/manager.js\";\nimport { SSEChannel } from \"./notifications/sse.js\";\nimport { WebhookChannel } from \"./notifications/webhook.js\";\nimport type { RegistryCommandArgs, RegistryConfig } from \"./types.js\";\nimport { buildVerdaccioConfig } from \"./verdaccio-config.js\";\n\nasync function resolveDir(dir: string): Promise<string> {\n if (path.isAbsolute(dir)) {\n await mkdir(dir, { recursive: true });\n return dir;\n }\n const found = await findUp(dir, { type: \"directory\" });\n if (!found) {\n await mkdir(dir, { recursive: true });\n return dir;\n }\n return found;\n}\n\nexport async function runRegistry(args: RegistryCommandArgs) {\n const {\n port,\n storageDir,\n cdnCacheDir,\n uplink,\n uplinkMaxage,\n webEnabled,\n webhooks,\n s3AccessKeyId,\n s3Bucket,\n s3Endpoint,\n s3ForcePathStyle,\n s3KeyPrefix,\n s3Region,\n s3SecretAccessKey,\n publicUrl,\n authRenown,\n renownUrl,\n verdaccioSecret: verdaccioSecretArg,\n localPackages,\n databaseUrl,\n pluginsDir,\n authStore,\n } = args;\n const storagePath = await resolveDir(storageDir);\n const cdnCachePath = await resolveDir(cdnCacheDir);\n\n // Per-pod random verdaccio JWT secret. The verdaccio-format token we mint\n // in the renown middleware never leaves the pod (it's swapped into the\n // request before verdaccio sees it), so a per-pod secret is sufficient.\n // An override is exposed for tests / multi-pod behaviors that depend on\n // shared verdaccio JWTs.\n const verdaccioSecret = verdaccioSecretArg ?? randomBytes(32).toString(\"hex\");\n\n // Renown auth turns on when the operator both opts in (`--auth-renown`,\n // default true via the CLI flag) and has set --public-url for the audience\n // claim. Tests / programmatic users that don't pass either keep the legacy\n // unsigned/htpasswd path with no warning.\n const renownEnabled = authRenown === true && Boolean(publicUrl);\n if (authRenown === true && !publicUrl) {\n console.warn(\n \"[registry] auth-renown is enabled but --public-url / PH_REGISTRY_PUBLIC_URL is not set; Renown auth will be disabled.\",\n );\n }\n // Renown auth is served by the registry-auth plugin, which loads only with a\n // database (it also holds ownership). Without one, renown can't engage.\n if (renownEnabled && !databaseUrl && !authStore) {\n console.warn(\n \"[registry] Renown auth requires a database (--database-url) for the auth plugin; renown will be inactive.\",\n );\n }\n\n console.log({\n storagePath,\n cdnCachePath,\n });\n\n const webhookConfigs = webhooks\n ?.split(\",\")\n .map((url) => url.trim())\n .filter(Boolean)\n .map((endpoint) => ({ endpoint }));\n\n const localPackagePatterns = localPackages\n ?.split(\",\")\n .map((p) => p.trim())\n .filter(Boolean);\n\n // One AuthStore, shared by the verdaccio auth plugin and the /packages owner\n // enrichment — a single Postgres pool per process (injected store wins).\n const sharedAuthStore: AuthStore | undefined =\n (authStore as AuthStore | undefined) ??\n (databaseUrl ? createPgStore(createPgPool(databaseUrl)) : undefined);\n // Token carries the store through verdaccio's plugin config; we later assert\n // the plugin loaded it, so a configured-but-broken auth setup fails to boot.\n const authStoreToken = sharedAuthStore\n ? stashAuthStore(sharedAuthStore)\n : undefined;\n\n const config: RegistryConfig = {\n port,\n storagePath,\n cdnCachePath,\n uplink,\n uplinkMaxage,\n webEnabled,\n verdaccioSecret,\n ...(localPackagePatterns?.length ? { localPackagePatterns } : {}),\n ...(renownEnabled && publicUrl\n ? { renown: { publicUrl, ...(renownUrl ? { renownUrl } : {}) } }\n : {}),\n ...(webhookConfigs?.length && {\n notify: { webhooks: webhookConfigs },\n }),\n ...(s3Bucket &&\n s3Endpoint &&\n s3Region && {\n s3: {\n bucket: s3Bucket,\n endpoint: s3Endpoint,\n region: s3Region,\n accessKeyId: s3AccessKeyId,\n secretAccessKey: s3SecretAccessKey,\n keyPrefix: s3KeyPrefix,\n s3ForcePathStyle,\n },\n }),\n ...(databaseUrl ? { databaseUrl } : {}),\n ...(pluginsDir ? { pluginsDir } : {}),\n ...(sharedAuthStore ? { authStore: sharedAuthStore } : {}),\n ...(authStoreToken ? { authStoreToken } : {}),\n };\n\n if (config.databaseUrl || config.authStore) {\n console.log(\n \"[registry] Postgres-backed auth plugin active (persistent accounts + package ownership)\",\n );\n }\n // Ensure directories exist (for relative paths resolved via findUp)\n await mkdir(storagePath, { recursive: true });\n await mkdir(cdnCachePath, { recursive: true });\n\n const verdaccioConfig = buildVerdaccioConfig(config);\n\n // verdaccio's runServer returns Promise<any> (upstream type limitation)\n const verdaccioServer = (await runServer(verdaccioConfig)) as Server;\n\n // Fail fast: a configured auth store that the plugin never loaded means\n // verdaccio silently fell back to no auth — refuse to run without ownership.\n if (authStoreToken && !wasStoreLoaded(authStoreToken)) {\n verdaccioServer.close();\n throw new Error(\n \"registry-auth plugin failed to load despite a configured database/auth store; refusing to start without auth and package-ownership enforcement.\",\n );\n }\n const verdaccioHandler = verdaccioServer.listeners(\"request\")[0] as (\n ...args: unknown[]\n ) => void;\n\n const app = express();\n\n const sseChannel = new SSEChannel();\n const webhookChannel = new WebhookChannel(config.storagePath, config.notify);\n const notifications = new NotificationManager([sseChannel, webhookChannel]);\n\n // Serve static assets (logo, etc.)\n const staticDir = await findUp(\"static\", { type: \"directory\" });\n if (staticDir) {\n app.use(\"/-/static\", express.static(staticDir));\n }\n\n // Our routes take priority over Verdaccio\n app.use(\n createPowerhouseRouter(config, sseChannel, webhookChannel, sharedAuthStore),\n );\n\n app.use(createPublishHook(config, notifications));\n app.use(createUnpublishHook(config, notifications));\n\n // Verdaccio handles everything else (npm protocol, web UI, auth)\n app.use((req, res) => verdaccioHandler(req, res));\n\n const server = app.listen(port, () => {\n console.log(`Powerhouse Registry running on http://localhost:${port}`);\n console.log(` CDN: http://localhost:${port}/-/cdn/`);\n console.log(` Packages: http://localhost:${port}/packages`);\n console.log(` npm: http://localhost:${port}/`);\n console.log(` Storage: ${storagePath}`);\n console.log(` CDN cache: ${cdnCachePath}`);\n if (config.s3) {\n console.log(` S3: ${config.s3.endpoint}/${config.s3.bucket}`);\n }\n if (config.renown) {\n console.log(` Renown auth: ${config.renown.publicUrl}`);\n }\n });\n\n return server;\n}\n","import {\n binary,\n command,\n flag,\n number,\n option,\n optional,\n run,\n string,\n} from \"cmd-ts\";\nimport {\n DEFAULT_PORT,\n DEFAULT_REGISTRY_CDN_CACHE_DIR_NAME,\n DEFAULT_STORAGE_DIR_NAME,\n} from \"./src/constants.js\";\nimport { runRegistry } from \"./src/run.js\";\n\nexport const registryCommand = command({\n name: \"Package registry\",\n args: {\n port: option({\n long: \"port\",\n type: number,\n defaultValue: () => Number(process.env.PORT) || DEFAULT_PORT,\n defaultValueIsSerializable: true,\n }),\n storageDir: option({\n long: \"storage-dir\",\n type: string,\n defaultValue: () =>\n process.env.REGISTRY_STORAGE || DEFAULT_STORAGE_DIR_NAME,\n defaultValueIsSerializable: true,\n }),\n cdnCacheDir: option({\n long: \"cdn-cache-dir\",\n type: string,\n defaultValue: () =>\n process.env.REGISTRY_CDN_CACHE || DEFAULT_REGISTRY_CDN_CACHE_DIR_NAME,\n defaultValueIsSerializable: true,\n }),\n uplink: option({\n long: \"uplink\",\n type: optional(string),\n defaultValue: () => process.env.REGISTRY_UPLINK,\n defaultValueIsSerializable: true,\n }),\n uplinkMaxage: option({\n long: \"uplink-maxage\",\n type: optional(string),\n description:\n \"How long verdaccio caches npmjs uplink metadata before refetching. \" +\n \"Accepts verdaccio time strings (e.g. '30s', '2m', '1h'). \" +\n \"Default '2m' matches verdaccio upstream — shortens the publish-to-\" +\n \"install propagation window in dev. Bump for production deployments \" +\n \"that want to reduce npmjs load.\",\n defaultValue: () => process.env.PH_REGISTRY_UPLINK_MAXAGE,\n defaultValueIsSerializable: true,\n }),\n s3Bucket: option({\n long: \"s3-bucket\",\n type: optional(string),\n defaultValue: () => process.env.S3_BUCKET,\n defaultValueIsSerializable: true,\n }),\n s3Endpoint: option({\n long: \"s3-endpoint\",\n type: optional(string),\n defaultValue: () => process.env.S3_ENDPOINT,\n defaultValueIsSerializable: true,\n }),\n s3Region: option({\n long: \"s3-region\",\n type: optional(string),\n defaultValue: () => process.env.S3_REGION,\n defaultValueIsSerializable: true,\n }),\n s3AccessKeyId: option({\n long: \"s3-access-key-id\",\n type: optional(string),\n defaultValue: () => process.env.S3_ACCESS_KEY_ID,\n defaultValueIsSerializable: true,\n }),\n s3SecretAccessKey: option({\n long: \"s3-secret-access-key\",\n type: optional(string),\n defaultValue: () => process.env.S3_SECRET_ACCESS_KEY,\n defaultValueIsSerializable: true,\n }),\n s3KeyPrefix: option({\n long: \"s3-key-prefix\",\n type: optional(string),\n defaultValue: () => process.env.S3_KEY_PREFIX,\n defaultValueIsSerializable: true,\n }),\n s3ForcePathStyle: flag({\n long: \"s3-force-path-style\",\n defaultValue: () => process.env.S3_FORCE_PATH_STYLE !== \"false\",\n defaultValueIsSerializable: true,\n }),\n webEnabled: flag({\n long: \"web-enabled\",\n defaultValue: () => process.env.REGISTRY_WEB !== \"false\",\n defaultValueIsSerializable: true,\n }),\n webhooks: option({\n long: \"webhook\",\n type: optional(string),\n description: \"Comma-separated webhook URLs to notify on publish\",\n defaultValue: () => process.env.REGISTRY_WEBHOOKS,\n defaultValueIsSerializable: true,\n }),\n publicUrl: option({\n long: \"public-url\",\n type: optional(string),\n description:\n \"Public origin of this registry (used as the JWT `aud` claim for Renown bearer tokens). Required when --auth-renown is true.\",\n defaultValue: () => process.env.PH_REGISTRY_PUBLIC_URL,\n defaultValueIsSerializable: true,\n }),\n authRenown: flag({\n long: \"auth-renown\",\n description:\n \"Verify Renown-signed bearer tokens in front of verdaccio (stateless). Disabled when --public-url is unset.\",\n defaultValue: () => process.env.PH_REGISTRY_AUTH_RENOWN === \"true\",\n defaultValueIsSerializable: true,\n }),\n renownUrl: option({\n long: \"renown-url\",\n type: optional(string),\n description:\n \"Renown service base URL for credential verification. Defaults to https://www.renown.id.\",\n defaultValue: () => process.env.PH_REGISTRY_RENOWN_URL,\n defaultValueIsSerializable: true,\n }),\n verdaccioSecret: option({\n long: \"verdaccio-secret\",\n type: optional(string),\n description:\n \"Override verdaccio's internal JWT signing secret. Default: random per pod (fine — the swapped JWT never leaves this process).\",\n defaultValue: () => process.env.PH_REGISTRY_VERDACCIO_SECRET,\n defaultValueIsSerializable: true,\n }),\n localPackages: option({\n long: \"local-packages\",\n type: optional(string),\n description:\n \"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.\",\n defaultValue: () => process.env.PH_REGISTRY_LOCAL_PACKAGES,\n defaultValueIsSerializable: true,\n }),\n databaseUrl: option({\n long: \"database-url\",\n type: optional(string),\n description:\n \"Postgres connection string. When set, the registry uses the DB-backed auth plugin (persistent accounts + npm-style package ownership) instead of the built-in htpasswd.\",\n defaultValue: () =>\n process.env.PH_REGISTRY_DATABASE_URL ?? process.env.DATABASE_URL,\n defaultValueIsSerializable: true,\n }),\n },\n handler: async (args) => {\n // Redact secrets — this object otherwise leaks the DB password / signing\n // secret into logs.\n const redact = (v?: string) => (v ? \"[redacted]\" : undefined);\n console.log({\n ...args,\n databaseUrl: redact(args.databaseUrl),\n verdaccioSecret: redact(args.verdaccioSecret),\n s3SecretAccessKey: redact(args.s3SecretAccessKey),\n });\n\n try {\n await runRegistry(args);\n } catch (error) {\n console.error(\"Failed to start registry:\");\n console.error(error);\n process.exit(1);\n }\n },\n});\n\nconst registryCli = binary(registryCommand);\n\nawait run(registryCli, process.argv);\n"],"mappings":"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"}
@@ -28,6 +28,7 @@ let _verdaccio_core = require("@verdaccio/core");
28
28
  let bcryptjs = require("bcryptjs");
29
29
  bcryptjs = __toESM(bcryptjs);
30
30
  let pg = require("pg");
31
+ let _renown_sdk = require("@renown/sdk");
31
32
  //#region src/auth/pg-store.ts
32
33
  /** Build a real Postgres pool from a connection string. */
33
34
  function createPgPool(databaseUrl) {
@@ -80,6 +81,14 @@ function createPgStore(pool) {
80
81
  async getOwners(pkg) {
81
82
  return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? null;
82
83
  },
84
+ async getOwnersFor(pkgs) {
85
+ if (pkgs.length === 0) return {};
86
+ const placeholders = pkgs.map((_, i) => `$${i + 1}`).join(",");
87
+ const res = await pool.query(`SELECT package_name, owners FROM registry_package_owners WHERE package_name IN (${placeholders})`, pkgs);
88
+ const out = {};
89
+ for (const row of res.rows) out[row.package_name] = row.owners;
90
+ return out;
91
+ },
83
92
  async claimOwner(pkg, username) {
84
93
  await pool.query(`INSERT INTO registry_package_owners (package_name, owners)
85
94
  VALUES ($1, ARRAY[$2]::text[])
@@ -92,6 +101,78 @@ function createPgStore(pool) {
92
101
  };
93
102
  }
94
103
  //#endregion
104
+ //#region src/auth/store-handoff.ts
105
+ const REGISTRY_KEY = Symbol.for("@powerhousedao/registry:auth-store-handoff");
106
+ function registry() {
107
+ const g = globalThis;
108
+ return g[REGISTRY_KEY] ??= /* @__PURE__ */ new Map();
109
+ }
110
+ /** Resolve a stashed store by token (undefined if unknown). */
111
+ function takeAuthStore(token) {
112
+ return registry().get(token)?.store;
113
+ }
114
+ /** Record that the plugin fully constructed with this token's store. */
115
+ function markStoreLoaded(token) {
116
+ const entry = registry().get(token);
117
+ if (entry) entry.loaded = true;
118
+ }
119
+ //#endregion
120
+ //#region src/auth/renown-verifier.ts
121
+ function createRenownVerifier(config) {
122
+ const ttl = config.cacheTtlMs ?? 6e4;
123
+ const maxEntries = 1e3;
124
+ const cache = /* @__PURE__ */ new Map();
125
+ return async (token) => {
126
+ if (ttl > 0) {
127
+ const cached = cache.get(token);
128
+ if (cached) {
129
+ if (cached.expiresAt > Date.now()) return cached.did;
130
+ cache.delete(token);
131
+ }
132
+ }
133
+ const did = (await (0, _renown_sdk.verifyAuthCredential)(token, {
134
+ audience: config.publicUrl,
135
+ renownUrl: config.renownUrl,
136
+ switchboardUrl: config.switchboardUrl
137
+ }))?.did;
138
+ if (ttl > 0 && did) {
139
+ cache.set(token, {
140
+ did,
141
+ expiresAt: Date.now() + ttl
142
+ });
143
+ if (cache.size > maxEntries) {
144
+ const oldest = cache.keys().next().value;
145
+ if (oldest !== void 0) cache.delete(oldest);
146
+ }
147
+ }
148
+ return did;
149
+ };
150
+ }
151
+ function renownApiJwtMiddleware(verify, helpers) {
152
+ return (req, res, next) => {
153
+ const anon = helpers.createAnonymousRemoteUser();
154
+ req.remote_user = anon;
155
+ res.locals.remote_user = anon;
156
+ const header = req.headers.authorization;
157
+ const token = header && /^Bearer /i.test(header) ? header.slice(7).trim() : void 0;
158
+ if (!token) {
159
+ next();
160
+ return;
161
+ }
162
+ req.pause();
163
+ verify(token).then((did) => {
164
+ if (did) {
165
+ const user = helpers.createRemoteUser(did, ["renown"]);
166
+ req.remote_user = user;
167
+ res.locals.remote_user = user;
168
+ }
169
+ }).catch(() => void 0).finally(() => {
170
+ req.resume();
171
+ next();
172
+ });
173
+ };
174
+ }
175
+ //#endregion
95
176
  //#region src/auth/registry-auth-plugin.ts
96
177
  const BCRYPT_ROUNDS = 10;
97
178
  function internal(err) {
@@ -102,7 +183,7 @@ function internal(err) {
102
183
  * Pure plugin factory over an injected store — the unit-testable core.
103
184
  * `registryAuthPlugin` below wires it to a real Postgres store.
104
185
  */
105
- function createRegistryAuthPlugin(store) {
186
+ function createRegistryAuthPlugin(store, renownVerifier) {
106
187
  const ready = store.init();
107
188
  return {
108
189
  authenticate(user, password, cb) {
@@ -138,15 +219,23 @@ function createRegistryAuthPlugin(store) {
138
219
  if (owners && owners.includes(username)) return cb(null, true);
139
220
  return cb(_verdaccio_core.errorUtils.getForbidden(`not authorized to unpublish "${name}"`));
140
221
  }).catch((err) => cb(internal(err)));
141
- }
222
+ },
223
+ ...renownVerifier ? { apiJWTmiddleware(helpers) {
224
+ return renownApiJwtMiddleware(renownVerifier, helpers);
225
+ } } : {}
142
226
  };
143
227
  }
144
228
  /** Verdaccio plugin entry. The loader calls this factory (or `new`s the
145
229
  * default export — both return the plugin object). */
146
230
  function registryAuthPlugin(config) {
147
- return createRegistryAuthPlugin(config.store ?? createPgStore(createPgPool(config.databaseUrl ?? (() => {
148
- throw new Error("registry-auth plugin requires a databaseUrl (or an injected store)");
149
- })())));
231
+ const plugin = createRegistryAuthPlugin((config.storeToken ? takeAuthStore(config.storeToken) : void 0) ?? createPgStore(createPgPool(config.databaseUrl ?? (() => {
232
+ throw new Error("registry-auth plugin requires a databaseUrl (or a store token)");
233
+ })())), config.publicUrl ? createRenownVerifier({
234
+ publicUrl: config.publicUrl,
235
+ renownUrl: config.renownUrl
236
+ }) : void 0);
237
+ if (config.storeToken) markStoreLoaded(config.storeToken);
238
+ return plugin;
150
239
  }
151
240
  //#endregion
152
241
  exports.createRegistryAuthPlugin = createRegistryAuthPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@powerhousedao/registry",
3
- "version": "6.2.2-dev.4",
3
+ "version": "6.2.2-dev.40",
4
4
  "description": "Powerhouse package registry — resolves and manages document model package dependencies within the Powerhouse ecosystem.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -39,8 +39,8 @@
39
39
  "tar": "^7.5.11",
40
40
  "verdaccio": "^6.5.0",
41
41
  "verdaccio-aws-s3-storage": "^10.4.0",
42
- "@powerhousedao/shared": "6.2.2-dev.4",
43
- "@renown/sdk": "6.2.2-dev.4"
42
+ "@powerhousedao/shared": "6.2.2-dev.40",
43
+ "@renown/sdk": "6.2.2-dev.40"
44
44
  },
45
45
  "devDependencies": {
46
46
  "tsdown": "0.21.1",