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
500 lines
14 KiB
JavaScript
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 };
|