@camstack/server 1.2.575 → 1.2.577

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.
@@ -1532,7 +1532,7 @@ function buildAddonsProvider(ar, ps, ls, moleculer, configService, ctx, agentReg
1532
1532
  listInstallations: async (input) => (0, addon_uninstall_js_1.listInstallations)(uninstallSeam, input.packageName),
1533
1533
  reloadPackages: async () => ps.reloadPackages(),
1534
1534
  searchAvailable: async (input) => {
1535
- const results = await ps.searchNpm(input?.query);
1535
+ const results = await ps.searchNpm(input?.query, input?.capability);
1536
1536
  const installedIds = new Set(ps.listInstalled().map((p) => p.name));
1537
1537
  return results.map((r) => ({
1538
1538
  ...r,
@@ -192,6 +192,7 @@ class AddonPackageService {
192
192
  cachedUpdates = null;
193
193
  searchCache = null;
194
194
  versionCache = new Map();
195
+ capabilityCache = new Map();
195
196
  updateAvailability;
196
197
  // -- Auto-update state ----------------------------------------------------
197
198
  autoUpdateConfig = {
@@ -217,6 +218,8 @@ class AddonPackageService {
217
218
  static UPDATE_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
218
219
  static SEARCH_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
219
220
  static VERSION_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
221
+ static CAPABILITY_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
222
+ static CAPABILITY_LOOKUP_CONCURRENCY = 6;
220
223
  static NPM_REGISTRY = 'https://registry.npmjs.org';
221
224
  static REGISTRY_TIMEOUT_MS = 10_000;
222
225
  constructor(loggingService, eventBusService, configService, addonRegistry, notificationService, toastService,
@@ -777,8 +780,15 @@ class AddonPackageService {
777
780
  // =========================================================================
778
781
  // npm search
779
782
  // =========================================================================
780
- /** Search npm for camstack addon packages, optionally filtered by query */
781
- async searchNpm(query) {
783
+ /**
784
+ * Search npm for camstack addon packages, optionally filtered by query and
785
+ * by a capability the package must declare. The capabilities come from each
786
+ * package's own `/latest` manifest (`camstack.addons[].capabilities`) — the
787
+ * single source of truth (D4), no hand-kept keywords. A package whose
788
+ * manifest could not be read carries `capabilities: null` and is excluded
789
+ * from a capability-filtered result (unknown is not a match).
790
+ */
791
+ async searchNpm(query, capability) {
782
792
  const npmResults = await this.fetchSearchFromNpm();
783
793
  let filtered = npmResults;
784
794
  if (query) {
@@ -787,7 +797,14 @@ class AddonPackageService {
787
797
  r.description?.toLowerCase().includes(q) ||
788
798
  r.keywords?.some((k) => k.toLowerCase().includes(q)));
789
799
  }
790
- return filtered.map((r) => ({
800
+ const capabilitiesByName = await this.lookupCapabilities(filtered.map((r) => r.name));
801
+ const failed = filtered.filter((r) => capabilitiesByName.get(r.name) === null).length;
802
+ if (failed > 0) {
803
+ this.logger.warn('npm capability lookup failed for some packages', {
804
+ meta: { failed, total: filtered.length, capability: capability ?? null },
805
+ });
806
+ }
807
+ const withCapabilities = filtered.map((r) => ({
791
808
  name: r.name,
792
809
  version: r.version,
793
810
  description: r.description ?? '',
@@ -796,7 +813,73 @@ class AddonPackageService {
796
813
  author: r.publisher?.username ?? '',
797
814
  installed: false, // caller will enrich
798
815
  installedVersion: undefined,
816
+ capabilities: capabilitiesByName.get(r.name) ?? null,
799
817
  }));
818
+ if (capability === undefined)
819
+ return withCapabilities;
820
+ return withCapabilities.filter((r) => r.capabilities?.includes(capability) === true);
821
+ }
822
+ /**
823
+ * Declared capabilities per package, through a bounded worker pool and the
824
+ * 10-minute cache. `null` = the lookup failed (not cached, so the next
825
+ * search retries).
826
+ */
827
+ async lookupCapabilities(names) {
828
+ const result = new Map();
829
+ const now = Date.now();
830
+ const pending = [];
831
+ for (const name of names) {
832
+ const cached = this.capabilityCache.get(name);
833
+ if (cached !== undefined && cached.expiresAt > now)
834
+ result.set(name, cached.capabilities);
835
+ else
836
+ pending.push(name);
837
+ }
838
+ let next = 0;
839
+ const worker = async () => {
840
+ while (next < pending.length) {
841
+ const name = pending[next++];
842
+ if (name === undefined)
843
+ return;
844
+ const capabilities = await this.fetchPackageCapabilities(name);
845
+ result.set(name, capabilities);
846
+ if (capabilities !== null) {
847
+ this.capabilityCache.set(name, {
848
+ capabilities,
849
+ expiresAt: Date.now() + AddonPackageService.CAPABILITY_CACHE_TTL_MS,
850
+ });
851
+ }
852
+ }
853
+ };
854
+ const poolSize = Math.min(AddonPackageService.CAPABILITY_LOOKUP_CONCURRENCY, pending.length);
855
+ await Promise.all(Array.from({ length: poolSize }, worker));
856
+ return result;
857
+ }
858
+ /** Read `camstack.addons[].capabilities` from a package's `/latest`; null on any failure. */
859
+ async fetchPackageCapabilities(packageName) {
860
+ try {
861
+ const url = `${this.resolveRegistryBase()}/${packageName.replace('/', '%2F')}/latest`;
862
+ const response = await fetch(url, {
863
+ signal: AbortSignal.timeout(AddonPackageService.REGISTRY_TIMEOUT_MS),
864
+ });
865
+ if (!response.ok)
866
+ return null;
867
+ const data = await fetchJsonObject(response);
868
+ const addons = asRecord(data['camstack'])['addons'];
869
+ const names = new Set();
870
+ for (const addon of Array.isArray(addons) ? addons : []) {
871
+ const caps = asRecord(addon)['capabilities'];
872
+ for (const cap of Array.isArray(caps) ? caps : []) {
873
+ const name = typeof cap === 'string' ? cap : asString(asRecord(cap)['name']);
874
+ if (name)
875
+ names.add(name);
876
+ }
877
+ }
878
+ return [...names];
879
+ }
880
+ catch {
881
+ return null;
882
+ }
800
883
  }
801
884
  // =========================================================================
802
885
  // Package versions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.575",
3
+ "version": "1.2.577",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -38,18 +38,18 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@camstack/addon-admin-ui": "1.2.425",
41
+ "@camstack/addon-admin-ui": "1.2.426",
42
42
  "@camstack/addon-agent-ui": "1.2.233",
43
43
  "@camstack/addon-auth": "1.2.145",
44
44
  "@camstack/addon-decoder-nodeav": "1.2.123",
45
45
  "@camstack/addon-notifiers": "1.2.134",
46
46
  "@camstack/addon-pipeline": "1.2.369",
47
- "@camstack/addon-pipeline-orchestrator": "1.2.321",
47
+ "@camstack/addon-pipeline-orchestrator": "1.2.322",
48
48
  "@camstack/addon-post-analysis": "1.2.369",
49
49
  "@camstack/develop": "0.1.10",
50
50
  "@camstack/sdk": "1.2.131",
51
- "@camstack/system": "1.2.434",
52
- "@camstack/types": "1.2.370",
51
+ "@camstack/system": "1.2.435",
52
+ "@camstack/types": "1.2.371",
53
53
  "@camstack/ui-library": "1.2.289",
54
54
  "@fastify/compress": "^9.0.0",
55
55
  "@fastify/cookie": "^11.0.2",