@powerhousedao/registry 6.2.2-dev.2 → 6.2.2-dev.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.mts +4 -0
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +292 -84
- package/dist/cli.mjs.map +1 -1
- package/dist/plugins/package.json +1 -0
- package/dist/plugins/verdaccio-registry-auth.js +242 -0
- package/package.json +9 -3
package/dist/cli.d.mts
CHANGED
|
@@ -21,8 +21,10 @@ declare const registryCommand: Partial<cmd_ts_dist_cjs_argparser_js0.Register> &
|
|
|
21
21
|
webhooks: string | undefined;
|
|
22
22
|
publicUrl: string | undefined;
|
|
23
23
|
authRenown: boolean;
|
|
24
|
+
renownUrl: string | undefined;
|
|
24
25
|
verdaccioSecret: string | undefined;
|
|
25
26
|
localPackages: string | undefined;
|
|
27
|
+
databaseUrl: string | undefined;
|
|
26
28
|
}>>;
|
|
27
29
|
} & cmd_ts_dist_cjs_helpdoc_js0.PrintHelp & cmd_ts_dist_cjs_helpdoc_js0.ProvidesHelp & cmd_ts_dist_cjs_helpdoc_js0.Named & Partial<cmd_ts_dist_cjs_helpdoc_js0.Versioned> & cmd_ts_dist_cjs_argparser_js0.Register & cmd_ts_dist_cjs_runner_js0.Handling<{
|
|
28
30
|
port: number;
|
|
@@ -41,8 +43,10 @@ declare const registryCommand: Partial<cmd_ts_dist_cjs_argparser_js0.Register> &
|
|
|
41
43
|
webhooks: string | undefined;
|
|
42
44
|
publicUrl: string | undefined;
|
|
43
45
|
authRenown: boolean;
|
|
46
|
+
renownUrl: string | undefined;
|
|
44
47
|
verdaccioSecret: string | undefined;
|
|
45
48
|
localPackages: string | undefined;
|
|
49
|
+
databaseUrl: string | undefined;
|
|
46
50
|
}, Promise<void>> & {
|
|
47
51
|
run(context: cmd_ts_dist_cjs_argparser_js0.ParseContext): Promise<cmd_ts_dist_cjs_argparser_js0.ParsingResult<Promise<void>>>;
|
|
48
52
|
} & Partial<cmd_ts_dist_cjs_helpdoc_js0.Versioned & cmd_ts_dist_cjs_helpdoc_js0.Descriptive & cmd_ts_dist_cjs_helpdoc_js0.Aliased>;
|
package/dist/cli.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.mts","names":[],"sources":["../cli.ts"],"mappings":";;;;;cAiBa,eAAA,EAAe,OAAA,
|
|
1
|
+
{"version":3,"file":"cli.d.mts","names":[],"sources":["../cli.ts"],"mappings":";;;;;cAiBa,eAAA,EAAe,OAAA,CAkK1B,6BAAA,CAlK0B,QAAA;iBAAA,6BAAA,CAAA,YAAA"}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,77 +1,110 @@
|
|
|
1
1
|
import { binary, command, flag, number, option, optional, run, string } from "cmd-ts";
|
|
2
2
|
import express, { Router } from "express";
|
|
3
3
|
import { findUp } from "find-up";
|
|
4
|
-
import crypto, { randomBytes } from "node:crypto";
|
|
4
|
+
import crypto, { randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import { mkdir } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { runServer } from "verdaccio";
|
|
8
|
-
import {
|
|
9
|
-
import { verifyAuthBearerToken } from "@renown/sdk/node";
|
|
8
|
+
import { Pool } from "pg";
|
|
10
9
|
import fs from "node:fs";
|
|
11
10
|
import { pipeline } from "node:stream/promises";
|
|
12
11
|
import { Readable } from "node:stream";
|
|
13
12
|
import { extract } from "tar";
|
|
14
13
|
import { slimManifest } from "@powerhousedao/shared/registry";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region src/auth/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return aud === expected;
|
|
16
|
+
//#region src/auth/pg-store.ts
|
|
17
|
+
/** Build a real Postgres pool from a connection string. */
|
|
18
|
+
function createPgPool(databaseUrl) {
|
|
19
|
+
return new Pool({ connectionString: databaseUrl });
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* Postgres-backed AuthStore. Two small tables:
|
|
23
|
+
* - registry_users(username PK, password_hash, created_at)
|
|
24
|
+
* - registry_package_owners(package_name PK, owners text[], claimed_at)
|
|
26
25
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* Ownership claim is race-free: `INSERT ... ON CONFLICT DO NOTHING` means the
|
|
27
|
+
* first publisher wins atomically; the follow-up read returns the actual
|
|
28
|
+
* owners so a losing racer is denied.
|
|
29
|
+
*
|
|
30
|
+
* Takes an already-built `pg.Pool` (tests inject a pg-mem pool cast to Pool).
|
|
32
31
|
*/
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
32
|
+
function createPgStore(pool) {
|
|
33
|
+
let initialized = null;
|
|
34
|
+
return {
|
|
35
|
+
init() {
|
|
36
|
+
initialized ??= (async () => {
|
|
37
|
+
await pool.query(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS registry_users (
|
|
39
|
+
username text PRIMARY KEY,
|
|
40
|
+
password_hash text NOT NULL,
|
|
41
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
42
|
+
)`);
|
|
43
|
+
await pool.query(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS registry_package_owners (
|
|
45
|
+
package_name text PRIMARY KEY,
|
|
46
|
+
owners text[] NOT NULL,
|
|
47
|
+
claimed_at timestamptz NOT NULL DEFAULT now()
|
|
48
|
+
)`);
|
|
49
|
+
})();
|
|
50
|
+
return initialized;
|
|
51
|
+
},
|
|
52
|
+
async getUser(username) {
|
|
53
|
+
const row = (await pool.query("SELECT password_hash FROM registry_users WHERE username = $1", [username])).rows[0];
|
|
54
|
+
return row ? { passwordHash: row.password_hash } : null;
|
|
55
|
+
},
|
|
56
|
+
async createUser(username, passwordHash) {
|
|
57
|
+
try {
|
|
58
|
+
await pool.query("INSERT INTO registry_users (username, password_hash) VALUES ($1, $2)", [username, passwordHash]);
|
|
59
|
+
return true;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (err.code === "23505") return false;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
async getOwners(pkg) {
|
|
66
|
+
return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? null;
|
|
67
|
+
},
|
|
68
|
+
async getOwnersFor(pkgs) {
|
|
69
|
+
if (pkgs.length === 0) return {};
|
|
70
|
+
const placeholders = pkgs.map((_, i) => `$${i + 1}`).join(",");
|
|
71
|
+
const res = await pool.query(`SELECT package_name, owners FROM registry_package_owners WHERE package_name IN (${placeholders})`, pkgs);
|
|
72
|
+
const out = {};
|
|
73
|
+
for (const row of res.rows) out[row.package_name] = row.owners;
|
|
74
|
+
return out;
|
|
75
|
+
},
|
|
76
|
+
async claimOwner(pkg, username) {
|
|
77
|
+
await pool.query(`INSERT INTO registry_package_owners (package_name, owners)
|
|
78
|
+
VALUES ($1, ARRAY[$2]::text[])
|
|
79
|
+
ON CONFLICT (package_name) DO NOTHING`, [pkg, username]);
|
|
80
|
+
return (await pool.query("SELECT owners FROM registry_package_owners WHERE package_name = $1", [pkg])).rows[0]?.owners ?? [];
|
|
81
|
+
},
|
|
82
|
+
close() {
|
|
83
|
+
return pool.end();
|
|
63
84
|
}
|
|
64
|
-
req.headers.authorization = `Bearer ${verdaccioJwt}`;
|
|
65
|
-
req.renownUser = {
|
|
66
|
-
address,
|
|
67
|
-
did: payload?.iss,
|
|
68
|
-
chainId: subject.chainId,
|
|
69
|
-
networkId: subject.networkId
|
|
70
|
-
};
|
|
71
|
-
return next();
|
|
72
85
|
};
|
|
73
86
|
}
|
|
74
87
|
//#endregion
|
|
88
|
+
//#region src/auth/store-handoff.ts
|
|
89
|
+
const REGISTRY_KEY = Symbol.for("@powerhousedao/registry:auth-store-handoff");
|
|
90
|
+
function registry() {
|
|
91
|
+
const g = globalThis;
|
|
92
|
+
return g[REGISTRY_KEY] ??= /* @__PURE__ */ new Map();
|
|
93
|
+
}
|
|
94
|
+
/** Stash a store instance and return a token to carry through plugin config. */
|
|
95
|
+
function stashAuthStore(store) {
|
|
96
|
+
const token = randomUUID();
|
|
97
|
+
registry().set(token, {
|
|
98
|
+
store,
|
|
99
|
+
loaded: false
|
|
100
|
+
});
|
|
101
|
+
return token;
|
|
102
|
+
}
|
|
103
|
+
/** True once the plugin has loaded the store for this token. */
|
|
104
|
+
function wasStoreLoaded(token) {
|
|
105
|
+
return registry().get(token)?.loaded ?? false;
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
75
108
|
//#region src/semver.ts
|
|
76
109
|
/**
|
|
77
110
|
* Compare two semver version strings for sorting.
|
|
@@ -245,6 +278,37 @@ var CdnCache = class {
|
|
|
245
278
|
if (fs.readdirSync(pkgDir).length === 0) fs.rmdirSync(pkgDir);
|
|
246
279
|
} catch {}
|
|
247
280
|
}
|
|
281
|
+
async reconcileWithRegistry(packageName) {
|
|
282
|
+
const url = `${this.registryUrl}/${encodeURIComponent(packageName)}`;
|
|
283
|
+
let versions;
|
|
284
|
+
try {
|
|
285
|
+
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
286
|
+
if (!res.ok) return [];
|
|
287
|
+
const meta = await res.json();
|
|
288
|
+
versions = Object.keys(meta.versions ?? {});
|
|
289
|
+
} catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
if (versions.length === 0) return [];
|
|
293
|
+
return this.reconcileVersions(packageName, versions);
|
|
294
|
+
}
|
|
295
|
+
reconcileVersions(packageName, keepVersions) {
|
|
296
|
+
const pkgDir = path.join(this.cdnCachePath, packageName);
|
|
297
|
+
const keep = new Set(keepVersions);
|
|
298
|
+
const removed = [];
|
|
299
|
+
let entries;
|
|
300
|
+
try {
|
|
301
|
+
entries = fs.readdirSync(pkgDir, { withFileTypes: true });
|
|
302
|
+
} catch {
|
|
303
|
+
return removed;
|
|
304
|
+
}
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
if (!entry.isDirectory() || keep.has(entry.name)) continue;
|
|
307
|
+
this.invalidateVersion(packageName, entry.name);
|
|
308
|
+
removed.push(entry.name);
|
|
309
|
+
}
|
|
310
|
+
return removed;
|
|
311
|
+
}
|
|
248
312
|
/** Remove all cached version directories except the specified one. */
|
|
249
313
|
pruneOldVersions(packageName, keepVersion) {
|
|
250
314
|
const pkgDir = path.join(this.cdnCachePath, packageName);
|
|
@@ -340,17 +404,40 @@ function getLatestVersionDir(pkgDir) {
|
|
|
340
404
|
versions.sort(compareSemver);
|
|
341
405
|
return path.join(pkgDir, versions[versions.length - 1]);
|
|
342
406
|
}
|
|
343
|
-
function loadPackage(cdnCachePath, name, version) {
|
|
407
|
+
function loadPackage(cdnCachePath, name, version, storagePath) {
|
|
344
408
|
const pkgDir = path.join(cdnCachePath, name);
|
|
345
409
|
const manifestDir = (version ? path.join(pkgDir, version) : getLatestVersionDir(pkgDir)) ?? pkgDir;
|
|
346
410
|
const manifest = readManifest(manifestDir);
|
|
347
411
|
if (!manifest) return null;
|
|
412
|
+
const resolvedName = manifest.name || name;
|
|
413
|
+
const { distTags, versions } = readPackageMetadata(storagePath, resolvedName);
|
|
348
414
|
return {
|
|
349
|
-
name:
|
|
415
|
+
name: resolvedName,
|
|
350
416
|
path: `/-/cdn/${name}`,
|
|
351
417
|
manifest,
|
|
352
418
|
documentTypes: getDocumentTypesFromManifest(manifest),
|
|
353
|
-
version: readPackageJsonVersion(manifestDir)
|
|
419
|
+
version: readPackageJsonVersion(manifestDir),
|
|
420
|
+
distTags,
|
|
421
|
+
versions
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Project a full {@link PackageInfo} down to the trimmed shape the paginated
|
|
426
|
+
* package-listing UI renders per row. Version metadata and documentTypes are
|
|
427
|
+
* intentionally dropped — the client fetches them on demand.
|
|
428
|
+
*/
|
|
429
|
+
function toPackageListItem(pkg) {
|
|
430
|
+
const publisher = pkg.manifest?.publisher;
|
|
431
|
+
return {
|
|
432
|
+
name: pkg.name,
|
|
433
|
+
path: pkg.path,
|
|
434
|
+
version: pkg.version,
|
|
435
|
+
description: pkg.manifest?.description ?? void 0,
|
|
436
|
+
category: pkg.manifest?.category ?? void 0,
|
|
437
|
+
publisher: publisher ? {
|
|
438
|
+
name: publisher.name,
|
|
439
|
+
url: publisher.url
|
|
440
|
+
} : void 0
|
|
354
441
|
};
|
|
355
442
|
}
|
|
356
443
|
function getDocumentTypesFromManifest(manifest) {
|
|
@@ -479,6 +566,20 @@ function createWarmer(config, cdn) {
|
|
|
479
566
|
}
|
|
480
567
|
//#endregion
|
|
481
568
|
//#region src/middleware.ts
|
|
569
|
+
const DEFAULT_PAGE_SIZE = 30;
|
|
570
|
+
const MAX_PAGE_SIZE = 50;
|
|
571
|
+
/** Parse+clamp a `limit` query param to 1..MAX_PAGE_SIZE (default 30). */
|
|
572
|
+
function clampPageSize(raw) {
|
|
573
|
+
const n = Number.parseInt(String(raw), 10);
|
|
574
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_PAGE_SIZE;
|
|
575
|
+
return Math.min(n, MAX_PAGE_SIZE);
|
|
576
|
+
}
|
|
577
|
+
/** Parse a non-negative `offset` query param (default 0). */
|
|
578
|
+
function parseOffset(raw) {
|
|
579
|
+
const n = Number.parseInt(String(raw), 10);
|
|
580
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
581
|
+
return n;
|
|
582
|
+
}
|
|
482
583
|
const MIME_TYPES = {
|
|
483
584
|
".js": "application/javascript",
|
|
484
585
|
".mjs": "application/javascript",
|
|
@@ -492,6 +593,15 @@ const MIME_TYPES = {
|
|
|
492
593
|
function getContentType(filePath) {
|
|
493
594
|
return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
494
595
|
}
|
|
596
|
+
function publisherFromRequest(req) {
|
|
597
|
+
const name = req.remote_user?.name;
|
|
598
|
+
if (!name) return void 0;
|
|
599
|
+
if (name.startsWith("did:pkh:")) return {
|
|
600
|
+
address: (name.split(":").pop() ?? name).toLowerCase(),
|
|
601
|
+
did: name
|
|
602
|
+
};
|
|
603
|
+
return { address: name };
|
|
604
|
+
}
|
|
495
605
|
/**
|
|
496
606
|
* Resolve a package version. Exact versions skip the network call. Upstream
|
|
497
607
|
* errors fall back to the latest cached version; genuine not-found falls back
|
|
@@ -531,9 +641,23 @@ function etagMatches(header, etag) {
|
|
|
531
641
|
return tag === "*" || opaqueTag(tag) === target;
|
|
532
642
|
});
|
|
533
643
|
}
|
|
534
|
-
function createPowerhouseRouter(config, sse, webhooks) {
|
|
644
|
+
function createPowerhouseRouter(config, sse, webhooks, ownerStore) {
|
|
535
645
|
const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
|
|
536
646
|
const router = Router();
|
|
647
|
+
const withOwners = async (pkgs) => {
|
|
648
|
+
if (!ownerStore || pkgs.length === 0) return pkgs;
|
|
649
|
+
try {
|
|
650
|
+
await ownerStore.init();
|
|
651
|
+
const map = await ownerStore.getOwnersFor(pkgs.map((p) => p.name));
|
|
652
|
+
return pkgs.map((p) => p.name in map ? {
|
|
653
|
+
...p,
|
|
654
|
+
owners: map[p.name]
|
|
655
|
+
} : p);
|
|
656
|
+
} catch (err) {
|
|
657
|
+
console.error("[registry] owner lookup failed:", err);
|
|
658
|
+
return pkgs;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
537
661
|
router.use((_req, res, next) => {
|
|
538
662
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
539
663
|
next();
|
|
@@ -573,16 +697,32 @@ function createPowerhouseRouter(config, sse, webhooks) {
|
|
|
573
697
|
});
|
|
574
698
|
const warm = createWarmer(config, cdn);
|
|
575
699
|
warm();
|
|
576
|
-
router.get("/packages", (req, res) => {
|
|
700
|
+
router.get("/packages", async (req, res) => {
|
|
577
701
|
warm();
|
|
578
702
|
const packages = scanPackages(config.cdnCachePath, config.storagePath);
|
|
579
703
|
const documentType = req.query.documentType;
|
|
580
704
|
if (documentType) {
|
|
581
705
|
const filtered = packages.filter((pkg) => pkg.manifest?.documentModels?.some((m) => m.id === documentType));
|
|
582
|
-
res.json(filtered);
|
|
706
|
+
res.json(await withOwners(filtered));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
if (req.query.limit === void 0) {
|
|
710
|
+
res.json(await withOwners(packages));
|
|
583
711
|
return;
|
|
584
712
|
}
|
|
585
|
-
|
|
713
|
+
const limit = clampPageSize(req.query.limit);
|
|
714
|
+
const offset = parseOffset(req.query.offset);
|
|
715
|
+
const search = typeof req.query.search === "string" ? req.query.search.trim().toLowerCase() : "";
|
|
716
|
+
const sorted = (search ? packages.filter((pkg) => pkg.name.toLowerCase().includes(search)) : packages).slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
717
|
+
const total = sorted.length;
|
|
718
|
+
const items = sorted.slice(offset, offset + limit).map(toPackageListItem);
|
|
719
|
+
res.json({
|
|
720
|
+
items,
|
|
721
|
+
total,
|
|
722
|
+
limit,
|
|
723
|
+
offset,
|
|
724
|
+
hasMore: offset + limit < total
|
|
725
|
+
});
|
|
586
726
|
});
|
|
587
727
|
router.get("/packages/by-document-type", (req, res) => {
|
|
588
728
|
const documentType = req.query.type;
|
|
@@ -602,12 +742,12 @@ function createPowerhouseRouter(config, sse, webhooks) {
|
|
|
602
742
|
return;
|
|
603
743
|
}
|
|
604
744
|
const version = resolution.kind === "ok" ? resolution.version : void 0;
|
|
605
|
-
const pkg = loadPackage(config.cdnCachePath, name, version);
|
|
745
|
+
const pkg = loadPackage(config.cdnCachePath, name, version, config.storagePath);
|
|
606
746
|
if (!pkg) {
|
|
607
747
|
res.status(404).send("Package not found");
|
|
608
748
|
return;
|
|
609
749
|
}
|
|
610
|
-
res.json(pkg);
|
|
750
|
+
res.json((await withOwners([pkg]))[0]);
|
|
611
751
|
});
|
|
612
752
|
router.get("/-/cdn/*", async (req, res) => {
|
|
613
753
|
const fullPath = req.params[0];
|
|
@@ -693,9 +833,41 @@ function parseUnpublishRequest(reqPath) {
|
|
|
693
833
|
version
|
|
694
834
|
};
|
|
695
835
|
}
|
|
836
|
+
function parseManifestRewrite(reqPath) {
|
|
837
|
+
const revIdx = reqPath.indexOf("/-rev/");
|
|
838
|
+
if (revIdx <= 0) return null;
|
|
839
|
+
const beforeRev = reqPath.slice(1, revIdx);
|
|
840
|
+
if (beforeRev.includes("/-/")) return null;
|
|
841
|
+
return { packageName: decodeURIComponent(beforeRev) };
|
|
842
|
+
}
|
|
696
843
|
function createUnpublishHook(config, notifications) {
|
|
697
844
|
const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
|
|
845
|
+
const handleManifestRewrite = (req, res) => {
|
|
846
|
+
const rewrite = parseManifestRewrite(req.path);
|
|
847
|
+
if (!rewrite) return;
|
|
848
|
+
const originalEnd = res.end.bind(res);
|
|
849
|
+
res.end = function(chunk, encoding, cb) {
|
|
850
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
851
|
+
const publishedBy = publisherFromRequest(req);
|
|
852
|
+
cdn.reconcileWithRegistry(rewrite.packageName).then((removed) => {
|
|
853
|
+
for (const version of removed) notifications.notifyUnpublish({
|
|
854
|
+
packageName: rewrite.packageName,
|
|
855
|
+
version,
|
|
856
|
+
publishedBy
|
|
857
|
+
});
|
|
858
|
+
}).catch((err) => {
|
|
859
|
+
console.error(`[registry] CDN reconcile failed for ${rewrite.packageName}:`, err);
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
return originalEnd(chunk, encoding, cb);
|
|
863
|
+
};
|
|
864
|
+
};
|
|
698
865
|
return (req, res, next) => {
|
|
866
|
+
if (req.method === "PUT") {
|
|
867
|
+
handleManifestRewrite(req, res);
|
|
868
|
+
next();
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
699
871
|
if (req.method !== "DELETE") {
|
|
700
872
|
next();
|
|
701
873
|
return;
|
|
@@ -710,14 +882,10 @@ function createUnpublishHook(config, notifications) {
|
|
|
710
882
|
if (res.statusCode >= 200 && res.statusCode < 300) try {
|
|
711
883
|
if (parsed.version) cdn.invalidateVersion(parsed.packageName, parsed.version);
|
|
712
884
|
else cdn.invalidate(parsed.packageName);
|
|
713
|
-
const renownUser = req.renownUser;
|
|
714
885
|
notifications.notifyUnpublish({
|
|
715
886
|
packageName: parsed.packageName,
|
|
716
887
|
version: parsed.version,
|
|
717
|
-
publishedBy:
|
|
718
|
-
address: renownUser.address,
|
|
719
|
-
did: renownUser.did
|
|
720
|
-
} : void 0
|
|
888
|
+
publishedBy: publisherFromRequest(req)
|
|
721
889
|
});
|
|
722
890
|
} catch (err) {
|
|
723
891
|
console.error(`[registry] CDN purge failed for ${parsed.packageName}${parsed.version ? `@${parsed.version}` : ""}:`, err);
|
|
@@ -747,11 +915,7 @@ function createPublishHook(config, notifications) {
|
|
|
747
915
|
return originalEnd(chunk, encoding, cb);
|
|
748
916
|
}
|
|
749
917
|
if (versions.length > 1) console.warn(`[registry] Multiple versions published for ${packageName}: ${JSON.stringify(versions)}`);
|
|
750
|
-
const
|
|
751
|
-
const publishedBy = renownUser ? {
|
|
752
|
-
address: renownUser.address,
|
|
753
|
-
did: renownUser.did
|
|
754
|
-
} : void 0;
|
|
918
|
+
const publishedBy = publisherFromRequest(req);
|
|
755
919
|
cdn.extractTarball(packageName, version).then(() => {
|
|
756
920
|
notifications.notifyPublish({
|
|
757
921
|
packageName,
|
|
@@ -888,6 +1052,17 @@ var WebhookChannel = class {
|
|
|
888
1052
|
function buildVerdaccioConfig(config) {
|
|
889
1053
|
const htpasswdPath = path.join(config.storagePath, "htpasswd");
|
|
890
1054
|
const uplinkUrl = config.uplink ?? "https://registry.npmjs.org/";
|
|
1055
|
+
const usePgAuth = Boolean(config.databaseUrl || config.authStore);
|
|
1056
|
+
const storeToken = config.authStoreToken ?? (config.authStore ? stashAuthStore(config.authStore) : void 0);
|
|
1057
|
+
const pluginsDir = config.pluginsDir ?? path.join(path.dirname(fileURLToPath(import.meta.url)), "plugins");
|
|
1058
|
+
const auth = usePgAuth ? { "registry-auth": {
|
|
1059
|
+
...config.databaseUrl ? { databaseUrl: config.databaseUrl } : {},
|
|
1060
|
+
...storeToken ? { storeToken } : {},
|
|
1061
|
+
...config.renown ? {
|
|
1062
|
+
publicUrl: config.renown.publicUrl,
|
|
1063
|
+
...config.renown.renownUrl ? { renownUrl: config.renown.renownUrl } : {}
|
|
1064
|
+
} : {}
|
|
1065
|
+
} } : { htpasswd: { file: htpasswdPath } };
|
|
891
1066
|
const base = {
|
|
892
1067
|
storage: config.storagePath,
|
|
893
1068
|
self_path: "./",
|
|
@@ -896,7 +1071,8 @@ function buildVerdaccioConfig(config) {
|
|
|
896
1071
|
sign: { expiresIn: "90d" },
|
|
897
1072
|
verify: {}
|
|
898
1073
|
} } },
|
|
899
|
-
auth
|
|
1074
|
+
auth,
|
|
1075
|
+
...usePgAuth ? { plugins: pluginsDir } : {},
|
|
900
1076
|
uplinks: { npmjs: {
|
|
901
1077
|
url: uplinkUrl,
|
|
902
1078
|
maxage: config.uplinkMaxage ?? "2m",
|
|
@@ -965,18 +1141,21 @@ async function resolveDir(dir) {
|
|
|
965
1141
|
return found;
|
|
966
1142
|
}
|
|
967
1143
|
async function runRegistry(args) {
|
|
968
|
-
const { port, storageDir, cdnCacheDir, uplink, uplinkMaxage, webEnabled, webhooks, s3AccessKeyId, s3Bucket, s3Endpoint, s3ForcePathStyle, s3KeyPrefix, s3Region, s3SecretAccessKey, publicUrl, authRenown, verdaccioSecret: verdaccioSecretArg, localPackages } = args;
|
|
1144
|
+
const { port, storageDir, cdnCacheDir, uplink, uplinkMaxage, webEnabled, webhooks, s3AccessKeyId, s3Bucket, s3Endpoint, s3ForcePathStyle, s3KeyPrefix, s3Region, s3SecretAccessKey, publicUrl, authRenown, renownUrl, verdaccioSecret: verdaccioSecretArg, localPackages, databaseUrl, pluginsDir, authStore } = args;
|
|
969
1145
|
const storagePath = await resolveDir(storageDir);
|
|
970
1146
|
const cdnCachePath = await resolveDir(cdnCacheDir);
|
|
971
1147
|
const verdaccioSecret = verdaccioSecretArg ?? randomBytes(32).toString("hex");
|
|
972
1148
|
const renownEnabled = authRenown === true && Boolean(publicUrl);
|
|
973
1149
|
if (authRenown === true && !publicUrl) console.warn("[registry] auth-renown is enabled but --public-url / PH_REGISTRY_PUBLIC_URL is not set; Renown auth will be disabled.");
|
|
1150
|
+
if (renownEnabled && !databaseUrl && !authStore) console.warn("[registry] Renown auth requires a database (--database-url) for the auth plugin; renown will be inactive.");
|
|
974
1151
|
console.log({
|
|
975
1152
|
storagePath,
|
|
976
1153
|
cdnCachePath
|
|
977
1154
|
});
|
|
978
1155
|
const webhookConfigs = webhooks?.split(",").map((url) => url.trim()).filter(Boolean).map((endpoint) => ({ endpoint }));
|
|
979
1156
|
const localPackagePatterns = localPackages?.split(",").map((p) => p.trim()).filter(Boolean);
|
|
1157
|
+
const sharedAuthStore = authStore ?? (databaseUrl ? createPgStore(createPgPool(databaseUrl)) : void 0);
|
|
1158
|
+
const authStoreToken = sharedAuthStore ? stashAuthStore(sharedAuthStore) : void 0;
|
|
980
1159
|
const config = {
|
|
981
1160
|
port,
|
|
982
1161
|
storagePath,
|
|
@@ -986,7 +1165,10 @@ async function runRegistry(args) {
|
|
|
986
1165
|
webEnabled,
|
|
987
1166
|
verdaccioSecret,
|
|
988
1167
|
...localPackagePatterns?.length ? { localPackagePatterns } : {},
|
|
989
|
-
...renownEnabled && publicUrl ? { renown: {
|
|
1168
|
+
...renownEnabled && publicUrl ? { renown: {
|
|
1169
|
+
publicUrl,
|
|
1170
|
+
...renownUrl ? { renownUrl } : {}
|
|
1171
|
+
} } : {},
|
|
990
1172
|
...webhookConfigs?.length && { notify: { webhooks: webhookConfigs } },
|
|
991
1173
|
...s3Bucket && s3Endpoint && s3Region && { s3: {
|
|
992
1174
|
bucket: s3Bucket,
|
|
@@ -996,22 +1178,28 @@ async function runRegistry(args) {
|
|
|
996
1178
|
secretAccessKey: s3SecretAccessKey,
|
|
997
1179
|
keyPrefix: s3KeyPrefix,
|
|
998
1180
|
s3ForcePathStyle
|
|
999
|
-
} }
|
|
1181
|
+
} },
|
|
1182
|
+
...databaseUrl ? { databaseUrl } : {},
|
|
1183
|
+
...pluginsDir ? { pluginsDir } : {},
|
|
1184
|
+
...sharedAuthStore ? { authStore: sharedAuthStore } : {},
|
|
1185
|
+
...authStoreToken ? { authStoreToken } : {}
|
|
1000
1186
|
};
|
|
1187
|
+
if (config.databaseUrl || config.authStore) console.log("[registry] Postgres-backed auth plugin active (persistent accounts + package ownership)");
|
|
1001
1188
|
await mkdir(storagePath, { recursive: true });
|
|
1002
1189
|
await mkdir(cdnCachePath, { recursive: true });
|
|
1003
|
-
const
|
|
1190
|
+
const verdaccioServer = await runServer(buildVerdaccioConfig(config));
|
|
1191
|
+
if (authStoreToken && !wasStoreLoaded(authStoreToken)) {
|
|
1192
|
+
verdaccioServer.close();
|
|
1193
|
+
throw new Error("registry-auth plugin failed to load despite a configured database/auth store; refusing to start without auth and package-ownership enforcement.");
|
|
1194
|
+
}
|
|
1195
|
+
const verdaccioHandler = verdaccioServer.listeners("request")[0];
|
|
1004
1196
|
const app = express();
|
|
1005
1197
|
const sseChannel = new SSEChannel();
|
|
1006
1198
|
const webhookChannel = new WebhookChannel(config.storagePath, config.notify);
|
|
1007
1199
|
const notifications = new NotificationManager([sseChannel, webhookChannel]);
|
|
1008
1200
|
const staticDir = await findUp("static", { type: "directory" });
|
|
1009
1201
|
if (staticDir) app.use("/-/static", express.static(staticDir));
|
|
1010
|
-
app.use(createPowerhouseRouter(config, sseChannel, webhookChannel));
|
|
1011
|
-
if (config.renown) app.use(createRenownAuthMiddleware({
|
|
1012
|
-
publicUrl: config.renown.publicUrl,
|
|
1013
|
-
verdaccioSecret
|
|
1014
|
-
}));
|
|
1202
|
+
app.use(createPowerhouseRouter(config, sseChannel, webhookChannel, sharedAuthStore));
|
|
1015
1203
|
app.use(createPublishHook(config, notifications));
|
|
1016
1204
|
app.use(createUnpublishHook(config, notifications));
|
|
1017
1205
|
app.use((req, res) => verdaccioHandler(req, res));
|
|
@@ -1128,6 +1316,13 @@ const registryCommand = command({
|
|
|
1128
1316
|
defaultValue: () => process.env.PH_REGISTRY_AUTH_RENOWN === "true",
|
|
1129
1317
|
defaultValueIsSerializable: true
|
|
1130
1318
|
}),
|
|
1319
|
+
renownUrl: option({
|
|
1320
|
+
long: "renown-url",
|
|
1321
|
+
type: optional(string),
|
|
1322
|
+
description: "Renown service base URL for credential verification. Defaults to https://www.renown.id.",
|
|
1323
|
+
defaultValue: () => process.env.PH_REGISTRY_RENOWN_URL,
|
|
1324
|
+
defaultValueIsSerializable: true
|
|
1325
|
+
}),
|
|
1131
1326
|
verdaccioSecret: option({
|
|
1132
1327
|
long: "verdaccio-secret",
|
|
1133
1328
|
type: optional(string),
|
|
@@ -1141,10 +1336,23 @@ const registryCommand = command({
|
|
|
1141
1336
|
description: "Comma-separated globs (e.g. '@powerhousedao/*,document-model,ph-cmd') served locally only — no npmjs uplink proxy. Lets you re-publish a workspace package whose version already exists on npmjs without bumping.",
|
|
1142
1337
|
defaultValue: () => process.env.PH_REGISTRY_LOCAL_PACKAGES,
|
|
1143
1338
|
defaultValueIsSerializable: true
|
|
1339
|
+
}),
|
|
1340
|
+
databaseUrl: option({
|
|
1341
|
+
long: "database-url",
|
|
1342
|
+
type: optional(string),
|
|
1343
|
+
description: "Postgres connection string. When set, the registry uses the DB-backed auth plugin (persistent accounts + npm-style package ownership) instead of the built-in htpasswd.",
|
|
1344
|
+
defaultValue: () => process.env.PH_REGISTRY_DATABASE_URL ?? process.env.DATABASE_URL,
|
|
1345
|
+
defaultValueIsSerializable: true
|
|
1144
1346
|
})
|
|
1145
1347
|
},
|
|
1146
1348
|
handler: async (args) => {
|
|
1147
|
-
|
|
1349
|
+
const redact = (v) => v ? "[redacted]" : void 0;
|
|
1350
|
+
console.log({
|
|
1351
|
+
...args,
|
|
1352
|
+
databaseUrl: redact(args.databaseUrl),
|
|
1353
|
+
verdaccioSecret: redact(args.verdaccioSecret),
|
|
1354
|
+
s3SecretAccessKey: redact(args.s3SecretAccessKey)
|
|
1355
|
+
});
|
|
1148
1356
|
try {
|
|
1149
1357
|
await runRegistry(args);
|
|
1150
1358
|
} catch (error) {
|