Files
wehub-resource-sync 23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:02:19 +08:00

500 lines
14 KiB
JavaScript

/**
* Claude Flow Plugin Registry Cloud Function
*
* Secure IPFS publishing with:
* - Ed25519 signature verification
* - Model import/export tracking
* - Plugin sharing analytics
* - Download tracking
*
* Deploy:
* gcloud functions deploy publish-registry \
* --gen2 --runtime=nodejs20 --region=us-central1 \
* --trigger-http --allow-unauthenticated \
* --service-account=plugin-registry-publisher@claude-flow.iam.gserviceaccount.com
*/
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
import { Storage } from '@google-cloud/storage';
import * as ed from '@noble/ed25519';
const secretManager = new SecretManagerServiceClient();
const storage = new Storage();
const PROJECT_ID = process.env.GCP_PROJECT || 'claude-flow';
const BUCKET_NAME = 'claude-flow-plugin-registry';
const PINATA_API_URL = 'https://api.pinata.cloud';
/**
* Get secret from Secret Manager
*/
async function getSecret(name) {
const [version] = await secretManager.accessSecretVersion({
name: `projects/${PROJECT_ID}/secrets/${name}/versions/latest`,
});
return version.payload.data.toString();
}
/**
* Sign registry with Ed25519
*/
async function signRegistry(registry, privateKeyHex) {
const privateKey = Buffer.from(privateKeyHex, 'hex');
const publicKey = await ed.getPublicKeyAsync(privateKey);
// Create copy without signature fields
const registryToSign = { ...registry };
delete registryToSign.registrySignature;
delete registryToSign.registryPublicKey;
const message = JSON.stringify(registryToSign);
const signature = await ed.signAsync(
new TextEncoder().encode(message),
privateKey
);
return {
signature: Buffer.from(signature).toString('hex'),
publicKey: `ed25519:${Buffer.from(publicKey).toString('hex')}`,
};
}
/**
* Pin JSON to IPFS via Pinata
*/
async function pinToIPFS(data, name, jwt) {
const response = await fetch(`${PINATA_API_URL}/pinning/pinJSONToIPFS`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwt}`,
},
body: JSON.stringify({
pinataContent: data,
pinataMetadata: {
name,
keyvalues: {
type: 'plugin-registry',
publishedAt: new Date().toISOString(),
version: data.version || '1.0.0',
},
},
pinataOptions: {
cidVersion: 1,
},
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Pinata error: ${response.status} - ${error}`);
}
return response.json();
}
/**
* Fetch npm stats for packages
*/
async function fetchNpmStats(packageName) {
try {
const [downloadsRes, packageRes] = await Promise.all([
fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`, {
signal: AbortSignal.timeout(5000),
}),
fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, {
signal: AbortSignal.timeout(5000),
}),
]);
const downloads = downloadsRes.ok ? (await downloadsRes.json()).downloads || 0 : 0;
const version = packageRes.ok ? (await packageRes.json()).version || 'unknown' : 'unknown';
return { downloads, version };
} catch {
return { downloads: 0, version: 'unknown' };
}
}
/**
* Track plugin download/share event
*/
async function trackEvent(bucket, eventType, pluginId, metadata = {}) {
const file = bucket.file(`analytics/${eventType}/${new Date().toISOString().split('T')[0]}.jsonl`);
const event = JSON.stringify({
timestamp: new Date().toISOString(),
eventType,
pluginId,
...metadata,
}) + '\n';
try {
await file.save(event, { resumable: false });
} catch {
// Append to existing file
const [exists] = await file.exists();
if (exists) {
const [content] = await file.download();
await file.save(content.toString() + event);
}
}
}
/**
* Export model/pattern to IPFS
*/
async function exportModel(modelData, jwt) {
const result = await pinToIPFS(modelData, `claude-flow-model-${Date.now()}`, jwt);
return {
cid: result.IpfsHash,
size: result.PinSize,
gateways: [
`https://gateway.pinata.cloud/ipfs/${result.IpfsHash}`,
`https://ipfs.io/ipfs/${result.IpfsHash}`,
],
};
}
/**
* Import model from IPFS CID
*/
async function importModel(cid) {
const gateways = [
'https://gateway.pinata.cloud',
'https://ipfs.io',
'https://dweb.link',
];
for (const gateway of gateways) {
try {
const response = await fetch(`${gateway}/ipfs/${cid}`, {
signal: AbortSignal.timeout(30000),
headers: { 'Accept': 'application/json' },
});
if (response.ok) {
return {
success: true,
data: await response.json(),
gateway,
};
}
} catch {
continue;
}
}
return { success: false, error: 'Model not found on any gateway' };
}
/**
* Main Cloud Function handler
*/
export async function publishRegistry(req, res) {
// CORS headers
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.status(204).send('');
}
try {
const { action = 'publish' } = req.query;
const bucket = storage.bucket(BUCKET_NAME);
switch (action) {
case 'publish': {
// Get secrets
const [pinataJwt, privateKey] = await Promise.all([
getSecret('pinata-jwt'),
getSecret('registry-private-key'),
]);
// Fetch registry from GCS
const file = bucket.file('registry.json');
const [content] = await file.download();
const registry = JSON.parse(content.toString());
// Update timestamps and fetch live npm stats
registry.updatedAt = new Date().toISOString();
// Fetch live stats for each plugin
for (const plugin of registry.plugins) {
const stats = await fetchNpmStats(plugin.id);
plugin.downloads = stats.downloads;
plugin.version = stats.version !== 'unknown' ? stats.version : plugin.version;
}
registry.totalDownloads = registry.plugins.reduce((sum, p) => sum + p.downloads, 0);
// Sign registry
const { signature, publicKey } = await signRegistry(registry, privateKey);
registry.registrySignature = signature;
registry.registryPublicKey = publicKey;
// Pin to IPFS
const pinResult = await pinToIPFS(registry, 'claude-flow-plugin-registry', pinataJwt);
// Save latest CID to GCS
await bucket.file('latest-cid.txt').save(pinResult.IpfsHash);
// Track publish event
await trackEvent(bucket, 'publish', 'registry', {
cid: pinResult.IpfsHash,
pluginCount: registry.plugins.length,
});
return res.json({
success: true,
cid: pinResult.IpfsHash,
updatedAt: registry.updatedAt,
pluginCount: registry.plugins.length,
totalDownloads: registry.totalDownloads,
gateways: [
`https://gateway.pinata.cloud/ipfs/${pinResult.IpfsHash}`,
`https://ipfs.io/ipfs/${pinResult.IpfsHash}`,
`https://dweb.link/ipfs/${pinResult.IpfsHash}`,
],
publicKey,
});
}
case 'export-model': {
const pinataJwt = await getSecret('pinata-jwt');
const modelData = req.body;
if (!modelData) {
return res.status(400).json({ error: 'Model data required' });
}
const result = await exportModel(modelData, pinataJwt);
await trackEvent(bucket, 'model-export', modelData.id || 'unknown');
return res.json({
success: true,
...result,
});
}
case 'import-model': {
const { cid } = req.query;
if (!cid) {
return res.status(400).json({ error: 'CID required' });
}
const result = await importModel(cid);
await trackEvent(bucket, 'model-import', cid);
return res.json(result);
}
case 'track-download': {
const { pluginId } = req.body || req.query;
if (!pluginId) {
return res.status(400).json({ error: 'Plugin ID required' });
}
await trackEvent(bucket, 'download', pluginId);
return res.json({ success: true });
}
case 'analytics': {
const { period = '7d' } = req.query;
// Read analytics files
const [files] = await bucket.getFiles({ prefix: 'analytics/' });
const analytics = {
downloads: {},
exports: 0,
imports: 0,
publishes: 0,
};
for (const file of files.slice(-7)) { // Last 7 files
try {
const [content] = await file.download();
const lines = content.toString().split('\n').filter(Boolean);
for (const line of lines) {
const event = JSON.parse(line);
if (event.eventType === 'download') {
analytics.downloads[event.pluginId] = (analytics.downloads[event.pluginId] || 0) + 1;
} else if (event.eventType === 'model-export') {
analytics.exports++;
} else if (event.eventType === 'model-import') {
analytics.imports++;
} else if (event.eventType === 'publish') {
analytics.publishes++;
}
}
} catch {
continue;
}
}
return res.json(analytics);
}
case 'status': {
// Get latest CID
try {
const [content] = await bucket.file('latest-cid.txt').download();
const cid = content.toString().trim();
return res.json({
healthy: true,
latestCid: cid,
gateways: [
`https://gateway.pinata.cloud/ipfs/${cid}`,
`https://ipfs.io/ipfs/${cid}`,
],
});
} catch {
return res.json({
healthy: false,
error: 'No registry published yet',
});
}
}
case 'rate': {
// Rate a plugin or model (stored in GCS, no SQL)
const { itemId, itemType = 'plugin', rating, userId } = req.body || {};
if (!itemId || !rating || rating < 1 || rating > 5) {
return res.status(400).json({ error: 'itemId and rating (1-5) required' });
}
// Generate anonymous user ID if not provided
const finalUserId = userId || `anon-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
// Store rating in GCS (JSON file per item)
const ratingsFile = bucket.file(`ratings/${itemType}/${itemId}.json`);
let ratings = { ratings: [], average: 0, count: 0 };
try {
const [exists] = await ratingsFile.exists();
if (exists) {
const [content] = await ratingsFile.download();
ratings = JSON.parse(content.toString());
}
} catch {
// File doesn't exist, use defaults
}
// Check if user already rated (prevent duplicate ratings)
const existingIndex = ratings.ratings.findIndex(r => r.userId === finalUserId);
if (existingIndex >= 0) {
// Update existing rating
ratings.ratings[existingIndex] = {
userId: finalUserId,
rating: Number(rating),
timestamp: new Date().toISOString(),
};
} else {
// Add new rating
ratings.ratings.push({
userId: finalUserId,
rating: Number(rating),
timestamp: new Date().toISOString(),
});
}
// Recalculate average
ratings.count = ratings.ratings.length;
ratings.average = ratings.ratings.reduce((sum, r) => sum + r.rating, 0) / ratings.count;
// Save updated ratings
await ratingsFile.save(JSON.stringify(ratings, null, 2), {
contentType: 'application/json',
});
// Track rating event
await trackEvent(bucket, 'rating', itemId, { rating, itemType });
return res.json({
success: true,
itemId,
average: Math.round(ratings.average * 10) / 10,
count: ratings.count,
});
}
case 'get-ratings': {
// Get ratings for a plugin or model
const { itemId, itemType = 'plugin' } = req.query;
if (!itemId) {
return res.status(400).json({ error: 'itemId required' });
}
const ratingsFile = bucket.file(`ratings/${itemType}/${itemId}.json`);
try {
const [exists] = await ratingsFile.exists();
if (!exists) {
return res.json({ itemId, average: 0, count: 0, ratings: [] });
}
const [content] = await ratingsFile.download();
const ratings = JSON.parse(content.toString());
return res.json({
itemId,
average: Math.round(ratings.average * 10) / 10,
count: ratings.count,
// Don't expose individual user ratings
});
} catch {
return res.json({ itemId, average: 0, count: 0 });
}
}
case 'bulk-ratings': {
// Get ratings for multiple items at once
const { itemIds, itemType = 'plugin' } = req.body || req.query;
if (!itemIds || !Array.isArray(itemIds)) {
return res.status(400).json({ error: 'itemIds array required' });
}
const results = {};
for (const itemId of itemIds.slice(0, 50)) { // Limit to 50 items
try {
const ratingsFile = bucket.file(`ratings/${itemType}/${itemId}.json`);
const [exists] = await ratingsFile.exists();
if (exists) {
const [content] = await ratingsFile.download();
const ratings = JSON.parse(content.toString());
results[itemId] = {
average: Math.round(ratings.average * 10) / 10,
count: ratings.count,
};
} else {
results[itemId] = { average: 0, count: 0 };
}
} catch {
results[itemId] = { average: 0, count: 0 };
}
}
return res.json(results);
}
default:
return res.status(400).json({ error: `Unknown action: ${action}` });
}
} catch (error) {
console.error('Function error:', error);
return res.status(500).json({ error: error.message });
}
}
// For local testing
export { signRegistry, fetchNpmStats, trackEvent };