monomind 1.11.14 → 1.12.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/.claude/agents/generated/channel-intelligence-director.md +87 -0
- package/.claude/agents/generated/chief-growth-officer.md +88 -0
- package/.claude/agents/generated/content-seo-strategist.md +90 -0
- package/.claude/agents/generated/developer-community-strategist.md +91 -0
- package/.claude/agents/generated/outreach-partnership-strategist.md +90 -0
- package/.claude/agents/generated/social-media-strategist.md +91 -0
- package/.claude/agents/generated/video-visual-strategist.md +90 -0
- package/.claude/commands/mastermind/master.md +1 -1
- package/.claude/helpers/auto-memory-hook.mjs +13 -4
- package/.claude/helpers/control-start.cjs +5 -0
- package/.claude/helpers/event-logger.cjs +114 -0
- package/.claude/helpers/handlers/adr-draft-handler.cjs +19 -5
- package/.claude/helpers/handlers/agent-start-handler.cjs +13 -4
- package/.claude/helpers/handlers/compact-handler.cjs +2 -0
- package/.claude/helpers/handlers/edit-handler.cjs +1 -1
- package/.claude/helpers/handlers/gates-handler.cjs +3 -0
- package/.claude/helpers/handlers/graph-status-handler.cjs +14 -8
- package/.claude/helpers/handlers/loops-status-handler.cjs +5 -2
- package/.claude/helpers/handlers/route-handler.cjs +13 -6
- package/.claude/helpers/handlers/session-handler.cjs +11 -4
- package/.claude/helpers/handlers/session-restore-handler.cjs +21 -11
- package/.claude/helpers/handlers/task-handler.cjs +13 -5
- package/.claude/helpers/intelligence.cjs +7 -2
- package/.claude/helpers/loop-tracker.cjs +15 -3
- package/.claude/helpers/memory.cjs +6 -1
- package/.claude/helpers/router.cjs +5 -2
- package/.claude/helpers/session.cjs +2 -0
- package/.claude/helpers/statusline.cjs +10 -2
- package/.claude/helpers/utils/micro-agents.cjs +20 -4
- package/.claude/scheduled_tasks.lock +1 -1
- package/.claude/settings.json +92 -1
- package/.claude/skills/mastermind/_protocol.md +25 -15
- package/.claude/skills/mastermind/architect.md +3 -3
- package/.claude/skills/mastermind/autodev.md +4 -2
- package/.claude/skills/mastermind/idea.md +10 -0
- package/.claude/skills/mastermind/ops.md +3 -3
- package/.claude/skills/mastermind/runorg.md +153 -86
- package/package.json +20 -3
- package/packages/@monomind/cli/dist/src/agents/registry-builder.js +2 -0
- package/packages/@monomind/cli/dist/src/autopilot-state.js +10 -5
- package/packages/@monomind/cli/dist/src/benchmarks/benchmark-runner.js +13 -0
- package/packages/@monomind/cli/dist/src/benchmarks/metric-evaluators.js +20 -9
- package/packages/@monomind/cli/dist/src/browser/actions.js +10 -3
- package/packages/@monomind/cli/dist/src/browser/browser.js +12 -2
- package/packages/@monomind/cli/dist/src/browser/cdp.js +21 -3
- package/packages/@monomind/cli/dist/src/browser/har.js +27 -5
- package/packages/@monomind/cli/dist/src/commands/agent.js +11 -8
- package/packages/@monomind/cli/dist/src/commands/analyze.js +36 -21
- package/packages/@monomind/cli/dist/src/commands/autopilot.js +12 -4
- package/packages/@monomind/cli/dist/src/commands/benchmark.js +51 -8
- package/packages/@monomind/cli/dist/src/commands/browse.js +5 -2
- package/packages/@monomind/cli/dist/src/commands/claims.js +29 -11
- package/packages/@monomind/cli/dist/src/commands/cleanup.js +25 -5
- package/packages/@monomind/cli/dist/src/commands/config.js +15 -7
- package/packages/@monomind/cli/dist/src/commands/daemon.js +6 -0
- package/packages/@monomind/cli/dist/src/commands/deployment.js +34 -19
- package/packages/@monomind/cli/dist/src/commands/doctor.js +97 -20
- package/packages/@monomind/cli/dist/src/commands/guidance.js +15 -2
- package/packages/@monomind/cli/dist/src/commands/hive-mind.js +37 -14
- package/packages/@monomind/cli/dist/src/commands/hooks.js +42 -25
- package/packages/@monomind/cli/dist/src/commands/init.js +9 -4
- package/packages/@monomind/cli/dist/src/commands/issues.js +29 -26
- package/packages/@monomind/cli/dist/src/commands/mcp.js +11 -5
- package/packages/@monomind/cli/dist/src/commands/memory.js +10 -0
- package/packages/@monomind/cli/dist/src/commands/migrate.js +5 -5
- package/packages/@monomind/cli/dist/src/commands/monograph.js +18 -5
- package/packages/@monomind/cli/dist/src/commands/monovector/backup.js +8 -2
- package/packages/@monomind/cli/dist/src/commands/monovector/benchmark.js +20 -7
- package/packages/@monomind/cli/dist/src/commands/monovector/import.js +15 -0
- package/packages/@monomind/cli/dist/src/commands/monovector/migrate.js +4 -1
- package/packages/@monomind/cli/dist/src/commands/monovector/optimize.js +11 -0
- package/packages/@monomind/cli/dist/src/commands/monovector/setup.js +11 -1
- package/packages/@monomind/cli/dist/src/commands/neural.js +1 -1
- package/packages/@monomind/cli/dist/src/commands/performance.js +20 -7
- package/packages/@monomind/cli/dist/src/commands/platforms.js +90 -8
- package/packages/@monomind/cli/dist/src/commands/plugins.js +12 -5
- package/packages/@monomind/cli/dist/src/commands/process.js +33 -10
- package/packages/@monomind/cli/dist/src/commands/progress.js +5 -3
- package/packages/@monomind/cli/dist/src/commands/providers.js +5 -5
- package/packages/@monomind/cli/dist/src/commands/replay.js +8 -2
- package/packages/@monomind/cli/dist/src/commands/route.js +27 -7
- package/packages/@monomind/cli/dist/src/commands/security.js +4 -0
- package/packages/@monomind/cli/dist/src/commands/session.js +12 -1
- package/packages/@monomind/cli/dist/src/commands/start.js +11 -4
- package/packages/@monomind/cli/dist/src/commands/status.js +7 -4
- package/packages/@monomind/cli/dist/src/commands/swarm.js +27 -13
- package/packages/@monomind/cli/dist/src/commands/task.js +26 -11
- package/packages/@monomind/cli/dist/src/commands/tokens.js +7 -2
- package/packages/@monomind/cli/dist/src/commands/transfer-store.js +36 -22
- package/packages/@monomind/cli/dist/src/commands/update.js +15 -3
- package/packages/@monomind/cli/dist/src/commands/workflow.js +39 -6
- package/packages/@monomind/cli/dist/src/consensus/audit-writer.js +18 -7
- package/packages/@monomind/cli/dist/src/consensus/vote-signer.js +25 -8
- package/packages/@monomind/cli/dist/src/index.js +7 -3
- package/packages/@monomind/cli/dist/src/init/executor.js +14 -11
- package/packages/@monomind/cli/dist/src/init/shared-instructions-generator.js +20 -4
- package/packages/@monomind/cli/dist/src/init/statusline-generator.js +36 -15
- package/packages/@monomind/cli/dist/src/mcp-tools/a2a-tools.js +98 -13
- package/packages/@monomind/cli/dist/src/mcp-tools/agent-tools.js +16 -3
- package/packages/@monomind/cli/dist/src/mcp-tools/analyze-tools.js +80 -17
- package/packages/@monomind/cli/dist/src/mcp-tools/browser-tools.js +84 -22
- package/packages/@monomind/cli/dist/src/mcp-tools/claims-tools.js +35 -7
- package/packages/@monomind/cli/dist/src/mcp-tools/config-tools.js +82 -17
- package/packages/@monomind/cli/dist/src/mcp-tools/coordination-tools.js +37 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/daa-tools.js +49 -7
- package/packages/@monomind/cli/dist/src/mcp-tools/embeddings-tools.js +45 -18
- package/packages/@monomind/cli/dist/src/mcp-tools/github-tools.js +75 -25
- package/packages/@monomind/cli/dist/src/mcp-tools/guidance-tools.js +32 -10
- package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +91 -20
- package/packages/@monomind/cli/dist/src/mcp-tools/hooks-tools.js +188 -29
- package/packages/@monomind/cli/dist/src/mcp-tools/memory-tools.js +25 -7
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +11 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +148 -26
- package/packages/@monomind/cli/dist/src/mcp-tools/neural-tools.js +44 -9
- package/packages/@monomind/cli/dist/src/mcp-tools/performance-tools.js +45 -10
- package/packages/@monomind/cli/dist/src/mcp-tools/progress-tools.js +7 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/request-tracker.js +15 -1
- package/packages/@monomind/cli/dist/src/mcp-tools/security-tools.js +61 -9
- package/packages/@monomind/cli/dist/src/mcp-tools/session-tools.js +45 -14
- package/packages/@monomind/cli/dist/src/mcp-tools/swarm-tools.js +15 -3
- package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +14 -7
- package/packages/@monomind/cli/dist/src/mcp-tools/task-tools.js +52 -10
- package/packages/@monomind/cli/dist/src/mcp-tools/terminal-tools.js +40 -6
- package/packages/@monomind/cli/dist/src/mcp-tools/transfer-tools.js +37 -4
- package/packages/@monomind/cli/dist/src/mcp-tools/workflow-tools.js +29 -6
- package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.js +26 -10
- package/packages/@monomind/cli/dist/src/memory/intelligence.js +80 -19
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +21 -2
- package/packages/@monomind/cli/dist/src/memory/memory-initializer.js +67 -3
- package/packages/@monomind/cli/dist/src/memory/sona-optimizer.js +14 -4
- package/packages/@monomind/cli/dist/src/monovector/command-outcomes.js +43 -7
- package/packages/@monomind/cli/dist/src/monovector/coverage-router.js +8 -4
- package/packages/@monomind/cli/dist/src/monovector/coverage-tools.js +6 -3
- package/packages/@monomind/cli/dist/src/monovector/diff-classifier.js +13 -0
- package/packages/@monomind/cli/dist/src/monovector/route-outcomes.d.ts +2 -1
- package/packages/@monomind/cli/dist/src/monovector/route-outcomes.js +46 -4
- package/packages/@monomind/cli/dist/src/plugins/manager.js +8 -3
- package/packages/@monomind/cli/dist/src/plugins/store/discovery.js +46 -2
- package/packages/@monomind/cli/dist/src/plugins/store/search.js +5 -4
- package/packages/@monomind/cli/dist/src/production/circuit-breaker.js +17 -3
- package/packages/@monomind/cli/dist/src/production/error-handler.js +3 -0
- package/packages/@monomind/cli/dist/src/production/monitoring.js +20 -3
- package/packages/@monomind/cli/dist/src/production/rate-limiter.js +13 -4
- package/packages/@monomind/cli/dist/src/production/retry.js +17 -9
- package/packages/@monomind/cli/dist/src/routing/embed-worker.js +6 -2
- package/packages/@monomind/cli/dist/src/routing/embedder.js +0 -0
- package/packages/@monomind/cli/dist/src/routing/llm-caller.js +13 -2
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +18 -3
- package/packages/@monomind/cli/dist/src/services/claim-service.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/services/claim-service.js +8 -0
- package/packages/@monomind/cli/dist/src/services/config-file-manager.js +14 -2
- package/packages/@monomind/cli/dist/src/services/headless-worker-executor.js +18 -2
- package/packages/@monomind/cli/dist/src/services/worker-daemon.js +53 -12
- package/packages/@monomind/cli/dist/src/transfer/anonymization/index.d.ts +0 -3
- package/packages/@monomind/cli/dist/src/transfer/anonymization/index.js +16 -1
- package/packages/@monomind/cli/dist/src/transfer/export.js +8 -0
- package/packages/@monomind/cli/dist/src/transfer/ipfs/upload.js +33 -3
- package/packages/@monomind/cli/dist/src/transfer/serialization/cfp.js +9 -3
- package/packages/@monomind/cli/dist/src/transfer/storage/gcs.js +37 -3
- package/packages/@monomind/cli/dist/src/transfer/store/discovery.js +45 -3
- package/packages/@monomind/cli/dist/src/transfer/store/download.js +5 -0
- package/packages/@monomind/cli/dist/src/transfer/store/publish.js +13 -1
- package/packages/@monomind/cli/dist/src/transfer/store/registry.d.ts +8 -0
- package/packages/@monomind/cli/dist/src/transfer/store/registry.js +30 -5
- package/packages/@monomind/cli/dist/src/transfer/store/search.js +20 -5
- package/packages/@monomind/cli/dist/src/update/checker.js +59 -7
- package/packages/@monomind/cli/dist/src/update/executor.js +50 -3
- package/packages/@monomind/cli/dist/src/update/index.js +18 -1
- package/packages/@monomind/cli/dist/src/update/rate-limiter.d.ts +6 -0
- package/packages/@monomind/cli/dist/src/update/rate-limiter.js +79 -7
- package/packages/@monomind/cli/dist/src/update/validator.js +52 -1
- package/packages/@monomind/cli/package.json +2 -3
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Shared JSON config file persistence with atomic writes and Zod validation
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs';
|
|
6
|
+
import * as os from 'os';
|
|
6
7
|
import * as path from 'path';
|
|
7
8
|
/** Config file search paths in priority order */
|
|
8
9
|
const CONFIG_FILENAMES = [
|
|
@@ -123,7 +124,7 @@ export class ConfigFileManager {
|
|
|
123
124
|
* drop the first writer's API key.
|
|
124
125
|
*/
|
|
125
126
|
set(cwd, key, value) {
|
|
126
|
-
const KNOWN_SET_SECTIONS = new Set(['version', 'agents', 'swarm', 'memory', 'mcp', 'cli', 'hooks', 'neural']);
|
|
127
|
+
const KNOWN_SET_SECTIONS = new Set(['version', 'agents', 'swarm', 'memory', 'mcp', 'cli', 'hooks', 'neural', 'providers']);
|
|
127
128
|
const topSection = String(key).split('.')[0];
|
|
128
129
|
if (!KNOWN_SET_SECTIONS.has(topSection)) {
|
|
129
130
|
throw new Error(`Unknown config section: "${topSection}". Allowed: ${[...KNOWN_SET_SECTIONS].join(', ')}`);
|
|
@@ -181,6 +182,17 @@ export class ConfigFileManager {
|
|
|
181
182
|
/** Import config from a specific path */
|
|
182
183
|
importFrom(cwd, importPath) {
|
|
183
184
|
const resolved = path.resolve(cwd, importPath);
|
|
185
|
+
// Guard against path traversal: the resolved path must be within the
|
|
186
|
+
// project cwd or the user's home directory. Without this check an
|
|
187
|
+
// automated script can pass "/etc/passwd" or "../../.env" and exfiltrate
|
|
188
|
+
// files outside the project tree.
|
|
189
|
+
const projectRoot = path.resolve(cwd);
|
|
190
|
+
const home = os.homedir();
|
|
191
|
+
const isUnderProject = resolved === projectRoot || resolved.startsWith(projectRoot + path.sep);
|
|
192
|
+
const isUnderHome = resolved === home || resolved.startsWith(home + path.sep);
|
|
193
|
+
if (!isUnderProject && !isUnderHome) {
|
|
194
|
+
throw new Error(`Import path must be within the project directory or home directory: ${resolved}`);
|
|
195
|
+
}
|
|
184
196
|
if (!fs.existsSync(resolved)) {
|
|
185
197
|
throw new Error(`Import file not found: ${resolved}`);
|
|
186
198
|
}
|
|
@@ -199,7 +211,7 @@ export class ConfigFileManager {
|
|
|
199
211
|
// KNOWN_SECTIONS only validates top-level keys, leaving nested
|
|
200
212
|
// {agents:{providers:[{__proto__:{...}}]}} unsanitized.
|
|
201
213
|
const imported = sanitizeConfigObject(importedRaw);
|
|
202
|
-
const KNOWN_SECTIONS = new Set(['version', 'agents', 'swarm', 'memory', 'mcp', 'cli', 'hooks']);
|
|
214
|
+
const KNOWN_SECTIONS = new Set(['version', 'agents', 'swarm', 'memory', 'mcp', 'cli', 'hooks', 'neural', 'providers']);
|
|
203
215
|
for (const key of Object.keys(imported)) {
|
|
204
216
|
if (!KNOWN_SECTIONS.has(key)) {
|
|
205
217
|
throw new Error(`Unknown config section: "${key}"`);
|
|
@@ -1017,6 +1017,12 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
1017
1017
|
let stdout = '';
|
|
1018
1018
|
let stderr = '';
|
|
1019
1019
|
let resolved = false;
|
|
1020
|
+
// Cap stdout + stderr to 10 MB each to prevent OOM if a spawned Claude
|
|
1021
|
+
// Code process emits unexpectedly large output (e.g. a worker that dumps
|
|
1022
|
+
// a large file listing). Once the cap is reached we stop appending; the
|
|
1023
|
+
// truncation marker lets callers detect that output was cut.
|
|
1024
|
+
const MAX_HEADLESS_OUTPUT_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
1025
|
+
const TRUNCATION_MARKER = '\n[... output truncated at 10 MB ...]';
|
|
1020
1026
|
const cleanup = () => {
|
|
1021
1027
|
clearTimeout(timeoutHandle);
|
|
1022
1028
|
clearTimeout(sigkillTimer);
|
|
@@ -1024,7 +1030,12 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
1024
1030
|
};
|
|
1025
1031
|
child.stdout?.on('data', (data) => {
|
|
1026
1032
|
const chunk = data.toString();
|
|
1027
|
-
stdout
|
|
1033
|
+
if (stdout.length < MAX_HEADLESS_OUTPUT_BYTES) {
|
|
1034
|
+
stdout += chunk;
|
|
1035
|
+
if (stdout.length >= MAX_HEADLESS_OUTPUT_BYTES) {
|
|
1036
|
+
stdout = stdout.slice(0, MAX_HEADLESS_OUTPUT_BYTES) + TRUNCATION_MARKER;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1028
1039
|
this.emit('output', {
|
|
1029
1040
|
executionId: options.executionId,
|
|
1030
1041
|
type: 'stdout',
|
|
@@ -1033,7 +1044,12 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
1033
1044
|
});
|
|
1034
1045
|
child.stderr?.on('data', (data) => {
|
|
1035
1046
|
const chunk = data.toString();
|
|
1036
|
-
stderr
|
|
1047
|
+
if (stderr.length < MAX_HEADLESS_OUTPUT_BYTES) {
|
|
1048
|
+
stderr += chunk;
|
|
1049
|
+
if (stderr.length >= MAX_HEADLESS_OUTPUT_BYTES) {
|
|
1050
|
+
stderr = stderr.slice(0, MAX_HEADLESS_OUTPUT_BYTES) + TRUNCATION_MARKER;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1037
1053
|
this.emit('output', {
|
|
1038
1054
|
executionId: options.executionId,
|
|
1039
1055
|
type: 'stderr',
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - testgaps: Test coverage analysis (20 min interval)
|
|
11
11
|
*/
|
|
12
12
|
import { EventEmitter } from 'events';
|
|
13
|
-
import { existsSync, mkdirSync, writeFileSync, renameSync, readFileSync, appendFileSync, unlinkSync } from 'fs';
|
|
13
|
+
import { existsSync, mkdirSync, writeFileSync, renameSync, readFileSync, appendFileSync, unlinkSync, statSync } from 'fs';
|
|
14
14
|
import { cpus } from 'os';
|
|
15
15
|
import { join, resolve } from 'path';
|
|
16
16
|
import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
|
|
@@ -106,14 +106,15 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
106
106
|
this.headlessAvailable = await this.headlessExecutor.isAvailable();
|
|
107
107
|
if (this.headlessAvailable) {
|
|
108
108
|
this.log('info', 'Claude Code headless mode available - AI workers enabled');
|
|
109
|
-
// Forward headless executor events
|
|
110
|
-
|
|
109
|
+
// Forward headless executor events.
|
|
110
|
+
// The executor emits 'start', 'complete', 'error' (not 'execution:*').
|
|
111
|
+
this.headlessExecutor.on('start', (data) => {
|
|
111
112
|
this.emit('headless:start', data);
|
|
112
113
|
});
|
|
113
|
-
this.headlessExecutor.on('
|
|
114
|
+
this.headlessExecutor.on('complete', (data) => {
|
|
114
115
|
this.emit('headless:complete', data);
|
|
115
116
|
});
|
|
116
|
-
this.headlessExecutor.on('
|
|
117
|
+
this.headlessExecutor.on('error', (data) => {
|
|
117
118
|
this.emit('headless:error', data);
|
|
118
119
|
});
|
|
119
120
|
this.headlessExecutor.on('output', (data) => {
|
|
@@ -189,6 +190,12 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
189
190
|
return {};
|
|
190
191
|
}
|
|
191
192
|
try {
|
|
193
|
+
// Guard against OOM from an oversized config file (tampered or corrupted).
|
|
194
|
+
const configSize = statSync(configPath).size;
|
|
195
|
+
if (configSize > 1_048_576 /* 1 MB */) {
|
|
196
|
+
this.log('warn', `config.json is unusually large (${configSize} bytes) — ignoring daemon config`);
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
192
199
|
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
193
200
|
// Support both flat keys at root and nested under scopes.project
|
|
194
201
|
const cfg = raw?.scopes?.project ?? raw;
|
|
@@ -271,6 +278,12 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
271
278
|
// Try to restore state from file
|
|
272
279
|
if (existsSync(this.config.stateFile)) {
|
|
273
280
|
try {
|
|
281
|
+
// Guard against OOM from an oversized state file (tampered or corrupted).
|
|
282
|
+
const stateSize = statSync(this.config.stateFile).size;
|
|
283
|
+
if (stateSize > 5_242_880 /* 5 MB */) {
|
|
284
|
+
this.log('warn', `daemon-state.json is unusually large (${stateSize} bytes) — starting with fresh state`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
274
287
|
const saved = JSON.parse(readFileSync(this.config.stateFile, 'utf-8'));
|
|
275
288
|
// CRITICAL: Restore worker config (including enabled flag) from saved state
|
|
276
289
|
// This fixes #950: daemon enable command not persisting worker state
|
|
@@ -555,6 +568,17 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
555
568
|
const state = this.workers.get(workerConfig.type);
|
|
556
569
|
const workerId = `${workerConfig.type}_${Date.now()}`;
|
|
557
570
|
const startTime = Date.now();
|
|
571
|
+
// Track the headless executionId assigned by HeadlessWorkerExecutor so we
|
|
572
|
+
// can cancel it precisely on timeout. The executor generates its own ID
|
|
573
|
+
// (format: `${type}_${ts}_${random}`) which never matches workerId, so we
|
|
574
|
+
// capture it from the 'start' event instead.
|
|
575
|
+
let headlessExecutionId = null;
|
|
576
|
+
const onHeadlessStart = (data) => {
|
|
577
|
+
headlessExecutionId = data.executionId ?? null;
|
|
578
|
+
};
|
|
579
|
+
if (this.headlessExecutor) {
|
|
580
|
+
this.headlessExecutor.on('start', onHeadlessStart);
|
|
581
|
+
}
|
|
558
582
|
// Track running worker
|
|
559
583
|
this.runningWorkers.add(workerConfig.type);
|
|
560
584
|
state.isRunning = true;
|
|
@@ -566,11 +590,13 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
566
590
|
// cancelAll() was too broad — it would kill concurrent healthy workers.
|
|
567
591
|
const output = await this.runWithTimeout(() => this.runWorkerLogic(workerConfig), this.config.workerTimeoutMs, `Worker ${workerConfig.type} timed out after ${this.config.workerTimeoutMs / 1000}s`, () => {
|
|
568
592
|
if (this.headlessExecutor) {
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
593
|
+
// Use the exact executionId captured from the 'start' event.
|
|
594
|
+
// Fall back to cancelByType only if we didn't capture an ID yet
|
|
595
|
+
// (e.g. timeout fired before the executor emitted 'start').
|
|
596
|
+
if (headlessExecutionId && this.headlessExecutor.cancel(headlessExecutionId)) {
|
|
597
|
+
return;
|
|
573
598
|
}
|
|
599
|
+
this.headlessExecutor.cancelByType(workerConfig.type);
|
|
574
600
|
}
|
|
575
601
|
});
|
|
576
602
|
const durationMs = Date.now() - startTime;
|
|
@@ -615,6 +641,10 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
615
641
|
finally {
|
|
616
642
|
// Remove from running set and process queue
|
|
617
643
|
this.runningWorkers.delete(workerConfig.type);
|
|
644
|
+
// Unsubscribe the executionId capture listener regardless of outcome
|
|
645
|
+
if (this.headlessExecutor) {
|
|
646
|
+
this.headlessExecutor.off('start', onHeadlessStart);
|
|
647
|
+
}
|
|
618
648
|
this.processPendingWorkers();
|
|
619
649
|
}
|
|
620
650
|
}
|
|
@@ -752,7 +782,7 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
752
782
|
checks: {
|
|
753
783
|
envFilesProtected: !existsSync(join(this.projectRoot, '.env.local')),
|
|
754
784
|
gitIgnoreExists: existsSync(join(this.projectRoot, '.gitignore')),
|
|
755
|
-
noHardcodedSecrets:
|
|
785
|
+
noHardcodedSecrets: null, // Not checked in local mode — requires AI-powered scan
|
|
756
786
|
},
|
|
757
787
|
riskLevel: 'low',
|
|
758
788
|
recommendations: [],
|
|
@@ -779,8 +809,8 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
779
809
|
memoryUsage: process.memoryUsage(),
|
|
780
810
|
uptime: process.uptime(),
|
|
781
811
|
optimizations: {
|
|
782
|
-
cacheHitRate:
|
|
783
|
-
avgResponseTime:
|
|
812
|
+
cacheHitRate: null, // Not measured in local mode — requires AI-powered analysis
|
|
813
|
+
avgResponseTime: null, // Not measured in local mode — requires AI-powered analysis
|
|
784
814
|
},
|
|
785
815
|
note: 'Install Claude Code CLI for AI-powered optimization suggestions',
|
|
786
816
|
};
|
|
@@ -1042,6 +1072,17 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
1042
1072
|
try {
|
|
1043
1073
|
const logFile = join(this.config.logDir, 'daemon.log');
|
|
1044
1074
|
appendFileSync(logFile, logMessage + '\n');
|
|
1075
|
+
// Opportunistic rotation: keep the log under 10 MB. When exceeded,
|
|
1076
|
+
// discard the oldest half so recent entries are always retained.
|
|
1077
|
+
const MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
1078
|
+
if (statSync(logFile).size > MAX_LOG_BYTES) {
|
|
1079
|
+
const content = readFileSync(logFile, 'utf-8');
|
|
1080
|
+
const lines = content.split('\n').filter(Boolean);
|
|
1081
|
+
const trimmed = lines.slice(Math.floor(lines.length / 2)).join('\n') + '\n';
|
|
1082
|
+
const tmp = `${logFile}.${process.pid}.${Date.now()}.tmp`;
|
|
1083
|
+
writeFileSync(tmp, trimmed);
|
|
1084
|
+
renameSync(tmp, logFile);
|
|
1085
|
+
}
|
|
1045
1086
|
}
|
|
1046
1087
|
catch {
|
|
1047
1088
|
// Ignore log write errors
|
|
@@ -11,9 +11,6 @@ export declare function detectPII(content: string): PIIDetectionResult;
|
|
|
11
11
|
* Redact PII from a string
|
|
12
12
|
*/
|
|
13
13
|
export declare function redactPII(content: string): string;
|
|
14
|
-
/**
|
|
15
|
-
* Apply anonymization to CFP document
|
|
16
|
-
*/
|
|
17
14
|
export declare function anonymizeCFP(cfp: CFPFormat, level: AnonymizationLevel): {
|
|
18
15
|
cfp: CFPFormat;
|
|
19
16
|
transforms: string[];
|
|
@@ -35,10 +35,15 @@ const REDACTIONS = {
|
|
|
35
35
|
function hash(input) {
|
|
36
36
|
return crypto.createHash('sha256').update(input).digest('hex');
|
|
37
37
|
}
|
|
38
|
+
/** Maximum content size for PII scanning/redaction (4 MB). */
|
|
39
|
+
const MAX_SCAN_SIZE = 4 * 1024 * 1024;
|
|
38
40
|
/**
|
|
39
41
|
* Detect PII in a string
|
|
40
42
|
*/
|
|
41
43
|
export function detectPII(content) {
|
|
44
|
+
if (content.length > MAX_SCAN_SIZE) {
|
|
45
|
+
throw new Error(`detectPII: content too large (${content.length} bytes; max ${MAX_SCAN_SIZE})`);
|
|
46
|
+
}
|
|
42
47
|
const result = {
|
|
43
48
|
found: false,
|
|
44
49
|
count: 0,
|
|
@@ -85,6 +90,9 @@ function getSeverity(type) {
|
|
|
85
90
|
* Redact PII from a string
|
|
86
91
|
*/
|
|
87
92
|
export function redactPII(content) {
|
|
93
|
+
if (content.length > MAX_SCAN_SIZE) {
|
|
94
|
+
throw new Error(`redactPII: content too large (${content.length} bytes; max ${MAX_SCAN_SIZE})`);
|
|
95
|
+
}
|
|
88
96
|
let result = content;
|
|
89
97
|
for (const [type, pattern] of Object.entries(PII_PATTERNS)) {
|
|
90
98
|
const replacement = REDACTIONS[type];
|
|
@@ -100,9 +108,16 @@ export function redactPII(content) {
|
|
|
100
108
|
/**
|
|
101
109
|
* Apply anonymization to CFP document
|
|
102
110
|
*/
|
|
111
|
+
/** Maximum CFP payload size accepted for anonymization (10 MB). */
|
|
112
|
+
const MAX_CFP_ANONYMIZE_SIZE = 10 * 1024 * 1024;
|
|
103
113
|
export function anonymizeCFP(cfp, level) {
|
|
114
|
+
// Guard before deep clone to prevent OOM on a crafted large object
|
|
115
|
+
const serialized = JSON.stringify(cfp);
|
|
116
|
+
if (serialized.length > MAX_CFP_ANONYMIZE_SIZE) {
|
|
117
|
+
throw new Error(`anonymizeCFP: CFP payload too large (${serialized.length} bytes; max ${MAX_CFP_ANONYMIZE_SIZE})`);
|
|
118
|
+
}
|
|
104
119
|
const transforms = [];
|
|
105
|
-
const anonymized = JSON.parse(
|
|
120
|
+
const anonymized = JSON.parse(serialized);
|
|
106
121
|
// Level: Minimal
|
|
107
122
|
if (['minimal', 'standard', 'strict', 'paranoid'].includes(level)) {
|
|
108
123
|
// Redact author display name
|
|
@@ -49,6 +49,14 @@ export async function exportPatterns(cfp, options = {}) {
|
|
|
49
49
|
// Write to file
|
|
50
50
|
const ext = getFileExtension(format);
|
|
51
51
|
outputPath = output.endsWith(ext) ? output : output + ext;
|
|
52
|
+
// Path traversal guard — output must resolve within the current working
|
|
53
|
+
// directory to prevent callers from writing to arbitrary filesystem paths.
|
|
54
|
+
const projectRoot = path.resolve(process.cwd());
|
|
55
|
+
const resolvedOutputPath = path.resolve(process.cwd(), outputPath);
|
|
56
|
+
if (!resolvedOutputPath.startsWith(projectRoot + path.sep) && resolvedOutputPath !== projectRoot) {
|
|
57
|
+
throw new Error(`Output path must resolve within the project directory: ${projectRoot}`);
|
|
58
|
+
}
|
|
59
|
+
outputPath = resolvedOutputPath;
|
|
52
60
|
// Ensure directory exists
|
|
53
61
|
const dir = path.dirname(outputPath);
|
|
54
62
|
if (!fs.existsSync(dir)) {
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
* @version 3.0.0
|
|
7
7
|
*/
|
|
8
8
|
import * as crypto from 'crypto';
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize a user-supplied filename before embedding it inside a multipart
|
|
11
|
+
* Content-Disposition header. Strip all CR/LF characters (MIME header injection)
|
|
12
|
+
* and cap length to prevent oversized headers.
|
|
13
|
+
*/
|
|
14
|
+
function sanitizeFileName(name) {
|
|
15
|
+
return name.replace(/[\r\n]/g, '').slice(0, 200);
|
|
16
|
+
}
|
|
9
17
|
/**
|
|
10
18
|
* Get web3.storage token from environment or config
|
|
11
19
|
*/
|
|
@@ -42,7 +50,7 @@ async function uploadToWeb3Storage(content, options) {
|
|
|
42
50
|
'Get a free token at: https://web3.storage');
|
|
43
51
|
}
|
|
44
52
|
const endpoint = options.endpoint || 'https://api.web3.storage';
|
|
45
|
-
const name = options.name || 'pattern.cfp.json';
|
|
53
|
+
const name = sanitizeFileName(options.name || 'pattern.cfp.json');
|
|
46
54
|
console.log(`[IPFS] Uploading ${content.length} bytes to web3.storage...`);
|
|
47
55
|
// Create FormData-like body for upload
|
|
48
56
|
const boundary = '----WebKitFormBoundary' + crypto.randomBytes(16).toString('hex');
|
|
@@ -91,7 +99,7 @@ async function uploadToPinata(content, options) {
|
|
|
91
99
|
throw new Error('Pinata API credentials not found. Set PINATA_API_KEY and PINATA_API_SECRET.\n' +
|
|
92
100
|
'Get credentials at: https://pinata.cloud');
|
|
93
101
|
}
|
|
94
|
-
const name = options.name || 'pattern.cfp.json';
|
|
102
|
+
const name = sanitizeFileName(options.name || 'pattern.cfp.json');
|
|
95
103
|
console.log(`[IPFS] Uploading ${content.length} bytes to Pinata...`);
|
|
96
104
|
const boundary = '----WebKitFormBoundary' + crypto.randomBytes(16).toString('hex');
|
|
97
105
|
const metadata = JSON.stringify({ name });
|
|
@@ -277,14 +285,36 @@ export async function unpinContent(cid, options = {}) {
|
|
|
277
285
|
console.log(`[IPFS] Demo unpinned`);
|
|
278
286
|
return { success: true };
|
|
279
287
|
}
|
|
288
|
+
/** Allowlisted IPFS gateway hosts for SSRF prevention */
|
|
289
|
+
const ALLOWED_GATEWAY_HOSTS_UPLOAD = new Set([
|
|
290
|
+
'w3s.link',
|
|
291
|
+
'gateway.pinata.cloud',
|
|
292
|
+
'cloudflare-ipfs.com',
|
|
293
|
+
'ipfs.io',
|
|
294
|
+
'dweb.link',
|
|
295
|
+
]);
|
|
296
|
+
function isAllowedGateway(gateway) {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = new URL(gateway);
|
|
299
|
+
return parsed.protocol === 'https:' && ALLOWED_GATEWAY_HOSTS_UPLOAD.has(parsed.hostname);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
280
305
|
/**
|
|
281
306
|
* Check if content exists on IPFS
|
|
282
307
|
*/
|
|
283
308
|
export async function checkContent(cid, gateway = 'https://w3s.link') {
|
|
309
|
+
if (!isAllowedGateway(gateway)) {
|
|
310
|
+
console.warn(`[IPFS] Blocked checkContent: gateway not in allowlist: ${gateway}`);
|
|
311
|
+
return { exists: false };
|
|
312
|
+
}
|
|
284
313
|
console.log(`[IPFS] Checking ${cid}...`);
|
|
285
314
|
try {
|
|
286
315
|
const response = await fetch(`${gateway}/ipfs/${cid}`, {
|
|
287
316
|
method: 'HEAD',
|
|
317
|
+
signal: AbortSignal.timeout(10000),
|
|
288
318
|
});
|
|
289
319
|
if (response.ok) {
|
|
290
320
|
const size = parseInt(response.headers.get('content-length') || '0', 10);
|
|
@@ -315,7 +345,7 @@ export function getIPNSURL(name, gateway = 'https://w3s.link') {
|
|
|
315
345
|
*/
|
|
316
346
|
async function uploadToLocalIPFS(content, options) {
|
|
317
347
|
const apiUrl = process.env.IPFS_API_URL || 'http://localhost:5001';
|
|
318
|
-
const name = options.name || 'pattern.cfp.json';
|
|
348
|
+
const name = sanitizeFileName(options.name || 'pattern.cfp.json');
|
|
319
349
|
console.log(`[IPFS] Uploading ${content.length} bytes to ${apiUrl}...`);
|
|
320
350
|
const boundary = '----IPFSBoundary' + crypto.randomBytes(16).toString('hex');
|
|
321
351
|
const body = Buffer.concat([
|
|
@@ -107,11 +107,17 @@ export function serializeToBuffer(cfp, format) {
|
|
|
107
107
|
return Buffer.from(json, 'utf-8');
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
/** Maximum CFP payload size (10 MB) — prevents OOM on crafted inputs. */
|
|
111
|
+
const MAX_CFP_SIZE = 10 * 1024 * 1024;
|
|
110
112
|
/**
|
|
111
113
|
* Deserialize CFP from string/buffer
|
|
112
114
|
*/
|
|
113
115
|
export function deserializeCFP(data) {
|
|
114
116
|
const str = typeof data === 'string' ? data : data.toString('utf-8');
|
|
117
|
+
// Guard against OOM before parsing
|
|
118
|
+
if (str.length > MAX_CFP_SIZE) {
|
|
119
|
+
throw new Error(`CFP payload too large (${str.length} bytes; max ${MAX_CFP_SIZE})`);
|
|
120
|
+
}
|
|
115
121
|
let parsed;
|
|
116
122
|
try {
|
|
117
123
|
parsed = JSON.parse(str);
|
|
@@ -119,9 +125,9 @@ export function deserializeCFP(data) {
|
|
|
119
125
|
catch (e) {
|
|
120
126
|
throw new Error(`Invalid CFP file: ${e instanceof Error ? e.message : String(e)}`);
|
|
121
127
|
}
|
|
122
|
-
// Validate magic bytes
|
|
128
|
+
// Validate magic bytes — use a fixed message to avoid reflecting arbitrary input
|
|
123
129
|
if (parsed.magic !== 'CFP1') {
|
|
124
|
-
throw new Error(
|
|
130
|
+
throw new Error('Invalid CFP format: unexpected magic bytes');
|
|
125
131
|
}
|
|
126
132
|
return parsed;
|
|
127
133
|
}
|
|
@@ -131,7 +137,7 @@ export function deserializeCFP(data) {
|
|
|
131
137
|
export function validateCFP(cfp) {
|
|
132
138
|
const errors = [];
|
|
133
139
|
if (cfp.magic !== 'CFP1') {
|
|
134
|
-
errors.push(
|
|
140
|
+
errors.push('Invalid magic bytes');
|
|
135
141
|
}
|
|
136
142
|
if (!cfp.version) {
|
|
137
143
|
errors.push('Missing version');
|
|
@@ -105,7 +105,24 @@ export async function uploadToGCS(content, options = {}) {
|
|
|
105
105
|
execFileSync('gcloud', uploadArgs, { encoding: 'utf-8', stdio: 'pipe', timeout: 60000 });
|
|
106
106
|
// Set metadata if provided
|
|
107
107
|
if (options.metadata && Object.keys(options.metadata).length > 0) {
|
|
108
|
-
|
|
108
|
+
// Cap metadata to prevent unbounded flag values; restrict key charset to
|
|
109
|
+
// alphanumeric/dash/underscore to avoid injection in --custom-metadata=<json>.
|
|
110
|
+
const MAX_META_KEY_LEN = 128;
|
|
111
|
+
const MAX_META_VAL_LEN = 512;
|
|
112
|
+
const MAX_META_ENTRIES = 20;
|
|
113
|
+
const safeMetadata = {};
|
|
114
|
+
let metaCount = 0;
|
|
115
|
+
for (const [k, v] of Object.entries(options.metadata)) {
|
|
116
|
+
if (metaCount >= MAX_META_ENTRIES)
|
|
117
|
+
break;
|
|
118
|
+
if (typeof k !== 'string' || typeof v !== 'string')
|
|
119
|
+
continue;
|
|
120
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(k))
|
|
121
|
+
continue;
|
|
122
|
+
safeMetadata[k.slice(0, MAX_META_KEY_LEN)] = v.slice(0, MAX_META_VAL_LEN);
|
|
123
|
+
metaCount++;
|
|
124
|
+
}
|
|
125
|
+
const metadataJson = JSON.stringify(safeMetadata);
|
|
109
126
|
try {
|
|
110
127
|
const metaArgs = ['storage', 'objects', 'update', `gs://${config.bucket}/${objectPath}`, `--custom-metadata=${metadataJson}`];
|
|
111
128
|
if (config.projectId)
|
|
@@ -164,6 +181,15 @@ export async function downloadFromGCS(uri, config) {
|
|
|
164
181
|
if (cfg?.projectId)
|
|
165
182
|
downloadArgs.push(`--project=${cfg.projectId}`);
|
|
166
183
|
execFileSync('gcloud', downloadArgs, { encoding: 'utf-8', stdio: 'pipe' });
|
|
184
|
+
const MAX_GCS_DOWNLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
185
|
+
const fileSize = fs.statSync(tempFile).size;
|
|
186
|
+
if (fileSize > MAX_GCS_DOWNLOAD_BYTES) {
|
|
187
|
+
const resolvedTemp2 = path.resolve(tempFile);
|
|
188
|
+
if (resolvedTemp2.startsWith(path.resolve(tempDir)))
|
|
189
|
+
fs.unlinkSync(tempFile);
|
|
190
|
+
console.error(`[GCS] Downloaded file exceeds size limit (${fileSize} > ${MAX_GCS_DOWNLOAD_BYTES} bytes)`);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
167
193
|
const content = fs.readFileSync(tempFile);
|
|
168
194
|
const resolvedTemp = path.resolve(tempFile);
|
|
169
195
|
if (resolvedTemp.startsWith(path.resolve(tempDir))) {
|
|
@@ -215,9 +241,17 @@ export async function listGCSObjects(prefix, config) {
|
|
|
215
241
|
const listArgs = ['storage', 'ls', '-l', uri, '--format=json'];
|
|
216
242
|
if (cfg.projectId)
|
|
217
243
|
listArgs.push(`--project=${cfg.projectId}`);
|
|
218
|
-
const result = execFileSync('gcloud', listArgs, { encoding: 'utf-8', stdio: 'pipe' });
|
|
244
|
+
const result = execFileSync('gcloud', listArgs, { encoding: 'utf-8', stdio: 'pipe', maxBuffer: 10 * 1024 * 1024 });
|
|
245
|
+
// Guard against gcloud returning a huge JSON payload that could OOM Node.
|
|
246
|
+
const MAX_LIST_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
247
|
+
if (result.length > MAX_LIST_BYTES) {
|
|
248
|
+
console.error(`[GCS] listGCSObjects response too large (${result.length} bytes), truncating`);
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
219
251
|
const objects = JSON.parse(result);
|
|
220
|
-
|
|
252
|
+
if (!Array.isArray(objects))
|
|
253
|
+
return [];
|
|
254
|
+
return objects.slice(0, 10_000).map((obj) => ({
|
|
221
255
|
name: obj.name,
|
|
222
256
|
size: obj.size || 0,
|
|
223
257
|
updated: obj.updated || new Date().toISOString(),
|
|
@@ -3,6 +3,46 @@
|
|
|
3
3
|
* Secure discovery mechanism for finding patterns in decentralized environment
|
|
4
4
|
*/
|
|
5
5
|
import * as crypto from 'crypto';
|
|
6
|
+
/** Maximum bytes read from any IPFS gateway response to prevent OOM */
|
|
7
|
+
const MAX_DISCOVERY_RESPONSE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
8
|
+
/**
|
|
9
|
+
* Read a fetch response body with a hard byte cap.
|
|
10
|
+
* Aborts the stream early if the limit is exceeded.
|
|
11
|
+
*/
|
|
12
|
+
async function readBodyCapped(response, maxBytes = MAX_DISCOVERY_RESPONSE_BYTES) {
|
|
13
|
+
const lengthHeader = response.headers.get('content-length');
|
|
14
|
+
if (lengthHeader) {
|
|
15
|
+
const declared = parseInt(lengthHeader, 10);
|
|
16
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
17
|
+
throw new Error(`Response too large: ${declared} bytes (max ${maxBytes})`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const reader = response.body?.getReader();
|
|
21
|
+
if (!reader)
|
|
22
|
+
return '';
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let total = 0;
|
|
25
|
+
while (true) {
|
|
26
|
+
const { done, value } = await reader.read();
|
|
27
|
+
if (done)
|
|
28
|
+
break;
|
|
29
|
+
if (value) {
|
|
30
|
+
total += value.byteLength;
|
|
31
|
+
if (total > maxBytes) {
|
|
32
|
+
await reader.cancel();
|
|
33
|
+
throw new Error(`Response too large: exceeded ${maxBytes} bytes`);
|
|
34
|
+
}
|
|
35
|
+
chunks.push(value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const combined = new Uint8Array(total);
|
|
39
|
+
let offset = 0;
|
|
40
|
+
for (const c of chunks) {
|
|
41
|
+
combined.set(c, offset);
|
|
42
|
+
offset += c.byteLength;
|
|
43
|
+
}
|
|
44
|
+
return new TextDecoder('utf-8').decode(combined);
|
|
45
|
+
}
|
|
6
46
|
import { DEFAULT_STORE_CONFIG, } from './registry.js';
|
|
7
47
|
/**
|
|
8
48
|
* Pattern Store Discovery Service
|
|
@@ -136,7 +176,8 @@ export class PatternDiscovery {
|
|
|
136
176
|
signal: AbortSignal.timeout(10000),
|
|
137
177
|
});
|
|
138
178
|
if (response.ok) {
|
|
139
|
-
const
|
|
179
|
+
const text = await readBodyCapped(response, 64 * 1024); // IPNS resolve is tiny
|
|
180
|
+
const data = JSON.parse(text);
|
|
140
181
|
const cid = data.Path?.replace('/ipfs/', '') || '';
|
|
141
182
|
if (cid) {
|
|
142
183
|
const resolution = {
|
|
@@ -226,7 +267,7 @@ export class PatternDiscovery {
|
|
|
226
267
|
signal: AbortSignal.timeout(30000),
|
|
227
268
|
});
|
|
228
269
|
if (response.ok) {
|
|
229
|
-
const text = await response
|
|
270
|
+
const text = await readBodyCapped(response);
|
|
230
271
|
try {
|
|
231
272
|
const registry = JSON.parse(text);
|
|
232
273
|
console.log(`[Discovery] Fetched registry with ${registry.patterns?.length || 0} patterns`);
|
|
@@ -257,7 +298,8 @@ export class PatternDiscovery {
|
|
|
257
298
|
signal: AbortSignal.timeout(15000),
|
|
258
299
|
});
|
|
259
300
|
if (response.ok) {
|
|
260
|
-
const
|
|
301
|
+
const altText = await readBodyCapped(response);
|
|
302
|
+
const registry = JSON.parse(altText);
|
|
261
303
|
console.log(`[Discovery] Fetched registry from ${altGateway}`);
|
|
262
304
|
return registry;
|
|
263
305
|
}
|
|
@@ -11,6 +11,7 @@ import { DEFAULT_STORE_CONFIG } from './registry.js';
|
|
|
11
11
|
* Handles secure download and verification of patterns
|
|
12
12
|
*/
|
|
13
13
|
const MAX_DOWNLOAD_CACHE = 500;
|
|
14
|
+
const MAX_PATTERN_FILE_BYTES = 50 * 1024 * 1024; // 50 MB — matches client.ts MAX_IPFS_RESPONSE_BYTES
|
|
14
15
|
const ALLOWED_GATEWAYS = new Set([
|
|
15
16
|
'https://w3s.link',
|
|
16
17
|
'https://dweb.link',
|
|
@@ -362,6 +363,10 @@ export class PatternDownloader {
|
|
|
362
363
|
async importPattern(filePath, strategy = 'merge') {
|
|
363
364
|
console.log(`[Download] Importing pattern with strategy: ${strategy}`);
|
|
364
365
|
try {
|
|
366
|
+
if (fs.statSync(filePath).size > MAX_PATTERN_FILE_BYTES) {
|
|
367
|
+
console.error(`[Download] Pattern file exceeds size limit (${MAX_PATTERN_FILE_BYTES} bytes)`);
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
365
370
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
366
371
|
const cfp = JSON.parse(content);
|
|
367
372
|
// In production: Import to local pattern store
|
|
@@ -137,13 +137,17 @@ export class PatternPublisher {
|
|
|
137
137
|
}
|
|
138
138
|
catch (error) {
|
|
139
139
|
console.error(`[Publish] Failed:`, error);
|
|
140
|
+
// Sanitize error message: strip control chars and cap length to prevent
|
|
141
|
+
// internal paths / stack traces from leaking into CLI output or logs.
|
|
142
|
+
const rawMsg = error instanceof Error ? error.message : String(error);
|
|
143
|
+
const safeMsg = rawMsg.replace(/[\x00-\x1f\x7f]/g, '?').slice(0, 256);
|
|
140
144
|
return {
|
|
141
145
|
success: false,
|
|
142
146
|
patternId: '',
|
|
143
147
|
cid: '',
|
|
144
148
|
registryCid: '',
|
|
145
149
|
gatewayUrl: '',
|
|
146
|
-
message: `Publish failed: ${
|
|
150
|
+
message: `Publish failed: ${safeMsg}`,
|
|
147
151
|
};
|
|
148
152
|
}
|
|
149
153
|
}
|
|
@@ -152,6 +156,14 @@ export class PatternPublisher {
|
|
|
152
156
|
*/
|
|
153
157
|
async signContent(content, privateKeyPath) {
|
|
154
158
|
const ed = await import('@noble/ed25519');
|
|
159
|
+
// Guard: cap key file size to prevent OOM on oversized or malicious files.
|
|
160
|
+
// A valid 32-byte Ed25519 seed encoded as hex is 64 chars; even with a
|
|
161
|
+
// newline and BOM the file should never exceed a few hundred bytes.
|
|
162
|
+
const MAX_KEY_FILE_BYTES = 4 * 1024; // 4 KB — generous upper bound
|
|
163
|
+
const keyFileStat = fs.statSync(privateKeyPath);
|
|
164
|
+
if (keyFileStat.size > MAX_KEY_FILE_BYTES) {
|
|
165
|
+
throw new Error(`Private key file exceeds size limit (${MAX_KEY_FILE_BYTES} bytes)`);
|
|
166
|
+
}
|
|
155
167
|
const keyHex = fs.readFileSync(privateKeyPath, 'utf-8').trim();
|
|
156
168
|
// Accept 64-char hex (32-byte seed) directly; otherwise derive via SHA-256
|
|
157
169
|
const privKeyBytes = keyHex.length === 64
|
|
@@ -37,6 +37,14 @@ export declare function removePatternFromRegistry(registry: PatternRegistry, pat
|
|
|
37
37
|
export declare function serializeRegistry(registry: PatternRegistry): string;
|
|
38
38
|
/**
|
|
39
39
|
* Deserialize registry from JSON
|
|
40
|
+
*
|
|
41
|
+
* Caps the input string length before parsing to prevent OOM on a malicious or
|
|
42
|
+
* oversized registry fetched from IPFS / Pinata. The in-flight body is already
|
|
43
|
+
* capped by readBodyWithLimit (50 MB), but deserializeRegistry is also called
|
|
44
|
+
* with locally-cached data, so we add a 10 MB guard here too.
|
|
45
|
+
*
|
|
46
|
+
* We also reject non-semver and suspiciously long version strings to prevent
|
|
47
|
+
* version fields being used as a side-channel for large-payload injection.
|
|
40
48
|
*/
|
|
41
49
|
export declare function deserializeRegistry(json: string): PatternRegistry;
|
|
42
50
|
/**
|