@powerhousedao/registry 6.2.3-dev.9 → 6.2.3

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.mjs CHANGED
@@ -9,8 +9,10 @@ import { Pool } from "pg";
9
9
  import fs from "node:fs";
10
10
  import { pipeline } from "node:stream/promises";
11
11
  import { Readable } from "node:stream";
12
- import { extract } from "tar";
12
+ import { create, extract, list } from "tar";
13
13
  import { slimManifest } from "@powerhousedao/shared/registry";
14
+ import { promisify } from "node:util";
15
+ import { gunzip } from "node:zlib";
14
16
  import { fileURLToPath } from "node:url";
15
17
  //#endregion
16
18
  //#region src/auth/pg-store.ts
@@ -106,27 +108,48 @@ function wasStoreLoaded(token) {
106
108
  }
107
109
  //#endregion
108
110
  //#region src/semver.ts
109
- /**
110
- * Compare two semver version strings for sorting.
111
- * Returns negative if a < b, positive if a > b, 0 if equal.
112
- *
113
- * Handles numeric component comparison (so "1.0.10" > "1.0.9")
114
- * and prerelease ordering (release > prerelease).
115
- */
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. */
116
142
  function compareSemver(a, b) {
117
- const [coreA, preA] = a.split("-", 2);
118
- const [coreB, preB] = b.split("-", 2);
119
- const partsA = coreA.split(".").map(Number);
120
- const partsB = coreB.split(".").map(Number);
121
- for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
122
- const na = partsA[i] ?? 0;
123
- 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;
124
148
  if (na !== nb) return na - nb;
125
149
  }
126
- if (!preA && preB) return 1;
127
- if (preA && !preB) return -1;
128
- if (preA && preB) return preA < preB ? -1 : preA > preB ? 1 : 0;
129
- 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);
130
153
  }
131
154
  //#endregion
132
155
  //#region src/cdn.ts
@@ -165,10 +188,15 @@ function isExactVersion(tag) {
165
188
  return !!tag && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(tag);
166
189
  }
