@powerhousedao/registry 6.2.2-dev.1 → 6.2.2-dev.10
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 +4 -0
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +235 -84
- package/dist/cli.mjs.map +1 -1
- package/dist/plugins/package.json +1 -0
- package/dist/plugins/verdaccio-registry-auth.js +241 -0
- package/package.json +9 -3
package/dist/cli.d.mts
CHANGED
|
@@ -21,8 +21,10 @@ 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;
|
|
27
|
+
databaseUrl: string | undefined;
|
|
26
28
|
}>>;
|
|
27
29
|
} & cmd_ts_dist_cjs_helpdoc_js0.PrintHelp & cmd_ts_dist_cjs_helpdoc_js0.ProvidesHelp & cmd_ts_dist_cjs_helpdoc_js0.Named & Partial<cmd_ts_dist_cjs_helpdoc_js0.Versioned> & cmd_ts_dist_cjs_argparser_js0.Register & cmd_ts_dist_cjs_runner_js0.Handling<{
|
|
28
30
|
port: number;
|
|
@@ -41,8 +43,10 @@ declare const registryCommand: Partial<cmd_ts_dist_cjs_argparser_js0.Register> &
|
|
|
41
43
|
webhooks: string | undefined;
|
|
42
44
|
publicUrl: string | undefined;
|
|
43
45
|
authRenown: boolean;
|
|
46
|
+
renownUrl: string | undefined;
|
|
44
47
|
verdaccioSecret: string | undefined;
|
|
45
48
|
localPackages: string | undefined;
|
|
49
|
+
databaseUrl: string | undefined;
|
|
46
50
|
}, Promise<void>> & {
|
|
47
51
|
run(context: cmd_ts_dist_cjs_argparser_js0.ParseContext): Promise<cmd_ts_dist_cjs_argparser_js0.ParsingResult<Promise<void>>>;
|
|
48
52
|
} & Partial<cmd_ts_dist_cjs_helpdoc_js0.Versioned & cmd_ts_dist_cjs_helpdoc_js0.Descriptive & cmd_ts_dist_cjs_helpdoc_js0.Aliased>;
|
package/dist/cli.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.mts","names":[],"sources":["../cli.ts"],"mappings":";;;;;cAiBa,eAAA,EAAe,OAAA,
|
|
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,77 +1,110 @@
|
|
|
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 {
|
|
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";
|
|
13
12
|
import { extract } from "tar";
|
|
14
13
|
import { slimManifest } from "@powerhousedao/shared/registry";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region src/auth/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
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 });
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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)
|
|
26
25
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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).
|
|
32
31
|
*/
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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();
|
|
63
84
|
}
|
|
64
|
-
req.headers.authorization = `Bearer ${verdaccioJwt}`;
|
|
65
|
-
req.renownUser = {
|
|
66
|
-
address,
|
|
67
|
-
did: payload?.iss,
|
|
68
|
-
chainId: subject.chainId,
|
|
69
|
-
networkId: subject.networkId
|
|
70
|
-
};
|
|
71
|
-
return next();
|
|
72
85
|
};
|
|
73
86
|
}
|
|
74
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
|
|
75
108
|
//#region src/semver.ts
|
|
76
109
|
/**
|
|
77
110
|
* Compare two semver version strings for sorting.
|
|
@@ -245,6 +278,37 @@ var CdnCache = class {
|
|
|
245
278
|
if (fs.readdirSync(pkgDir).length === 0) fs.rmdirSync(pkgDir);
|
|
246
279
|
} catch {}
|
|
247
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
|
+
}
|
|
248
312
|
/** Remove all cached version directories except the specified one. */
|
|
249
313
|
pruneOldVersions(packageName, keepVersion) {
|
|
250
314
|
const pkgDir = path.join(this.cdnCachePath, packageName);
|
|
@@ -492,6 +556,15 @@ const MIME_TYPES = {
|
|
|
492
556
|
function getContentType(filePath) {
|
|
493
557
|
return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
494
558
|
}
|
|
559
|
+
function publisherFromRequest(req) {
|
|
560
|
+
const name = req.remote_user?.name;
|
|
561
|
+
if (!name) return void 0;
|
|
562
|
+
if (name.startsWith("did:pkh:")) return {
|
|
563
|
+
address: (name.split(":").pop() ?? name).toLowerCase(),
|
|
564
|
+
did: name
|
|
565
|
+
};
|
|
566
|
+
return { address: name };
|
|
567
|
+
}
|
|
495
568
|
/**
|
|
496
569
|
* Resolve a package version. Exact versions skip the network call. Upstream
|
|
497
570
|
* errors fall back to the latest cached version; genuine not-found falls back
|
|
@@ -531,9 +604,23 @@ function etagMatches(header, etag) {
|
|
|
531
604
|
return tag === "*" || opaqueTag(tag) === target;
|
|
532
605
|
});
|
|
533
606
|
}
|
|
534
|
-
function createPowerhouseRouter(config, sse, webhooks) {
|
|
607
|
+
function createPowerhouseRouter(config, sse, webhooks, ownerStore) {
|
|
535
608
|
const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
|
|
536
609
|
const router = Router();
|
|
610
|
+
const withOwners = async (pkgs) => {
|
|
611
|
+
if (!ownerStore || pkgs.length === 0) return pkgs;
|
|
612
|
+
try {
|
|
613
|
+
await ownerStore.init();
|
|
614
|
+
const map = await ownerStore.getOwnersFor(pkgs.map((p) => p.name));
|
|
615
|
+
return pkgs.map((p) => p.name in map ? {
|
|
616
|
+
...p,
|
|
617
|
+
owners: map[p.name]
|
|
618
|
+
} : p);
|
|
619
|
+
} catch (err) {
|
|
620
|
+
console.error("[registry] owner lookup failed:", err);
|
|
621
|
+
return pkgs;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
537
624
|
router.use((_req, res, next) => {
|
|
538
625
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
539
626
|
next();
|
|
@@ -573,16 +660,12 @@ function createPowerhouseRouter(config, sse, webhooks) {
|
|
|
573
660
|
});
|
|
574
661
|
const warm = createWarmer(config, cdn);
|
|
575
662
|
warm();
|
|
576
|
-
router.get("/packages", (req, res) => {
|
|
663
|
+
router.get("/packages", async (req, res) => {
|
|
577
664
|
warm();
|
|
578
665
|
const packages = scanPackages(config.cdnCachePath, config.storagePath);
|
|
579
666
|
const documentType = req.query.documentType;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
res.json(filtered);
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
res.json(packages);
|
|
667
|
+
const selected = documentType ? packages.filter((pkg) => pkg.manifest?.documentModels?.some((m) => m.id === documentType)) : packages;
|
|
668
|
+
res.json(await withOwners(selected));
|
|
586
669
|
});
|
|
587
670
|
router.get("/packages/by-document-type", (req, res) => {
|
|
588
671
|
const documentType = req.query.type;
|
|
@@ -607,7 +690,7 @@ function createPowerhouseRouter(config, sse, webhooks) {
|
|
|
607
690
|
res.status(404).send("Package not found");
|
|
608
691
|
return;
|
|
609
692
|
}
|
|
610
|
-
res.json(pkg);
|
|
693
|
+
res.json((await withOwners([pkg]))[0]);
|
|
611
694
|
});
|
|
612
695
|
router.get("/-/cdn/*", async (req, res) => {
|
|
613
696
|
const fullPath = req.params[0];
|
|
@@ -693,9 +776,41 @@ function parseUnpublishRequest(reqPath) {
|
|
|
693
776
|
version
|
|
694
777
|
};
|
|
695
778
|
}
|
|
779
|
+
function parseManifestRewrite(reqPath) {
|
|
780
|
+
const revIdx = reqPath.indexOf("/-rev/");
|
|
781
|
+
if (revIdx <= 0) return null;
|
|
782
|
+
const beforeRev = reqPath.slice(1, revIdx);
|
|
783
|
+
if (beforeRev.includes("/-/")) return null;
|
|
784
|
+
return { packageName: decodeURIComponent(beforeRev) };
|
|
785
|
+
}
|
|
696
786
|
function createUnpublishHook(config, notifications) {
|
|
697
787
|
const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
|
|
788
|
+
const handleManifestRewrite = (req, res) => {
|
|
789
|
+
const rewrite = parseManifestRewrite(req.path);
|
|
790
|
+
if (!rewrite) return;
|
|
791
|
+
const originalEnd = res.end.bind(res);
|
|
792
|
+
res.end = function(chunk, encoding, cb) {
|
|
793
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
794
|
+
const publishedBy = publisherFromRequest(req);
|
|
795
|
+
cdn.reconcileWithRegistry(rewrite.packageName).then((removed) => {
|
|
796
|
+
for (const version of removed) notifications.notifyUnpublish({
|
|
797
|
+
packageName: rewrite.packageName,
|
|
798
|
+
version,
|
|
799
|
+
publishedBy
|
|
800
|
+
});
|
|
801
|
+
}).catch((err) => {
|
|
802
|
+
console.error(`[registry] CDN reconcile failed for ${rewrite.packageName}:`, err);
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return originalEnd(chunk, encoding, cb);
|
|
806
|
+
};
|
|
807
|
+
};
|
|
698
808
|
return (req, res, next) => {
|
|
809
|
+
if (req.method === "PUT") {
|
|
810
|
+
handleManifestRewrite(req, res);
|
|
811
|
+
next();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
699
814
|
if (req.method !== "DELETE") {
|
|
700
815
|
next();
|
|
701
816
|
return;
|
|
@@ -710,14 +825,10 @@ function createUnpublishHook(config, notifications) {
|
|
|
710
825
|
if (res.statusCode >= 200 && res.statusCode < 300) try {
|
|
711
826
|
if (parsed.version) cdn.invalidateVersion(parsed.packageName, parsed.version);
|
|
712
827
|
else cdn.invalidate(parsed.packageName);
|
|
713
|
-
const renownUser = req.renownUser;
|
|
714
828
|
notifications.notifyUnpublish({
|
|
715
829
|
packageName: parsed.packageName,
|
|
716
830
|
version: parsed.version,
|
|
717
|
-
publishedBy:
|
|
718
|
-
address: renownUser.address,
|
|
719
|
-
did: renownUser.did
|
|
720
|
-
} : void 0
|
|
831
|
+
publishedBy: publisherFromRequest(req)
|
|
721
832
|
});
|
|
722
833
|
} catch (err) {
|
|
723
834
|
console.error(`[registry] CDN purge failed for ${parsed.packageName}${parsed.version ? `@${parsed.version}` : ""}:`, err);
|
|
@@ -747,11 +858,7 @@ function createPublishHook(config, notifications) {
|
|
|
747
858
|
return originalEnd(chunk, encoding, cb);
|
|
748
859
|
}
|
|
749
860
|
if (versions.length > 1) console.warn(`[registry] Multiple versions published for ${packageName}: ${JSON.stringify(versions)}`);
|
|
750
|
-
const
|
|
751
|
-
const publishedBy = renownUser ? {
|
|
752
|
-
address: renownUser.address,
|
|
753
|
-
did: renownUser.did
|
|
754
|
-
} : void 0;
|
|
861
|
+
const publishedBy = publisherFromRequest(req);
|
|
755
862
|
cdn.extractTarball(packageName, version).then(() => {
|
|
756
863
|
notifications.notifyPublish({
|
|
757
864
|
packageName,
|
|
@@ -888,6 +995,17 @@ var WebhookChannel = class {
|
|
|
888
995
|
function buildVerdaccioConfig(config) {
|
|
889
996
|
const htpasswdPath = path.join(config.storagePath, "htpasswd");
|
|
890
997
|
const uplinkUrl = config.uplink ?? "https://registry.npmjs.org/";
|
|
998
|
+
const usePgAuth = Boolean(config.databaseUrl || config.authStore);
|
|
999
|
+
const storeToken = config.authStoreToken ?? (config.authStore ? stashAuthStore(config.authStore) : void 0);
|
|
1000
|
+
const pluginsDir = config.pluginsDir ?? path.join(path.dirname(fileURLToPath(import.meta.url)), "plugins");
|
|
1001
|
+
const auth = usePgAuth ? { "registry-auth": {
|
|
1002
|
+
...config.databaseUrl ? { databaseUrl: config.databaseUrl } : {},
|
|
1003
|
+
...storeToken ? { storeToken } : {},
|
|
1004
|
+
...config.renown ? {
|
|
1005
|
+
publicUrl: config.renown.publicUrl,
|
|
1006
|
+
...config.renown.renownUrl ? { renownUrl: config.renown.renownUrl } : {}
|
|
1007
|
+
} : {}
|
|
1008
|
+
} } : { htpasswd: { file: htpasswdPath } };
|
|
891
1009
|
const base = {
|
|
892
1010
|
storage: config.storagePath,
|
|
893
1011
|
self_path: "./",
|
|
@@ -896,7 +1014,8 @@ function buildVerdaccioConfig(config) {
|
|
|
896
1014
|
sign: { expiresIn: "90d" },
|
|
897
1015
|
verify: {}
|
|
898
1016
|
} } },
|
|
899
|
-
auth
|
|
1017
|
+
auth,
|
|
1018
|
+
...usePgAuth ? { plugins: pluginsDir } : {},
|
|
900
1019
|
uplinks: { npmjs: {
|
|
901
1020
|
url: uplinkUrl,
|
|
902
1021
|
maxage: config.uplinkMaxage ?? "2m",
|
|
@@ -965,18 +1084,21 @@ async function resolveDir(dir) {
|
|
|
965
1084
|
return found;
|
|
966
1085
|
}
|
|
967
1086
|
async function runRegistry(args) {
|
|
968
|
-
const { port, storageDir, cdnCacheDir, uplink, uplinkMaxage, webEnabled, webhooks, s3AccessKeyId, s3Bucket, s3Endpoint, s3ForcePathStyle, s3KeyPrefix, s3Region, s3SecretAccessKey, publicUrl, authRenown, verdaccioSecret: verdaccioSecretArg, localPackages } = args;
|
|
1087
|
+
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;
|
|
969
1088
|
const storagePath = await resolveDir(storageDir);
|
|
970
1089
|
const cdnCachePath = await resolveDir(cdnCacheDir);
|
|
971
1090
|
const verdaccioSecret = verdaccioSecretArg ?? randomBytes(32).toString("hex");
|
|
972
1091
|
const renownEnabled = authRenown === true && Boolean(publicUrl);
|
|
973
1092
|
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.");
|
|
1093
|
+
if (renownEnabled && !databaseUrl && !authStore) console.warn("[registry] Renown auth requires a database (--database-url) for the auth plugin; renown will be inactive.");
|
|
974
1094
|
console.log({
|
|
975
1095
|
storagePath,
|
|
976
1096
|
cdnCachePath
|
|
977
1097
|
});
|
|
978
1098
|
const webhookConfigs = webhooks?.split(",").map((url) => url.trim()).filter(Boolean).map((endpoint) => ({ endpoint }));
|
|
979
1099
|
const localPackagePatterns = localPackages?.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1100
|
+
const sharedAuthStore = authStore ?? (databaseUrl ? createPgStore(createPgPool(databaseUrl)) : void 0);
|
|
1101
|
+
const authStoreToken = sharedAuthStore ? stashAuthStore(sharedAuthStore) : void 0;
|
|
980
1102
|
const config = {
|
|
981
1103
|
port,
|
|
982
1104
|
storagePath,
|
|
@@ -986,7 +1108,10 @@ async function runRegistry(args) {
|
|
|
986
1108
|
webEnabled,
|
|
987
1109
|
verdaccioSecret,
|
|
988
1110
|
...localPackagePatterns?.length ? { localPackagePatterns } : {},
|
|
989
|
-
...renownEnabled && publicUrl ? { renown: {
|
|
1111
|
+
...renownEnabled && publicUrl ? { renown: {
|
|
1112
|
+
publicUrl,
|
|
1113
|
+
...renownUrl ? { renownUrl } : {}
|
|
1114
|
+
} } : {},
|
|
990
1115
|
...webhookConfigs?.length && { notify: { webhooks: webhookConfigs } },
|
|
991
1116
|
...s3Bucket && s3Endpoint && s3Region && { s3: {
|
|
992
1117
|
bucket: s3Bucket,
|
|
@@ -996,22 +1121,28 @@ async function runRegistry(args) {
|
|
|
996
1121
|
secretAccessKey: s3SecretAccessKey,
|
|
997
1122
|
keyPrefix: s3KeyPrefix,
|
|
998
1123
|
s3ForcePathStyle
|
|
999
|
-
} }
|
|
1124
|
+
} },
|
|
1125
|
+
...databaseUrl ? { databaseUrl } : {},
|
|
1126
|
+
...pluginsDir ? { pluginsDir } : {},
|
|
1127
|
+
...sharedAuthStore ? { authStore: sharedAuthStore } : {},
|
|
1128
|
+
...authStoreToken ? { authStoreToken } : {}
|
|
1000
1129
|
};
|
|
1130
|
+
if (config.databaseUrl || config.authStore) console.log("[registry] Postgres-backed auth plugin active (persistent accounts + package ownership)");
|
|
1001
1131
|
await mkdir(storagePath, { recursive: true });
|
|
1002
1132
|
await mkdir(cdnCachePath, { recursive: true });
|
|
1003
|
-
const
|
|
1133
|
+
const verdaccioServer = await runServer(buildVerdaccioConfig(config));
|
|
1134
|
+
if (authStoreToken && !wasStoreLoaded(authStoreToken)) {
|
|
1135
|
+
verdaccioServer.close();
|
|
1136
|
+
throw new Error("registry-auth plugin failed to load despite a configured database/auth store; refusing to start without auth and package-ownership enforcement.");
|
|
1137
|
+
}
|
|
1138
|
+
const verdaccioHandler = verdaccioServer.listeners("request")[0];
|
|
1004
1139
|
const app = express();
|
|
1005
1140
|
const sseChannel = new SSEChannel();
|
|
1006
1141
|
const webhookChannel = new WebhookChannel(config.storagePath, config.notify);
|
|
1007
1142
|
const notifications = new NotificationManager([sseChannel, webhookChannel]);
|
|
1008
1143
|
const staticDir = await findUp("static", { type: "directory" });
|
|
1009
1144
|
if (staticDir) app.use("/-/static", express.static(staticDir));
|
|
1010
|
-
app.use(createPowerhouseRouter(config, sseChannel, webhookChannel));
|
|
1011
|
-
if (config.renown) app.use(createRenownAuthMiddleware({
|
|
1012
|
-
publicUrl: config.renown.publicUrl,
|
|
1013
|
-
verdaccioSecret
|
|
1014
|
-
}));
|
|
1145
|
+
app.use(createPowerhouseRouter(config, sseChannel, webhookChannel, sharedAuthStore));
|
|
1015
1146
|
app.use(createPublishHook(config, notifications));
|
|
1016
1147
|
app.use(createUnpublishHook(config, notifications));
|
|
1017
1148
|
app.use((req, res) => verdaccioHandler(req, res));
|
|
@@ -1128,6 +1259,13 @@ const registryCommand = command({
|
|
|
1128
1259
|
defaultValue: () => process.env.PH_REGISTRY_AUTH_RENOWN === "true",
|
|
1129
1260
|
defaultValueIsSerializable: true
|
|
1130
1261
|
}),
|
|
1262
|
+
renownUrl: option({
|
|
1263
|
+
long: "renown-url",
|
|
1264
|
+
type: optional(string),
|
|
1265
|
+
description: "Renown service base URL for credential verification. Defaults to https://www.renown.id.",
|
|
1266
|
+
defaultValue: () => process.env.PH_REGISTRY_RENOWN_URL,
|
|
1267
|
+
defaultValueIsSerializable: true
|
|
1268
|
+
}),
|
|
1131
1269
|
verdaccioSecret: option({
|
|
1132
1270
|
long: "verdaccio-secret",
|
|
1133
1271
|
type: optional(string),
|
|
@@ -1141,10 +1279,23 @@ const registryCommand = command({
|
|
|
1141
1279
|
description: "Comma-separated globs (e.g. '@powerhousedao/*,document-model,ph-cmd') served locally only — no npmjs uplink proxy. Lets you re-publish a workspace package whose version already exists on npmjs without bumping.",
|
|
1142
1280
|
defaultValue: () => process.env.PH_REGISTRY_LOCAL_PACKAGES,
|
|
1143
1281
|
defaultValueIsSerializable: true
|
|
1282
|
+
}),
|
|
1283
|
+
databaseUrl: option({
|
|
1284
|
+
long: "database-url",
|
|
1285
|
+
type: optional(string),
|
|
1286
|
+
description: "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.",
|
|
1287
|
+
defaultValue: () => process.env.PH_REGISTRY_DATABASE_URL ?? process.env.DATABASE_URL,
|
|
1288
|
+
defaultValueIsSerializable: true
|
|
1144
1289
|
})
|
|
1145
1290
|
},
|
|
1146
1291
|
handler: async (args) => {
|
|
1147
|
-
|
|
1292
|
+
const redact = (v) => v ? "[redacted]" : void 0;
|
|
1293
|
+
console.log({
|
|
1294
|
+
...args,
|
|
1295
|
+
databaseUrl: redact(args.databaseUrl),
|
|
1296
|
+
verdaccioSecret: redact(args.verdaccioSecret),
|
|
1297
|
+
s3SecretAccessKey: redact(args.s3SecretAccessKey)
|
|
1298
|
+
});
|
|
1148
1299
|
try {
|
|
1149
1300
|
await runRegistry(args);
|
|
1150
1301
|
} 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 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 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 htpasswd: {\n file: htpasswdPath,\n },\n },\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 } = 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 };\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 },\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 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 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} 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\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 const documentType = req.query.documentType as string | undefined;\n const selected = documentType\n ? packages.filter((pkg) =>\n pkg.manifest?.documentModels?.some((m) => m.id === documentType),\n )\n : packages;\n res.json(await withOwners(selected));\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((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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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
14
|
+
key = keys[i];
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
//#endregion
|
|
27
|
+
let _verdaccio_core = require("@verdaccio/core");
|
|
28
|
+
let bcryptjs = require("bcryptjs");
|
|
29
|
+
bcryptjs = __toESM(bcryptjs);
|
|
30
|
+
let pg = require("pg");
|
|
31
|
+
let _renown_sdk = require("@renown/sdk");
|
|
32
|
+
//#region src/auth/pg-store.ts
|
|
33
|
+
/** Build a real Postgres pool from a connection string. */
|
|
34
|
+
function createPgPool(databaseUrl) {
|
|
35
|
+
return new pg.Pool({ connectionString: databaseUrl });
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Postgres-backed AuthStore. Two small tables:
|
|
39
|
+
* - registry_users(username PK, password_hash, created_at)
|
|
40
|
+
* - registry_package_owners(package_name PK, owners text[], claimed_at)
|
|
41
|
+
*
|
|
42
|
+
* Ownership claim is race-free: `INSERT ... ON CONFLICT DO NOTHING` means the
|
|
43
|
+
* first publisher wins atomically; the follow-up read returns the actual
|
|
44
|
+
* owners so a losing racer is denied.
|
|
45
|
+
*
|
|
46
|
+
* Takes an already-built `pg.Pool` (tests inject a pg-mem pool cast to Pool).
|
|
47
|
+
*/
|
|
48
|
+
function createPgStore(pool) {
|
|
49
|
+
let initialized = null;
|
|
50
|
+
return {
|
|
51
|
+
init() {
|
|
52
|
+
initialized ??= (async () => {
|
|
53
|
+
await pool.query(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS registry_users (
|
|
55
|
+
username text PRIMARY KEY,
|
|
56
|
+
password_hash text NOT NULL,
|
|
57
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
58
|
+
)`);
|
|
59
|
+
await pool.query(`
|
|
60
|
+
CREATE TABLE IF NOT EXISTS registry_package_owners (
|
|
61
|
+
package_name text PRIMARY KEY,
|
|
62
|
+
owners text[] NOT NULL,
|
|
63
|
+
claimed_at timestamptz NOT NULL DEFAULT now()
|
|
64
|
+
)`);
|
|
65
|
+
})();
|
|
66
|
+
return initialized;
|
|
67
|
+
},
|
|
68
|
+
async getUser(username) {
|
|
69
|
+
const row = (await pool.query("SELECT password_hash FROM registry_users WHERE username = $1", [username])).rows[0];
|
|
70
|
+
return row ? { passwordHash: row.password_hash } : null;
|
|
71
|
+
},
|
|
72
|
+
async createUser(username, passwordHash) {
|
|
73
|
+
try {
|
|
74
|
+
await pool.query("INSERT INTO registry_users (username, password_hash) VALUES ($1, $2)", [username, passwordHash]);
|
|
75
|
+
return true;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err.code === "23505") return false;
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async getOwners(pkg) {
|
|
82
|
+
return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? null;
|
|
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
|
+
},
|
|
92
|
+
async claimOwner(pkg, username) {
|
|
93
|
+
await pool.query(`INSERT INTO registry_package_owners (package_name, owners)
|
|
94
|
+
VALUES ($1, ARRAY[$2]::text[])
|
|
95
|
+
ON CONFLICT (package_name) DO NOTHING`, [pkg, username]);
|
|
96
|
+
return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? [];
|
|
97
|
+
},
|
|
98
|
+
close() {
|
|
99
|
+
return pool.end();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
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
|
+
}))?.did;
|
|
137
|
+
if (ttl > 0 && did) {
|
|
138
|
+
cache.set(token, {
|
|
139
|
+
did,
|
|
140
|
+
expiresAt: Date.now() + ttl
|
|
141
|
+
});
|
|
142
|
+
if (cache.size > maxEntries) {
|
|
143
|
+
const oldest = cache.keys().next().value;
|
|
144
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return did;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function renownApiJwtMiddleware(verify, helpers) {
|
|
151
|
+
return (req, res, next) => {
|
|
152
|
+
const anon = helpers.createAnonymousRemoteUser();
|
|
153
|
+
req.remote_user = anon;
|
|
154
|
+
res.locals.remote_user = anon;
|
|
155
|
+
const header = req.headers.authorization;
|
|
156
|
+
const token = header && /^Bearer /i.test(header) ? header.slice(7).trim() : void 0;
|
|
157
|
+
if (!token) {
|
|
158
|
+
next();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
req.pause();
|
|
162
|
+
verify(token).then((did) => {
|
|
163
|
+
if (did) {
|
|
164
|
+
const user = helpers.createRemoteUser(did, ["renown"]);
|
|
165
|
+
req.remote_user = user;
|
|
166
|
+
res.locals.remote_user = user;
|
|
167
|
+
}
|
|
168
|
+
}).catch(() => void 0).finally(() => {
|
|
169
|
+
req.resume();
|
|
170
|
+
next();
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/auth/registry-auth-plugin.ts
|
|
176
|
+
const BCRYPT_ROUNDS = 10;
|
|
177
|
+
function internal(err) {
|
|
178
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
179
|
+
return _verdaccio_core.errorUtils.getInternalError(msg);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Pure plugin factory over an injected store — the unit-testable core.
|
|
183
|
+
* `registryAuthPlugin` below wires it to a real Postgres store.
|
|
184
|
+
*/
|
|
185
|
+
function createRegistryAuthPlugin(store, renownVerifier) {
|
|
186
|
+
const ready = store.init();
|
|
187
|
+
return {
|
|
188
|
+
authenticate(user, password, cb) {
|
|
189
|
+
ready.then(() => store.getUser(user)).then((rec) => {
|
|
190
|
+
if (!rec) return cb(null, false);
|
|
191
|
+
if (!bcryptjs.default.compareSync(password, rec.passwordHash)) return cb(_verdaccio_core.errorUtils.getUnauthorized("bad username or password"));
|
|
192
|
+
return cb(null, [user]);
|
|
193
|
+
}).catch((err) => cb(internal(err)));
|
|
194
|
+
},
|
|
195
|
+
adduser(user, password, cb) {
|
|
196
|
+
ready.then(() => store.createUser(user, bcryptjs.default.hashSync(password, BCRYPT_ROUNDS))).then((created) => {
|
|
197
|
+
if (!created) return cb(_verdaccio_core.errorUtils.getConflict("username already registered"));
|
|
198
|
+
return cb(null, true);
|
|
199
|
+
}).catch((err) => cb(internal(err)));
|
|
200
|
+
},
|
|
201
|
+
allow_access(_user, _pkg, cb) {
|
|
202
|
+
cb(null, true);
|
|
203
|
+
},
|
|
204
|
+
allow_publish(user, pkg, cb) {
|
|
205
|
+
const name = pkg.name ?? "";
|
|
206
|
+
const username = user.name;
|
|
207
|
+
if (!username) return cb(_verdaccio_core.errorUtils.getForbidden("authentication required to publish"));
|
|
208
|
+
ready.then(() => store.claimOwner(name, username)).then((owners) => {
|
|
209
|
+
if (owners.includes(username)) return cb(null, true);
|
|
210
|
+
return cb(_verdaccio_core.errorUtils.getForbidden(`not authorized to publish "${name}" (owned by another user)`));
|
|
211
|
+
}).catch((err) => cb(internal(err)));
|
|
212
|
+
},
|
|
213
|
+
allow_unpublish(user, pkg, cb) {
|
|
214
|
+
const name = pkg.name ?? "";
|
|
215
|
+
const username = user.name;
|
|
216
|
+
if (!username) return cb(_verdaccio_core.errorUtils.getForbidden("authentication required to unpublish"));
|
|
217
|
+
ready.then(() => store.getOwners(name)).then((owners) => {
|
|
218
|
+
if (owners && owners.includes(username)) return cb(null, true);
|
|
219
|
+
return cb(_verdaccio_core.errorUtils.getForbidden(`not authorized to unpublish "${name}"`));
|
|
220
|
+
}).catch((err) => cb(internal(err)));
|
|
221
|
+
},
|
|
222
|
+
...renownVerifier ? { apiJWTmiddleware(helpers) {
|
|
223
|
+
return renownApiJwtMiddleware(renownVerifier, helpers);
|
|
224
|
+
} } : {}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Verdaccio plugin entry. The loader calls this factory (or `new`s the
|
|
228
|
+
* default export — both return the plugin object). */
|
|
229
|
+
function registryAuthPlugin(config) {
|
|
230
|
+
const plugin = createRegistryAuthPlugin((config.storeToken ? takeAuthStore(config.storeToken) : void 0) ?? createPgStore(createPgPool(config.databaseUrl ?? (() => {
|
|
231
|
+
throw new Error("registry-auth plugin requires a databaseUrl (or a store token)");
|
|
232
|
+
})())), config.publicUrl ? createRenownVerifier({
|
|
233
|
+
publicUrl: config.publicUrl,
|
|
234
|
+
renownUrl: config.renownUrl
|
|
235
|
+
}) : void 0);
|
|
236
|
+
if (config.storeToken) markStoreLoaded(config.storeToken);
|
|
237
|
+
return plugin;
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
exports.createRegistryAuthPlugin = createRegistryAuthPlugin;
|
|
241
|
+
exports.default = registryAuthPlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerhousedao/registry",
|
|
3
|
-
"version": "6.2.2-dev.
|
|
3
|
+
"version": "6.2.2-dev.10",
|
|
4
4
|
"description": "Powerhouse package registry — resolves and manages document model package dependencies within the Powerhouse ecosystem.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -29,21 +29,27 @@
|
|
|
29
29
|
"author": "",
|
|
30
30
|
"license": "ISC",
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@verdaccio/core": "8.0.0-next-8.37",
|
|
32
33
|
"@verdaccio/signature": "8.0.0-next-8.29",
|
|
34
|
+
"bcryptjs": "^2.4.3",
|
|
33
35
|
"cmd-ts": "0.15.0",
|
|
34
36
|
"express": "^4.22.1",
|
|
35
37
|
"find-up": "^8.0.0",
|
|
38
|
+
"pg": "8.18.0",
|
|
36
39
|
"tar": "^7.5.11",
|
|
37
40
|
"verdaccio": "^6.5.0",
|
|
38
41
|
"verdaccio-aws-s3-storage": "^10.4.0",
|
|
39
|
-
"@powerhousedao/shared": "6.2.2-dev.
|
|
40
|
-
"@renown/sdk": "6.2.2-dev.
|
|
42
|
+
"@powerhousedao/shared": "6.2.2-dev.10",
|
|
43
|
+
"@renown/sdk": "6.2.2-dev.10"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"tsdown": "0.21.1",
|
|
47
|
+
"@types/bcryptjs": "^2.4.6",
|
|
44
48
|
"@types/express": "^4.17.21",
|
|
45
49
|
"@types/node": "25.2.3",
|
|
50
|
+
"@types/pg": "8.16.0",
|
|
46
51
|
"@types/supertest": "^6.0.2",
|
|
52
|
+
"pg-mem": "^3.0.14",
|
|
47
53
|
"supertest": "^7.1.0",
|
|
48
54
|
"vitest": "4.1.1"
|
|
49
55
|
},
|