@powerhousedao/registry 6.2.3-dev.30 → 6.2.3-dev.31

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/README.md CHANGED
@@ -32,6 +32,36 @@ If the package is not published locally, Verdaccio transparently proxies metadat
32
32
 
33
33
  Responses carry caching headers keyed on the request shape. Version-pinned requests (`<pkg>@1.2.3/...`) are served `Cache-Control: public, max-age=31536000, immutable`; moving requests (a dist-tag like `@dev`/`@latest`, or untagged) get `public, max-age=60, must-revalidate`. A version-derived weak `ETag` is sent, and a matching `If-None-Match` returns `304`. When the upstream metadata lookup fails (as opposed to a genuine not-found), the endpoint serves the latest cached version if present, otherwise responds `503` rather than a cacheable `404`.
34
34
 
35
+ ### Pieces
36
+
37
+ A reactor's workflow runtime reads pieces from these endpoints, ahead of the
38
+ Activepieces catalogue.
39
+
40
+ #### `GET /pieces`
41
+
42
+ The catalog of pieces that published packages ship, in the shape of
43
+ cloud.activepieces.com's list endpoint. `?suggestionType=ACTION_AND_TRIGGER`
44
+ adds suggested actions and triggers.
45
+
46
+ #### `GET /pieces/<name>` and `GET /pieces/<name>?version=<v>`
47
+
48
+ One piece's detail: the latest version, or the one named. An unknown version
49
+ returns `404` with `available`, the versions the registry has.
50
+
51
+ #### `GET /pieces/<name>/versions`
52
+
53
+ Every version of the piece: `[{ version, packageVersion, publishedAt }]`. A
54
+ piece's version is always the version of the package that ships it.
55
+
56
+ #### `GET /-/pieces/bundled/<file>.tgz`
57
+
58
+ The piece directory as an npm-shaped tarball, the file a reactor's piece worker
59
+ downloads. The file name is the piece name with `/` replaced by `-`, then
60
+ `-<version>`, as cdn.activepieces.com names its own. It is served immutable.
61
+
62
+ Piece names under `@activepieces/` are reserved: a package that claims one is
63
+ refused.
64
+
35
65
  ### Publish Notifications
36
66
 
37
67
  When a package is published, the registry can notify subscribers in real time via Server-Sent Events (SSE) and webhooks.
package/dist/cli.mjs CHANGED
@@ -108,27 +108,48 @@ function wasStoreLoaded(token) {
108
108
  }
109
109
  //#endregion
110
110
  //#region src/semver.ts
111
- /**
112
- * Compare two semver version strings for sorting.
113
- * Returns negative if a < b, positive if a > b, 0 if equal.
114
- *
115
- * Handles numeric component comparison (so "1.0.10" > "1.0.9")
116
- * and prerelease ordering (release > prerelease).
117
- */
111
+ function splitVersion(version) {
112
+ const withoutBuild = version.split("+", 1)[0];
113
+ const dash = withoutBuild.indexOf("-");
114
+ const core = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
115
+ const pre = dash === -1 ? "" : withoutBuild.slice(dash + 1);
116
+ return {
117
+ core: core.split(".").map(Number),
118
+ pre: pre ? pre.split(".") : []
119
+ };
120
+ }
121
+ const NUMERIC = /^\d+$/;
122
+ function comparePrerelease(a, b) {
123
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
124
+ if (i >= a.length) return -1;
125
+ if (i >= b.length) return 1;
126
+ const x = a[i];
127
+ const y = b[i];
128
+ const xNum = NUMERIC.test(x);
129
+ const yNum = NUMERIC.test(y);
130
+ if (xNum && yNum) {
131
+ const diff = Number(x) - Number(y);
132
+ if (diff !== 0) return diff;
133
+ continue;
134
+ }
135
+ if (xNum) return -1;
136
+ if (yNum) return 1;
137
+ if (x !== y) return x < y ? -1 : 1;
138
+ }
139
+ return 0;
140
+ }
141
+ /** Semver sort order: negative if a < b, positive if a > b, 0 if equal. */
118
142
  function compareSemver(a, b) {
119
- const [coreA, preA] = a.split("-", 2);
120
- const [coreB, preB] = b.split("-", 2);
121
- const partsA = coreA.split(".").map(Number);
122
- const partsB = coreB.split(".").map(Number);
123
- for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
124
- const na = partsA[i] ?? 0;
125
- const nb = partsB[i] ?? 0;
143
+ const va = splitVersion(a);
144
+ const vb = splitVersion(b);
145
+ for (let i = 0; i < Math.max(va.core.length, vb.core.length); i++) {
146
+ const na = va.core[i] ?? 0;
147
+ const nb = vb.core[i] ?? 0;
126
148
  if (na !== nb) return na - nb;
127
149
  }
128
- if (!preA && preB) return 1;
129
- if (preA && !preB) return -1;
130
- if (preA && preB) return preA < preB ? -1 : preA > preB ? 1 : 0;
131
- return 0;
150
+ if (va.pre.length === 0 && vb.pre.length > 0) return 1;
151
+ if (va.pre.length > 0 && vb.pre.length === 0) return -1;
152
+ return comparePrerelease(va.pre, vb.pre);
132
153
  }
