@powerhousedao/registry 6.2.2-dev.4 → 6.2.2-dev.5
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 +2 -0
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +216 -84
- package/dist/cli.mjs.map +1 -1
- package/dist/plugins/verdaccio-registry-auth.js +93 -5
- package/package.json +3 -3
package/dist/cli.d.mts
CHANGED
|
@@ -21,6 +21,7 @@ declare const registryCommand: Partial<cmd_ts_dist_cjs_argparser_js0.Register> &
|
|
|
21
21
|
webhooks: string | undefined;
|
|
22
22
|
publicUrl: string | undefined;
|
|
23
23
|
authRenown: boolean;
|
|
24
|
+
renownUrl: string | undefined;
|
|
24
25
|
verdaccioSecret: string | undefined;
|
|
25
26
|
localPackages: string | undefined;
|
|
26
27
|
databaseUrl: string | undefined;
|
|
@@ -42,6 +43,7 @@ declare const registryCommand: Partial<cmd_ts_dist_cjs_argparser_js0.Register> &
|
|
|
42
43
|
webhooks: string | undefined;
|
|
43
44
|
publicUrl: string | undefined;
|
|
44
45
|
authRenown: boolean;
|
|
46
|
+
renownUrl: string | undefined;
|
|
45
47
|
verdaccioSecret: string | undefined;
|
|
46
48
|
localPackages: string | undefined;
|
|
47
49
|
databaseUrl: string | undefined;
|
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,12 +1,11 @@
|
|
|
1
1
|
import { binary, command, flag, number, option, optional, run, string } from "cmd-ts";
|
|
2
2
|
import express, { Router } from "express";
|
|
3
3
|
import { findUp } from "find-up";
|
|
4
|
-
import crypto, { randomBytes } from "node:crypto";
|
|
4
|
+
import crypto, { randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { mkdir } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { runServer } from "verdaccio";
|
|
8
|
-
import {
|
|
9
|
-
import { verifyAuthBearerToken } from "@renown/sdk/node";
|
|
8
|
+
import { Pool } from "pg";
|
|
10
9
|
import fs from "node:fs";
|
|
11
10
|
import { pipeline } from "node:stream/promises";
|
|
12
11
|
import { Readable } from "node:stream";
|
|
@@ -14,65 +13,98 @@ import { extract } from "tar";
|
|
|
14
13
|
import { slimManifest } from "@powerhousedao/shared/registry";
|
|
15
14
|
import { fileURLToPath } from "node:url";
|
|
16
15
|
//#endregion
|
|
17
|
-
//#region src/auth/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return aud === expected;
|
|
16
|
+
//#region src/auth/pg-store.ts
|
|
17
|
+
/** Build a real Postgres pool from a connection string. */
|
|
18
|
+
function createPgPool(databaseUrl) {
|
|
19
|
+
return new Pool({ connectionString: databaseUrl });
|
|
22
20
|
}
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* Postgres-backed AuthStore. Two small tables:
|
|
23
|
+
* - registry_users(username PK, password_hash, created_at)
|
|
24
|
+
* - registry_package_owners(package_name PK, owners text[], claimed_at)
|
|
27
25
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* Ownership claim is race-free: `INSERT ... ON CONFLICT DO NOTHING` means the
|
|
27
|
+
* first publisher wins atomically; the follow-up read returns the actual
|
|
28
|
+
* owners so a losing racer is denied.
|
|
29
|
+
*
|
|
30
|
+
* Takes an already-built `pg.Pool` (tests inject a pg-mem pool cast to Pool).
|
|
33
31
|
*/
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
return
|
|
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
|
-
|
|
63
|
-
|
|
32
|
+
function createPgStore(pool) {
|
|
33
|
+
let initialized = null;
|
|
34
|
+
return {
|
|
35
|
+
init() {
|
|
36
|
+
initialized ??= (async () => {
|
|
37
|
+
await pool.query(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS registry_users (
|
|
39
|
+
username text PRIMARY KEY,
|
|
40
|
+
password_hash text NOT NULL,
|
|
41
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
42
|
+
)`);
|
|
43
|
+
await pool.query(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS registry_package_owners (
|
|
45
|
+
package_name text PRIMARY KEY,
|
|
46
|
+
owners text[] NOT NULL,
|
|
47
|
+
claimed_at timestamptz NOT NULL DEFAULT now()
|
|
48
|
+
)`);
|
|
49
|
+
})();
|
|
50
|
+
return initialized;
|
|
51
|
+
},
|
|
52
|
+
async getUser(username) {
|
|
53
|
+
const row = (await pool.query("SELECT password_hash FROM registry_users WHERE username = $1", [username])).rows[0];
|
|
54
|
+
return row ? { passwordHash: row.password_hash } : null;
|
|
55
|
+
},
|
|
56
|
+
async createUser(username, passwordHash) {
|
|
57
|
+
try {
|
|
58
|
+
await pool.query("INSERT INTO registry_users (username, password_hash) VALUES ($1, $2)", [username, passwordHash]);
|
|
59
|
+
return true;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err.code === "23505") return false;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async getOwners(pkg) {
|
|
66
|
+
return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? null;
|
|
67
|
+
},
|
|
68
|
+
async getOwnersFor(pkgs) {
|
|
69
|
+
if (pkgs.length === 0) return {};
|
|
70
|
+
const placeholders = pkgs.map((_, i) => `$${i + 1}`).join(",");
|
|
71
|
+
const res = await pool.query(`SELECT package_name, owners FROM registry_package_owners WHERE package_name IN (${placeholders})`, pkgs);
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const row of res.rows) out[row.package_name] = row.owners;
|
|
74
|
+
return out;
|
|
75
|
+
},
|
|
76
|
+
async claimOwner(pkg, username) {
|
|
77
|
+
await pool.query(`INSERT INTO registry_package_owners (package_name, owners)
|
|
78
|
+
VALUES ($1, ARRAY[$2]::text[])
|
|
79
|
+
ON CONFLICT (package_name) DO NOTHING`, [pkg, username]);
|
|
80
|
+
return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? [];
|
|
81
|
+
},
|
|
82
|
+
close() {
|
|
83
|
+
return pool.end();
|
|
64
84
|
}
|
|
65
|
-
req.headers.authorization = `Bearer ${verdaccioJwt}`;
|
|
66
|
-
req.renownUser = {
|
|
67
|
-
address,
|
|
68
|
-
did: payload?.iss,
|
|
69
|
-
chainId: subject.chainId,
|
|
70
|
-
networkId: subject.networkId
|
|
71
|
-
};
|
|
72
|
-
return next();
|
|
73
85
|
};
|
|
74
86
|
}
|
|
75
87
|
//#endregion
|
|
88
|
+
//#region src/auth/store-handoff.ts
|
|
89
|
+
const REGISTRY_KEY = Symbol.for("@powerhousedao/registry:auth-store-handoff");
|
|
90
|
+
function registry() {
|
|
91
|
+
const g = globalThis;
|
|
92
|
+
return g[REGISTRY_KEY] ??= /* @__PURE__ */ new Map();
|
|
93
|
+
}
|
|
94
|
+
/** Stash a store instance and return a token to carry through plugin config. */
|
|
95
|
+
function stashAuthStore(store) {
|
|
96
|
+
const token = randomUUID();
|
|
97
|
+
registry().set(token, {
|
|
98
|
+
store,
|
|
99
|
+
loaded: false
|
|
100
|
+
});
|
|
101
|
+
return token;
|
|
102
|
+
}
|
|
103
|
+
/** True once the plugin has loaded the store for this token. */
|
|
104
|
+
function wasStoreLoaded(token) {
|
|
105
|
+
return registry().get(token)?.loaded ?? false;
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
76
108
|
//#region src/semver.ts
|
|
77
109
|
/**
|
|
78
110
|
* Compare two semver version strings for sorting.
|
|
@@ -246,6 +278,37 @@ var CdnCache = class {
|
|
|
246
278
|
if (fs.readdirSync(pkgDir).length === 0) fs.rmdirSync(pkgDir);
|
|
247
279
|
} catch {}
|
|
248
280
|
}
|
|
281
|
+
async reconcileWithRegistry(packageName) {
|
|
282
|
+
const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;
|
|
283
|
+
let versions;
|
|
284
|
+
try {
|
|
285
|
+
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
286
|
+
if (!res.ok) return [];
|
|
287
|
+
const meta = await res.json();
|
|
288
|
+
versions = Object.keys(meta.versions ?? {});
|
|
289
|
+
} catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
if (versions.length === 0) return [];
|
|
293
|
+
return this.reconcileVersions(packageName, versions);
|
|
294
|
+
}
|
|
295
|
+
reconcileVersions(packageName, keepVersions) {
|
|
296
|
+
const pkgDir = path.join(this.cdnCachePath, packageName);
|
|
297
|
+
const keep = new Set(keepVersions);
|
|
298
|
+
const removed = [];
|
|
299
|
+
let entries;
|
|
300
|
+
try {
|
|
301
|
+
entries = fs.readdirSync(pkgDir, { withFileTypes: true });
|
|
302
|
+
} catch {
|
|
303
|
+
return removed;
|
|
304
|
+
}
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
if (!entry.isDirectory() || keep.has(entry.name)) continue;
|
|
307
|
+
this.invalidateVersion(packageName, entry.name);
|
|
308
|
+
removed.push(entry.name);
|
|
309
|
+
}
|
|
310
|
+
return removed;
|
|
311
|
+
}
|
|
249
312
|
/** Remove all cached version directories except the specified one. */
|
|
250
313
|
pruneOldVersions(packageName, keepVersion) {
|
|
251
314
|
const pkgDir = path.join(this.cdnCachePath, packageName);
|
|
@@ -493,6 +556,15 @@ const MIME_TYPES = {
|
|
|
493
556
|
function getContentType(filePath) {
|
|
494
557
|
return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
495
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
|
+
}
|
|
496
568
|
/**
|
|
497
569
|
* Resolve a package version. Exact versions skip the network call. Upstream
|
|
498
570
|
* errors fall back to the latest cached version; genuine not-found falls back
|
|
@@ -532,9 +604,23 @@ function etagMatches(header, etag) {
|
|
|
532
604
|
return tag === "*" || opaqueTag(tag) === target;
|
|
533
605
|
});
|
|
534
606
|
}
|
|
535
|
-
function createPowerhouseRouter(config, sse, webhooks) {
|
|
607
|
+
function createPowerhouseRouter(config, sse, webhooks, ownerStore) {
|
|
536
608
|
const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
|
|
537
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
|
+
};
|
|
538
624
|
router.use((_req, res, next) => {
|
|
539
625
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
540
626
|
next();
|
|
@@ -574,16 +660,12 @@ function createPowerhouseRouter(config, sse, webhooks) {
|
|
|
574
660
|
});
|
|
575
661
|
const warm = createWarmer(config, cdn);
|
|
576
662
|
warm();
|
|
577
|
-
router.get("/packages", (req, res) => {
|
|
663
|
+
router.get("/packages", async (req, res) => {
|
|
578
664
|
warm();
|
|
579
665
|
const packages = scanPackages(config.cdnCachePath, config.storagePath);
|
|
580
666
|
const documentType = req.query.documentType;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
res.json(filtered);
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
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));
|
|
587
669
|
});
|
|
588
670
|
router.get("/packages/by-document-type", (req, res) => {
|
|
589
671
|
const documentType = req.query.type;
|
|
@@ -608,7 +690,7 @@ function createPowerhouseRouter(config, sse, webhooks) {
|
|
|
608
690
|
res.status(404).send("Package not found");
|
|
609
691
|
return;
|
|
610
692
|
}
|
|
611
|
-
res.json(pkg);
|
|
693
|
+
res.json((await withOwners([pkg]))[0]);
|
|
612
694
|
});
|
|
613
695
|
router.get("/-/cdn/*", async (req, res) => {
|
|
614
696
|
const fullPath = req.params[0];
|
|
@@ -694,9 +776,41 @@ function parseUnpublishRequest(reqPath) {
|
|
|
694
776
|
version
|
|
695
777
|
};
|
|
696
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
|
+
}
|
|
697
786
|
function createUnpublishHook(config, notifications) {
|
|
698
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
|
+
};
|
|
699
808
|
return (req, res, next) => {
|
|
809
|
+
if (req.method === "PUT") {
|
|
810
|
+
handleManifestRewrite(req, res);
|
|
811
|
+
next();
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
700
814
|
if (req.method !== "DELETE") {
|
|
701
815
|
next();
|
|
702
816
|
return;
|
|
@@ -711,14 +825,10 @@ function createUnpublishHook(config, notifications) {
|
|
|
711
825
|
if (res.statusCode >= 200 && res.statusCode < 300) try {
|
|
712
826
|
if (parsed.version) cdn.invalidateVersion(parsed.packageName, parsed.version);
|
|
713
827
|
else cdn.invalidate(parsed.packageName);
|
|
714
|
-
const renownUser = req.renownUser;
|
|
715
828
|
notifications.notifyUnpublish({
|
|
716
829
|
packageName: parsed.packageName,
|
|
717
830
|
version: parsed.version,
|
|
718
|
-
publishedBy:
|
|
719
|
-
address: renownUser.address,
|
|
720
|
-
did: renownUser.did
|
|
721
|
-
} : void 0
|
|
831
|
+
publishedBy: publisherFromRequest(req)
|
|
722
832
|
});
|
|
723
833
|
} catch (err) {
|
|
724
834
|
console.error(`[registry] CDN purge failed for ${parsed.packageName}${parsed.version ? `@${parsed.version}` : ""}:`, err);
|
|
@@ -748,11 +858,7 @@ function createPublishHook(config, notifications) {
|
|
|
748
858
|
return originalEnd(chunk, encoding, cb);
|
|
749
859
|
}
|
|
750
860
|
if (versions.length > 1) console.warn(`[registry] Multiple versions published for ${packageName}: ${JSON.stringify(versions)}`);
|
|
751
|
-
const
|
|
752
|
-
const publishedBy = renownUser ? {
|
|
753
|
-
address: renownUser.address,
|
|
754
|
-
did: renownUser.did
|
|
755
|
-
} : void 0;
|
|
861
|
+
const publishedBy = publisherFromRequest(req);
|
|
756
862
|
cdn.extractTarball(packageName, version).then(() => {
|
|
757
863
|
notifications.notifyPublish({
|
|
758
864
|
packageName,
|
|
@@ -890,10 +996,15 @@ function buildVerdaccioConfig(config) {
|
|
|
890
996
|
const htpasswdPath = path.join(config.storagePath, "htpasswd");
|
|
891
997
|
const uplinkUrl = config.uplink ?? "https://registry.npmjs.org/";
|
|
892
998
|
const usePgAuth = Boolean(config.databaseUrl || config.authStore);
|
|
999
|
+
const storeToken = config.authStoreToken ?? (config.authStore ? stashAuthStore(config.authStore) : void 0);
|
|
893
1000
|
const pluginsDir = config.pluginsDir ?? path.join(path.dirname(fileURLToPath(import.meta.url)), "plugins");
|
|
894
1001
|
const auth = usePgAuth ? { "registry-auth": {
|
|
895
1002
|
...config.databaseUrl ? { databaseUrl: config.databaseUrl } : {},
|
|
896
|
-
...
|
|
1003
|
+
...storeToken ? { storeToken } : {},
|
|
1004
|
+
...config.renown ? {
|
|
1005
|
+
publicUrl: config.renown.publicUrl,
|
|
1006
|
+
...config.renown.renownUrl ? { renownUrl: config.renown.renownUrl } : {}
|
|
1007
|
+
} : {}
|
|
897
1008
|
} } : { htpasswd: { file: htpasswdPath } };
|
|
898
1009
|
const base = {
|
|
899
1010
|
storage: config.storagePath,
|
|
@@ -973,18 +1084,21 @@ async function resolveDir(dir) {
|
|
|
973
1084
|
return found;
|
|
974
1085
|
}
|
|
975
1086
|
async function runRegistry(args) {
|
|
976
|
-
const { port, storageDir, cdnCacheDir, uplink, uplinkMaxage, webEnabled, webhooks, s3AccessKeyId, s3Bucket, s3Endpoint, s3ForcePathStyle, s3KeyPrefix, s3Region, s3SecretAccessKey, publicUrl, authRenown, verdaccioSecret: verdaccioSecretArg, localPackages, databaseUrl, pluginsDir, authStore } = args;
|
|
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;
|
|
977
1088
|
const storagePath = await resolveDir(storageDir);
|
|
978
1089
|
const cdnCachePath = await resolveDir(cdnCacheDir);
|
|
979
1090
|
const verdaccioSecret = verdaccioSecretArg ?? randomBytes(32).toString("hex");
|
|
980
1091
|
const renownEnabled = authRenown === true && Boolean(publicUrl);
|
|
981
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.");
|
|
982
1094
|
console.log({
|
|
983
1095
|
storagePath,
|
|
984
1096
|
cdnCachePath
|
|
985
1097
|
});
|
|
986
1098
|
const webhookConfigs = webhooks?.split(",").map((url) => url.trim()).filter(Boolean).map((endpoint) => ({ endpoint }));
|
|
987
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;
|
|
988
1102
|
const config = {
|
|
989
1103
|
port,
|
|
990
1104
|
storagePath,
|
|
@@ -994,7 +1108,10 @@ async function runRegistry(args) {
|
|
|
994
1108
|
webEnabled,
|
|
995
1109
|
verdaccioSecret,
|
|
996
1110
|
...localPackagePatterns?.length ? { localPackagePatterns } : {},
|
|
997
|
-
...renownEnabled && publicUrl ? { renown: {
|
|
1111
|
+
...renownEnabled && publicUrl ? { renown: {
|
|
1112
|
+
publicUrl,
|
|
1113
|
+
...renownUrl ? { renownUrl } : {}
|
|
1114
|
+
} } : {},
|
|
998
1115
|
...webhookConfigs?.length && { notify: { webhooks: webhookConfigs } },
|
|
999
1116
|
...s3Bucket && s3Endpoint && s3Region && { s3: {
|
|
1000
1117
|
bucket: s3Bucket,
|
|
@@ -1007,23 +1124,25 @@ async function runRegistry(args) {
|
|
|
1007
1124
|
} },
|
|
1008
1125
|
...databaseUrl ? { databaseUrl } : {},
|
|
1009
1126
|
...pluginsDir ? { pluginsDir } : {},
|
|
1010
|
-
...
|
|
1127
|
+
...sharedAuthStore ? { authStore: sharedAuthStore } : {},
|
|
1128
|
+
...authStoreToken ? { authStoreToken } : {}
|
|
1011
1129
|
};
|
|
1012
1130
|
if (config.databaseUrl || config.authStore) console.log("[registry] Postgres-backed auth plugin active (persistent accounts + package ownership)");
|
|
1013
1131
|
await mkdir(storagePath, { recursive: true });
|
|
1014
1132
|
await mkdir(cdnCachePath, { recursive: true });
|
|
1015
|
-
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];
|
|
1016
1139
|
const app = express();
|
|
1017
1140
|
const sseChannel = new SSEChannel();
|
|
1018
1141
|
const webhookChannel = new WebhookChannel(config.storagePath, config.notify);
|
|
1019
1142
|
const notifications = new NotificationManager([sseChannel, webhookChannel]);
|
|
1020
1143
|
const staticDir = await findUp("static", { type: "directory" });
|
|
1021
1144
|
if (staticDir) app.use("/-/static", express.static(staticDir));
|
|
1022
|
-
app.use(createPowerhouseRouter(config, sseChannel, webhookChannel));
|
|
1023
|
-
if (config.renown) app.use(createRenownAuthMiddleware({
|
|
1024
|
-
publicUrl: config.renown.publicUrl,
|
|
1025
|
-
verdaccioSecret
|
|
1026
|
-
}));
|
|
1145
|
+
app.use(createPowerhouseRouter(config, sseChannel, webhookChannel, sharedAuthStore));
|
|
1027
1146
|
app.use(createPublishHook(config, notifications));
|
|
1028
1147
|
app.use(createUnpublishHook(config, notifications));
|
|
1029
1148
|
app.use((req, res) => verdaccioHandler(req, res));
|
|
@@ -1140,6 +1259,13 @@ const registryCommand = command({
|
|
|
1140
1259
|
defaultValue: () => process.env.PH_REGISTRY_AUTH_RENOWN === "true",
|
|
1141
1260
|
defaultValueIsSerializable: true
|
|
1142
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
|
+
}),
|
|
1143
1269
|
verdaccioSecret: option({
|
|
1144
1270
|
long: "verdaccio-secret",
|
|
1145
1271
|
type: optional(string),
|
|
@@ -1163,7 +1289,13 @@ const registryCommand = command({
|
|
|
1163
1289
|
})
|
|
1164
1290
|
},
|
|
1165
1291
|
handler: async (args) => {
|
|
1166
|
-
|
|
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
|
+
});
|
|
1167
1299
|
try {
|
|
1168
1300
|
await runRegistry(args);
|
|
1169
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 { fileURLToPath } from \"node:url\";\nimport type { RegistryConfig } from \"./types.js\";\n\nexport function buildVerdaccioConfig(config: RegistryConfig) {\n const htpasswdPath = path.join(config.storagePath, \"htpasswd\");\n\n const uplinkUrl = config.uplink ?? \"https://registry.npmjs.org/\";\n\n // With a database configured, use the Postgres-backed auth plugin\n // (persistent accounts + npm-style package ownership). Verdaccio loads it\n // via require() from the dist/plugins dir. Without a DB (local dev / tests),\n // keep the built-in htpasswd path.\n const usePgAuth = Boolean(config.databaseUrl || config.authStore);\n const pluginsDir =\n config.pluginsDir ??\n path.join(path.dirname(fileURLToPath(import.meta.url)), \"plugins\");\n const auth = usePgAuth\n ? {\n \"registry-auth\": {\n ...(config.databaseUrl ? { databaseUrl: config.databaseUrl } : {}),\n ...(config.authStore ? { store: config.authStore } : {}),\n },\n }\n : { htpasswd: { file: htpasswdPath } };\n\n const base: Record<string, unknown> = {\n storage: config.storagePath,\n self_path: \"./\",\n // Top-level secret used by verdaccio to sign / verify its API JWTs.\n // The renown middleware mints a verdaccio-format JWT with the same\n // secret so verdaccio's apiJWTmiddleware accepts the swapped token.\n ...(config.verdaccioSecret ? { secret: config.verdaccioSecret } : {}),\n // Force JWT mode for the npm API. Without this verdaccio falls back to\n // its legacy aes-encrypted token format, which signPayload won't produce.\n security: {\n api: {\n jwt: {\n sign: { expiresIn: \"90d\" },\n verify: {},\n },\n },\n },\n auth,\n ...(usePgAuth ? { plugins: pluginsDir } : {}),\n uplinks: {\n npmjs: {\n url: uplinkUrl,\n // Defaults to verdaccio's own default of 2m. The previous 15m\n // hardcoded value made publish-to-install dev loops painful —\n // when a newly-published version landed on npmjs, our registry\n // kept handing out the pre-publish packument for up to 15min.\n // Operators that want heavier upstream caching for production\n // can opt in via --uplink-maxage / PH_REGISTRY_UPLINK_MAXAGE.\n maxage: config.uplinkMaxage ?? \"2m\",\n timeout: \"30s\",\n cache: true,\n },\n },\n // Verdaccio matches packages config top-to-bottom (first match wins),\n // so local-only globs must come first. We also skip emitting the\n // default proxied entries when a glob with the same key is in the\n // local-only list — otherwise the value would be overwritten but the\n // iteration position would still be the default's earlier slot.\n packages: (() => {\n const local = config.localPackagePatterns ?? [];\n const localSet = new Set(local);\n const access = {\n access: \"$all\",\n publish: \"$authenticated\",\n unpublish: \"$authenticated\",\n };\n const entries: [string, Record<string, unknown>][] = [];\n // Locals first — no proxy means verdaccio resolves them from local\n // storage only, so re-publishing a version that exists on npmjs\n // doesn't 409.\n for (const pattern of local) {\n entries.push([pattern, { ...access }]);\n }\n // Defaults follow, skipping any glob the caller already overrode.\n if (!localSet.has(\"@powerhousedao/*\")) {\n entries.push([\"@powerhousedao/*\", { ...access, proxy: \"npmjs\" }]);\n }\n if (!localSet.has(\"**\")) {\n entries.push([\"**\", { ...access, proxy: \"npmjs\" }]);\n }\n return Object.fromEntries(entries);\n })(),\n web: {\n enable: config.webEnabled !== false,\n title: \"Powerhouse Registry\",\n logo: \"https://raw.githubusercontent.com/powerhouse-inc/powerhouse/main/packages/registry/static/logo.svg\",\n favicon: \"/-/static/favicon.ico\",\n primary_color: \"#38C780\",\n darkMode: true,\n },\n server: {\n keepAliveTimeout: 60,\n },\n log: {\n type: \"stdout\",\n format: \"pretty\",\n level: \"warn\",\n },\n max_body_size: config.maxBodySize ?? \"300mb\",\n };\n\n if (config.s3) {\n base.store = {\n \"aws-s3-storage\": {\n bucket: config.s3.bucket,\n endpoint: config.s3.endpoint,\n region: config.s3.region,\n s3ForcePathStyle: config.s3.s3ForcePathStyle ?? true,\n ...(config.s3.keyPrefix && { keyPrefix: config.s3.keyPrefix }),\n ...(config.s3.accessKeyId && { accessKeyId: config.s3.accessKeyId }),\n ...(config.s3.secretAccessKey && {\n secretAccessKey: config.s3.secretAccessKey,\n }),\n },\n };\n }\n\n return base;\n}\n","import express from \"express\";\nimport { findUp } from \"find-up\";\nimport { randomBytes } from \"node:crypto\";\nimport { mkdir } from \"node:fs/promises\";\nimport type { Server } from \"node:http\";\nimport path from \"node:path\";\nimport { runServer } from \"verdaccio\";\nimport { createRenownAuthMiddleware } from \"./auth/renown-middleware.js\";\nimport {\n createPowerhouseRouter,\n createPublishHook,\n createUnpublishHook,\n} from \"./middleware.js\";\nimport { NotificationManager } from \"./notifications/manager.js\";\nimport { SSEChannel } from \"./notifications/sse.js\";\nimport { WebhookChannel } from \"./notifications/webhook.js\";\nimport type { RegistryCommandArgs, RegistryConfig } from \"./types.js\";\nimport { buildVerdaccioConfig } from \"./verdaccio-config.js\";\n\nasync function resolveDir(dir: string): Promise<string> {\n if (path.isAbsolute(dir)) {\n await mkdir(dir, { recursive: true });\n return dir;\n }\n const found = await findUp(dir, { type: \"directory\" });\n if (!found) {\n await mkdir(dir, { recursive: true });\n return dir;\n }\n return found;\n}\n\nexport async function runRegistry(args: RegistryCommandArgs) {\n const {\n port,\n storageDir,\n cdnCacheDir,\n uplink,\n uplinkMaxage,\n webEnabled,\n webhooks,\n s3AccessKeyId,\n s3Bucket,\n s3Endpoint,\n s3ForcePathStyle,\n s3KeyPrefix,\n s3Region,\n s3SecretAccessKey,\n publicUrl,\n authRenown,\n verdaccioSecret: verdaccioSecretArg,\n localPackages,\n databaseUrl,\n pluginsDir,\n authStore,\n } = args;\n const storagePath = await resolveDir(storageDir);\n const cdnCachePath = await resolveDir(cdnCacheDir);\n\n // Per-pod random verdaccio JWT secret. The verdaccio-format token we mint\n // in the renown middleware never leaves the pod (it's swapped into the\n // request before verdaccio sees it), so a per-pod secret is sufficient.\n // An override is exposed for tests / multi-pod behaviors that depend on\n // shared verdaccio JWTs.\n const verdaccioSecret = verdaccioSecretArg ?? randomBytes(32).toString(\"hex\");\n\n // Renown auth turns on when the operator both opts in (`--auth-renown`,\n // default true via the CLI flag) and has set --public-url for the audience\n // claim. Tests / programmatic users that don't pass either keep the legacy\n // unsigned/htpasswd path with no warning.\n const renownEnabled = authRenown === true && Boolean(publicUrl);\n if (authRenown === true && !publicUrl) {\n console.warn(\n \"[registry] auth-renown is enabled but --public-url / PH_REGISTRY_PUBLIC_URL is not set; Renown auth will be disabled.\",\n );\n }\n\n console.log({\n storagePath,\n cdnCachePath,\n });\n\n const webhookConfigs = webhooks\n ?.split(\",\")\n .map((url) => url.trim())\n .filter(Boolean)\n .map((endpoint) => ({ endpoint }));\n\n const localPackagePatterns = localPackages\n ?.split(\",\")\n .map((p) => p.trim())\n .filter(Boolean);\n\n const config: RegistryConfig = {\n port,\n storagePath,\n cdnCachePath,\n uplink,\n uplinkMaxage,\n webEnabled,\n verdaccioSecret,\n ...(localPackagePatterns?.length ? { localPackagePatterns } : {}),\n ...(renownEnabled && publicUrl ? { renown: { publicUrl } } : {}),\n ...(webhookConfigs?.length && {\n notify: { webhooks: webhookConfigs },\n }),\n ...(s3Bucket &&\n s3Endpoint &&\n s3Region && {\n s3: {\n bucket: s3Bucket,\n endpoint: s3Endpoint,\n region: s3Region,\n accessKeyId: s3AccessKeyId,\n secretAccessKey: s3SecretAccessKey,\n keyPrefix: s3KeyPrefix,\n s3ForcePathStyle,\n },\n }),\n ...(databaseUrl ? { databaseUrl } : {}),\n ...(pluginsDir ? { pluginsDir } : {}),\n ...(authStore ? { authStore } : {}),\n };\n\n if (config.databaseUrl || config.authStore) {\n console.log(\n \"[registry] Postgres-backed auth plugin active (persistent accounts + package ownership)\",\n );\n }\n // Ensure directories exist (for relative paths resolved via findUp)\n await mkdir(storagePath, { recursive: true });\n await mkdir(cdnCachePath, { recursive: true });\n\n const verdaccioConfig = buildVerdaccioConfig(config);\n\n // verdaccio's runServer returns Promise<any> (upstream type limitation)\n const verdaccioServer = (await runServer(verdaccioConfig)) as Server;\n const verdaccioHandler = verdaccioServer.listeners(\"request\")[0] as (\n ...args: unknown[]\n ) => void;\n\n const app = express();\n\n const sseChannel = new SSEChannel();\n const webhookChannel = new WebhookChannel(config.storagePath, config.notify);\n const notifications = new NotificationManager([sseChannel, webhookChannel]);\n\n // Serve static assets (logo, etc.)\n const staticDir = await findUp(\"static\", { type: \"directory\" });\n if (staticDir) {\n app.use(\"/-/static\", express.static(staticDir));\n }\n\n // Our routes take priority over Verdaccio\n app.use(createPowerhouseRouter(config, sseChannel, webhookChannel));\n\n // Renown bearer-token auth runs before the publish/unpublish hooks so they\n // see `req.renownUser`, and before verdaccio so the swapped Authorization\n // header reaches verdaccio's apiJWTmiddleware.\n if (config.renown) {\n app.use(\n createRenownAuthMiddleware({\n publicUrl: config.renown.publicUrl,\n verdaccioSecret,\n }),\n );\n }\n\n app.use(createPublishHook(config, notifications));\n app.use(createUnpublishHook(config, notifications));\n\n // Verdaccio handles everything else (npm protocol, web UI, auth)\n app.use((req, res) => verdaccioHandler(req, res));\n\n const server = app.listen(port, () => {\n console.log(`Powerhouse Registry running on http://localhost:${port}`);\n console.log(` CDN: http://localhost:${port}/-/cdn/`);\n console.log(` Packages: http://localhost:${port}/packages`);\n console.log(` npm: http://localhost:${port}/`);\n console.log(` Storage: ${storagePath}`);\n console.log(` CDN cache: ${cdnCachePath}`);\n if (config.s3) {\n console.log(` S3: ${config.s3.endpoint}/${config.s3.bucket}`);\n }\n if (config.renown) {\n console.log(` Renown auth: ${config.renown.publicUrl}`);\n }\n });\n\n return server;\n}\n","import {\n binary,\n command,\n flag,\n number,\n option,\n optional,\n run,\n string,\n} from \"cmd-ts\";\nimport {\n DEFAULT_PORT,\n DEFAULT_REGISTRY_CDN_CACHE_DIR_NAME,\n DEFAULT_STORAGE_DIR_NAME,\n} from \"./src/constants.js\";\nimport { runRegistry } from \"./src/run.js\";\n\nexport const registryCommand = command({\n name: \"Package registry\",\n args: {\n port: option({\n long: \"port\",\n type: number,\n defaultValue: () => Number(process.env.PORT) || DEFAULT_PORT,\n defaultValueIsSerializable: true,\n }),\n storageDir: option({\n long: \"storage-dir\",\n type: string,\n defaultValue: () =>\n process.env.REGISTRY_STORAGE || DEFAULT_STORAGE_DIR_NAME,\n defaultValueIsSerializable: true,\n }),\n cdnCacheDir: option({\n long: \"cdn-cache-dir\",\n type: string,\n defaultValue: () =>\n process.env.REGISTRY_CDN_CACHE || DEFAULT_REGISTRY_CDN_CACHE_DIR_NAME,\n defaultValueIsSerializable: true,\n }),\n uplink: option({\n long: \"uplink\",\n type: optional(string),\n defaultValue: () => process.env.REGISTRY_UPLINK,\n defaultValueIsSerializable: true,\n }),\n uplinkMaxage: option({\n long: \"uplink-maxage\",\n type: optional(string),\n description:\n \"How long verdaccio caches npmjs uplink metadata before refetching. \" +\n \"Accepts verdaccio time strings (e.g. '30s', '2m', '1h'). \" +\n \"Default '2m' matches verdaccio upstream — shortens the publish-to-\" +\n \"install propagation window in dev. Bump for production deployments \" +\n \"that want to reduce npmjs load.\",\n defaultValue: () => process.env.PH_REGISTRY_UPLINK_MAXAGE,\n defaultValueIsSerializable: true,\n }),\n s3Bucket: option({\n long: \"s3-bucket\",\n type: optional(string),\n defaultValue: () => process.env.S3_BUCKET,\n defaultValueIsSerializable: true,\n }),\n s3Endpoint: option({\n long: \"s3-endpoint\",\n type: optional(string),\n defaultValue: () => process.env.S3_ENDPOINT,\n defaultValueIsSerializable: true,\n }),\n s3Region: option({\n long: \"s3-region\",\n type: optional(string),\n defaultValue: () => process.env.S3_REGION,\n defaultValueIsSerializable: true,\n }),\n s3AccessKeyId: option({\n long: \"s3-access-key-id\",\n type: optional(string),\n defaultValue: () => process.env.S3_ACCESS_KEY_ID,\n defaultValueIsSerializable: true,\n }),\n s3SecretAccessKey: option({\n long: \"s3-secret-access-key\",\n type: optional(string),\n defaultValue: () => process.env.S3_SECRET_ACCESS_KEY,\n defaultValueIsSerializable: true,\n }),\n s3KeyPrefix: option({\n long: \"s3-key-prefix\",\n type: optional(string),\n defaultValue: () => process.env.S3_KEY_PREFIX,\n defaultValueIsSerializable: true,\n }),\n s3ForcePathStyle: flag({\n long: \"s3-force-path-style\",\n defaultValue: () => process.env.S3_FORCE_PATH_STYLE !== \"false\",\n defaultValueIsSerializable: true,\n }),\n webEnabled: flag({\n long: \"web-enabled\",\n defaultValue: () => process.env.REGISTRY_WEB !== \"false\",\n defaultValueIsSerializable: true,\n }),\n webhooks: option({\n long: \"webhook\",\n type: optional(string),\n description: \"Comma-separated webhook URLs to notify on publish\",\n defaultValue: () => process.env.REGISTRY_WEBHOOKS,\n defaultValueIsSerializable: true,\n }),\n publicUrl: option({\n long: \"public-url\",\n type: optional(string),\n description:\n \"Public origin of this registry (used as the JWT `aud` claim for Renown bearer tokens). Required when --auth-renown is true.\",\n defaultValue: () => process.env.PH_REGISTRY_PUBLIC_URL,\n defaultValueIsSerializable: true,\n }),\n authRenown: flag({\n long: \"auth-renown\",\n description:\n \"Verify Renown-signed bearer tokens in front of verdaccio (stateless). Disabled when --public-url is unset.\",\n defaultValue: () => process.env.PH_REGISTRY_AUTH_RENOWN === \"true\",\n defaultValueIsSerializable: true,\n }),\n verdaccioSecret: option({\n long: \"verdaccio-secret\",\n type: optional(string),\n description:\n \"Override verdaccio's internal JWT signing secret. Default: random per pod (fine — the swapped JWT never leaves this process).\",\n defaultValue: () => process.env.PH_REGISTRY_VERDACCIO_SECRET,\n defaultValueIsSerializable: true,\n }),\n localPackages: option({\n long: \"local-packages\",\n type: optional(string),\n description:\n \"Comma-separated globs (e.g. '@powerhousedao/*,document-model,ph-cmd') served locally only — no npmjs uplink proxy. Lets you re-publish a workspace package whose version already exists on npmjs without bumping.\",\n defaultValue: () => process.env.PH_REGISTRY_LOCAL_PACKAGES,\n defaultValueIsSerializable: true,\n }),\n databaseUrl: option({\n long: \"database-url\",\n type: optional(string),\n description:\n \"Postgres connection string. When set, the registry uses the DB-backed auth plugin (persistent accounts + npm-style package ownership) instead of the built-in htpasswd.\",\n defaultValue: () =>\n process.env.PH_REGISTRY_DATABASE_URL ?? process.env.DATABASE_URL,\n defaultValueIsSerializable: true,\n }),\n },\n handler: async (args) => {\n console.log(args);\n\n try {\n await runRegistry(args);\n } catch (error) {\n console.error(\"Failed to start registry:\");\n console.error(error);\n process.exit(1);\n }\n },\n});\n\nconst registryCli = binary(registryCommand);\n\nawait run(registryCli, process.argv);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["#resolveFile","#extractWithLock","#extractionLocks","#channels","#clients","#broadcast","#storagePath","#predefined","#dynamic","#load","#save","#post","#filePath"],"sources":["../src/constants.ts","../src/auth/pg-store.ts","../src/auth/store-handoff.ts","../src/semver.ts","../src/cdn.ts","../src/packages.ts","../src/warmup.ts","../src/middleware.ts","../src/notifications/manager.ts","../src/notifications/sse.ts","../src/notifications/webhook.ts","../src/verdaccio-config.ts","../src/run.ts","../cli.ts"],"sourcesContent":["export const DEFAULT_PORT = 8080;\nexport const DEFAULT_STORAGE_DIR_NAME = \"./storage\" as const;\nexport const DEFAULT_REGISTRY_CDN_CACHE_DIR_NAME = \"./cdn-cache\" as const;\n","import { Pool } from \"pg\";\nimport type { AuthStore, UserRecord } from \"./auth-store.js\";\n\n/** Build a real Postgres pool from a connection string. */\nexport function createPgPool(databaseUrl: string): Pool {\n return new Pool({ connectionString: databaseUrl });\n}\n\n/**\n * Postgres-backed AuthStore. Two small tables:\n * - registry_users(username PK, password_hash, created_at)\n * - registry_package_owners(package_name PK, owners text[], claimed_at)\n *\n * Ownership claim is race-free: `INSERT ... ON CONFLICT DO NOTHING` means the\n * first publisher wins atomically; the follow-up read returns the actual\n * owners so a losing racer is denied.\n *\n * Takes an already-built `pg.Pool` (tests inject a pg-mem pool cast to Pool).\n */\nexport function createPgStore(pool: Pool): AuthStore {\n let initialized: Promise<void> | null = null;\n\n return {\n init(): Promise<void> {\n initialized ??= (async () => {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS registry_users (\n username text PRIMARY KEY,\n password_hash text NOT NULL,\n created_at timestamptz NOT NULL DEFAULT now()\n )`);\n await pool.query(`\n CREATE TABLE IF NOT EXISTS registry_package_owners (\n package_name text PRIMARY KEY,\n owners text[] NOT NULL,\n claimed_at timestamptz NOT NULL DEFAULT now()\n )`);\n })();\n return initialized;\n },\n\n async getUser(username: string): Promise<UserRecord | null> {\n const res = await pool.query<{ password_hash: string }>(\n \"SELECT password_hash FROM registry_users WHERE username = $1\",\n [username],\n );\n const row = res.rows[0] as { password_hash: string } | undefined;\n return row ? { passwordHash: row.password_hash } : null;\n },\n\n async createUser(username: string, passwordHash: string): Promise<boolean> {\n // Atomic: the PRIMARY KEY enforces uniqueness. A duplicate raises a\n // unique-violation (SQLSTATE 23505) — catch it as \"already registered\".\n try {\n await pool.query(\n \"INSERT INTO registry_users (username, password_hash) VALUES ($1, $2)\",\n [username, passwordHash],\n );\n return true;\n } catch (err) {\n if ((err as { code?: string }).code === \"23505\") return false;\n throw err;\n }\n },\n\n async getOwners(pkg: string): Promise<string[] | null> {\n const res = await pool.query<{ owners: string[] }>(\n \"SELECT owners FROM registry_package_owners WHERE package_name = $1\",\n [pkg],\n );\n return res.rows[0]?.owners ?? null;\n },\n\n async getOwnersFor(pkgs: string[]): Promise<Record<string, string[]>> {\n if (pkgs.length === 0) return {};\n // IN (...) with per-value placeholders — portable across pg and pg-mem,\n // unlike `= ANY($1)` array binding.\n const placeholders = pkgs.map((_, i) => `$${i + 1}`).join(\",\");\n const res = await pool.query<{ package_name: string; owners: string[] }>(\n `SELECT package_name, owners FROM registry_package_owners WHERE package_name IN (${placeholders})`,\n pkgs,\n );\n const out: Record<string, string[]> = {};\n for (const row of res.rows) out[row.package_name] = row.owners;\n return out;\n },\n\n async claimOwner(pkg: string, username: string): Promise<string[]> {\n // First publisher wins atomically; then read the actual owners.\n await pool.query(\n `INSERT INTO registry_package_owners (package_name, owners)\n VALUES ($1, ARRAY[$2]::text[])\n ON CONFLICT (package_name) DO NOTHING`,\n [pkg, username],\n );\n const res = await pool.query<{ owners: string[] }>(\n \"SELECT owners FROM registry_package_owners WHERE package_name = $1\",\n [pkg],\n );\n return res.rows[0]?.owners ?? [];\n },\n\n close(): Promise<void> {\n return pool.end();\n },\n };\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { AuthStore } from \"./auth-store.js\";\n\n// Carry a live AuthStore to the plugin as a token (verdaccio merges the app\n// config's `store` block into plugin configs), tracking whether it loaded.\nconst REGISTRY_KEY = Symbol.for(\"@powerhousedao/registry:auth-store-handoff\");\n\ninterface Entry {\n store: AuthStore;\n loaded: boolean;\n}\n\nfunction registry(): Map<string, Entry> {\n const g = globalThis as { [REGISTRY_KEY]?: Map<string, Entry> };\n return (g[REGISTRY_KEY] ??= new Map<string, Entry>());\n}\n\n/** Stash a store instance and return a token to carry through plugin config. */\nexport function stashAuthStore(store: AuthStore): string {\n const token = randomUUID();\n registry().set(token, { store, loaded: false });\n return token;\n}\n\n/** Resolve a stashed store by token (undefined if unknown). */\nexport function takeAuthStore(token: string): AuthStore | undefined {\n return registry().get(token)?.store;\n}\n\n/** Record that the plugin fully constructed with this token's store. */\nexport function markStoreLoaded(token: string): void {\n const entry = registry().get(token);\n if (entry) entry.loaded = true;\n}\n\n/** True once the plugin has loaded the store for this token. */\nexport function wasStoreLoaded(token: string): boolean {\n return registry().get(token)?.loaded ?? false;\n}\n","/**\n * Compare two semver version strings for sorting.\n * Returns negative if a < b, positive if a > b, 0 if equal.\n *\n * Handles numeric component comparison (so \"1.0.10\" > \"1.0.9\")\n * and prerelease ordering (release > prerelease).\n */\nexport function compareSemver(a: string, b: string): number {\n const [coreA, preA] = a.split(\"-\", 2);\n const [coreB, preB] = b.split(\"-\", 2);\n\n const partsA = coreA.split(\".\").map(Number);\n const partsB = coreB.split(\".\").map(Number);\n\n for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {\n const na = partsA[i] ?? 0;\n const nb = partsB[i] ?? 0;\n if (na !== nb) return na - nb;\n }\n\n // Equal core versions — release (no prerelease) sorts after prerelease\n if (!preA && preB) return 1;\n if (preA && !preB) return -1;\n if (preA && preB) return preA < preB ? -1 : preA > preB ? 1 : 0;\n\n return 0;\n}\n","import crypto from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { extract } from \"tar\";\nimport { compareSemver } from \"./semver.js\";\n\n/**\n * Parse a package specifier into name and version/tag.\n * Supports:\n * \"@scope/pkg\" -> { name: \"@scope/pkg\", tag: undefined }\n * \"@scope/pkg@dev\" -> { name: \"@scope/pkg\", tag: \"dev\" }\n * \"@scope/pkg@1.0.0\" -> { name: \"@scope/pkg\", tag: \"1.0.0\" }\n * \"pkg@latest\" -> { name: \"pkg\", tag: \"latest\" }\n */\nexport function parsePackageSpec(spec: string): {\n name: string;\n tag: string | undefined;\n} {\n // For scoped packages (@scope/name@tag), split on the last @\n // For unscoped packages (name@tag), split on the first @\n if (spec.startsWith(\"@\")) {\n // Scoped: find the @ after the scope/name portion\n const lastAt = spec.lastIndexOf(\"@\");\n if (lastAt > 0 && lastAt !== spec.indexOf(\"@\")) {\n return { name: spec.slice(0, lastAt), tag: spec.slice(lastAt + 1) };\n }\n return { name: spec, tag: undefined };\n }\n const atIndex = spec.indexOf(\"@\");\n if (atIndex > 0) {\n return { name: spec.slice(0, atIndex), tag: spec.slice(atIndex + 1) };\n }\n return { name: spec, tag: undefined };\n}\n\n/** True when tag is a concrete semver, false for dist-tag names or undefined. */\nexport function isExactVersion(tag?: string): boolean {\n // Strict semver charset: the version flows into the ETag header, so\n // arbitrary characters after the prerelease/build separator must not match.\n return (\n !!tag &&\n /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/.test(tag)\n );\n}\n\nexport class CdnCache {\n #extractionLocks = new Map<string, Promise<void>>();\n\n constructor(\n private registryUrl: string,\n private cdnCachePath: string,\n ) {}\n\n async getFileByVersion(\n packageName: string,\n version: string,\n filePath: string,\n ): Promise<string | null> {\n const versionDir = path.join(this.cdnCachePath, packageName, version);\n\n // Check all possible paths before attempting extraction\n const resolved = this.#resolveFile(versionDir, filePath);\n if (resolved) return resolved;\n\n // File not found in any location — extract tarball and try again\n await this.#extractWithLock(packageName, version);\n\n return this.#resolveFile(versionDir, filePath);\n }\n\n #resolveFile(versionDir: string, filePath: string): string | null {\n // Check direct path first, then fall back to cdn/ and dist/cdn/ subdirectories\n // (npm tarballs contain files under dist/, bun bundles go to cdn/)\n const candidates = [\n path.join(versionDir, filePath),\n path.join(versionDir, \"cdn\", filePath),\n path.join(versionDir, \"dist\", \"cdn\", filePath),\n path.join(versionDir, \"dist\", filePath),\n ];\n\n for (const candidate of candidates) {\n if (this.isSafePath(candidate) && fs.existsSync(candidate))\n return candidate;\n }\n\n return null;\n }\n\n async #extractWithLock(packageName: string, version: string): Promise<void> {\n const key = `${packageName}@${version}`;\n const existing = this.#extractionLocks.get(key);\n if (existing) return existing;\n\n const promise = this.extractTarball(packageName, version).finally(() => {\n this.#extractionLocks.delete(key);\n });\n this.#extractionLocks.set(key, promise);\n return promise;\n }\n\n getLatestCachedVersion(packageName: string): string | null {\n const pkgDir = path.join(this.cdnCachePath, packageName);\n try {\n const entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n const versions = entries\n .filter((e) => e.isDirectory())\n .map((e) => e.name);\n if (versions.length === 0) return null;\n versions.sort(compareSemver);\n return versions[versions.length - 1];\n } catch {\n return null;\n }\n }\n\n /**\n * Resolve a version for a package. If tag is a semver version that exists\n * in the registry, return it directly. If tag is a dist-tag name (e.g.\n * \"dev\", \"latest\"), resolve it to the concrete version. If no tag is\n * provided, prefer \"latest\", then fall back to any available dist-tag.\n * Returns null only for genuine not-found (404, or absent tag/version).\n * Throws on network errors and non-OK responses other than 404.\n */\n async resolveVersion(\n packageName: string,\n tag?: string,\n ): Promise<string | null> {\n const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;\n const res = await fetch(url, {\n headers: { Accept: \"application/json\" },\n });\n if (res.status === 404) return null;\n if (!res.ok) {\n throw new Error(\n `Upstream metadata for ${packageName} returned ${res.status}`,\n );\n }\n const metadata = (await res.json()) as Record<string, unknown>;\n const distTags = metadata[\"dist-tags\"] as\n | Record<string, string>\n | undefined;\n const versions = metadata[\"versions\"] as\n | Record<string, unknown>\n | undefined;\n\n if (tag) {\n // If the tag matches an exact version in the registry, use it directly\n if (versions && tag in versions) return tag;\n // Otherwise treat it as a dist-tag name\n if (distTags && tag in distTags) return distTags[tag];\n // Tag not found\n return null;\n }\n\n if (!distTags) return null;\n // No tag specified: prefer \"latest\", fall back to any available tag\n return distTags.latest ?? Object.values(distTags)[0] ?? null;\n }\n\n async extractTarball(packageName: string, version: string): Promise<void> {\n const destDir = path.join(this.cdnCachePath, packageName, version);\n\n // Idempotence guard. extractTarball is hot-path-called from\n // warmCdnCacheFromVerdaccio on every /packages request (which the\n // deployment's readiness probe hits every 5s). Without this skip the\n // tarball is re-fetched (multi-MB over S3) and re-extracted on each\n // call, pinning CPU at multi-vCPU per pod and triggering HPA spirals.\n // We treat the presence of package.json as the marker for \"already\n // extracted\" — it's the first file npm tarballs put under the version\n // directory and removing it (e.g. by invalidate*) requires the rest to\n // go too.\n if (fs.existsSync(path.join(destDir, \"package.json\"))) return;\n\n const shortName = packageName.startsWith(\"@\")\n ? packageName.split(\"/\")[1]\n : packageName;\n const tarballUrl = `${this.registryUrl}/${encodeURIComponent(packageName)}/-/${shortName}-${version}.tgz`;\n\n let res: Response;\n try {\n res = await fetch(tarballUrl);\n if (!res.ok || !res.body) return;\n } catch {\n return;\n }\n\n fs.mkdirSync(destDir, { recursive: true });\n\n const tmpFile = path.join(\n destDir,\n `.tmp-tarball-${crypto.randomUUID()}.tgz`,\n );\n try {\n const fileStream = fs.createWriteStream(tmpFile);\n await pipeline(Readable.fromWeb(res.body as never), fileStream);\n await extract({ file: tmpFile, cwd: destDir, strip: 1 });\n } finally {\n fs.rmSync(tmpFile, { force: true });\n }\n }\n\n invalidate(packageName: string): void {\n const cacheDir = path.join(this.cdnCachePath, packageName);\n if (!this.isSafePath(cacheDir)) return;\n fs.rmSync(cacheDir, { recursive: true, force: true });\n }\n\n invalidateVersion(packageName: string, version: string): void {\n const versionDir = path.join(this.cdnCachePath, packageName, version);\n if (!this.isSafePath(versionDir)) return;\n fs.rmSync(versionDir, { recursive: true, force: true });\n // If the package dir is now empty, remove it too so the scanner doesn't\n // keep returning a ghost entry with no versions.\n const pkgDir = path.join(this.cdnCachePath, packageName);\n try {\n if (fs.readdirSync(pkgDir).length === 0) {\n fs.rmdirSync(pkgDir);\n }\n } catch {\n // ignore — dir may not exist\n }\n }\n\n // Reconcile the cache against the registry's current version set; returns\n // removed versions. Empty/failed listings are a no-op (never mass-purge).\n async reconcileWithRegistry(packageName: string): Promise<string[]> {\n const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;\n let versions: string[];\n try {\n const res = await fetch(url, { headers: { Accept: \"application/json\" } });\n if (!res.ok) return [];\n const meta = (await res.json()) as { versions?: Record<string, unknown> };\n versions = Object.keys(meta.versions ?? {});\n } catch {\n return [];\n }\n if (versions.length === 0) return [];\n return this.reconcileVersions(packageName, versions);\n }\n\n // Drop cached version dirs absent from `keepVersions`; returns the removed\n // ones. Mirrors single-version unpublish (a manifest rewrite, no DELETE).\n reconcileVersions(\n packageName: string,\n keepVersions: Iterable<string>,\n ): string[] {\n const pkgDir = path.join(this.cdnCachePath, packageName);\n const keep = new Set(keepVersions);\n const removed: string[] = [];\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n } catch {\n return removed;\n }\n for (const entry of entries) {\n if (!entry.isDirectory() || keep.has(entry.name)) continue;\n this.invalidateVersion(packageName, entry.name);\n removed.push(entry.name);\n }\n return removed;\n }\n\n /** Remove all cached version directories except the specified one. */\n pruneOldVersions(packageName: string, keepVersion: string): void {\n const pkgDir = path.join(this.cdnCachePath, packageName);\n try {\n const entries = fs.readdirSync(pkgDir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() && entry.name !== keepVersion) {\n const dir = path.join(pkgDir, entry.name);\n if (this.isSafePath(dir)) {\n fs.rmSync(dir, { recursive: true, force: true });\n }\n }\n }\n } catch {\n // ignore — directory may not exist yet\n }\n }\n\n private isSafePath(filePath: string): boolean {\n const resolved = path.resolve(filePath);\n const cacheRoot = path.resolve(this.cdnCachePath);\n return resolved.startsWith(cacheRoot + path.sep) || resolved === cacheRoot;\n }\n}\n","import type { Manifest } from \"@powerhousedao/shared\";\nimport { slimManifest } from \"@powerhousedao/shared/registry\";\nimport 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":";;;;;;;;;;;;;;;;;ACIA,SAAgB,aAAa,aAA2B;AACtD,QAAO,IAAI,KAAK,EAAE,kBAAkB,aAAa,CAAC;;;;;;;;;;;;;AAcpD,SAAgB,cAAc,MAAuB;CACnD,IAAI,cAAoC;AAExC,QAAO;EACL,OAAsB;AACpB,oBAAiB,YAAY;AAC3B,UAAM,KAAK,MAAM;;;;;aAKZ;AACL,UAAM,KAAK,MAAM;;;;;aAKZ;OACH;AACJ,UAAO;;EAGT,MAAM,QAAQ,UAA8C;GAK1D,MAAM,OAJM,MAAM,KAAK,MACrB,gEACA,CAAC,SAAS,CACX,EACe,KAAK;AACrB,UAAO,MAAM,EAAE,cAAc,IAAI,eAAe,GAAG;;EAGrD,MAAM,WAAW,UAAkB,cAAwC;AAGzE,OAAI;AACF,UAAM,KAAK,MACT,wEACA,CAAC,UAAU,aAAa,CACzB;AACD,WAAO;YACA,KAAK;AACZ,QAAK,IAA0B,SAAS,QAAS,QAAO;AACxD,UAAM;;;EAIV,MAAM,UAAU,KAAuC;AAKrD,WAJY,MAAM,KAAK,MACrB,sEACA,CAAC,IAAI,CACN,EACU,KAAK,IAAI,UAAU;;EAGhC,MAAM,aAAa,MAAmD;AACpE,OAAI,KAAK,WAAW,EAAG,QAAO,EAAE;GAGhC,MAAM,eAAe,KAAK,KAAK,GAAG,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI;GAC9D,MAAM,MAAM,MAAM,KAAK,MACrB,mFAAmF,aAAa,IAChG,KACD;GACD,MAAM,MAAgC,EAAE;AACxC,QAAK,MAAM,OAAO,IAAI,KAAM,KAAI,IAAI,gBAAgB,IAAI;AACxD,UAAO;;EAGT,MAAM,WAAW,KAAa,UAAqC;AAEjE,SAAM,KAAK,MACT;;iDAGA,CAAC,KAAK,SAAS,CAChB;AAKD,WAJY,MAAM,KAAK,MACrB,sEACA,CAAC,IAAI,CACN,EACU,KAAK,IAAI,UAAU,EAAE;;EAGlC,QAAuB;AACrB,UAAO,KAAK,KAAK;;EAEpB;;;;ACpGH,MAAM,eAAe,OAAO,IAAI,6CAA6C;AAO7E,SAAS,WAA+B;CACtC,MAAM,IAAI;AACV,QAAQ,EAAE,kCAAkB,IAAI,KAAoB;;;AAItD,SAAgB,eAAe,OAA0B;CACvD,MAAM,QAAQ,YAAY;AAC1B,WAAU,CAAC,IAAI,OAAO;EAAE;EAAO,QAAQ;EAAO,CAAC;AAC/C,QAAO;;;AAeT,SAAgB,eAAe,OAAwB;AACrD,QAAO,UAAU,CAAC,IAAI,MAAM,EAAE,UAAU;;;;;;;;;;;AC9B1C,SAAgB,cAAc,GAAW,GAAmB;CAC1D,MAAM,CAAC,OAAO,QAAQ,EAAE,MAAM,KAAK,EAAE;CACrC,MAAM,CAAC,OAAO,QAAQ,EAAE,MAAM,KAAK,EAAE;CAErC,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC,IAAI,OAAO;CAC3C,MAAM,SAAS,MAAM,MAAM,IAAI,CAAC,IAAI,OAAO;AAE3C,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,IAAI,OAAO,QAAQ,OAAO,OAAO,EAAE,KAAK;EAC/D,MAAM,KAAK,OAAO,MAAM;EACxB,MAAM,KAAK,OAAO,MAAM;AACxB,MAAI,OAAO,GAAI,QAAO,KAAK;;AAI7B,KAAI,CAAC,QAAQ,KAAM,QAAO;AAC1B,KAAI,QAAQ,CAAC,KAAM,QAAO;AAC1B,KAAI,QAAQ,KAAM,QAAO,OAAO,OAAO,KAAK,OAAO,OAAO,IAAI;AAE9D,QAAO;;;;;;;;;;;;ACTT,SAAgB,iBAAiB,MAG/B;AAGA,KAAI,KAAK,WAAW,IAAI,EAAE;EAExB,MAAM,SAAS,KAAK,YAAY,IAAI;AACpC,MAAI,SAAS,KAAK,WAAW,KAAK,QAAQ,IAAI,CAC5C,QAAO;GAAE,MAAM,KAAK,MAAM,GAAG,OAAO;GAAE,KAAK,KAAK,MAAM,SAAS,EAAE;GAAE;AAErE,SAAO;GAAE,MAAM;GAAM,KAAK,KAAA;GAAW;;CAEvC,MAAM,UAAU,KAAK,QAAQ,IAAI;AACjC,KAAI,UAAU,EACZ,QAAO;EAAE,MAAM,KAAK,MAAM,GAAG,QAAQ;EAAE,KAAK,KAAK,MAAM,UAAU,EAAE;EAAE;AAEvE,QAAO;EAAE,MAAM;EAAM,KAAK,KAAA;EAAW;;;AAIvC,SAAgB,eAAe,KAAuB;AAGpD,QACE,CAAC,CAAC,OACF,2DAA2D,KAAK,IAAI;;AAIxE,IAAa,WAAb,MAAsB;CACpB,mCAAmB,IAAI,KAA4B;CAEnD,YACE,aACA,cACA;AAFQ,OAAA,cAAA;AACA,OAAA,eAAA;;CAGV,MAAM,iBACJ,aACA,SACA,UACwB;EACxB,MAAM,aAAa,KAAK,KAAK,KAAK,cAAc,aAAa,QAAQ;EAGrE,MAAM,WAAW,MAAA,YAAkB,YAAY,SAAS;AACxD,MAAI,SAAU,QAAO;AAGrB,QAAM,MAAA,gBAAsB,aAAa,QAAQ;AAEjD,SAAO,MAAA,YAAkB,YAAY,SAAS;;CAGhD,aAAa,YAAoB,UAAiC;EAGhE,MAAM,aAAa;GACjB,KAAK,KAAK,YAAY,SAAS;GAC/B,KAAK,KAAK,YAAY,OAAO,SAAS;GACtC,KAAK,KAAK,YAAY,QAAQ,OAAO,SAAS;GAC9C,KAAK,KAAK,YAAY,QAAQ,SAAS;GACxC;AAED,OAAK,MAAM,aAAa,WACtB,KAAI,KAAK,WAAW,UAAU,IAAI,GAAG,WAAW,UAAU,CACxD,QAAO;AAGX,SAAO;;CAGT,OAAA,gBAAuB,aAAqB,SAAgC;EAC1E,MAAM,MAAM,GAAG,YAAY,GAAG;EAC9B,MAAM,WAAW,MAAA,gBAAsB,IAAI,IAAI;AAC/C,MAAI,SAAU,QAAO;EAErB,MAAM,UAAU,KAAK,eAAe,aAAa,QAAQ,CAAC,cAAc;AACtE,SAAA,gBAAsB,OAAO,IAAI;IACjC;AACF,QAAA,gBAAsB,IAAI,KAAK,QAAQ;AACvC,SAAO;;CAGT,uBAAuB,aAAoC;EACzD,MAAM,SAAS,KAAK,KAAK,KAAK,cAAc,YAAY;AACxD,MAAI;GAEF,MAAM,WADU,GAAG,YAAY,QAAQ,EAAE,eAAe,MAAM,CAAC,CAE5D,QAAQ,MAAM,EAAE,aAAa,CAAC,CAC9B,KAAK,MAAM,EAAE,KAAK;AACrB,OAAI,SAAS,WAAW,EAAG,QAAO;AAClC,YAAS,KAAK,cAAc;AAC5B,UAAO,SAAS,SAAS,SAAS;UAC5B;AACN,UAAO;;;;;;;;;;;CAYX,MAAM,eACJ,aACA,KACwB;EACxB,MAAM,MAAM,GAAG,KAAK,YAAY,GAAG,mBAAmB,YAAY;EAClE,MAAM,MAAM,MAAM,MAAM,KAAK,EAC3B,SAAS,EAAE,QAAQ,oBAAoB,EACxC,CAAC;AACF,MAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,MAAI,CAAC,IAAI,GACP,OAAM,IAAI,MACR,yBAAyB,YAAY,YAAY,IAAI,SACtD;EAEH,MAAM,WAAY,MAAM,IAAI,MAAM;EAClC,MAAM,WAAW,SAAS;EAG1B,MAAM,WAAW,SAAS;AAI1B,MAAI,KAAK;AAEP,OAAI,YAAY,OAAO,SAAU,QAAO;AAExC,OAAI,YAAY,OAAO,SAAU,QAAO,SAAS;AAEjD,UAAO;;AAGT,MAAI,CAAC,SAAU,QAAO;AAEtB,SAAO,SAAS,UAAU,OAAO,OAAO,SAAS,CAAC,MAAM;;CAG1D,MAAM,eAAe,aAAqB,SAAgC;EACxE,MAAM,UAAU,KAAK,KAAK,KAAK,cAAc,aAAa,QAAQ;AAWlE,MAAI,GAAG,WAAW,KAAK,KAAK,SAAS,eAAe,CAAC,CAAE;EAEvD,MAAM,YAAY,YAAY,WAAW,IAAI,GACzC,YAAY,MAAM,IAAI,CAAC,KACvB;EACJ,MAAM,aAAa,GAAG,KAAK,YAAY,GAAG,mBAAmB,YAAY,CAAC,KAAK,UAAU,GAAG,QAAQ;EAEpG,IAAI;AACJ,MAAI;AACF,SAAM,MAAM,MAAM,WAAW;AAC7B,OAAI,CAAC,IAAI,MAAM,CAAC,IAAI,KAAM;UACpB;AACN;;AAGF,KAAG,UAAU,SAAS,EAAE,WAAW,MAAM,CAAC;EAE1C,MAAM,UAAU,KAAK,KACnB,SACA,gBAAgB,OAAO,YAAY,CAAC,MACrC;AACD,MAAI;GACF,MAAM,aAAa,GAAG,kBAAkB,QAAQ;AAChD,SAAM,SAAS,SAAS,QAAQ,IAAI,KAAc,EAAE,WAAW;AAC/D,SAAM,QAAQ;IAAE,MAAM;IAAS,KAAK;IAAS,OAAO;IAAG,CAAC;YAChD;AACR,MAAG,OAAO,SAAS,EAAE,OAAO,MAAM,CAAC;;;CAIvC,WAAW,aAA2B;EACpC,MAAM,WAAW,KAAK,KAAK,KAAK,cAAc,YAAY;AAC1D,MAAI,CAAC,KAAK,WAAW,SAAS,CAAE;AAChC,KAAG,OAAO,UAAU;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC;;CAGvD,kBAAkB,aAAqB,SAAuB;EAC5D,MAAM,aAAa,KAAK,KAAK,KAAK,cAAc,aAAa,QAAQ;AACrE,MAAI,CAAC,KAAK,WAAW,WAAW,CAAE;AAClC,KAAG,OAAO,YAAY;GAAE,WAAW;GAAM,OAAO;GAAM,CAAC;EAGvD,MAAM,SAAS,KAAK,KAAK,KAAK,cAAc,YAAY;AACxD,MAAI;AACF,OAAI,GAAG,YAAY,OAAO,CAAC,WAAW,EACpC,IAAG,UAAU,OAAO;UAEhB;;CAOV,MAAM,sBAAsB,aAAwC;EAClE,MAAM,MAAM,GAAG,KAAK,YAAY,GAAG,mBAAmB,YAAY;EAClE,IAAI;AACJ,MAAI;GACF,MAAM,MAAM,MAAM,MAAM,KAAK,EAAE,SAAS,EAAE,QAAQ,oBAAoB,EAAE,CAAC;AACzE,OAAI,CAAC,IAAI,GAAI,QAAO,EAAE;GACtB,MAAM,OAAQ,MAAM,IAAI,MAAM;AAC9B,cAAW,OAAO,KAAK,KAAK,YAAY,EAAE,CAAC;UACrC;AACN,UAAO,EAAE;;AAEX,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE;AACpC,SAAO,KAAK,kBAAkB,aAAa,SAAS;;CAKtD,kBACE,aACA,cACU;EACV,MAAM,SAAS,KAAK,KAAK,KAAK,cAAc,YAAY;EACxD,MAAM,OAAO,IAAI,IAAI,aAAa;EAClC,MAAM,UAAoB,EAAE;EAC5B,IAAI;AACJ,MAAI;AACF,aAAU,GAAG,YAAY,QAAQ,EAAE,eAAe,MAAM,CAAC;UACnD;AACN,UAAO;;AAET,OAAK,MAAM,SAAS,SAAS;AAC3B,OAAI,CAAC,MAAM,aAAa,IAAI,KAAK,IAAI,MAAM,KAAK,CAAE;AAClD,QAAK,kBAAkB,aAAa,MAAM,KAAK;AAC/C,WAAQ,KAAK,MAAM,KAAK;;AAE1B,SAAO;;;CAIT,iBAAiB,aAAqB,aAA2B;EAC/D,MAAM,SAAS,KAAK,KAAK,KAAK,cAAc,YAAY;AACxD,MAAI;GACF,MAAM,UAAU,GAAG,YAAY,QAAQ,EAAE,eAAe,MAAM,CAAC;AAC/D,QAAK,MAAM,SAAS,QAClB,KAAI,MAAM,aAAa,IAAI,MAAM,SAAS,aAAa;IACrD,MAAM,MAAM,KAAK,KAAK,QAAQ,MAAM,KAAK;AACzC,QAAI,KAAK,WAAW,IAAI,CACtB,IAAG,OAAO,KAAK;KAAE,WAAW;KAAM,OAAO;KAAM,CAAC;;UAIhD;;CAKV,WAAmB,UAA2B;EAC5C,MAAM,WAAW,KAAK,QAAQ,SAAS;EACvC,MAAM,YAAY,KAAK,QAAQ,KAAK,aAAa;AACjD,SAAO,SAAS,WAAW,YAAY,KAAK,IAAI,IAAI,aAAa;;;;;;;;;;;;;;;;;;;ACzQrE,SAAS,oBACP,aACA,aAKA;AACA,KAAI,CAAC,YAAa,QAAO,EAAE,kBAAkB,KAAA,GAAW;AACxD,KAAI;EACF,MAAM,eAAe,KAAK,KAAK,aAAa,aAAa,eAAe;EACxE,MAAM,MAAM,GAAG,aAAa,cAAc,QAAQ;EAClD,MAAM,SAAS,KAAK,MAAM,IAAI;EAK9B,MAAM,WAAW,OAAO;EAExB,MAAM,YADc,OAAO,WAAW,OAAO,KAAK,OAAO,SAAS,GAAG,EAAE,EAC1C,OAAO,CAAC,KAAK,cAAc;EACxD,MAAM,mBACJ,CAAC,CAAC,OAAO,gBAAgB,OAAO,KAAK,OAAO,aAAa,CAAC,SAAS;AACrE,SAAO;GACL,UACE,YAAY,OAAO,KAAK,SAAS,CAAC,SAAS,IAAI,WAAW,KAAA;GAC5D,UAAU,SAAS,SAAS,IAAI,WAAW,KAAA;GAC3C;GACD;SACK;AACN,SAAO,EAAE,kBAAkB,KAAA,GAAW;;;;;;;;AAS1C,SAAgB,mBACd,aACA,aACqB;AACrB,QAAO,oBAAoB,aAAa,YAAY,CAAC;;AAGvD,SAAS,aAAa,KAA8B;CAClD,MAAM,aAAa;EACjB,KAAK,KAAK,KAAK,2BAA2B;EAC1C,KAAK,KAAK,KAAK,OAAO,2BAA2B;EACjD,KAAK,KAAK,KAAK,QAAQ,2BAA2B;EACnD;AACD,MAAK,MAAM,gBAAgB,WACzB,KAAI;EACF,MAAM,MAAM,GAAG,aAAa,cAAc,QAAQ;AAMlD,SAAO,aAAa,KAAK,MAAM,IAAI,CAAa;SAC1C;AAIV,QAAO;;AAGT,SAAS,uBAAuB,KAAiC;AAC/D,KAAI;EACF,MAAM,MAAM,GAAG,aAAa,KAAK,KAAK,KAAK,eAAe,EAAE,QAAQ;EACpE,MAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,SAAO,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU,KAAA;SACjD;AACN;;;AAIJ,SAAS,oBAAoB,QAA+B;CAC1D,IAAI;AACJ,KAAI;AACF,YAAU,GAAG,YAAY,QAAQ,EAAE,eAAe,MAAM,CAAC;SACnD;AACN,SAAO;;CAET,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,aAAa,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK;AAC1E,KAAI,SAAS,WAAW,EAAG,QAAO;AAClC,UAAS,KAAK,cAAc;AAC5B,QAAO,KAAK,KAAK,QAAQ,SAAS,SAAS,SAAS,GAAG;;AAGzD,SAAgB,YACd,cACA,MACA,SACoB;CACpB,MAAM,SAAS,KAAK,KAAK,cAAc,KAAK;CAI5C,MAAM,eAHa,UACf,KAAK,KAAK,QAAQ,QAAQ,GAC1B,oBAAoB,OAAO,KACG;CAClC,MAAM,WAAW,aAAa,YAAY;AAE1C,KAAI,CAAC,SACH,QAAO;AAET,QAAO;EACL,MAAM,SAAS,QAAQ;EACvB,MAAM,UAAU;EAChB;EACA,eAAe,6BAA6B,SAAS;EACrD,SAAS,uBAAuB,YAAY;EAC7C;;AAGH,SAAS,6BAA6B,UAAuC;AAC3E,KAAI,CAAC,SAAU,QAAO,EAAE;CAExB,MAAM,gBAA0B,EAAE;CAClC,MAAM,EAAE,MAAM,gBAAgB,SAAS,cAAc;AAErD,KAAI,MAAM,OACR,eAAc,KAAK,4BAA4B;AAEjD,eAAc,KACZ,IAAI,kBAAkB,EAAE,EAAE,KAAK,OAAO,GAAG,GAAG,EAC5C,IAAI,WAAW,EAAE,EACd,SAAS,MAAM,EAAE,cAAc,CAC/B,QAAQ,OAAO,OAAO,KAAA,EAAU,EACnC,IAAI,aAAa,EAAE,EAChB,SAAS,MAAM,EAAE,cAAc,CAC/B,QAAQ,OAAO,OAAO,KAAA,EAAU,CACpC;AAED,QAAO;;AAGT,SAAgB,aACd,cACA,aACe;CACf,MAAM,SAAS,KAAK,QAAQ,aAAa;CACzC,MAAM,WAA0B,EAAE;CAElC,IAAI;AACJ,KAAI;AACF,YAAU,GAAG,YAAY,QAAQ,EAAE,eAAe,MAAM,CAAC;SACnD;AACN,SAAO;;AAGT,MAAK,MAAM,SAAS,SAAS;AAC3B,MAAI,CAAC,MAAM,aAAa,CAAE;AAE1B,MAAI,MAAM,KAAK,WAAW,IAAI,EAAE;GAC9B,MAAM,WAAW,KAAK,KAAK,QAAQ,MAAM,KAAK;GAC9C,IAAI;AACJ,OAAI;AACF,oBAAgB,GAAG,YAAY,UAAU,EAAE,eAAe,MAAM,CAAC;YAC1D,OAAO;AACd,YAAQ,IAAI,MAAM;AAClB;;AAEF,QAAK,MAAM,eAAe,eAAe;AACvC,QAAI,CAAC,YAAY,aAAa,CAAE;IAChC,MAAM,UAAU,GAAG,MAAM,KAAK,GAAG,YAAY;IAC7C,MAAM,SAAS,KAAK,KAAK,UAAU,YAAY,KAAK;IAEpD,MAAM,cADa,oBAAoB,OAAO,IACZ;IAClC,MAAM,WAAW,aAAa,YAAY;IAG1C,MAAM,OAAO,UAAU,QAAQ;IAC/B,MAAM,EAAE,UAAU,UAAU,qBAAqB,oBAC/C,aACA,KACD;AAOD,QAAI,qBAAqB,MAAO;AAChC,aAAS,KAAK;KACZ;KACA,MAAM,UAAU;KAChB;KACA,eAAe,6BAA6B,SAAS;KACrD,SAAS,uBAAuB,YAAY;KAC5C;KACA;KACD,CAAC;;SAEC;GACL,MAAM,SAAS,KAAK,KAAK,QAAQ,MAAM,KAAK;GAE5C,MAAM,cADa,oBAAoB,OAAO,IACZ;GAClC,MAAM,WAAW,aAAa,YAAY;GAC1C,MAAM,OAAO,UAAU,QAAQ,MAAM;GACrC,MAAM,EAAE,UAAU,UAAU,qBAAqB,oBAC/C,aACA,KACD;AACD,OAAI,qBAAqB,MAAO;AAChC,YAAS,KAAK;IACZ;IACA,MAAM,UAAU,MAAM;IACtB;IACA,eAAe,6BAA6B,SAAS;IACrD,SAAS,uBAAuB,YAAY;IAC5C;IACA;IACD,CAAC;;;AAIN,QAAO;;AAGT,SAAgB,2BACd,aACA,cACe;AAGf,QAFoB,aAAa,YAAY,CAE1B,QAAQ,QAAQ;AACjC,MAAI,CAAC,IAAI,UAAU,eACjB,QAAO;AAET,SAAO,IAAI,SAAS,eAAe,MAAM,OAAO,GAAG,OAAO,aAAa;GACvE;;;;ACrPJ,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;;;;;;AAYzB,SAAgB,aACd,QACA,KACqB;CACrB,IAAI,eAAe;CACnB,IAAI,aAAa;AAEjB,QAAO,eAAe,OAAsB;AAC1C,MAAI,aAAc;AAClB,MAAI,KAAK,KAAK,GAAG,aAAa,iBAAkB;AAChD,iBAAe;AACf,MAAI;GACF,MAAM,IAAI,MAAM,MACd,oBAAoB,OAAO,KAAK,4BACjC;AACD,OAAI,CAAC,EAAE,IAAI;AACT,YAAQ,MACN,iDAAiD,EAAE,SACpD;AACD;;GAEF,MAAM,SAAU,MAAM,EAAE,MAAM;GAI9B,MAAM,+BAAe,IAAI,KAAqB;AAC9C,QAAK,MAAM,OAAO,QAAQ;AACxB,QAAI,CAAC,IAAI,QAAS;AAClB,QAAI,mBAAmB,OAAO,aAAa,IAAI,KAAK,KAAK,MACvD;AACF,iBAAa,IAAI,IAAI,MAAM,IAAI,QAAQ;;GAGzC,MAAM,UAAU,CAAC,GAAG,aAAa,SAAS,CAAC;GAC3C,IAAI,SAAS;GACb,MAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,kBAAkB,CAAC,CAAC,IAAI,YAAY;AACvE,WAAO,SAAS,QAAQ,QAAQ;KAC9B,MAAM,CAAC,MAAM,WAAW,QAAQ;AAChC,SAAI;AACF,YAAM,IAAI,eAAe,MAAM,QAAQ;cAChC,KAAK;AACZ,cAAQ,MACN,uCAAuC,KAAK,GAAG,QAAQ,IACvD,IACD;;;KAGL;AACF,SAAM,QAAQ,IAAI,QAAQ;AAC1B,WAAQ,IAAI,sCAAsC,QAAQ,OAAO,QAAQ;AAGzE,gBAAa,KAAK,KAAK;WAChB,KAAK;AACZ,WAAQ,MAAM,wCAAwC,IAAI;YAClD;AACR,kBAAe;;;;;;ACnDrB,MAAM,aAAqC;CACzC,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;CACT,QAAQ;CACR,SAAS;CACT,QAAQ;CACT;AAED,SAAS,eAAe,UAA0B;AAEhD,QAAO,WADK,KAAK,QAAQ,SAAS,CAAC,aAAa,KACtB;;AAK5B,SAAS,qBACP,KAC+C;CAC/C,MAAM,OAAQ,IAA4C,aAAa;AACvE,KAAI,CAAC,KAAM,QAAO,KAAA;AAClB,KAAI,KAAK,WAAW,WAAW,CAE7B,QAAO;EAAE,UADQ,KAAK,MAAM,IAAI,CAAC,KAAK,IAAI,MAAM,aAAa;EAC3C,KAAK;EAAM;AAE/B,QAAO,EAAE,SAAS,MAAM;;;;;;;AAa1B,eAAe,sBACb,KACA,aACA,KAC4B;AAC5B,KAAI,OAAO,eAAe,IAAI,CAAE,QAAO;EAAE,MAAM;EAAM,SAAS;EAAK;AAEnE,KAAI;EACF,MAAM,WACH,MAAM,IAAI,eAAe,aAAa,IAAI,IAC3C,IAAI,uBAAuB,YAAY;AACzC,MAAI,CAAC,SAAU,QAAO,EAAE,MAAM,aAAa;AAC3C,SAAO;GAAE,MAAM;GAAM,SAAS;GAAU;SAClC;EACN,MAAM,SAAS,IAAI,uBAAuB,YAAY;AACtD,MAAI,CAAC,OAAQ,QAAO,EAAE,MAAM,kBAAkB;AAC9C,SAAO;GAAE,MAAM;GAAM,SAAS;GAAQ;;;;AAK1C,SAAS,UAAU,KAAqB;AACtC,QAAO,IAAI,WAAW,KAAK,GAAG,IAAI,MAAM,EAAE,GAAG;;;AAI/C,SAAS,YACP,QACA,MACS;AACT,KAAI,CAAC,OAAQ,QAAO;CACpB,MAAM,SAAS,UAAU,KAAK;AAE9B,SADc,MAAM,QAAQ,OAAO,GAAG,OAAO,KAAK,IAAI,GAAG,QAC5C,MAAM,IAAI,CAAC,MAAM,cAAc;EAC1C,MAAM,MAAM,UAAU,MAAM;AAC5B,SAAO,QAAQ,OAAO,UAAU,IAAI,KAAK;GACzC;;AAGJ,SAAgB,uBACd,QACA,KACA,UACA,YACQ;CACR,MAAM,MAAM,IAAI,SACd,oBAAoB,OAAO,QAC3B,OAAO,aACR;CACD,MAAM,SAAS,QAAQ;CAIvB,MAAM,aAAa,OAAO,SAAgD;AACxE,MAAI,CAAC,cAAc,KAAK,WAAW,EAAG,QAAO;AAC7C,MAAI;AACF,SAAM,WAAW,MAAM;GACvB,MAAM,MAAM,MAAM,WAAW,aAAa,KAAK,KAAK,MAAM,EAAE,KAAK,CAAC;AAClE,UAAO,KAAK,KAAK,MACf,EAAE,QAAQ,MAAM;IAAE,GAAG;IAAG,QAAQ,IAAI,EAAE;IAAO,GAAG,EACjD;WACM,KAAK;AACZ,WAAQ,MAAM,mCAAmC,IAAI;AACrD,UAAO;;;AAKX,QAAO,KAAK,MAAe,KAAe,SAAuB;AAC/D,MAAI,UAAU,+BAA+B,IAAI;AACjD,QAAM;GACN;AAGF,QAAO,IAAI,cAAc,MAAe,QAAkB;AACxD,MAAI,UAAU,IAAI;GAClB;AAGF,QAAO,IAAI,gBAAgB,MAAe,QAAkB;AAC1D,MAAI,KAAK,SAAS,aAAa,CAAC;GAChC;AAEF,QAAO,KAAK,eAAe,QAAQ,MAAM,GAAG,KAAc,QAAkB;EAC1E,MAAM,EAAE,UAAU,YAAY,IAAI;AAIlC,MAAI,CAAC,UAAU;AACb,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,oCAAoC,CAAC;AACnE;;AAEF,WAAS,WAAW;GAAE;GAAU;GAAS,CAAC;AAC1C,MAAI,OAAO,IAAI,CAAC,KAAK;GAAE;GAAU;GAAS,CAAC;GAC3C;AAEF,QAAO,OACL,eACA,QAAQ,MAAM,GACb,KAAc,QAAkB;EAC/B,MAAM,EAAE,aAAa,IAAI;AACzB,MAAI,CAAC,UAAU;AACb,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,oCAAoC,CAAC;AACnE;;AAGF,MAAI,CADY,SAAS,cAAc,SAAS,EAClC;AACZ,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,qBAAqB,CAAC;AACpD;;AAEF,MAAI,OAAO,IAAI,CAAC,KAAK;GAExB;CAED,MAAM,OAAO,aAAa,QAAQ,IAAI;AAIjC,OAAM;AAQX,QAAO,IAAI,aAAa,OAAO,KAAc,QAAkB;AACxD,QAAM;EACX,MAAM,WAAW,aAAa,OAAO,cAAc,OAAO,YAAY;EACtE,MAAM,eAAe,IAAI,MAAM;EAC/B,MAAM,WAAW,eACb,SAAS,QAAQ,QACf,IAAI,UAAU,gBAAgB,MAAM,MAAM,EAAE,OAAO,aAAa,CACjE,GACD;AACJ,MAAI,KAAK,MAAM,WAAW,SAAS,CAAC;GACpC;AAGF,QAAO,IAAI,+BAA+B,KAAc,QAAkB;EACxE,MAAM,eAAe,IAAI,MAAM;AAE/B,MAAI,OAAO,iBAAiB,YAAY,CAAC,cAAc;AACrD,OAAI,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,0CAA0C,CAAC;AACzE;;EAOF,MAAM,eAJW,2BACf,OAAO,cACP,aACD,CAC6B,KAAK,QAAQ,IAAI,KAAK;AACpD,MAAI,KAAK,aAAa;GACtB;AAGF,QAAO,IAAI,eAAe,OAAO,KAAc,QAAkB;EAC/D,MAAM,MAAO,IAAI,OAAkC;EACnD,MAAM,EAAE,MAAM,QAAQ,iBAAiB,IAAI;EAC3C,MAAM,aAAa,MAAM,sBAAsB,KAAK,MAAM,IAAI;AAC9D,MAAI,WAAW,SAAS,kBAAkB;AACxC,OAAI,OAAO,IAAI,CAAC,KAAK,gCAAgC;AACrD;;EAEF,MAAM,UAAU,WAAW,SAAS,OAAO,WAAW,UAAU,KAAA;EAChE,MAAM,MAAM,YAAY,OAAO,cAAc,MAAM,QAAQ;AAC3D,MAAI,CAAC,KAAK;AACR,OAAI,OAAO,IAAI,CAAC,KAAK,oBAAoB;AACzC;;AAEF,MAAI,MAAM,MAAM,WAAW,CAAC,IAAI,CAAC,EAAE,GAAG;GACtC;AAGF,QAAO,IAAI,YAAY,OAAO,KAAc,QAAkB;EAC5D,MAAM,WAAY,IAAI,OAAkC;EAGxD,IAAI;EACJ,IAAI;AAEJ,MAAI,SAAS,WAAW,IAAI,EAAE;GAE5B,MAAM,WAAW,SAAS,MAAM,IAAI;AACpC,OAAI,SAAS,SAAS,GAAG;AACvB,QAAI,OAAO,IAAI,CAAC,KAAK,uBAAuB;AAC5C;;AAEF,iBAAc,GAAG,SAAS,GAAG,GAAG,SAAS;AACzC,cAAW,SAAS,MAAM,EAAE,CAAC,KAAK,IAAI,IAAI;SACrC;GAEL,MAAM,WAAW,SAAS,MAAM,IAAI;AACpC,iBAAc,SAAS;AACvB,cAAW,SAAS,MAAM,EAAE,CAAC,KAAK,IAAI,IAAI;;EAG5C,MAAM,EAAE,MAAM,aAAa,QAAQ,iBAAiB,YAAY;EAChE,MAAM,SAAS,eAAe,IAAI;EAClC,MAAM,aAAa,MAAM,sBAAsB,KAAK,aAAa,IAAI;AACrE,MAAI,WAAW,SAAS,kBAAkB;AACxC,OAAI,OAAO,IAAI,CAAC,KAAK,gCAAgC;AACrD;;AAEF,MAAI,WAAW,SAAS,aAAa;AACnC,OAAI,OAAO,IAAI,CAAC,KAAK,iBAAiB;AACtC;;EAEF,MAAM,UAAU,WAAW;EAE3B,MAAM,WAAW,MAAM,IAAI,iBAAiB,aAAa,SAAS,SAAS;AAC3E,MAAI,CAAC,UAAU;AAIb,OAAI,OACF,KAAI;AACF,UAAM,IAAI,eAAe,aAAa,IAAI;WACpC;AACN,QAAI,OAAO,IAAI,CAAC,KAAK,gCAAgC;AACrD;;AAGJ,OAAI,OAAO,IAAI,CAAC,KAAK,iBAAiB;AACtC;;AAKF,MAAI,UACF,iBACA,SACI,wCACA,sCACL;EASD,MAAM,OAAO,MAAM,QAAQ,GALV,OACd,WAAW,OAAO,CAClB,OAAO,SAAS,CAChB,OAAO,MAAM,CACb,MAAM,GAAG,GAAG,CACwB;AACvC,MAAI,UAAU,QAAQ,KAAK;AAC3B,MAAI,YAAY,IAAI,QAAQ,kBAAkB,KAAK,EAAE;AACnD,OAAI,OAAO,IAAI,CAAC,KAAK;AACrB;;AAGF,MAAI,UAAU,gBAAgB,eAAe,SAAS,CAAC;AACvD,MAAI;AACF,SAAM,SAAS,GAAG,iBAAiB,SAAS,EAAE,IAAI;UAC5C;AAGN,OAAI,SAAS;;GAEf;AAEF,QAAO;;;;;;;;;AAUT,SAAgB,sBACd,SACwD;CACxD,MAAM,SAAS,QAAQ,QAAQ,SAAS;AACxC,KAAI,UAAU,EAAG,QAAO;CACxB,MAAM,YAAY,QAAQ,MAAM,GAAG,OAAO;CAG1C,MAAM,aAAa,UAAU,QADP,MAC6B;AACnD,KAAI,eAAe,GAGjB,QAAO;EAAE,aADW,mBAAmB,UAAU;EAC3B,SAAS;EAAM;CAGvC,MAAM,cAAc,mBAAmB,UAAU,MAAM,GAAG,WAAW,CAAC;CACtE,MAAM,cAAc,UAAU,MAAM,aAAa,EAAqB;AACtE,KAAI,CAAC,YAAY,SAAS,OAAO,CAAE,QAAO;CAI1C,MAAM,SAAS,GAHG,YAAY,WAAW,IAAI,GACzC,YAAY,MAAM,IAAI,CAAC,KACvB,YACwB;AAC5B,KAAI,CAAC,YAAY,WAAW,OAAO,CAAE,QAAO;CAC5C,MAAM,UAAU,YAAY,MAAM,OAAO,QAAQ,GAAe;AAChE,KAAI,CAAC,QAAS,QAAO;AACrB,QAAO;EAAE;EAAa;EAAS;;AAKjC,SAAgB,qBACd,SACgC;CAChC,MAAM,SAAS,QAAQ,QAAQ,SAAS;AACxC,KAAI,UAAU,EAAG,QAAO;CACxB,MAAM,YAAY,QAAQ,MAAM,GAAG,OAAO;AAC1C,KAAI,UAAU,SAAS,MAAM,CAAE,QAAO;AACtC,QAAO,EAAE,aAAa,mBAAmB,UAAU,EAAE;;AAGvD,SAAgB,oBACd,QACA,eACA;CACA,MAAM,MAAM,IAAI,SACd,oBAAoB,OAAO,QAC3B,OAAO,aACR;CAID,MAAM,yBAAyB,KAAc,QAAkB;EAC7D,MAAM,UAAU,qBAAqB,IAAI,KAAK;AAC9C,MAAI,CAAC,QAAS;EAEd,MAAM,cAAc,IAAI,IAAI,KAAK,IAAI;AACrC,MAAI,MAAM,SAER,OACA,UACA,IACA;AACA,OAAI,IAAI,cAAc,OAAO,IAAI,aAAa,KAAK;IACjD,MAAM,cAAc,qBAAqB,IAAI;AAC7C,QACG,sBAAsB,QAAQ,YAAY,CAC1C,MAAM,YAAY;AACjB,UAAK,MAAM,WAAW,QACpB,eAAc,gBAAgB;MAC5B,aAAa,QAAQ;MACrB;MACA;MACD,CAAC;MAEJ,CACD,OAAO,QAAQ;AACd,aAAQ,MACN,uCAAuC,QAAQ,YAAY,IAC3D,IACD;MACD;;AAEN,UAAO,YAAY,OAAO,UAA4B,GAAG;;;AAI7D,SAAQ,KAAc,KAAe,SAAuB;AAC1D,MAAI,IAAI,WAAW,OAAO;AACxB,yBAAsB,KAAK,IAAI;AAC/B,SAAM;AACN;;AAEF,MAAI,IAAI,WAAW,UAAU;AAC3B,SAAM;AACN;;EAGF,MAAM,SAAS,sBAAsB,IAAI,KAAK;AAC9C,MAAI,CAAC,QAAQ;AACX,SAAM;AACN;;EAGF,MAAM,cAAc,IAAI,IAAI,KAAK,IAAI;AACrC,MAAI,MAAM,SAER,OACA,UACA,IACA;AACA,OAAI,IAAI,cAAc,OAAO,IAAI,aAAa,IAC5C,KAAI;AACF,QAAI,OAAO,QACT,KAAI,kBAAkB,OAAO,aAAa,OAAO,QAAQ;QAEzD,KAAI,WAAW,OAAO,YAAY;AAEpC,kBAAc,gBAAgB;KAC5B,aAAa,OAAO;KACpB,SAAS,OAAO;KAChB,aAAa,qBAAqB,IAAI;KACvC,CAAC;YACK,KAAK;AACZ,YAAQ,MACN,mCAAmC,OAAO,cAAc,OAAO,UAAU,IAAI,OAAO,YAAY,GAAG,IACnG,IACD;;AAGL,UAAO,YAAY,OAAO,UAA4B,GAAG;;AAG3D,QAAM;;;AAIV,SAAgB,kBACd,QACA,eACA;CACA,MAAM,MAAM,IAAI,SACd,oBAAoB,OAAO,QAC3B,OAAO,aACR;AAED,SAAQ,KAAc,KAAe,SAAuB;AAI1D,MAAI,IAAI,WAAW,SAAS,IAAI,KAAK,SAAS,SAAS,EAAE;AACvD,SAAM;AACN;;EAGF,MAAM,cAAc,IAAI,IAAI,KAAK,IAAI;AACrC,MAAI,MAAM,SAER,OACA,UACA,IACA;GACA,MAAM,UAAU,IAAI,KAAK,QAAQ,OAAO,GAAG;AAC3C,OACE,IAAI,aAAa,OACjB,IAAI,cAAc,OAClB,CAAC,WACD,QAAQ,WAAW,IAAI,CAEvB,QAAO,YAAY,OAAO,UAA4B,GAAG;GAE3D,MAAM,cAAc,mBAAmB,QAAQ;GAC/C,MAAM,cAAe,IAAI,KACtB;GACH,MAAM,WAAW,OAAO,KAAK,YAAY;GACzC,MAAM,UAAU,SAAS,GAAG,EAAE;AAC9B,OAAI,CAAC,SAAS;AACZ,YAAQ,MAAM,mCAAmC,cAAc;AAC/D,WAAO,YAAY,OAAO,UAA4B,GAAG;;AAE3D,OAAI,SAAS,SAAS,EACpB,SAAQ,KACN,8CAA8C,YAAY,IAAI,KAAK,UAAU,SAAS,GACvF;GAGH,MAAM,cAAc,qBAAqB,IAAI;AAC7C,OACG,eAAe,aAAa,QAAQ,CACpC,WAAW;AACV,kBAAc,cAAc;KAAE;KAAa;KAAS;KAAa,CAAC;KAClE,CACD,OAAO,QAAQ;AACd,YAAQ,MACN,gCAAgC,YAAY,iBAC5C,IACD;KACD;AAEJ,UAAO,YAAY,OAAO,UAA4B,GAAG;;AAG3D,QAAM;;;;;ACjhBV,IAAa,sBAAb,MAAgE;CAC9D;CAEA,YAAY,UAAiC;AAC3C,QAAA,WAAiB;;CAGnB,cAAc,OAA2B;AACvC,OAAK,MAAM,WAAW,MAAA,SACpB,SAAQ,cAAc,MAAM;;CAIhC,gBAAgB,OAA6B;AAC3C,OAAK,MAAM,WAAW,MAAA,SACpB,SAAQ,gBAAgB,MAAM;;;;;ACdpC,IAAa,aAAb,MAAuD;CACrD,2BAAW,IAAI,KAAe;CAE9B,UAAU,KAAqB;AAC7B,MAAI,UAAU,KAAK;GACjB,gBAAgB;GAChB,iBAAiB;GACjB,YAAY;GACZ,+BAA+B;GAChC,CAAC;AACF,MAAI,MAAM,iCAAiC;AAE3C,QAAA,QAAc,IAAI,IAAI;AACtB,MAAI,GAAG,eAAe;AACpB,SAAA,QAAc,OAAO,IAAI;IACzB;;CAGJ,cAAc,OAA2B;AACvC,QAAA,UAAgB,WAAW,MAAM;;CAGnC,gBAAgB,OAA6B;AAC3C,QAAA,UAAgB,aAAa,MAAM;;CAGrC,WAAW,WAAmB,OAA4C;EACxE,MAAM,UAAU,UAAU,UAAU,UAAU,KAAK,UAAU,MAAM,CAAC;AACpE,OAAK,MAAM,UAAU,MAAA,QACnB,KAAI;AACF,UAAO,MAAM,QAAQ;WACd,KAAK;AACZ,WAAQ,MAAM,uCAAuC,IAAI;AACzD,SAAA,QAAc,OAAO,OAAO;;;;;;AC/BpC,MAAM,gBAAgB;AAEtB,IAAa,iBAAb,MAA2D;CACzD;CACA;CACA;CAEA,YAAY,aAAqB,QAAuB;AACtD,QAAA,cAAoB;AACpB,QAAA,aAAmB,QAAQ,YAAY,EAAE;AACzC,QAAA,UAAgB,MAAA,MAAY;;CAG9B,cAA+B;AAC7B,SAAO,CAAC,GAAG,MAAA,YAAkB,GAAG,MAAA,QAAc;;CAGhD,WAAW,SAA8B;AAIvC,MAHe,KAAK,aAAa,CAAC,MAC/B,MAAM,EAAE,aAAa,QAAQ,SAC/B,CACW;AACZ,QAAA,QAAc,KAAK,QAAQ;AAC3B,QAAA,MAAY;;CAGd,cAAc,UAA2B;EACvC,MAAM,SAAS,MAAA,QAAc;AAC7B,QAAA,UAAgB,MAAA,QAAc,QAAQ,MAAM,EAAE,aAAa,SAAS;AACpE,MAAI,MAAA,QAAc,WAAW,OAAQ,QAAO;AAC5C,QAAA,MAAY;AACZ,SAAO;;CAGT,cAAc,OAA2B;AACvC,QAAA,KAAW;GAAE,MAAM;GAAW,GAAG;GAAO,CAAC;;CAG3C,gBAAgB,OAA6B;AAC3C,QAAA,KAAW;GAAE,MAAM;GAAa,GAAG;GAAO,CAAC;;CAG7C,MAAM,MAAqC;AACzC,OAAK,MAAM,WAAW,KAAK,aAAa,EAAE;GACxC,MAAM,UAAkC;IACtC,gBAAgB;IAChB,GAAG,QAAQ;IACZ;AAED,SAAM,QAAQ,UAAU;IACtB,QAAQ;IACR;IACA,MAAM,KAAK,UAAU,KAAK;IAC3B,CAAC,CAAC,OAAO,QAAiB;AACzB,YAAQ,MAAM,yBAAyB,QAAQ,SAAS,WAAW,IAAI;KACvE;;;CAIN,YAAoB;AAClB,SAAO,KAAK,KAAK,MAAA,aAAmB,cAAc;;CAGpD,QAAyB;AACvB,MAAI;GACF,MAAM,MAAM,GAAG,aAAa,MAAA,UAAgB,EAAE,QAAQ;AACtD,UAAO,KAAK,MAAM,IAAI;UAChB;AACN,UAAO,EAAE;;;CAIb,QAAc;AACZ,KAAG,UAAU,MAAA,aAAmB,EAAE,WAAW,MAAM,CAAC;AACpD,KAAG,cAAc,MAAA,UAAgB,EAAE,KAAK,UAAU,MAAA,SAAe,MAAM,EAAE,CAAC;;;;;AC7E9E,SAAgB,qBAAqB,QAAwB;CAC3D,MAAM,eAAe,KAAK,KAAK,OAAO,aAAa,WAAW;CAE9D,MAAM,YAAY,OAAO,UAAU;CAMnC,MAAM,YAAY,QAAQ,OAAO,eAAe,OAAO,UAAU;CAGjE,MAAM,aACJ,OAAO,mBACN,OAAO,YACJ,eAAe,OAAO,UAAuB,GAC7C,KAAA;CACN,MAAM,aACJ,OAAO,cACP,KAAK,KAAK,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,UAAU;CACpE,MAAM,OAAO,YACT,EACE,iBAAiB;EACf,GAAI,OAAO,cAAc,EAAE,aAAa,OAAO,aAAa,GAAG,EAAE;EACjE,GAAI,aAAa,EAAE,YAAY,GAAG,EAAE;EACpC,GAAI,OAAO,SACP;GACE,WAAW,OAAO,OAAO;GACzB,GAAI,OAAO,OAAO,YACd,EAAE,WAAW,OAAO,OAAO,WAAW,GACtC,EAAE;GACP,GACD,EAAE;EACP,EACF,GACD,EAAE,UAAU,EAAE,MAAM,cAAc,EAAE;CAExC,MAAM,OAAgC;EACpC,SAAS,OAAO;EAChB,WAAW;EAIX,GAAI,OAAO,kBAAkB,EAAE,QAAQ,OAAO,iBAAiB,GAAG,EAAE;EAGpE,UAAU,EACR,KAAK,EACH,KAAK;GACH,MAAM,EAAE,WAAW,OAAO;GAC1B,QAAQ,EAAE;GACX,EACF,EACF;EACD;EACA,GAAI,YAAY,EAAE,SAAS,YAAY,GAAG,EAAE;EAC5C,SAAS,EACP,OAAO;GACL,KAAK;GAOL,QAAQ,OAAO,gBAAgB;GAC/B,SAAS;GACT,OAAO;GACR,EACF;EAMD,iBAAiB;GACf,MAAM,QAAQ,OAAO,wBAAwB,EAAE;GAC/C,MAAM,WAAW,IAAI,IAAI,MAAM;GAC/B,MAAM,SAAS;IACb,QAAQ;IACR,SAAS;IACT,WAAW;IACZ;GACD,MAAM,UAA+C,EAAE;AAIvD,QAAK,MAAM,WAAW,MACpB,SAAQ,KAAK,CAAC,SAAS,EAAE,GAAG,QAAQ,CAAC,CAAC;AAGxC,OAAI,CAAC,SAAS,IAAI,mBAAmB,CACnC,SAAQ,KAAK,CAAC,oBAAoB;IAAE,GAAG;IAAQ,OAAO;IAAS,CAAC,CAAC;AAEnE,OAAI,CAAC,SAAS,IAAI,KAAK,CACrB,SAAQ,KAAK,CAAC,MAAM;IAAE,GAAG;IAAQ,OAAO;IAAS,CAAC,CAAC;AAErD,UAAO,OAAO,YAAY,QAAQ;MAChC;EACJ,KAAK;GACH,QAAQ,OAAO,eAAe;GAC9B,OAAO;GACP,MAAM;GACN,SAAS;GACT,eAAe;GACf,UAAU;GACX;EACD,QAAQ,EACN,kBAAkB,IACnB;EACD,KAAK;GACH,MAAM;GACN,QAAQ;GACR,OAAO;GACR;EACD,eAAe,OAAO,eAAe;EACtC;AAED,KAAI,OAAO,GACT,MAAK,QAAQ,EACX,kBAAkB;EAChB,QAAQ,OAAO,GAAG;EAClB,UAAU,OAAO,GAAG;EACpB,QAAQ,OAAO,GAAG;EAClB,kBAAkB,OAAO,GAAG,oBAAoB;EAChD,GAAI,OAAO,GAAG,aAAa,EAAE,WAAW,OAAO,GAAG,WAAW;EAC7D,GAAI,OAAO,GAAG,eAAe,EAAE,aAAa,OAAO,GAAG,aAAa;EACnE,GAAI,OAAO,GAAG,mBAAmB,EAC/B,iBAAiB,OAAO,GAAG,iBAC5B;EACF,EACF;AAGH,QAAO;;;;ACvHT,eAAe,WAAW,KAA8B;AACtD,KAAI,KAAK,WAAW,IAAI,EAAE;AACxB,QAAM,MAAM,KAAK,EAAE,WAAW,MAAM,CAAC;AACrC,SAAO;;CAET,MAAM,QAAQ,MAAM,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AACtD,KAAI,CAAC,OAAO;AACV,QAAM,MAAM,KAAK,EAAE,WAAW,MAAM,CAAC;AACrC,SAAO;;AAET,QAAO;;AAGT,eAAsB,YAAY,MAA2B;CAC3D,MAAM,EACJ,MACA,YACA,aACA,QACA,cACA,YACA,UACA,eACA,UACA,YACA,kBACA,aACA,UACA,mBACA,WACA,YACA,WACA,iBAAiB,oBACjB,eACA,aACA,YACA,cACE;CACJ,MAAM,cAAc,MAAM,WAAW,WAAW;CAChD,MAAM,eAAe,MAAM,WAAW,YAAY;CAOlD,MAAM,kBAAkB,sBAAsB,YAAY,GAAG,CAAC,SAAS,MAAM;CAM7E,MAAM,gBAAgB,eAAe,QAAQ,QAAQ,UAAU;AAC/D,KAAI,eAAe,QAAQ,CAAC,UAC1B,SAAQ,KACN,wHACD;AAIH,KAAI,iBAAiB,CAAC,eAAe,CAAC,UACpC,SAAQ,KACN,4GACD;AAGH,SAAQ,IAAI;EACV;EACA;EACD,CAAC;CAEF,MAAM,iBAAiB,UACnB,MAAM,IAAI,CACX,KAAK,QAAQ,IAAI,MAAM,CAAC,CACxB,OAAO,QAAQ,CACf,KAAK,cAAc,EAAE,UAAU,EAAE;CAEpC,MAAM,uBAAuB,eACzB,MAAM,IAAI,CACX,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,OAAO,QAAQ;CAIlB,MAAM,kBACH,cACA,cAAc,cAAc,aAAa,YAAY,CAAC,GAAG,KAAA;CAG5D,MAAM,iBAAiB,kBACnB,eAAe,gBAAgB,GAC/B,KAAA;CAEJ,MAAM,SAAyB;EAC7B;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAI,sBAAsB,SAAS,EAAE,sBAAsB,GAAG,EAAE;EAChE,GAAI,iBAAiB,YACjB,EAAE,QAAQ;GAAE;GAAW,GAAI,YAAY,EAAE,WAAW,GAAG,EAAE;GAAG,EAAE,GAC9D,EAAE;EACN,GAAI,gBAAgB,UAAU,EAC5B,QAAQ,EAAE,UAAU,gBAAgB,EACrC;EACD,GAAI,YACF,cACA,YAAY,EACV,IAAI;GACF,QAAQ;GACR,UAAU;GACV,QAAQ;GACR,aAAa;GACb,iBAAiB;GACjB,WAAW;GACX;GACD,EACF;EACH,GAAI,cAAc,EAAE,aAAa,GAAG,EAAE;EACtC,GAAI,aAAa,EAAE,YAAY,GAAG,EAAE;EACpC,GAAI,kBAAkB,EAAE,WAAW,iBAAiB,GAAG,EAAE;EACzD,GAAI,iBAAiB,EAAE,gBAAgB,GAAG,EAAE;EAC7C;AAED,KAAI,OAAO,eAAe,OAAO,UAC/B,SAAQ,IACN,0FACD;AAGH,OAAM,MAAM,aAAa,EAAE,WAAW,MAAM,CAAC;AAC7C,OAAM,MAAM,cAAc,EAAE,WAAW,MAAM,CAAC;CAK9C,MAAM,kBAAmB,MAAM,UAHP,qBAAqB,OAAO,CAGK;AAIzD,KAAI,kBAAkB,CAAC,eAAe,eAAe,EAAE;AACrD,kBAAgB,OAAO;AACvB,QAAM,IAAI,MACR,kJACD;;CAEH,MAAM,mBAAmB,gBAAgB,UAAU,UAAU,CAAC;CAI9D,MAAM,MAAM,SAAS;CAErB,MAAM,aAAa,IAAI,YAAY;CACnC,MAAM,iBAAiB,IAAI,eAAe,OAAO,aAAa,OAAO,OAAO;CAC5E,MAAM,gBAAgB,IAAI,oBAAoB,CAAC,YAAY,eAAe,CAAC;CAG3E,MAAM,YAAY,MAAM,OAAO,UAAU,EAAE,MAAM,aAAa,CAAC;AAC/D,KAAI,UACF,KAAI,IAAI,aAAa,QAAQ,OAAO,UAAU,CAAC;AAIjD,KAAI,IACF,uBAAuB,QAAQ,YAAY,gBAAgB,gBAAgB,CAC5E;AAED,KAAI,IAAI,kBAAkB,QAAQ,cAAc,CAAC;AACjD,KAAI,IAAI,oBAAoB,QAAQ,cAAc,CAAC;AAGnD,KAAI,KAAK,KAAK,QAAQ,iBAAiB,KAAK,IAAI,CAAC;AAiBjD,QAfe,IAAI,OAAO,YAAY;AACpC,UAAQ,IAAI,mDAAmD,OAAO;AACtE,UAAQ,IAAI,gCAAgC,KAAK,SAAS;AAC1D,UAAQ,IAAI,gCAAgC,KAAK,WAAW;AAC5D,UAAQ,IAAI,gCAAgC,KAAK,GAAG;AACpD,UAAQ,IAAI,eAAe,cAAc;AACzC,UAAQ,IAAI,gBAAgB,eAAe;AAC3C,MAAI,OAAO,GACT,SAAQ,IAAI,eAAe,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS;AAEtE,MAAI,OAAO,OACT,SAAQ,IAAI,kBAAkB,OAAO,OAAO,YAAY;GAE1D;;;;ACjMJ,MAAa,kBAAkB,QAAQ;CACrC,MAAM;CACN,MAAM;EACJ,MAAM,OAAO;GACX,MAAM;GACN,MAAM;GACN,oBAAoB,OAAO,QAAQ,IAAI,KAAK,IAAA;GAC5C,4BAA4B;GAC7B,CAAC;EACF,YAAY,OAAO;GACjB,MAAM;GACN,MAAM;GACN,oBACE,QAAQ,IAAI,oBAAA;GACd,4BAA4B;GAC7B,CAAC;EACF,aAAa,OAAO;GAClB,MAAM;GACN,MAAM;GACN,oBACE,QAAQ,IAAI,sBAAA;GACd,4BAA4B;GAC7B,CAAC;EACF,QAAQ,OAAO;GACb,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,cAAc,OAAO;GACnB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,aACE;GAKF,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,UAAU,OAAO;GACf,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,YAAY,OAAO;GACjB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,UAAU,OAAO;GACf,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,eAAe,OAAO;GACpB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,mBAAmB,OAAO;GACxB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,aAAa,OAAO;GAClB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,kBAAkB,KAAK;GACrB,MAAM;GACN,oBAAoB,QAAQ,IAAI,wBAAwB;GACxD,4BAA4B;GAC7B,CAAC;EACF,YAAY,KAAK;GACf,MAAM;GACN,oBAAoB,QAAQ,IAAI,iBAAiB;GACjD,4BAA4B;GAC7B,CAAC;EACF,UAAU,OAAO;GACf,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,aAAa;GACb,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,WAAW,OAAO;GAChB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,aACE;GACF,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,YAAY,KAAK;GACf,MAAM;GACN,aACE;GACF,oBAAoB,QAAQ,IAAI,4BAA4B;GAC5D,4BAA4B;GAC7B,CAAC;EACF,WAAW,OAAO;GAChB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,aACE;GACF,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,iBAAiB,OAAO;GACtB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,aACE;GACF,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,eAAe,OAAO;GACpB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,aACE;GACF,oBAAoB,QAAQ,IAAI;GAChC,4BAA4B;GAC7B,CAAC;EACF,aAAa,OAAO;GAClB,MAAM;GACN,MAAM,SAAS,OAAO;GACtB,aACE;GACF,oBACE,QAAQ,IAAI,4BAA4B,QAAQ,IAAI;GACtD,4BAA4B;GAC7B,CAAC;EACH;CACD,SAAS,OAAO,SAAS;EAGvB,MAAM,UAAU,MAAgB,IAAI,eAAe,KAAA;AACnD,UAAQ,IAAI;GACV,GAAG;GACH,aAAa,OAAO,KAAK,YAAY;GACrC,iBAAiB,OAAO,KAAK,gBAAgB;GAC7C,mBAAmB,OAAO,KAAK,kBAAkB;GAClD,CAAC;AAEF,MAAI;AACF,SAAM,YAAY,KAAK;WAChB,OAAO;AACd,WAAQ,MAAM,4BAA4B;AAC1C,WAAQ,MAAM,MAAM;AACpB,WAAQ,KAAK,EAAE;;;CAGpB,CAAC;AAIF,MAAM,IAFc,OAAO,gBAAgB,EAEpB,QAAQ,KAAK"}
|
|
@@ -28,6 +28,7 @@ let _verdaccio_core = require("@verdaccio/core");
|
|
|
28
28
|
let bcryptjs = require("bcryptjs");
|
|
29
29
|
bcryptjs = __toESM(bcryptjs);
|
|
30
30
|
let pg = require("pg");
|
|
31
|
+
let _renown_sdk = require("@renown/sdk");
|
|
31
32
|
//#region src/auth/pg-store.ts
|
|
32
33
|
/** Build a real Postgres pool from a connection string. */
|
|
33
34
|
function createPgPool(databaseUrl) {
|
|
@@ -80,6 +81,14 @@ function createPgStore(pool) {
|
|
|
80
81
|
async getOwners(pkg) {
|
|
81
82
|
return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? null;
|
|
82
83
|
},
|
|
84
|
+
async getOwnersFor(pkgs) {
|
|
85
|
+
if (pkgs.length === 0) return {};
|
|
86
|
+
const placeholders = pkgs.map((_, i) => `$${i + 1}`).join(",");
|
|
87
|
+
const res = await pool.query(`SELECT package_name, owners FROM registry_package_owners WHERE package_name IN (${placeholders})`, pkgs);
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const row of res.rows) out[row.package_name] = row.owners;
|
|
90
|
+
return out;
|
|
91
|
+
},
|
|
83
92
|
async claimOwner(pkg, username) {
|
|
84
93
|
await pool.query(`INSERT INTO registry_package_owners (package_name, owners)
|
|
85
94
|
VALUES ($1, ARRAY[$2]::text[])
|
|
@@ -92,6 +101,77 @@ function createPgStore(pool) {
|
|
|
92
101
|
};
|
|
93
102
|
}
|
|
94
103
|
//#endregion
|
|
104
|
+
//#region src/auth/store-handoff.ts
|
|
105
|
+
const REGISTRY_KEY = Symbol.for("@powerhousedao/registry:auth-store-handoff");
|
|
106
|
+
function registry() {
|
|
107
|
+
const g = globalThis;
|
|
108
|
+
return g[REGISTRY_KEY] ??= /* @__PURE__ */ new Map();
|
|
109
|
+
}
|
|
110
|
+
/** Resolve a stashed store by token (undefined if unknown). */
|
|
111
|
+
function takeAuthStore(token) {
|
|
112
|
+
return registry().get(token)?.store;
|
|
113
|
+
}
|
|
114
|
+
/** Record that the plugin fully constructed with this token's store. */
|
|
115
|
+
function markStoreLoaded(token) {
|
|
116
|
+
const entry = registry().get(token);
|
|
117
|
+
if (entry) entry.loaded = true;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/auth/renown-verifier.ts
|
|
121
|
+
function createRenownVerifier(config) {
|
|
122
|
+
const ttl = config.cacheTtlMs ?? 6e4;
|
|
123
|
+
const maxEntries = 1e3;
|
|
124
|
+
const cache = /* @__PURE__ */ new Map();
|
|
125
|
+
return async (token) => {
|
|
126
|
+
if (ttl > 0) {
|
|
127
|
+
const cached = cache.get(token);
|
|
128
|
+
if (cached) {
|
|
129
|
+
if (cached.expiresAt > Date.now()) return cached.did;
|
|
130
|
+
cache.delete(token);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const did = (await (0, _renown_sdk.verifyAuthCredential)(token, {
|
|
134
|
+
audience: config.publicUrl,
|
|
135
|
+
renownUrl: config.renownUrl
|
|
136
|
+
}))?.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
|
|
95
175
|
//#region src/auth/registry-auth-plugin.ts
|
|
96
176
|
const BCRYPT_ROUNDS = 10;
|
|
97
177
|
function internal(err) {
|
|
@@ -102,7 +182,7 @@ function internal(err) {
|
|
|
102
182
|
* Pure plugin factory over an injected store — the unit-testable core.
|
|
103
183
|
* `registryAuthPlugin` below wires it to a real Postgres store.
|
|
104
184
|
*/
|
|
105
|
-
function createRegistryAuthPlugin(store) {
|
|
185
|
+
function createRegistryAuthPlugin(store, renownVerifier) {
|
|
106
186
|
const ready = store.init();
|
|
107
187
|
return {
|
|
108
188
|
authenticate(user, password, cb) {
|
|
@@ -138,15 +218,23 @@ function createRegistryAuthPlugin(store) {
|
|
|
138
218
|
if (owners && owners.includes(username)) return cb(null, true);
|
|
139
219
|
return cb(_verdaccio_core.errorUtils.getForbidden(`not authorized to unpublish "${name}"`));
|
|
140
220
|
}).catch((err) => cb(internal(err)));
|
|
141
|
-
}
|
|
221
|
+
},
|
|
222
|
+
...renownVerifier ? { apiJWTmiddleware(helpers) {
|
|
223
|
+
return renownApiJwtMiddleware(renownVerifier, helpers);
|
|
224
|
+
} } : {}
|
|
142
225
|
};
|
|
143
226
|
}
|
|
144
227
|
/** Verdaccio plugin entry. The loader calls this factory (or `new`s the
|
|
145
228
|
* default export — both return the plugin object). */
|
|
146
229
|
function registryAuthPlugin(config) {
|
|
147
|
-
|
|
148
|
-
throw new Error("registry-auth plugin requires a databaseUrl (or
|
|
149
|
-
})()))
|
|
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;
|
|
150
238
|
}
|
|
151
239
|
//#endregion
|
|
152
240
|
exports.createRegistryAuthPlugin = createRegistryAuthPlugin;
|
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.5",
|
|
4
4
|
"description": "Powerhouse package registry — resolves and manages document model package dependencies within the Powerhouse ecosystem.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"tar": "^7.5.11",
|
|
40
40
|
"verdaccio": "^6.5.0",
|
|
41
41
|
"verdaccio-aws-s3-storage": "^10.4.0",
|
|
42
|
-
"@powerhousedao/shared": "6.2.2-dev.
|
|
43
|
-
"@renown/sdk": "6.2.2-dev.
|
|
42
|
+
"@powerhousedao/shared": "6.2.2-dev.5",
|
|
43
|
+
"@renown/sdk": "6.2.2-dev.5"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"tsdown": "0.21.1",
|