cipher-security 2.0.0
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/bin/cipher.js +566 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +238 -0
- package/lib/brand.js +105 -0
- package/lib/commands.js +100 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +991 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +288 -0
- package/package.json +31 -0
package/lib/api/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CIPHER API module — barrel export.
|
|
6
|
+
*
|
|
7
|
+
* Re-exports all public symbols from the API submodules for clean
|
|
8
|
+
* consumption by downstream slices (MCP, bot, bridge removal).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Compliance engine
|
|
12
|
+
export {
|
|
13
|
+
ComplianceFramework,
|
|
14
|
+
ControlStatus,
|
|
15
|
+
ControlAssessment,
|
|
16
|
+
ComplianceReport,
|
|
17
|
+
ComplianceEngine,
|
|
18
|
+
} from './compliance.js';
|
|
19
|
+
|
|
20
|
+
// Control data
|
|
21
|
+
export { CONTROL_MAPS, CATEGORY_KEYWORDS, SEVERITY_WEIGHTS } from './controls.js';
|
|
22
|
+
|
|
23
|
+
// REST server
|
|
24
|
+
export {
|
|
25
|
+
createAPIServer,
|
|
26
|
+
APIConfig,
|
|
27
|
+
APIResponse,
|
|
28
|
+
RateLimiter,
|
|
29
|
+
AuthHandler,
|
|
30
|
+
validateScanTarget,
|
|
31
|
+
} from './server.js';
|
|
32
|
+
|
|
33
|
+
// Billing
|
|
34
|
+
export {
|
|
35
|
+
MeteringEngine,
|
|
36
|
+
BillingTier,
|
|
37
|
+
TIER_LIMITS,
|
|
38
|
+
ENDPOINT_CREDITS,
|
|
39
|
+
} from './billing.js';
|
|
40
|
+
|
|
41
|
+
// Marketplace
|
|
42
|
+
export { SkillMarketplace } from './marketplace.js';
|
|
43
|
+
|
|
44
|
+
// OpenAI proxy
|
|
45
|
+
export {
|
|
46
|
+
createOpenAIProxy,
|
|
47
|
+
ProxyConfig,
|
|
48
|
+
findMatchingSkills,
|
|
49
|
+
} from './openai-proxy.js';
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CIPHER Skill Marketplace — publish, discover, and install CIPHER skills.
|
|
6
|
+
*
|
|
7
|
+
* SQLite-backed CRUD with ratings/reviews, search, and JSON-based
|
|
8
|
+
* export/import (simplified from Python's tar.gz to reduce complexity).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join, dirname, resolve as resolvePath } from 'node:path';
|
|
15
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync, rmdirSync } from 'node:fs';
|
|
16
|
+
import { createGzip, createGunzip } from 'node:zlib';
|
|
17
|
+
|
|
18
|
+
/** @type {typeof import('better-sqlite3')} */
|
|
19
|
+
let Database;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compute SHA-256 checksum over all file contents sorted by name.
|
|
23
|
+
* @param {Record<string, string>} files
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function computeChecksum(files) {
|
|
27
|
+
const h = createHash('sha256');
|
|
28
|
+
for (const name of Object.keys(files).sort()) {
|
|
29
|
+
h.update(name, 'utf8');
|
|
30
|
+
h.update(files[name], 'utf8');
|
|
31
|
+
}
|
|
32
|
+
return h.digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Skill marketplace for publishing, discovering, and installing CIPHER skills.
|
|
37
|
+
*/
|
|
38
|
+
export class SkillMarketplace {
|
|
39
|
+
/**
|
|
40
|
+
* @param {object} [opts]
|
|
41
|
+
* @param {string} [opts.dbPath] - Path to SQLite DB. Default: ~/.cipher/marketplace.db
|
|
42
|
+
* @param {string} [opts.skillsDir] - Default install directory for skills. Default: 'skills'
|
|
43
|
+
*/
|
|
44
|
+
constructor(opts = {}) {
|
|
45
|
+
if (!Database) {
|
|
46
|
+
Database = require('better-sqlite3');
|
|
47
|
+
}
|
|
48
|
+
const dbPath = opts.dbPath || join(homedir(), '.cipher', 'marketplace.db');
|
|
49
|
+
this.skillsDir = opts.skillsDir || 'skills';
|
|
50
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
51
|
+
this._db = new Database(dbPath);
|
|
52
|
+
this._db.pragma('journal_mode = WAL');
|
|
53
|
+
this._initDb();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_initDb() {
|
|
57
|
+
this._db.exec(`
|
|
58
|
+
CREATE TABLE IF NOT EXISTS packages (
|
|
59
|
+
package_id TEXT PRIMARY KEY,
|
|
60
|
+
name TEXT NOT NULL,
|
|
61
|
+
domain TEXT NOT NULL DEFAULT '',
|
|
62
|
+
version TEXT NOT NULL DEFAULT '0.1.0',
|
|
63
|
+
description TEXT NOT NULL DEFAULT '',
|
|
64
|
+
author TEXT NOT NULL DEFAULT '',
|
|
65
|
+
license TEXT NOT NULL DEFAULT 'AGPL-3.0',
|
|
66
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
67
|
+
files TEXT NOT NULL DEFAULT '{}',
|
|
68
|
+
checksum TEXT NOT NULL DEFAULT '',
|
|
69
|
+
downloads INTEGER NOT NULL DEFAULT 0,
|
|
70
|
+
rating REAL NOT NULL DEFAULT 0.0,
|
|
71
|
+
ratings_count INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
created_at TEXT NOT NULL,
|
|
73
|
+
updated_at TEXT NOT NULL
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS reviews (
|
|
77
|
+
review_id TEXT PRIMARY KEY,
|
|
78
|
+
package_id TEXT NOT NULL,
|
|
79
|
+
reviewer TEXT NOT NULL,
|
|
80
|
+
rating REAL NOT NULL,
|
|
81
|
+
comment TEXT NOT NULL DEFAULT '',
|
|
82
|
+
created_at TEXT NOT NULL,
|
|
83
|
+
FOREIGN KEY (package_id) REFERENCES packages(package_id)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
CREATE TABLE IF NOT EXISTS download_log (
|
|
87
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
88
|
+
package_id TEXT NOT NULL,
|
|
89
|
+
downloaded_at TEXT NOT NULL,
|
|
90
|
+
target_dir TEXT NOT NULL DEFAULT '',
|
|
91
|
+
FOREIGN KEY (package_id) REFERENCES packages(package_id)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_packages_domain ON packages(domain);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_packages_rating ON packages(rating DESC);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_reviews_package ON reviews(package_id);
|
|
97
|
+
`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// -- Publish / get / list / search ----------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Publish a skill package to the marketplace. Returns package_id.
|
|
104
|
+
* @param {object} pkg
|
|
105
|
+
* @returns {string} package_id
|
|
106
|
+
*/
|
|
107
|
+
publish(pkg) {
|
|
108
|
+
const packageId = pkg.packageId || randomUUID();
|
|
109
|
+
const now = new Date().toISOString();
|
|
110
|
+
const files = pkg.files || {};
|
|
111
|
+
const checksum = pkg.checksum || (Object.keys(files).length ? computeChecksum(files) : '');
|
|
112
|
+
const createdAt = pkg.createdAt || now;
|
|
113
|
+
|
|
114
|
+
this._db
|
|
115
|
+
.prepare(
|
|
116
|
+
`INSERT INTO packages (
|
|
117
|
+
package_id, name, domain, version, description, author,
|
|
118
|
+
license, tags, files, checksum, downloads, rating,
|
|
119
|
+
ratings_count, created_at, updated_at
|
|
120
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
121
|
+
ON CONFLICT(package_id) DO UPDATE SET
|
|
122
|
+
name=excluded.name, domain=excluded.domain,
|
|
123
|
+
version=excluded.version, description=excluded.description,
|
|
124
|
+
author=excluded.author, license=excluded.license,
|
|
125
|
+
tags=excluded.tags, files=excluded.files,
|
|
126
|
+
checksum=excluded.checksum, updated_at=excluded.updated_at`,
|
|
127
|
+
)
|
|
128
|
+
.run(
|
|
129
|
+
packageId,
|
|
130
|
+
pkg.name || '',
|
|
131
|
+
pkg.domain || '',
|
|
132
|
+
pkg.version || '0.1.0',
|
|
133
|
+
pkg.description || '',
|
|
134
|
+
pkg.author || '',
|
|
135
|
+
pkg.license || 'AGPL-3.0',
|
|
136
|
+
JSON.stringify(pkg.tags || []),
|
|
137
|
+
JSON.stringify(files),
|
|
138
|
+
checksum,
|
|
139
|
+
pkg.downloads || 0,
|
|
140
|
+
pkg.rating || 0,
|
|
141
|
+
pkg.ratingsCount || 0,
|
|
142
|
+
createdAt,
|
|
143
|
+
now,
|
|
144
|
+
);
|
|
145
|
+
return packageId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Retrieve a single package by ID.
|
|
150
|
+
* @param {string} packageId
|
|
151
|
+
* @returns {object|null}
|
|
152
|
+
*/
|
|
153
|
+
getPackage(packageId) {
|
|
154
|
+
const row = this._db.prepare('SELECT * FROM packages WHERE package_id = ?').get(packageId);
|
|
155
|
+
return row ? this._rowToPackage(row) : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* List packages with optional domain filter and sorting.
|
|
160
|
+
* @param {object} [opts]
|
|
161
|
+
* @param {string} [opts.domain]
|
|
162
|
+
* @param {string} [opts.sort='rating'] - 'rating'|'downloads'|'name'|'updated'|'created'
|
|
163
|
+
* @param {number} [opts.limit=20]
|
|
164
|
+
* @returns {object[]}
|
|
165
|
+
*/
|
|
166
|
+
listPackages(opts = {}) {
|
|
167
|
+
const sortMap = {
|
|
168
|
+
rating: 'rating DESC',
|
|
169
|
+
downloads: 'downloads DESC',
|
|
170
|
+
name: 'name ASC',
|
|
171
|
+
updated: 'updated_at DESC',
|
|
172
|
+
created: 'created_at DESC',
|
|
173
|
+
};
|
|
174
|
+
const order = sortMap[opts.sort || 'rating'] || 'rating DESC';
|
|
175
|
+
const limit = opts.limit || 20;
|
|
176
|
+
|
|
177
|
+
let rows;
|
|
178
|
+
if (opts.domain) {
|
|
179
|
+
rows = this._db
|
|
180
|
+
.prepare(`SELECT * FROM packages WHERE domain = ? ORDER BY ${order} LIMIT ?`)
|
|
181
|
+
.all(opts.domain, limit);
|
|
182
|
+
} else {
|
|
183
|
+
rows = this._db
|
|
184
|
+
.prepare(`SELECT * FROM packages ORDER BY ${order} LIMIT ?`)
|
|
185
|
+
.all(limit);
|
|
186
|
+
}
|
|
187
|
+
return rows.map((r) => this._rowToPackage(r));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Search packages by name, description, and optional filters.
|
|
192
|
+
* @param {string} query
|
|
193
|
+
* @param {object} [opts]
|
|
194
|
+
* @param {string} [opts.domain]
|
|
195
|
+
* @param {string[]} [opts.tags]
|
|
196
|
+
* @returns {object[]}
|
|
197
|
+
*/
|
|
198
|
+
search(query, opts = {}) {
|
|
199
|
+
const conditions = ['(name LIKE ? OR description LIKE ? OR tags LIKE ?)'];
|
|
200
|
+
const params = [`%${query}%`, `%${query}%`, `%${query}%`];
|
|
201
|
+
|
|
202
|
+
if (opts.domain) {
|
|
203
|
+
conditions.push('domain = ?');
|
|
204
|
+
params.push(opts.domain);
|
|
205
|
+
}
|
|
206
|
+
if (opts.tags) {
|
|
207
|
+
for (const tag of opts.tags) {
|
|
208
|
+
conditions.push('tags LIKE ?');
|
|
209
|
+
params.push(`%"${tag}"%`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const where = conditions.join(' AND ');
|
|
214
|
+
const rows = this._db
|
|
215
|
+
.prepare(`SELECT * FROM packages WHERE ${where} ORDER BY rating DESC`)
|
|
216
|
+
.all(...params);
|
|
217
|
+
return rows.map((r) => this._rowToPackage(r));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// -- Install / uninstall --------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Install a skill package to the target directory.
|
|
224
|
+
* @param {string} packageId
|
|
225
|
+
* @param {string} [targetDir]
|
|
226
|
+
* @returns {boolean}
|
|
227
|
+
*/
|
|
228
|
+
install(packageId, targetDir) {
|
|
229
|
+
const pkg = this.getPackage(packageId);
|
|
230
|
+
if (!pkg) return false;
|
|
231
|
+
|
|
232
|
+
const dest = resolvePath(targetDir || join(this.skillsDir, pkg.name));
|
|
233
|
+
mkdirSync(dest, { recursive: true });
|
|
234
|
+
|
|
235
|
+
for (const [filename, content] of Object.entries(pkg.files)) {
|
|
236
|
+
const filepath = resolvePath(dest, filename);
|
|
237
|
+
// Prevent path traversal
|
|
238
|
+
if (!filepath.startsWith(dest)) continue;
|
|
239
|
+
mkdirSync(dirname(filepath), { recursive: true });
|
|
240
|
+
writeFileSync(filepath, content, 'utf8');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Record download
|
|
244
|
+
const now = new Date().toISOString();
|
|
245
|
+
this._db.prepare('UPDATE packages SET downloads = downloads + 1 WHERE package_id = ?').run(packageId);
|
|
246
|
+
this._db
|
|
247
|
+
.prepare('INSERT INTO download_log (package_id, downloaded_at, target_dir) VALUES (?, ?, ?)')
|
|
248
|
+
.run(packageId, now, dest);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Remove installed skill files from disk.
|
|
254
|
+
* @param {string} packageId
|
|
255
|
+
* @returns {boolean}
|
|
256
|
+
*/
|
|
257
|
+
uninstall(packageId) {
|
|
258
|
+
const pkg = this.getPackage(packageId);
|
|
259
|
+
if (!pkg) return false;
|
|
260
|
+
|
|
261
|
+
const possibleDirs = [resolvePath(this.skillsDir, pkg.name)];
|
|
262
|
+
|
|
263
|
+
// Check download_log
|
|
264
|
+
const row = this._db
|
|
265
|
+
.prepare('SELECT target_dir FROM download_log WHERE package_id = ? ORDER BY downloaded_at DESC LIMIT 1')
|
|
266
|
+
.get(packageId);
|
|
267
|
+
if (row?.target_dir) {
|
|
268
|
+
possibleDirs.unshift(row.target_dir);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let removed = false;
|
|
272
|
+
for (const dir of possibleDirs) {
|
|
273
|
+
if (!existsSync(dir)) continue;
|
|
274
|
+
for (const filename of Object.keys(pkg.files)) {
|
|
275
|
+
const fpath = join(dir, filename);
|
|
276
|
+
if (existsSync(fpath)) {
|
|
277
|
+
try { unlinkSync(fpath); } catch { /* skip */ }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Try to remove empty directory
|
|
281
|
+
try {
|
|
282
|
+
const remaining = readdirSync(dir);
|
|
283
|
+
if (remaining.length === 0) rmdirSync(dir);
|
|
284
|
+
} catch { /* directory not empty or doesn't exist */ }
|
|
285
|
+
removed = true;
|
|
286
|
+
}
|
|
287
|
+
return removed;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// -- Ratings / reviews ----------------------------------------------------
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Add a rating/review for a package.
|
|
294
|
+
* @param {string} packageId
|
|
295
|
+
* @param {string} reviewer
|
|
296
|
+
* @param {number} rating - 1.0 to 5.0
|
|
297
|
+
* @param {string} [comment='']
|
|
298
|
+
* @returns {boolean}
|
|
299
|
+
*/
|
|
300
|
+
rate(packageId, reviewer, rating, comment = '') {
|
|
301
|
+
if (!this.getPackage(packageId)) return false;
|
|
302
|
+
|
|
303
|
+
const clampedRating = Math.max(1.0, Math.min(5.0, rating));
|
|
304
|
+
const reviewId = randomUUID();
|
|
305
|
+
const now = new Date().toISOString();
|
|
306
|
+
|
|
307
|
+
this._db
|
|
308
|
+
.prepare(
|
|
309
|
+
'INSERT INTO reviews (review_id, package_id, reviewer, rating, comment, created_at) VALUES (?, ?, ?, ?, ?, ?)',
|
|
310
|
+
)
|
|
311
|
+
.run(reviewId, packageId, reviewer, clampedRating, comment, now);
|
|
312
|
+
|
|
313
|
+
// Recompute average rating
|
|
314
|
+
const stats = this._db
|
|
315
|
+
.prepare('SELECT AVG(rating) as avg_r, COUNT(*) as cnt FROM reviews WHERE package_id = ?')
|
|
316
|
+
.get(packageId);
|
|
317
|
+
this._db
|
|
318
|
+
.prepare('UPDATE packages SET rating = ?, ratings_count = ? WHERE package_id = ?')
|
|
319
|
+
.run(Math.round(stats.avg_r * 100) / 100, stats.cnt, packageId);
|
|
320
|
+
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get all reviews for a package.
|
|
326
|
+
* @param {string} packageId
|
|
327
|
+
* @returns {object[]}
|
|
328
|
+
*/
|
|
329
|
+
getReviews(packageId) {
|
|
330
|
+
return this._db
|
|
331
|
+
.prepare('SELECT * FROM reviews WHERE package_id = ? ORDER BY created_at DESC')
|
|
332
|
+
.all(packageId)
|
|
333
|
+
.map((r) => ({
|
|
334
|
+
reviewId: r.review_id,
|
|
335
|
+
packageId: r.package_id,
|
|
336
|
+
reviewer: r.reviewer,
|
|
337
|
+
rating: r.rating,
|
|
338
|
+
comment: r.comment,
|
|
339
|
+
createdAt: r.created_at,
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// -- Marketplace index ----------------------------------------------------
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Build a summary index of the marketplace.
|
|
347
|
+
* @returns {object}
|
|
348
|
+
*/
|
|
349
|
+
getIndex() {
|
|
350
|
+
const totalPackages = this._db.prepare('SELECT COUNT(*) as cnt FROM packages').get().cnt;
|
|
351
|
+
const totalDownloads = this._db.prepare('SELECT COALESCE(SUM(downloads), 0) as total FROM packages').get().total;
|
|
352
|
+
|
|
353
|
+
const catRows = this._db.prepare('SELECT domain, COUNT(*) as cnt FROM packages GROUP BY domain').all();
|
|
354
|
+
const categories = {};
|
|
355
|
+
for (const r of catRows) categories[r.domain] = r.cnt;
|
|
356
|
+
|
|
357
|
+
const topRated = this._db
|
|
358
|
+
.prepare('SELECT package_id, name, domain, rating, downloads FROM packages ORDER BY rating DESC LIMIT 10')
|
|
359
|
+
.all();
|
|
360
|
+
const mostDownloaded = this._db
|
|
361
|
+
.prepare('SELECT package_id, name, domain, rating, downloads FROM packages ORDER BY downloads DESC LIMIT 10')
|
|
362
|
+
.all();
|
|
363
|
+
const recentlyUpdated = this._db
|
|
364
|
+
.prepare('SELECT package_id, name, domain, version, updated_at FROM packages ORDER BY updated_at DESC LIMIT 10')
|
|
365
|
+
.all();
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
totalPackages,
|
|
369
|
+
totalDownloads,
|
|
370
|
+
categories,
|
|
371
|
+
topRated,
|
|
372
|
+
mostDownloaded,
|
|
373
|
+
recentlyUpdated,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// -- Export / import as JSON archive ----------------------------------------
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Export a package as a JSON archive string.
|
|
381
|
+
* @param {string} packageId
|
|
382
|
+
* @returns {string} JSON string
|
|
383
|
+
*/
|
|
384
|
+
exportPackage(packageId) {
|
|
385
|
+
const pkg = this.getPackage(packageId);
|
|
386
|
+
if (!pkg) throw new Error(`Package not found: ${packageId}`);
|
|
387
|
+
return JSON.stringify(
|
|
388
|
+
{
|
|
389
|
+
manifest: {
|
|
390
|
+
package_id: pkg.packageId,
|
|
391
|
+
name: pkg.name,
|
|
392
|
+
domain: pkg.domain,
|
|
393
|
+
version: pkg.version,
|
|
394
|
+
description: pkg.description,
|
|
395
|
+
author: pkg.author,
|
|
396
|
+
license: pkg.license,
|
|
397
|
+
tags: pkg.tags,
|
|
398
|
+
checksum: pkg.checksum,
|
|
399
|
+
created_at: pkg.createdAt,
|
|
400
|
+
updated_at: pkg.updatedAt,
|
|
401
|
+
},
|
|
402
|
+
files: pkg.files,
|
|
403
|
+
},
|
|
404
|
+
null,
|
|
405
|
+
2,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Import a package from a JSON archive string.
|
|
411
|
+
* @param {string} jsonStr
|
|
412
|
+
* @returns {object} The imported package object
|
|
413
|
+
*/
|
|
414
|
+
importPackage(jsonStr) {
|
|
415
|
+
const data = JSON.parse(jsonStr);
|
|
416
|
+
const manifest = data.manifest || {};
|
|
417
|
+
const files = data.files || {};
|
|
418
|
+
|
|
419
|
+
// Verify checksum
|
|
420
|
+
const computed = computeChecksum(files);
|
|
421
|
+
if (manifest.checksum && manifest.checksum !== computed) {
|
|
422
|
+
throw new Error(`Checksum mismatch: expected ${manifest.checksum}, got ${computed}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
packageId: manifest.package_id || randomUUID(),
|
|
427
|
+
name: manifest.name || '',
|
|
428
|
+
domain: manifest.domain || '',
|
|
429
|
+
version: manifest.version || '0.1.0',
|
|
430
|
+
description: manifest.description || '',
|
|
431
|
+
author: manifest.author || '',
|
|
432
|
+
license: manifest.license || 'AGPL-3.0',
|
|
433
|
+
tags: manifest.tags || [],
|
|
434
|
+
files,
|
|
435
|
+
checksum: computed,
|
|
436
|
+
createdAt: manifest.created_at || '',
|
|
437
|
+
updatedAt: manifest.updated_at || '',
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// -- Helpers ---------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
_rowToPackage(row) {
|
|
444
|
+
return {
|
|
445
|
+
packageId: row.package_id,
|
|
446
|
+
name: row.name,
|
|
447
|
+
domain: row.domain,
|
|
448
|
+
version: row.version,
|
|
449
|
+
description: row.description,
|
|
450
|
+
author: row.author,
|
|
451
|
+
license: row.license,
|
|
452
|
+
tags: JSON.parse(row.tags),
|
|
453
|
+
files: JSON.parse(row.files),
|
|
454
|
+
checksum: row.checksum,
|
|
455
|
+
downloads: row.downloads,
|
|
456
|
+
rating: row.rating,
|
|
457
|
+
ratingsCount: row.ratings_count,
|
|
458
|
+
createdAt: row.created_at,
|
|
459
|
+
updatedAt: row.updated_at,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Close the database connection. */
|
|
464
|
+
close() {
|
|
465
|
+
this._db.close();
|
|
466
|
+
}
|
|
467
|
+
}
|