133
154
  //#endregion
134
155
  //#region src/cdn.ts
@@ -167,12 +188,15 @@ function isExactVersion(tag) {
167
188
  return !!tag && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(tag);
168
189
  }
169
190
  var CdnCache = class {
170
- registryUrl;
171
191
  cdnCachePath;
172
192
  #extractionLocks = /* @__PURE__ */ new Map();
193
+ #registryUrl;
173
194
  constructor(registryUrl, cdnCachePath) {
174
- this.registryUrl = registryUrl;
175
195
  this.cdnCachePath = cdnCachePath;
196
+ this.#registryUrl = registryUrl;
197
+ }
198
+ get registryUrl() {
199
+ return typeof this.#registryUrl === "function" ? this.#registryUrl() : this.#registryUrl;
176
200
  }
177
201
  async getFileByVersion(packageName, version, filePath) {
178
202
  const versionDir = path.join(this.cdnCachePath, packageName, version);
@@ -358,9 +382,15 @@ function readPackageMetadata(storagePath, packageName) {
358
382
  const distTags = parsed["dist-tags"];
359
383
  const versions = (parsed.versions ? Object.keys(parsed.versions) : []).slice().sort(compareSemver);
360
384
  const locallyPublished = !!parsed._attachments && Object.keys(parsed._attachments).length > 0;
385
+ const time = {};
386
+ for (const version of versions) {
387
+ const at = parsed.time?.[version];
388
+ if (typeof at === "string") time[version] = at;
389
+ }
361
390
  return {
362
391
  distTags: distTags && Object.keys(distTags).length > 0 ? distTags : void 0,
363
392
  versions: versions.length > 0 ? versions : void 0,
393
+ time,
364
394
  locallyPublished
365
395
  };
366
396
  } catch {
@@ -396,16 +426,19 @@ function readPackageJsonVersion(dir) {
396
426
  return;
397
427
  }
398
428
  }
399
- function getLatestVersionDir(pkgDir) {
429
+ /** Extracted version directories of a package, oldest first. */
430
+ function listVersionDirs(pkgDir) {
400
431
  let entries;
401
432
  try {
402
433
  entries = fs.readdirSync(pkgDir, { withFileTypes: true });
403
434
  } catch {
404
- return null;
435
+ return [];
405
436
  }
406
- const versions = entries.filter((e) => e.isDirectory()).map((e) => e.name);
437
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort(compareSemver);
438
+ }
439
+ function getLatestVersionDir(pkgDir) {
440
+ const versions = listVersionDirs(pkgDir);
407
441
  if (versions.length === 0) return null;
408
- versions.sort(compareSemver);
409
442
  return path.join(pkgDir, versions[versions.length - 1]);
410
443
  }
411
444
  function loadPackage(cdnCachePath, name, version, storagePath) {
@@ -454,18 +487,23 @@ function getDocumentTypesFromManifest(manifest) {
454
487
  }
455
488
  function readPackageDir(cdnCachePath, dirName, storagePath) {
456
489
  const pkgDir = path.join(cdnCachePath, dirName);
457
- const manifestDir = getLatestVersionDir(pkgDir) ?? pkgDir;
490
+ const versionDirs = listVersionDirs(pkgDir);
491
+ const latest = versionDirs.at(-1);
492
+ const manifestDir = latest ? path.join(pkgDir, latest) : pkgDir;
458
493
  const manifest = readManifest(manifestDir);
459
494
  const name = manifest?.name || dirName;
460
- const { distTags, versions, locallyPublished } = readPackageMetadata(storagePath, name);
495
+ const { distTags, versions, time, locallyPublished } = readPackageMetadata(storagePath, name);
461
496
  return {
462
497
  dirName,
498
+ packageDir: pkgDir,
463
499
  manifestDir,
500
+ versionDirs,
464
501
  manifest,
465
502
  name,
466
503
  version: readPackageJsonVersion(manifestDir),
467
504
  distTags,
468
505
  versions,
506
+ time,
469
507
  locallyPublished
470
508
  };
471
509
  }
@@ -522,12 +560,17 @@ function findPackagesByDocumentType(packagesDir, documentType) {
522
560
  //#endregion
523
561
  //#region src/pieces.ts
524
562
  const gunzip$1 = promisify(gunzip);
563
+ /** Piece names in this scope belong to Activepieces; no package may claim one. */
564
+ const RESERVED_PIECE_SCOPE = "@activepieces/";
525
565
  const INDEX_TTL_MS = 3e4;
526
566
  const indexes = /* @__PURE__ */ new Map();
567
+ const versionPieces = /* @__PURE__ */ new Map();
527
568
  const descriptors = /* @__PURE__ */ new Map();
569
+ const materializing = /* @__PURE__ */ new Map();
528
570
  /** Drop the cached index and descriptors — a publish or unpublish moved them. */
529
571
  function invalidatePieceIndex() {
530
572
  indexes.clear();
573
+ versionPieces.clear();
531
574
  descriptors.clear();
532
575
  }
533
576
  function indexKey(cdnCachePath, storagePath) {
@@ -536,34 +579,77 @@ function indexKey(cdnCachePath, storagePath) {
536
579
  function piecesOf(manifest) {
537
580
  return manifest?.pieces ?? [];
538
581
  }
539
- function toIndexEntry(piece, packageName, packageVersion, packageDir) {
540
- const { id, version, bundle } = piece;
541
- if (!id || !version || !bundle) return null;
582
+ function piecesAt(versionDir) {
583
+ const hit = versionPieces.get(versionDir);
584
+ if (hit) return hit;
585
+ if (!fs.existsSync(path.join(versionDir, "package.json"))) return [];
586
+ const manifest = readManifest(versionDir);
587
+ if (!manifest) return [];
588
+ const pieces = piecesOf(manifest);
589
+ versionPieces.set(versionDir, pieces);
590
+ return pieces;
591
+ }
592
+ function toIndexEntry(piece, packageName, packageVersion, packageDir, publishedAt) {
593
+ const { id, bundle } = piece;
594
+ if (!id || !bundle) return null;
542
595
  return {
543
596
  name: id,
544
597
  displayName: piece.name || id,
545
598
  ...piece.description ? { description: piece.description } : {},
546
- version,
599
+ version: packageVersion,
547
600
  packageName,
548
601
  packageVersion,
549
602
  packageDir,
550
603
  bundle,
551
- descriptor: piece.descriptor ?? `${bundle}/descriptor.json`
604
+ descriptor: piece.descriptor ?? `${bundle}/descriptor.json`,
605
+ ...publishedAt ? { publishedAt } : {}
552
606
  };
553
607
  }
608
+ function indexVersion(index, pkg, version) {
609
+ const versionDir = path.join(pkg.dir, version);
610
+ const held = [];
611
+ for (const piece of piecesAt(versionDir)) {
612
+ const entry = toIndexEntry(piece, pkg.name, version, versionDir, pkg.publishedAt[version]);
613
+ if (!entry) continue;
614
+ let line = index.get(entry.name);
615
+ if (line && line.packageName !== pkg.name) {
616
+ console.warn(`[registry] piece "${entry.name}" is claimed by both ${line.packageName} and ${pkg.name}; keeping ${line.packageName}`);
617
+ continue;
618
+ }
619
+ if (!line) {
620
+ line = {
621
+ packageName: pkg.name,
622
+ versions: /* @__PURE__ */ new Map(),
623
+ pending: /* @__PURE__ */ new Set(),
624
+ publishedAt: pkg.publishedAt
625
+ };
626
+ index.set(entry.name, line);
627
+ }
628
+ line.versions.set(version, entry);
629
+ held.push(entry.name);
630
+ }
631
+ return held;
632
+ }
554
633
  function buildPieceIndex(cdnCachePath, storagePath) {
555
634
  const index = /* @__PURE__ */ new Map();
556
- for (const pkg of scanPackageDirs(cdnCachePath, storagePath)) {
557
- if (pkg.locallyPublished === false) continue;
558
- for (const piece of piecesOf(pkg.manifest)) {
559
- const entry = toIndexEntry(piece, pkg.name, pkg.version, pkg.manifestDir);
560
- if (!entry) continue;
561
- const claimed = index.get(entry.name);
562
- if (claimed && claimed.packageName !== entry.packageName) {
563
- console.warn(`[registry] piece "${entry.name}" is claimed by both ${claimed.packageName} and ${entry.packageName}; keeping ${claimed.packageName}`);
564
- continue;
565
- }
566
- index.set(entry.name, entry);
635
+ for (const scanned of scanPackageDirs(cdnCachePath, storagePath)) {
636
+ if (scanned.locallyPublished === false) continue;
637
+ const pkg = {
638
+ name: scanned.name,
639
+ dir: scanned.packageDir,
640
+ publishedAt: scanned.time ?? {}
641
+ };
642
+ const names = /* @__PURE__ */ new Set();
643
+ const extracted = /* @__PURE__ */ new Set();
644
+ for (const version of scanned.versionDirs) {
645
+ const held = indexVersion(index, pkg, version);
646
+ if (fs.existsSync(path.join(pkg.dir, version, "package.json"))) extracted.add(version);
647
+ for (const name of held) names.add(name);
648
+ }
649
+ const pending = (scanned.versions ?? []).filter((v) => !extracted.has(v));
650
+ for (const name of names) {
651
+ const line = index.get(name);
652
+ for (const version of pending) line?.pending.add(version);
567
653
  }
568
654
  }
569
655
  return index;
@@ -580,8 +666,77 @@ function pieceIndex(cdnCachePath, storagePath) {
580
666
  });
581
667
  return index;
582
668
  }
669
+ /** The highest extracted version of a piece. */
670
+ function latestOf(line) {
671
+ let latest;
672
+ for (const entry of line.versions.values()) if (!latest || compareSemver(entry.version, latest.version) > 0) latest = entry;
673
+ return latest;
674
+ }
675
+ /** The latest version of a piece. */
583
676
  function findPiece(cdnCachePath, name, storagePath) {
584
- return pieceIndex(cdnCachePath, storagePath).get(name);
677
+ const line = pieceIndex(cdnCachePath, storagePath).get(name);
678
+ return line ? latestOf(line) : void 0;
679
+ }
680
+ async function materialize(extractor, cdnCachePath, index, line, version) {
681
+ const key = `${line.packageName}@${version}`;
682
+ const running = materializing.get(key);
683
+ if (running) return running;
684
+ const started = (async () => {
685
+ try {
686
+ await extractor.getFileByVersion(line.packageName, version, "package.json");
687
+ } catch (error) {
688
+ console.error(`[registry] failed to extract ${key}:`, error);
689
+ return;
690
+ }
691
+ const dir = path.join(cdnCachePath, line.packageName);
692
+ if (!fs.existsSync(path.join(dir, version, "package.json"))) return;
693
+ indexVersion(index, {
694
+ name: line.packageName,
695
+ dir,
696
+ publishedAt: line.publishedAt
697
+ }, version);
698
+ for (const other of index.values()) if (other.packageName === line.packageName) other.pending.delete(version);
699
+ })().finally(() => materializing.delete(key));
700
+ materializing.set(key, started);
701
+ return started;
702
+ }
703
+ async function materializeAll(extractor, cdnCachePath, index, line) {
704
+ await Promise.all([...line.pending].map((version) => materialize(extractor, cdnCachePath, index, line, version)));
705
+ }
706
+ function newestFirst(line) {
707
+ return [...line.versions.values()].sort((a, b) => compareSemver(b.version, a.version));
708
+ }
709
+ /** Every version of a piece, newest first; undefined for an unknown name. */
710
+ async function pieceVersions(extractor, cdnCachePath, name, storagePath) {
711
+ const index = pieceIndex(cdnCachePath, storagePath);
712
+ const line = index.get(name);
713
+ if (!line) return void 0;
714
+ await materializeAll(extractor, cdnCachePath, index, line);
715
+ return newestFirst(line);
716
+ }
717
+ function versionSummary(entry) {
718
+ return {
719
+ version: entry.version,
720
+ packageVersion: entry.packageVersion,
721
+ publishedAt: entry.publishedAt ?? null
722
+ };
723
+ }
724
+ /** One exact version of a piece, extracting its package version if needed. */
725
+ async function findPieceVersion(extractor, cdnCachePath, name, version, storagePath) {
726
+ const index = pieceIndex(cdnCachePath, storagePath);
727
+ const line = index.get(name);
728
+ if (!line) return { kind: "unknown-piece" };
729
+ if (!line.versions.has(version) && line.pending.has(version)) await materialize(extractor, cdnCachePath, index, line, version);
730
+ const entry = line.versions.get(version);
731
+ if (entry) return {
732
+ kind: "found",
733
+ entry
734
+ };
735
+ await materializeAll(extractor, cdnCachePath, index, line);
736
+ return {
737
+ kind: "unknown-version",
738
+ available: newestFirst(line).map((e) => e.version)
739
+ };
585
740
  }
586
741
  function insidePackage(entry, relative) {
587
742
  const root = path.resolve(entry.packageDir);
@@ -638,19 +793,20 @@ function catalogEntry(entry, descriptor, suggestions) {
638
793
  } : {}
639
794
  };
640
795
  }
641
- /** The catalog, sorted by display name the way the editor renders it. */
796
+ /** The catalog of latest versions, sorted by display name. */
642
797
  function pieceCatalog(cdnCachePath, options = {}) {
643
798
  const entries = [];
644
- for (const entry of pieceIndex(cdnCachePath, options.storagePath).values()) {
645
- const descriptor = readDescriptor(entry);
646
- if (!descriptor) continue;
799
+ for (const line of pieceIndex(cdnCachePath, options.storagePath).values()) {
800
+ const entry = latestOf(line);
801
+ const descriptor = entry ? readDescriptor(entry) : null;
802
+ if (!entry || !descriptor) continue;
647
803
  entries.push(catalogEntry(entry, descriptor, options.suggestions === true));
648
804
  }
649
805
  return entries.sort((a, b) => a.displayName.localeCompare(b.displayName));
650
806
  }
651
- /** The CDN path of a file inside the package that ships the piece. */
807
+ /** The CDN path of a file inside the package version that ships the piece. */
652
808
  function cdnPath(entry, relative) {
653
- return `/-/cdn/${entry.packageVersion ? `${entry.packageName}@${entry.packageVersion}` : entry.packageName}/${relative}`;
809
+ return `/-/cdn/${entry.packageName}@${entry.packageVersion}/${relative}`;
654
810
  }
655
811
  /** The tarball filename cdn.activepieces.com would use for the same piece. */
656
812
  function pieceTarballName(name, version) {
@@ -659,7 +815,7 @@ function pieceTarballName(name, version) {
659
815
  function pieceTarballPath(entry) {
660
816
  return `/-/pieces/bundled/${pieceTarballName(entry.name, entry.version)}`;
661
817
  }
662
- /** One piece: what provides it, and where its descriptor and bundle are. */
818
+ /** One piece version: what provides it, and where its descriptor and bundle are. */
663
819
  function pieceDetail(entry, origin) {
664
820
  const descriptor = readDescriptor(entry);
665
821
  if (!descriptor) return null;
@@ -673,8 +829,17 @@ function pieceDetail(entry, origin) {
673
829
  bundleUrl: `${origin}${pieceTarballPath(entry)}`
674
830
  };
675
831
  }
676
- function findPieceByTarball(cdnCachePath, filename, storagePath) {
677
- for (const entry of pieceIndex(cdnCachePath, storagePath).values()) if (pieceTarballName(entry.name, entry.version) === filename) return entry;
832
+ async function findPieceByTarball(extractor, cdnCachePath, filename, storagePath) {
833
+ if (!filename.endsWith(".tgz")) return void 0;
834
+ const stem = filename.slice(0, -4);
835
+ for (const name of pieceIndex(cdnCachePath, storagePath).keys()) {
836
+ const prefix = `${name.replace("/", "-")}-`;
837
+ if (!stem.startsWith(prefix)) continue;
838
+ const version = stem.slice(prefix.length);
839
+ if (!isExactVersion(version)) continue;
840
+ const found = await findPieceVersion(extractor, cdnCachePath, name, version, storagePath);
841
+ if (found.kind === "found") return found.entry;
842
+ }
678
843
  }
679
844
  function listFiles(dir, prefix = "") {
680
845
  const files = [];
@@ -776,7 +941,7 @@ function createWarmer(config, cdn) {
776
941
  let warmInFlight = false;
777
942
  let lastWarmAt = 0;
778
943
  return async function warm() {
779
- if (warmInFlight) return;
944
+ if (warmInFlight || config.port === 0) return;
780
945
  if (Date.now() - lastWarmAt < WARM_INTERVAL_MS) return;
781
946
  warmInFlight = true;
782
947
  try {
@@ -840,6 +1005,15 @@ const MIME_TYPES = {
840
1005
  ".html": "text/html",
841
1006
  ".svg": "image/svg+xml"
842
1007
  };
1008
+ function localUrl(config) {
1009
+ return () => `http://localhost:${config.port}`;
1010
+ }
1011
+ function versionsRouteName(raw) {
1012
+ if (!raw.endsWith("/versions")) return null;
1013
+ const name = raw.slice(0, -9);
1014
+ const slashes = name.split("/").length - 1;
1015
+ return (name.startsWith("@") ? slashes === 1 : slashes === 0) && name !== "" ? name : null;
1016
+ }
843
1017
  function getContentType(filePath) {
844
1018
  const ext = path.extname(filePath).toLowerCase();
845
1019
  return MIME_TYPES[ext] ?? "application/octet-stream";
@@ -896,7 +1070,7 @@ function etagMatches(header, etag) {
896
1070
  });
897
1071
  }
898
1072
  function createPowerhouseRouter(config, sse, webhooks, ownerStore) {
899
- const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
1073
+ const cdn = new CdnCache(localUrl(config), config.cdnCachePath);
900
1074
  const router = Router();
901
1075
  const withOwners = async (pkgs) => {
902
1076
  if (!ownerStore || pkgs.length === 0) return pkgs;
@@ -1010,21 +1184,54 @@ function createPowerhouseRouter(config, sse, webhooks, ownerStore) {
1010
1184
  suggestions: req.query.suggestionType === "ACTION_AND_TRIGGER"
1011
1185
  }));
1012
1186
  });
1013
- router.get("/pieces/*", (req, res) => {
1187
+ router.get("/pieces/*", async (req, res) => {
1014
1188
  warm();
1015
- const name = req.params[0];
1016
- const entry = findPiece(config.cdnCachePath, name, config.storagePath);
1017
- const detail = entry ? pieceDetail(entry, originOf(req)) : null;
1018
- if (!detail) {
1189
+ const raw = req.params[0];
1190
+ const versionsOf = versionsRouteName(raw);
1191
+ if (versionsOf) {
1192
+ const versions = await pieceVersions(cdn, config.cdnCachePath, versionsOf, config.storagePath);
1193
+ if (!versions) {
1194
+ res.status(404).json({ error: `Piece not found: ${versionsOf}` });
1195
+ return;
1196
+ }
1197
+ res.json(versions.map(versionSummary));
1198
+ return;
1199
+ }
1200
+ const name = raw;
1201
+ const version = typeof req.query.version === "string" ? req.query.version : void 0;
1202
+ if (version === void 0) {
1203
+ const entry = findPiece(config.cdnCachePath, name, config.storagePath);
1204
+ const detail = entry ? pieceDetail(entry, originOf(req)) : null;
1205
+ if (!detail) {
1206
+ res.status(404).json({ error: `Piece not found: ${name}` });
1207
+ return;
1208
+ }
1209
+ res.json(detail);
1210
+ return;
1211
+ }
1212
+ const found = await findPieceVersion(cdn, config.cdnCachePath, name, version, config.storagePath);
1213
+ if (found.kind === "unknown-piece") {
1019
1214
  res.status(404).json({ error: `Piece not found: ${name}` });
1020
1215
  return;
1021
1216
  }
1217
+ if (found.kind === "unknown-version") {
1218
+ res.status(404).json({
1219
+ error: `Piece version not found: ${name}@${version}`,
1220
+ available: found.available
1221
+ });
1222
+ return;
1223
+ }
1224
+ const detail = pieceDetail(found.entry, originOf(req));
1225
+ if (!detail) {
1226
+ res.status(404).json({ error: `Piece not found: ${name}@${version}` });
1227
+ return;
1228
+ }
1022
1229
  res.json(detail);
1023
1230
  });
1024
1231
  router.get("/-/pieces/bundled/*", async (req, res) => {
1025
1232
  warm();
1026
1233
  const filename = req.params[0];
1027
- const entry = findPieceByTarball(config.cdnCachePath, filename, config.storagePath);
1234
+ const entry = await findPieceByTarball(cdn, config.cdnCachePath, filename, config.storagePath);
1028
1235
  const file = entry ? await pieceTarball(entry) : null;
1029
1236
  if (!entry || !file) {
1030
1237
  res.status(404).json({ error: `Piece bundle not found: ${filename}` });
@@ -1136,7 +1343,7 @@ function parseManifestRewrite(reqPath) {
1136
1343
  return { packageName: decodeURIComponent(beforeRev) };
1137
1344
  }
1138
1345
  function createUnpublishHook(config, notifications) {
1139
- const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
1346
+ const cdn = new CdnCache(localUrl(config), config.cdnCachePath);
1140
1347
  const handleManifestRewrite = (req, res) => {
1141
1348
  const rewrite = parseManifestRewrite(req.path);
1142
1349
  if (!rewrite) return;
@@ -1193,7 +1400,7 @@ function createUnpublishHook(config, notifications) {
1193
1400
  };
1194
1401
  }
1195
1402
  function createPublishHook(config, notifications) {
1196
- const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
1403
+ const cdn = new CdnCache(localUrl(config), config.cdnCachePath);
1197
1404
  const parseBody = express.json({
1198
1405
  strict: false,
1199
1406
  limit: config.maxBodySize ?? "300mb"
@@ -1269,11 +1476,14 @@ function publishedTarball(body) {
1269
1476
  return null;
1270
1477
  }
1271
1478
  async function pieceClaimConflict(config, packageName, body) {
1272
- const index = pieceIndex(config.cdnCachePath, config.storagePath);
1273
- if (index.size === 0) return null;
1274
1479
  const tarball = publishedTarball(body);
1275
1480
  if (!tarball) return null;
1276
- for (const name of await pieceNamesInTarball(tarball)) {
1481
+ const names = await pieceNamesInTarball(tarball);
1482
+ if (names.length === 0) return null;
1483
+ const reserved = names.filter((name) => name.startsWith(RESERVED_PIECE_SCOPE));
1484
+ if (reserved.length > 0) return `Piece names in the ${RESERVED_PIECE_SCOPE} scope belong to Activepieces; ${packageName} cannot claim ${reserved.map((n) => `"${n}"`).join(", ")}. Rename the piece into your own scope.`;
1485
+ const index = pieceIndex(config.cdnCachePath, config.storagePath);
1486
+ for (const name of names) {
1277
1487
  const owner = index.get(name);
1278
1488
  if (owner && owner.packageName !== packageName) return `Piece "${name}" is already published by ${owner.packageName}; ${packageName} cannot claim it.`;
1279
1489
  }
@@ -1553,7 +1763,9 @@ async function runRegistry(args) {
1553
1763
  app.use(createPublishHook(config, notifications));
1554
1764
  app.use(createUnpublishHook(config, notifications));
1555
1765
  app.use((req, res) => verdaccioHandler(req, res));
1556
- return app.listen(port, () => {
1766
+ const server = app.listen(port, () => {
1767
+ config.port = server.address().port;
1768
+ const port = config.port;
1557
1769
  console.log(`Powerhouse Registry running on http://localhost:${port}`);
1558
1770
  console.log(` CDN: http://localhost:${port}/-/cdn/`);
1559
1771
  console.log(` Packages: http://localhost:${port}/packages`);
@@ -1563,6 +1775,7 @@ async function runRegistry(args) {
1563
1775
  if (config.s3) console.log(` S3: ${config.s3.endpoint}/${config.s3.bucket}`);
1564
1776
  if (config.renown) console.log(` Renown auth: ${config.renown.publicUrl}`);
1565
1777
  });
1778
+ return server;
1566
1779
  }
1567
1780
  //#endregion
1568
1781
  //#region cli.ts