167
190
  var CdnCache = class {
191
+ cdnCachePath;
168
192
  #extractionLocks = /* @__PURE__ */ new Map();
193
+ #registryUrl;
169
194
  constructor(registryUrl, cdnCachePath) {
170
- this.registryUrl = registryUrl;
171
195
  this.cdnCachePath = cdnCachePath;
196
+ this.#registryUrl = registryUrl;
197
+ }
198
+ get registryUrl() {
199
+ return typeof this.#registryUrl === "function" ? this.#registryUrl() : this.#registryUrl;
172
200
  }
173
201
  async getFileByVersion(packageName, version, filePath) {
174
202
  const versionDir = path.join(this.cdnCachePath, packageName, version);
@@ -354,9 +382,15 @@ function readPackageMetadata(storagePath, packageName) {
354
382
  const distTags = parsed["dist-tags"];
355
383
  const versions = (parsed.versions ? Object.keys(parsed.versions) : []).slice().sort(compareSemver);
356
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
+ }
357
390
  return {
358
391
  distTags: distTags && Object.keys(distTags).length > 0 ? distTags : void 0,
359
392
  versions: versions.length > 0 ? versions : void 0,
393
+ time,
360
394
  locallyPublished
361
395
  };
362
396
  } catch {
@@ -392,16 +426,19 @@ function readPackageJsonVersion(dir) {
392
426
  return;
393
427
  }
394
428
  }
395
- function getLatestVersionDir(pkgDir) {
429
+ /** Extracted version directories of a package, oldest first. */
430
+ function listVersionDirs(pkgDir) {
396
431
  let entries;
397
432
  try {
398
433
  entries = fs.readdirSync(pkgDir, { withFileTypes: true });
399
434
  } catch {
400
- return null;
435
+ return [];
401
436
  }
402
- 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);
403
441
  if (versions.length === 0) return null;
404
- versions.sort(compareSemver);
405
442
  return path.join(pkgDir, versions[versions.length - 1]);
406
443
  }
407
444
  function loadPackage(cdnCachePath, name, version, storagePath) {
@@ -448,63 +485,70 @@ function getDocumentTypesFromManifest(manifest) {
448
485
  documentTypes.push(...(documentModels ?? []).map((dm) => dm.id), ...(editors ?? []).flatMap((e) => e.documentTypes).filter((dt) => dt !== void 0), ...(subgraphs ?? []).flatMap((e) => e.documentTypes).filter((dt) => dt !== void 0));
449
486
  return documentTypes;
450
487
  }
451
- function scanPackages(cdnCachePath, storagePath) {
488
+ function readPackageDir(cdnCachePath, dirName, storagePath) {
489
+ const pkgDir = path.join(cdnCachePath, dirName);
490
+ const versionDirs = listVersionDirs(pkgDir);
491
+ const latest = versionDirs.at(-1);
492
+ const manifestDir = latest ? path.join(pkgDir, latest) : pkgDir;
493
+ const manifest = readManifest(manifestDir);
494
+ const name = manifest?.name || dirName;
495
+ const { distTags, versions, time, locallyPublished } = readPackageMetadata(storagePath, name);
496
+ return {
497
+ dirName,
498
+ packageDir: pkgDir,
499
+ manifestDir,
500
+ versionDirs,
501
+ manifest,
502
+ name,
503
+ version: readPackageJsonVersion(manifestDir),
504
+ distTags,
505
+ versions,
506
+ time,
507
+ locallyPublished
508
+ };
509
+ }
510
+ function* scanPackageDirs(cdnCachePath, storagePath) {
452
511
  const absDir = path.resolve(cdnCachePath);
453
- const packages = [];
454
512
  let entries;
455
513
  try {
456
514
  entries = fs.readdirSync(absDir, { withFileTypes: true });
457
515
  } catch {
458
- return packages;
516
+ return;
459
517
  }
460
518
  for (const entry of entries) {
461
519
  if (!entry.isDirectory()) continue;
462
- if (entry.name.startsWith("@")) {
463
- const scopeDir = path.join(absDir, entry.name);
464
- let scopedEntries;
465
- try {
466
- scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
467
- } catch (error) {
468
- console.log(error);
469
- continue;
470
- }
471
- for (const scopedEntry of scopedEntries) {
472
- if (!scopedEntry.isDirectory()) continue;
473
- const dirName = `${entry.name}/${scopedEntry.name}`;
474
- const pkgDir = path.join(scopeDir, scopedEntry.name);
475
- const manifestDir = getLatestVersionDir(pkgDir) ?? pkgDir;
476
- const manifest = readManifest(manifestDir);
477
- const name = manifest?.name || dirName;
478
- const { distTags, versions, locallyPublished } = readPackageMetadata(storagePath, name);
479
- if (locallyPublished === false) continue;
480
- packages.push({
481
- name,
482
- path: `/-/cdn/${dirName}`,
483
- manifest,
484
- documentTypes: getDocumentTypesFromManifest(manifest),
485
- version: readPackageJsonVersion(manifestDir),
486
- distTags,
487
- versions
488
- });
489
- }
490
- } else {
491
- const pkgDir = path.join(absDir, entry.name);
492
- const manifestDir = getLatestVersionDir(pkgDir) ?? pkgDir;
493
- const manifest = readManifest(manifestDir);
494
- const name = manifest?.name || entry.name;
495
- const { distTags, versions, locallyPublished } = readPackageMetadata(storagePath, name);
496
- if (locallyPublished === false) continue;
497
- packages.push({
498
- name,
499
- path: `/-/cdn/${entry.name}`,
500
- manifest,
501
- documentTypes: getDocumentTypesFromManifest(manifest),
502
- version: readPackageJsonVersion(manifestDir),
503
- distTags,
504
- versions
505
- });
520
+ if (!entry.name.startsWith("@")) {
521
+ yield readPackageDir(absDir, entry.name, storagePath);
522
+ continue;
523
+ }
524
+ const scopeDir = path.join(absDir, entry.name);
525
+ let scopedEntries;
526
+ try {
527
+ scopedEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
528
+ } catch (error) {
529
+ console.log(error);
530
+ continue;
531
+ }
532
+ for (const scopedEntry of scopedEntries) {
533
+ if (!scopedEntry.isDirectory()) continue;
534
+ yield readPackageDir(absDir, `${entry.name}/${scopedEntry.name}`, storagePath);
506
535
  }
507
536
  }
537
+ }
538
+ function scanPackages(cdnCachePath, storagePath) {
539
+ const packages = [];
540
+ for (const pkg of scanPackageDirs(cdnCachePath, storagePath)) {
541
+ if (pkg.locallyPublished === false) continue;
542
+ packages.push({
543
+ name: pkg.name,
544
+ path: `/-/cdn/${pkg.dirName}`,
545
+ manifest: pkg.manifest,
546
+ documentTypes: getDocumentTypesFromManifest(pkg.manifest),
547
+ version: pkg.version,
548
+ distTags: pkg.distTags,
549
+ versions: pkg.versions
550
+ });
551
+ }
508
552
  return packages;
509
553
  }
510
554
  function findPackagesByDocumentType(packagesDir, documentType) {
@@ -514,6 +558,377 @@ function findPackagesByDocumentType(packagesDir, documentType) {
514
558
  });
515
559
  }
516
560
  //#endregion
561
+ //#region src/pieces.ts
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/";
565
+ const INDEX_TTL_MS = 3e4;
566
+ const indexes = /* @__PURE__ */ new Map();
567
+ const versionPieces = /* @__PURE__ */ new Map();
568
+ const descriptors = /* @__PURE__ */ new Map();
569
+ const materializing = /* @__PURE__ */ new Map();
570
+ /** Drop the cached index and descriptors — a publish or unpublish moved them. */
571
+ function invalidatePieceIndex() {
572
+ indexes.clear();
573
+ versionPieces.clear();
574
+ descriptors.clear();
575
+ }
576
+ function indexKey(cdnCachePath, storagePath) {
577
+ return `${path.resolve(cdnCachePath)}\0${storagePath ?? ""}`;
578
+ }
579
+ function piecesOf(manifest) {
580
+ return manifest?.pieces ?? [];
581
+ }
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;
595
+ return {
596
+ name: id,
597
+ displayName: piece.name || id,
598
+ ...piece.description ? { description: piece.description } : {},
599
+ version: packageVersion,
600
+ packageName,
601
+ packageVersion,
602
+ packageDir,
603
+ bundle,
604
+ descriptor: piece.descriptor ?? `${bundle}/descriptor.json`,
605
+ ...publishedAt ? { publishedAt } : {}
606
+ };
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
+ }
633
+ function buildPieceIndex(cdnCachePath, storagePath) {
634
+ const index = /* @__PURE__ */ new Map();
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);
653
+ }
654
+ }
655
+ return index;
656
+ }
657
+ /** The piece index, rebuilt only when the cached one has gone stale. */
658
+ function pieceIndex(cdnCachePath, storagePath) {
659
+ const key = indexKey(cdnCachePath, storagePath);
660
+ const cached = indexes.get(key);
661
+ if (cached && Date.now() - cached.builtAt < INDEX_TTL_MS) return cached.index;
662
+ const index = buildPieceIndex(cdnCachePath, storagePath);
663
+ indexes.set(key, {
664
+ index,
665
+ builtAt: Date.now()
666
+ });
667
+ return index;
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. */
676
+ function findPiece(cdnCachePath, name, storagePath) {
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
+ };
740
+ }
741
+ function insidePackage(entry, relative) {
742
+ const root = path.resolve(entry.packageDir);
743
+ const resolved = path.resolve(root, relative);
744
+ if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
745
+ return resolved;
746
+ }
747
+ function readDescriptor(entry) {
748
+ const file = insidePackage(entry, entry.descriptor);
749
+ if (!file) return null;
750
+ const hit = descriptors.get(file);
751
+ if (hit !== void 0) return hit;
752
+ let parsed;
753
+ try {
754
+ parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
755
+ } catch {
756
+ parsed = null;
757
+ }
758
+ descriptors.set(file, parsed);
759
+ return parsed;
760
+ }
761
+ const count = (record) => record ? Object.keys(record).length : 0;
762
+ function suggested(blocks) {
763
+ return Object.entries(blocks ?? {}).map(([name, block]) => ({
764
+ name,
765
+ displayName: block.displayName ?? name,
766
+ description: block.description ?? ""
767
+ }));
768
+ }
769
+ function catalogEntry(entry, descriptor, suggestions) {
770
+ return {
771
+ name: entry.name,
772
+ displayName: descriptor.displayName || entry.displayName,
773
+ description: descriptor.description ?? entry.description ?? "",
774
+ logoUrl: descriptor.logoUrl ?? "",
775
+ version: entry.version,
776
+ authors: descriptor.authors ?? [],
777
+ categories: descriptor.categories ?? [],
778
+ auth: descriptor.auth ?? null,
779
+ actions: count(descriptor.actions),
780
+ triggers: count(descriptor.triggers),
781
+ ...descriptor.minimumSupportedRelease ? { minimumSupportedRelease: descriptor.minimumSupportedRelease } : {},
782
+ ...descriptor.maximumSupportedRelease ? { maximumSupportedRelease: descriptor.maximumSupportedRelease } : {},
783
+ package: entry.packageName,
784
+ packageVersion: entry.packageVersion,
785
+ ...suggestions ? {
786
+ suggestedActions: suggested(descriptor.actions),
787
+ suggestedTriggers: Object.entries(descriptor.triggers ?? {}).map(([name, block]) => ({
788
+ name,
789
+ displayName: block.displayName ?? name,
790
+ description: block.description ?? "",
791
+ type: block.type ?? ""
792
+ }))
793
+ } : {}
794
+ };
795
+ }
796
+ /** The catalog of latest versions, sorted by display name. */
797
+ function pieceCatalog(cdnCachePath, options = {}) {
798
+ const entries = [];
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;
803
+ entries.push(catalogEntry(entry, descriptor, options.suggestions === true));
804
+ }
805
+ return entries.sort((a, b) => a.displayName.localeCompare(b.displayName));
806
+ }
807
+ /** The CDN path of a file inside the package version that ships the piece. */
808
+ function cdnPath(entry, relative) {
809
+ return `/-/cdn/${entry.packageName}@${entry.packageVersion}/${relative}`;
810
+ }
811
+ /** The tarball filename cdn.activepieces.com would use for the same piece. */
812
+ function pieceTarballName(name, version) {
813
+ return `${name.replace("/", "-")}-${version}.tgz`;
814
+ }
815
+ function pieceTarballPath(entry) {
816
+ return `/-/pieces/bundled/${pieceTarballName(entry.name, entry.version)}`;
817
+ }
818
+ /** One piece version: what provides it, and where its descriptor and bundle are. */
819
+ function pieceDetail(entry, origin) {
820
+ const descriptor = readDescriptor(entry);
821
+ if (!descriptor) return null;
822
+ return {
823
+ ...descriptor,
824
+ name: entry.name,
825
+ version: entry.version,
826
+ package: entry.packageName,
827
+ packageVersion: entry.packageVersion,
828
+ descriptorUrl: `${origin}${cdnPath(entry, entry.descriptor)}`,
829
+ bundleUrl: `${origin}${pieceTarballPath(entry)}`
830
+ };
831
+ }
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
+ }
843
+ }
844
+ function listFiles(dir, prefix = "") {
845
+ const files = [];
846
+ for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
847
+ const rel = prefix ? `${prefix}/${item.name}` : item.name;
848
+ if (item.isDirectory()) files.push(...listFiles(path.join(dir, item.name), rel));
849
+ else if (item.isFile()) files.push(rel);
850
+ }
851
+ return files.sort();
852
+ }
853
+ const cuts = /* @__PURE__ */ new Map();
854
+ async function cutTarball(entry, pieceDir, target) {
855
+ const files = listFiles(pieceDir);
856
+ if (files.length === 0) return null;
857
+ fs.mkdirSync(path.dirname(target), { recursive: true });
858
+ const tmp = `${target}.tmp-${crypto.randomUUID()}`;
859
+ try {
860
+ await create({
861
+ file: tmp,
862
+ gzip: true,
863
+ cwd: pieceDir,
864
+ prefix: "package",
865
+ portable: true
866
+ }, files);
867
+ fs.renameSync(tmp, target);
868
+ } catch (error) {
869
+ fs.rmSync(tmp, { force: true });
870
+ console.error(`[registry] failed to pack piece ${entry.name}:`, error);
871
+ return null;
872
+ }
873
+ return target;
874
+ }
875
+ async function pieceTarball(entry) {
876
+ const pieceDir = insidePackage(entry, entry.bundle);
877
+ if (!pieceDir || !fs.existsSync(path.join(pieceDir, "package.json"))) return null;
878
+ const target = path.join(entry.packageDir, ".piece-bundles", pieceTarballName(entry.name, entry.version));
879
+ if (fs.existsSync(target)) return target;
880
+ const pending = cuts.get(target);
881
+ if (pending) return pending;
882
+ const started = cutTarball(entry, pieceDir, target).finally(() => {
883
+ cuts.delete(target);
884
+ });
885
+ cuts.set(target, started);
886
+ return started;
887
+ }
888
+ const MANIFEST_PATHS = [
889
+ "powerhouse.manifest.json",
890
+ "cdn/powerhouse.manifest.json",
891
+ "dist/powerhouse.manifest.json"
892
+ ];
893
+ const MAX_TARBALL_BYTES = 134217728;
894
+ function stripRoot(entryPath) {
895
+ return entryPath.replace(/^[^/]+\//, "");
896
+ }
897
+ function manifestsInTar(tar) {
898
+ const found = /* @__PURE__ */ new Map();
899
+ list({
900
+ sync: true,
901
+ filter: (entryPath) => MANIFEST_PATHS.includes(stripRoot(entryPath)),
902
+ onReadEntry: (entry) => {
903
+ const chunks = [];
904
+ entry.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
905
+ entry.on("end", () => {
906
+ found.set(stripRoot(String(entry.path)), Buffer.concat(chunks).toString("utf-8"));
907
+ });
908
+ }
909
+ }).end(tar);
910
+ return found;
911
+ }
912
+ /** The piece names a published tarball claims; empty when it claims none. */
913
+ async function pieceNamesInTarball(tgz) {
914
+ let manifests;
915
+ try {
916
+ manifests = manifestsInTar(await gunzip$1(tgz, { maxOutputLength: MAX_TARBALL_BYTES }));
917
+ } catch {
918
+ return [];
919
+ }
920
+ for (const candidate of MANIFEST_PATHS) {
921
+ const raw = manifests.get(candidate);
922
+ if (raw === void 0) continue;
923
+ try {
924
+ return piecesOf(JSON.parse(raw)).map((piece) => piece.id).filter((id) => typeof id === "string" && id !== "");
925
+ } catch {
926
+ return [];
927
+ }
928
+ }
929
+ return [];
930
+ }
931
+ //#endregion
517
932
  //#region src/warmup.ts
518
933
  const WARM_INTERVAL_MS = 3e4;
519
934
  const WARM_CONCURRENCY = 8;
@@ -526,7 +941,7 @@ function createWarmer(config, cdn) {
526
941
  let warmInFlight = false;
527
942
  let lastWarmAt = 0;
528
943
  return async function warm() {
529
- if (warmInFlight) return;
944
+ if (warmInFlight || config.port === 0) return;
530
945
  if (Date.now() - lastWarmAt < WARM_INTERVAL_MS) return;
531
946
  warmInFlight = true;
532
947
  try {
@@ -590,8 +1005,21 @@ const MIME_TYPES = {
590
1005
  ".html": "text/html",
591
1006
  ".svg": "image/svg+xml"
592
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
+ }
593
1017
  function getContentType(filePath) {
594
- return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
1018
+ const ext = path.extname(filePath).toLowerCase();
1019
+ return MIME_TYPES[ext] ?? "application/octet-stream";
1020
+ }
1021
+ function originOf(req) {
1022
+ return `${req.get("x-forwarded-proto")?.split(",")[0].trim() || req.protocol}://${req.get("host") ?? ""}`;
595
1023
  }
596
1024
  function publisherFromRequest(req) {
597
1025
  const name = req.remote_user?.name;
@@ -642,7 +1070,7 @@ function etagMatches(header, etag) {
642
1070
  });
643
1071
  }
644
1072
  function createPowerhouseRouter(config, sse, webhooks, ownerStore) {
645
- const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
1073
+ const cdn = new CdnCache(localUrl(config), config.cdnCachePath);
646
1074
  const router = Router();
647
1075
  const withOwners = async (pkgs) => {
648
1076
  if (!ownerStore || pkgs.length === 0) return pkgs;
@@ -749,6 +1177,80 @@ function createPowerhouseRouter(config, sse, webhooks, ownerStore) {
749
1177
  }
750
1178
  res.json((await withOwners([pkg]))[0]);
751
1179
  });
1180
+ router.get("/pieces", (req, res) => {
1181
+ warm();
1182
+ res.json(pieceCatalog(config.cdnCachePath, {
1183
+ storagePath: config.storagePath,
1184
+ suggestions: req.query.suggestionType === "ACTION_AND_TRIGGER"
1185
+ }));
1186
+ });
1187
+ router.get("/pieces/*", async (req, res) => {
1188
+ warm();
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") {
1214
+ res.status(404).json({ error: `Piece not found: ${name}` });
1215
+ return;
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
+ }
1229
+ res.json(detail);
1230
+ });
1231
+ router.get("/-/pieces/bundled/*", async (req, res) => {
1232
+ warm();
1233
+ const filename = req.params[0];
1234
+ const entry = await findPieceByTarball(cdn, config.cdnCachePath, filename, config.storagePath);
1235
+ const file = entry ? await pieceTarball(entry) : null;
1236
+ if (!entry || !file) {
1237
+ res.status(404).json({ error: `Piece bundle not found: ${filename}` });
1238
+ return;
1239
+ }
1240
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
1241
+ const etag = `"${entry.packageName}-${filename}"`;
1242
+ res.setHeader("ETag", etag);
1243
+ if (etagMatches(req.headers["if-none-match"], etag)) {
1244
+ res.status(304).end();
1245
+ return;
1246
+ }
1247
+ res.setHeader("Content-Type", "application/gzip");
1248
+ try {
1249
+ await pipeline(fs.createReadStream(file), res);
1250
+ } catch {
1251
+ res.destroy();
1252
+ }
1253
+ });
752
1254
  router.get("/-/cdn/*", async (req, res) => {
753
1255
  const fullPath = req.params[0];
754
1256
  let packageSpec;
@@ -841,7 +1343,7 @@ function parseManifestRewrite(reqPath) {
841
1343
  return { packageName: decodeURIComponent(beforeRev) };
842
1344
  }
843
1345
  function createUnpublishHook(config, notifications) {
844
- const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
1346
+ const cdn = new CdnCache(localUrl(config), config.cdnCachePath);
845
1347
  const handleManifestRewrite = (req, res) => {
846
1348
  const rewrite = parseManifestRewrite(req.path);
847
1349
  if (!rewrite) return;
@@ -850,6 +1352,7 @@ function createUnpublishHook(config, notifications) {
850
1352
  if (res.statusCode >= 200 && res.statusCode < 300) {
851
1353
  const publishedBy = publisherFromRequest(req);
852
1354
  cdn.reconcileWithRegistry(rewrite.packageName).then((removed) => {
1355
+ if (removed.length > 0) invalidatePieceIndex();
853
1356
  for (const version of removed) notifications.notifyUnpublish({
854
1357
  packageName: rewrite.packageName,
855
1358
  version,
@@ -882,6 +1385,7 @@ function createUnpublishHook(config, notifications) {
882
1385
  if (res.statusCode >= 200 && res.statusCode < 300) try {
883
1386
  if (parsed.version) cdn.invalidateVersion(parsed.packageName, parsed.version);
884
1387
  else cdn.invalidate(parsed.packageName);
1388
+ invalidatePieceIndex();
885
1389
  notifications.notifyUnpublish({
886
1390
  packageName: parsed.packageName,
887
1391
  version: parsed.version,
@@ -896,12 +1400,12 @@ function createUnpublishHook(config, notifications) {
896
1400
  };
897
1401
  }
898
1402
  function createPublishHook(config, notifications) {
899
- const cdn = new CdnCache(`http://localhost:${config.port}`, config.cdnCachePath);
900
- return (req, res, next) => {
901
- if (req.method !== "PUT" || req.path.includes("/-rev/")) {
902
- next();
903
- return;
904
- }
1403
+ const cdn = new CdnCache(localUrl(config), config.cdnCachePath);
1404
+ const parseBody = express.json({
1405
+ strict: false,
1406
+ limit: config.maxBodySize ?? "300mb"
1407
+ });
1408
+ const attachExtraction = (req, res) => {
905
1409
  const originalEnd = res.end.bind(res);
906
1410
  res.end = function(chunk, encoding, cb) {
907
1411
  const urlPath = req.path.replace(/^\//, "");
@@ -917,6 +1421,7 @@ function createPublishHook(config, notifications) {
917
1421
  if (versions.length > 1) console.warn(`[registry] Multiple versions published for ${packageName}: ${JSON.stringify(versions)}`);
918
1422
  const publishedBy = publisherFromRequest(req);
919
1423
  cdn.extractTarball(packageName, version).then(() => {
1424
+ invalidatePieceIndex();
920
1425
  notifications.notifyPublish({
921
1426
  packageName,
922
1427
  version,
@@ -927,8 +1432,62 @@ function createPublishHook(config, notifications) {
927
1432
  });
928
1433
  return originalEnd(chunk, encoding, cb);
929
1434
  };
930
- next();
931
1435
  };
1436
+ return (req, res, next) => {
1437
+ if (req.method !== "PUT" || req.path.includes("/-rev/")) {
1438
+ next();
1439
+ return;
1440
+ }
1441
+ const urlPath = req.path.replace(/^\//, "");
1442
+ if (!urlPath || urlPath.startsWith("-")) {
1443
+ attachExtraction(req, res);
1444
+ next();
1445
+ return;
1446
+ }
1447
+ parseBody(req, res, (parseError) => {
1448
+ if (parseError) {
1449
+ next();
1450
+ return;
1451
+ }
1452
+ pieceClaimConflict(config, decodeURIComponent(urlPath), req.body).then((conflict) => {
1453
+ if (conflict) {
1454
+ res.status(409).json({ error: conflict });
1455
+ return;
1456
+ }
1457
+ attachExtraction(req, res);
1458
+ next();
1459
+ }).catch((err) => {
1460
+ console.error("[registry] piece collision check failed:", err);
1461
+ attachExtraction(req, res);
1462
+ next();
1463
+ });
1464
+ });
1465
+ };
1466
+ }
1467
+ function publishedTarball(body) {
1468
+ if (body === null || typeof body !== "object") return null;
1469
+ const attachments = body._attachments;
1470
+ if (!attachments || typeof attachments !== "object") return null;
1471
+ for (const [name, attachment] of Object.entries(attachments)) {
1472
+ if (!name.endsWith(".tgz")) continue;
1473
+ const data = attachment.data;
1474
+ if (typeof data === "string") return Buffer.from(data, "base64");
1475
+ }
1476
+ return null;
1477
+ }
1478
+ async function pieceClaimConflict(config, packageName, body) {
1479
+ const tarball = publishedTarball(body);
1480
+ if (!tarball) return null;
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) {
1487
+ const owner = index.get(name);
1488
+ if (owner && owner.packageName !== packageName) return `Piece "${name}" is already published by ${owner.packageName}; ${packageName} cannot claim it.`;
1489
+ }
1490
+ return null;
932
1491
  }
933
1492
  //#endregion
934
1493
  //#region src/notifications/manager.ts
@@ -1187,7 +1746,8 @@ async function runRegistry(args) {
1187
1746
  if (config.databaseUrl || config.authStore) console.log("[registry] Postgres-backed auth plugin active (persistent accounts + package ownership)");
1188
1747
  await mkdir(storagePath, { recursive: true });
1189
1748
  await mkdir(cdnCachePath, { recursive: true });
1190
- const verdaccioServer = await runServer(buildVerdaccioConfig(config));
1749
+ const verdaccioConfig = buildVerdaccioConfig(config);
1750
+ const verdaccioServer = await runServer(verdaccioConfig);
1191
1751
  if (authStoreToken && !wasStoreLoaded(authStoreToken)) {
1192
1752
  verdaccioServer.close();
1193
1753
  throw new Error("registry-auth plugin failed to load despite a configured database/auth store; refusing to start without auth and package-ownership enforcement.");
@@ -1203,7 +1763,9 @@ async function runRegistry(args) {
1203
1763
  app.use(createPublishHook(config, notifications));
1204
1764
  app.use(createUnpublishHook(config, notifications));
1205
1765
  app.use((req, res) => verdaccioHandler(req, res));
1206
- return app.listen(port, () => {
1766
+ const server = app.listen(port, () => {
1767
+ config.port = server.address().port;
1768
+ const port = config.port;
1207
1769
  console.log(`Powerhouse Registry running on http://localhost:${port}`);
1208
1770
  console.log(` CDN: http://localhost:${port}/-/cdn/`);
1209
1771
  console.log(` Packages: http://localhost:${port}/packages`);
@@ -1213,6 +1775,7 @@ async function runRegistry(args) {
1213
1775
  if (config.s3) console.log(` S3: ${config.s3.endpoint}/${config.s3.bucket}`);
1214
1776
  if (config.renown) console.log(` Renown auth: ${config.renown.publicUrl}`);
1215
1777
  });
1778
+ return server;
1216
1779
  }
1217
1780
  //#endregion
1218
1781
  //#region cli.ts
@@ -1362,7 +1925,8 @@ const registryCommand = command({
1362
1925
  }
1363
1926
  }
1364
1927
  });
1365
- await run(binary(registryCommand), process.argv);
1928
+ const registryCli = binary(registryCommand);
1929
+ await run(registryCli, process.argv);
1366
1930
  //#endregion
1367
1931
  export { registryCommand };
1368
1932