wyrm-mcp 7.3.2 → 7.3.3
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/README.md +8 -14
- package/dist/activation.js +59 -1
- package/dist/agent-daemon.js +281 -4
- package/dist/agent-loop.js +332 -7
- package/dist/analytics.js +236 -13
- package/dist/attribution.js +49 -1
- package/dist/audit.js +457 -2
- package/dist/auto-capture.js +138 -3
- package/dist/auto-orchestrator.js +325 -1
- package/dist/autoconfig.d.ts +50 -0
- package/dist/autoconfig.d.ts.map +1 -1
- package/dist/autoconfig.js +1115 -39
- package/dist/autoconfig.js.map +1 -1
- package/dist/buddy-runner.js +109 -1
- package/dist/buddy.js +564 -14
- package/dist/build-flags.js +15 -1
- package/dist/capabilities.js +183 -3
- package/dist/capture.js +56 -1
- package/dist/causality.js +148 -8
- package/dist/cli.js +281 -20
- package/dist/cloud/cli.js +541 -5
- package/dist/cloud/client.js +221 -1
- package/dist/cloud/crypto.js +85 -1
- package/dist/cloud/machine-id.js +113 -2
- package/dist/cloud/recovery.js +60 -1
- package/dist/cloud/sync-engine.js +543 -7
- package/dist/cloud-backup.js +579 -5
- package/dist/cloud-profile.js +138 -1
- package/dist/cloud-sync-entrypoint.js +47 -1
- package/dist/cloud-sync.js +309 -2
- package/dist/connectors/bridge-source.d.ts +46 -0
- package/dist/connectors/bridge-source.d.ts.map +1 -0
- package/dist/connectors/bridge-source.js +77 -0
- package/dist/connectors/bridge-source.js.map +1 -0
- package/dist/connectors/index.d.ts +24 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +69 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/ingest.d.ts +16 -0
- package/dist/connectors/ingest.d.ts.map +1 -0
- package/dist/connectors/ingest.js +116 -0
- package/dist/connectors/ingest.js.map +1 -0
- package/dist/connectors/types.d.ts +99 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +17 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/constellation.js +168 -12
- package/dist/content-signature.js +45 -1
- package/dist/context-build-budgeted.js +144 -4
- package/dist/context-ranking.js +69 -1
- package/dist/crypto.js +179 -1
- package/dist/daemon-write-endpoint.js +290 -1
- package/dist/daemon-writer.js +406 -2
- package/dist/database.js +1278 -53
- package/dist/deprecations.js +162 -2
- package/dist/design.js +141 -13
- package/dist/event-replication.js +112 -1
- package/dist/events-sse.js +43 -7
- package/dist/events.js +238 -6
- package/dist/failure-patterns.d.ts +107 -0
- package/dist/failure-patterns.d.ts.map +1 -1
- package/dist/failure-patterns.js +924 -43
- package/dist/failure-patterns.js.map +1 -1
- package/dist/federation.js +236 -12
- package/dist/goals.js +101 -13
- package/dist/golden.js +355 -3
- package/dist/handlers/agent.js +165 -4
- package/dist/handlers/alias-adapters.js +129 -1
- package/dist/handlers/aliases.js +171 -1
- package/dist/handlers/audit.js +87 -1
- package/dist/handlers/boundary.js +221 -1
- package/dist/handlers/capture.js +1114 -73
- package/dist/handlers/causality.js +119 -9
- package/dist/handlers/cloud.js +382 -85
- package/dist/handlers/companion.js +459 -28
- package/dist/handlers/datalake.js +187 -7
- package/dist/handlers/dispatch-context.js +22 -0
- package/dist/handlers/entity.js +256 -25
- package/dist/handlers/events.js +335 -16
- package/dist/handlers/failure.d.ts.map +1 -1
- package/dist/handlers/failure.js +408 -13
- package/dist/handlers/failure.js.map +1 -1
- package/dist/handlers/goals.js +296 -4
- package/dist/handlers/intelligence.js +681 -126
- package/dist/handlers/invoicing.js +70 -1
- package/dist/handlers/mcpclient.js +137 -6
- package/dist/handlers/orchestration.js +125 -40
- package/dist/handlers/output-schemas.js +24 -1
- package/dist/handlers/presence.js +99 -3
- package/dist/handlers/project.js +182 -28
- package/dist/handlers/prompts.js +157 -6
- package/dist/handlers/quest.js +224 -4
- package/dist/handlers/recall.js +237 -13
- package/dist/handlers/registry.js +167 -1
- package/dist/handlers/resources.js +288 -1
- package/dist/handlers/review.js +74 -11
- package/dist/handlers/run.js +498 -16
- package/dist/handlers/search.js +338 -15
- package/dist/handlers/session.js +643 -31
- package/dist/handlers/share.js +184 -8
- package/dist/handlers/shims.js +464 -1
- package/dist/handlers/skill.js +449 -67
- package/dist/handlers/survivors.js +120 -1
- package/dist/handlers/symbols.js +109 -8
- package/dist/handlers/syncops.js +302 -4
- package/dist/handlers/types.js +27 -1
- package/dist/harvest.js +191 -5
- package/dist/hours.js +156 -7
- package/dist/http-auth.js +321 -3
- package/dist/http-fast.js +1302 -22
- package/dist/icons.js +47 -1
- package/dist/importers.js +268 -1
- package/dist/index.js +840 -2
- package/dist/indexer.js +145 -4
- package/dist/intelligence.js +261 -31
- package/dist/internal-dispatch.js +212 -3
- package/dist/keyset.js +110 -1
- package/dist/knowledge-graph.js +176 -12
- package/dist/license.js +441 -2
- package/dist/logger.js +199 -2
- package/dist/maintenance.js +148 -2
- package/dist/mcp-client.js +262 -6
- package/dist/memory-artifacts.js +596 -32
- package/dist/migrate-prompt.js +124 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +799 -42
- package/dist/migrations.js.map +1 -1
- package/dist/performance.js +228 -1
- package/dist/presence.js +140 -11
- package/dist/priority-embed.js +164 -5
- package/dist/providers/embedding-provider.js +196 -1
- package/dist/readonly-gate.js +29 -1
- package/dist/receipt.js +43 -1
- package/dist/rehydration.js +157 -9
- package/dist/reindex.js +88 -1
- package/dist/render-target.js +544 -21
- package/dist/render.js +280 -4
- package/dist/repl-guard.js +173 -1
- package/dist/replication-daemon-entrypoint.js +31 -1
- package/dist/replication-daemon.js +262 -2
- package/dist/rerank.js +142 -1
- package/dist/resilience.js +591 -1
- package/dist/reverse-bridge.js +360 -5
- package/dist/security.js +244 -1
- package/dist/session-seen.js +51 -3
- package/dist/setup.js +260 -1
- package/dist/skill-author.js +168 -5
- package/dist/spec-kit.js +191 -1
- package/dist/sqlite-busy.js +154 -1
- package/dist/statusline.js +315 -11
- package/dist/sub-agent.js +262 -13
- package/dist/summarizer.js +139 -13
- package/dist/symbols.js +283 -7
- package/dist/sync.js +359 -5
- package/dist/tasks-dispatch.js +84 -1
- package/dist/tasks.js +282 -1
- package/dist/token-budget.js +143 -1
- package/dist/tool-analytics.js +129 -7
- package/dist/tool-annotations.js +365 -1
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +75 -1
- package/dist/trace-harvest.js +244 -6
- package/dist/types.js +30 -1
- package/dist/ui-dashboard.js +50 -41
- package/dist/ulid.js +81 -1
- package/dist/usage-tracker.js +66 -1
- package/dist/validate.js +129 -1
- package/dist/vault.js +534 -1
- package/dist/vector-init.js +67 -1
- package/dist/vectors.js +184 -3
- package/dist/version-check.js +136 -4
- package/dist/visibility.js +155 -19
- package/dist/wyrm-cli.js +2845 -101
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.d.ts.map +1 -1
- package/dist/wyrm-guard.js +475 -14
- package/dist/wyrm-guard.js.map +1 -1
- package/dist/wyrm-loop.js +150 -3
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +11 -1
- package/dist/wyrm-statusline.js +56 -4
- package/dist/wyrm-ui.js +77 -9
- package/package.json +1 -1
|
@@ -1,4 +1,144 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Budgeted wyrm_context_build path (Spec 014; v7 F4 T036 — extracted from the
|
|
3
|
+
* index.ts monolith). Renders a stable preamble (ground truths) + a dynamic
|
|
4
|
+
* body (best scaffold + recalled memory) under a token budget; items below the
|
|
5
|
+
* cutoff elide to one-line stubs the agent can re-`wyrm_recall`. Lifted VERBATIM
|
|
6
|
+
* behind a factory that captures the same index.ts singletons + budgeting
|
|
7
|
+
* helpers it always closed over (deterministic; no LLM, Article III).
|
|
8
|
+
*
|
|
9
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
10
|
+
* @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
|
|
11
|
+
*/
|
|
12
|
+
import { applyBudget, resolveBudget } from "./token-budget.js";
|
|
13
|
+
import { score as rankScore } from "./context-ranking.js";
|
|
14
|
+
export function makeRunBudgetedContextBuild(deps) {
|
|
15
|
+
const { groundTruths, scaffoldLib, memory, toolAnalytics, sessionSeen, tokenEstimator, rankWeights } = deps;
|
|
16
|
+
return function runBudgetedContextBuild(opts) {
|
|
17
|
+
// Resolve effective budget per spec 014 D1 precedence: call > env > fallback.
|
|
18
|
+
const budget = resolveBudget({
|
|
19
|
+
callArg: opts.maxTokens,
|
|
20
|
+
envValue: process.env.WYRM_CONTEXT_TOKEN_BUDGET,
|
|
21
|
+
clientName: null, // clientInfo wiring is a follow-up; env override is the lever today
|
|
22
|
+
});
|
|
23
|
+
const seen = opts.sessionId ? sessionSeen().getSeen(opts.sessionId) : new Set();
|
|
24
|
+
// --- Stable preamble (ground truths) ---
|
|
25
|
+
const truthsText = groundTruths.formatForContext(opts.project.id);
|
|
26
|
+
const truthsTokens = tokenEstimator.count(truthsText ?? '');
|
|
27
|
+
// Strict budget mode (D3) elides truths if even the preamble overflows;
|
|
28
|
+
// default behavior is to warn-and-overflow per spec 014.
|
|
29
|
+
let preambleText = truthsText ?? '';
|
|
30
|
+
let preambleEmitted = truthsTokens;
|
|
31
|
+
if (truthsTokens > budget * 0.5 && !opts.strictBudget) {
|
|
32
|
+
process.stderr.write(`[wyrm_context_build] ground truths consume ${truthsTokens} of ${budget}-token budget — body will be heavily elided. Pass strict_budget:true to elide truths too.\n`);
|
|
33
|
+
}
|
|
34
|
+
if (opts.strictBudget && truthsTokens > budget * 0.5) {
|
|
35
|
+
// Strict mode: keep truths brief
|
|
36
|
+
preambleText = (truthsText ?? '').slice(0, budget * 2); // 2 chars/token rough cap on chars
|
|
37
|
+
preambleEmitted = tokenEstimator.count(preambleText);
|
|
38
|
+
}
|
|
39
|
+
// --- Build candidate body items: best scaffold + memory artifacts ---
|
|
40
|
+
const items = [];
|
|
41
|
+
// Scaffold (treated as a single candidate)
|
|
42
|
+
const scaffoldMatch = scaffoldLib.findBest(opts.task, opts.project.id);
|
|
43
|
+
if (scaffoldMatch) {
|
|
44
|
+
const sText = scaffoldLib.formatForContext(scaffoldMatch);
|
|
45
|
+
const sId = scaffoldMatch.scaffold.id;
|
|
46
|
+
items.push({
|
|
47
|
+
item: { kind: 'scaffold', id: sId, title: scaffoldMatch.scaffold.problem_type, body: sText },
|
|
48
|
+
key: `scaffold:${sId}`,
|
|
49
|
+
inlineCost: tokenEstimator.count(sText),
|
|
50
|
+
stubCost: 20,
|
|
51
|
+
// Scaffold is high-relevance by definition; assign generous score.
|
|
52
|
+
score: rankScore({ confidence: 0.9, relevance: 0.9, updatedAt: new Date().toISOString() }, rankWeights),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Memory artifacts via recall (which returns id-level results with relevance scores)
|
|
56
|
+
const recalled = memory.recall(opts.project.id, opts.task, {
|
|
57
|
+
limit: 20,
|
|
58
|
+
minConfidence: opts.minConfidence ?? 0.2,
|
|
59
|
+
});
|
|
60
|
+
for (const r of recalled) {
|
|
61
|
+
const a = r.artifact;
|
|
62
|
+
// Skip kind filter mismatches if caller specified one
|
|
63
|
+
if (opts.kinds && opts.kinds.length > 0 && !opts.kinds.includes(a.kind))
|
|
64
|
+
continue;
|
|
65
|
+
const body = [
|
|
66
|
+
`**${a.kind.toUpperCase()}** · ${a.problem}`,
|
|
67
|
+
a.constraints ? `Constraints: ${a.constraints}` : '',
|
|
68
|
+
a.validated_fix ? `Fix: ${a.validated_fix}` : '',
|
|
69
|
+
a.why_it_worked ? `Why: ${a.why_it_worked}` : '',
|
|
70
|
+
].filter(Boolean).join('\n');
|
|
71
|
+
items.push({
|
|
72
|
+
item: { kind: 'memory', id: a.id, title: a.problem.slice(0, 60), body },
|
|
73
|
+
key: `memory:${a.id}`,
|
|
74
|
+
inlineCost: tokenEstimator.count(body),
|
|
75
|
+
stubCost: 25,
|
|
76
|
+
score: rankScore({
|
|
77
|
+
confidence: a.confidence,
|
|
78
|
+
relevance: r.relevance_score,
|
|
79
|
+
updatedAt: a.updated_at,
|
|
80
|
+
}, rankWeights),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// --- Apply budget ---
|
|
84
|
+
const result = applyBudget(items, {
|
|
85
|
+
budget,
|
|
86
|
+
alreadySeen: seen,
|
|
87
|
+
reserved: opts.strictBudget ? 0 : preambleEmitted,
|
|
88
|
+
});
|
|
89
|
+
// --- Mark inlined items as seen for this session ---
|
|
90
|
+
if (opts.sessionId) {
|
|
91
|
+
sessionSeen().markBulk(opts.sessionId, result.inline.map((it) => ({ id: it.id, kind: it.kind, mode: 'inline' })));
|
|
92
|
+
sessionSeen().markBulk(opts.sessionId, result.elided.map((e) => ({ id: e.item.id, kind: e.item.kind, mode: 'stub' })));
|
|
93
|
+
}
|
|
94
|
+
// --- Render output ---
|
|
95
|
+
const lines = [];
|
|
96
|
+
lines.push(` **Context Brief** — "${opts.task}"`);
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push('## Ground truths (stable preamble)');
|
|
99
|
+
lines.push('');
|
|
100
|
+
lines.push(preambleText || '_no ground truths set for this project yet_');
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push('## Task-relevant memory');
|
|
103
|
+
lines.push('');
|
|
104
|
+
for (const it of result.inline) {
|
|
105
|
+
lines.push(`### [${it.kind}:${it.id}] ${it.title}`);
|
|
106
|
+
lines.push(it.body);
|
|
107
|
+
lines.push('');
|
|
108
|
+
}
|
|
109
|
+
if (result.elided.length > 0) {
|
|
110
|
+
lines.push('## Elided to stubs');
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push('_Below the budget cutoff or already seen this session. Recall with `wyrm_recall(id)`:_');
|
|
113
|
+
lines.push('');
|
|
114
|
+
for (const e of result.elided) {
|
|
115
|
+
const tag = e.reason === 'seen' ? 'shown earlier in session' : `~${e.stubCost} tokens`;
|
|
116
|
+
lines.push(`- [${e.item.kind}:${e.item.id}] ${e.item.title} · ${tag}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push('');
|
|
119
|
+
}
|
|
120
|
+
lines.push('---');
|
|
121
|
+
lines.push(`_Budget: ${result.tokensInline}/${budget} tokens inline · ${result.elided.length} stub${result.elided.length === 1 ? '' : 's'} · estimator: ${tokenEstimator.source}_`);
|
|
122
|
+
const text = lines.join('\n');
|
|
123
|
+
// Telemetry — fire-and-forget via the existing tool_call_log surface.
|
|
124
|
+
try {
|
|
125
|
+
toolAnalytics.log({
|
|
126
|
+
tool_name: 'wyrm_context_build',
|
|
127
|
+
project_id: opts.project.id,
|
|
128
|
+
args: {
|
|
129
|
+
budget,
|
|
130
|
+
items_total: items.length,
|
|
131
|
+
items_inline: result.inline.length,
|
|
132
|
+
items_elided: result.elided.length,
|
|
133
|
+
tokens_inline: result.tokensInline,
|
|
134
|
+
tokens_stubs: result.tokensStubs,
|
|
135
|
+
},
|
|
136
|
+
success: true,
|
|
137
|
+
latency_ms: 0,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch { /* never fail context_build on telemetry */ }
|
|
141
|
+
return { content: [{ type: 'text', text }] };
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=context-build-budgeted.js.map
|
package/dist/context-ranking.js
CHANGED
|
@@ -1 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Combined-score computation for context-build candidates (spec 014).
|
|
3
|
+
*
|
|
4
|
+
* Combines four signals into a single score in [0, 1]:
|
|
5
|
+
*
|
|
6
|
+
* confidence × wC + recency × wR + relevance × wU + usefulness × wU
|
|
7
|
+
*
|
|
8
|
+
* Weights load from WYRM_RANK_WEIGHTS env (JSON) with sane defaults.
|
|
9
|
+
*
|
|
10
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
11
|
+
* @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
|
|
12
|
+
*/
|
|
13
|
+
import { logger } from './logger.js';
|
|
14
|
+
export const DEFAULT_WEIGHTS = {
|
|
15
|
+
confidence: 0.4,
|
|
16
|
+
recency: 0.2,
|
|
17
|
+
relevance: 0.3,
|
|
18
|
+
usefulness: 0.1,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Load weights from WYRM_RANK_WEIGHTS env var. Falls back to defaults
|
|
22
|
+
* on any parse/validation error. Logs the active weights at startup.
|
|
23
|
+
*/
|
|
24
|
+
export function loadWeightsFromEnv(env = process.env) {
|
|
25
|
+
const raw = env.WYRM_RANK_WEIGHTS;
|
|
26
|
+
if (!raw)
|
|
27
|
+
return { ...DEFAULT_WEIGHTS };
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
const w = {
|
|
31
|
+
confidence: clamp01(parsed.confidence ?? DEFAULT_WEIGHTS.confidence),
|
|
32
|
+
recency: clamp01(parsed.recency ?? DEFAULT_WEIGHTS.recency),
|
|
33
|
+
relevance: clamp01(parsed.relevance ?? DEFAULT_WEIGHTS.relevance),
|
|
34
|
+
usefulness: clamp01(parsed.usefulness ?? DEFAULT_WEIGHTS.usefulness),
|
|
35
|
+
};
|
|
36
|
+
return w;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
logger.warn(`Invalid WYRM_RANK_WEIGHTS, using defaults: ${e.message}`);
|
|
40
|
+
return { ...DEFAULT_WEIGHTS };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function clamp01(x) {
|
|
44
|
+
if (!Number.isFinite(x))
|
|
45
|
+
return 0;
|
|
46
|
+
return Math.max(0, Math.min(1, x));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Compute the combined score in [0, 1].
|
|
50
|
+
*
|
|
51
|
+
* Recency uses exponential decay over 30 days from updatedAt to now.
|
|
52
|
+
* Missing signals default to neutral (0.5 confidence, 0.5 relevance, 0.5
|
|
53
|
+
* usefulness; recency to 0 if no updatedAt).
|
|
54
|
+
*/
|
|
55
|
+
export function score(item, weights, now = new Date()) {
|
|
56
|
+
const c = clamp01(item.confidence ?? 0.5);
|
|
57
|
+
const u = clamp01(item.usefulness ?? 0.5);
|
|
58
|
+
const r = clamp01(item.relevance ?? 0.5);
|
|
59
|
+
const ageDays = item.updatedAt
|
|
60
|
+
? Math.max(0, (now.getTime() - new Date(item.updatedAt).getTime()) / (24 * 60 * 60 * 1000))
|
|
61
|
+
: Infinity;
|
|
62
|
+
const recency = Number.isFinite(ageDays) ? Math.exp(-ageDays / 30) : 0;
|
|
63
|
+
const total = c * weights.confidence +
|
|
64
|
+
recency * weights.recency +
|
|
65
|
+
r * weights.relevance +
|
|
66
|
+
u * weights.usefulness;
|
|
67
|
+
return clamp01(total);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=context-ranking.js.map
|
package/dist/crypto.js
CHANGED
|
@@ -1 +1,179 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Wyrm Encryption Module
|
|
3
|
+
* AES-256-GCM encryption for sensitive memory data
|
|
4
|
+
*
|
|
5
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
6
|
+
* @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
|
|
7
|
+
* @module crypto
|
|
8
|
+
* @version 3.0.0
|
|
9
|
+
*/
|
|
10
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash } from 'crypto';
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
const WYRM_DIR = join(homedir(), '.wyrm');
|
|
15
|
+
const MASTER_SALT_FILE = join(WYRM_DIR, 'crypto.salt');
|
|
16
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
17
|
+
const IV_LENGTH = 16;
|
|
18
|
+
const SALT_LENGTH = 32;
|
|
19
|
+
const TAG_LENGTH = 16;
|
|
20
|
+
const KEY_LENGTH = 32;
|
|
21
|
+
/**
|
|
22
|
+
* Wyrm Crypto - Handles encryption/decryption of sensitive data
|
|
23
|
+
*/
|
|
24
|
+
export class WyrmCrypto {
|
|
25
|
+
masterKey = null;
|
|
26
|
+
config;
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.config = {
|
|
29
|
+
enabled: config?.enabled ?? false,
|
|
30
|
+
keyDerivation: config?.keyDerivation ?? 'scrypt',
|
|
31
|
+
iterations: config?.iterations ?? 100000,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Initialize crypto with a master password
|
|
36
|
+
*/
|
|
37
|
+
initialize(password) {
|
|
38
|
+
if (!password || password.length < 8) {
|
|
39
|
+
throw new Error('Password must be at least 8 characters');
|
|
40
|
+
}
|
|
41
|
+
// Use a per-installation random salt (persisted, never derived from constant)
|
|
42
|
+
if (!existsSync(WYRM_DIR)) {
|
|
43
|
+
mkdirSync(WYRM_DIR, { recursive: true, mode: 0o700 });
|
|
44
|
+
}
|
|
45
|
+
let salt;
|
|
46
|
+
if (existsSync(MASTER_SALT_FILE)) {
|
|
47
|
+
salt = Buffer.from(readFileSync(MASTER_SALT_FILE, 'utf8').trim(), 'hex');
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
salt = randomBytes(32);
|
|
51
|
+
writeFileSync(MASTER_SALT_FILE, salt.toString('hex'), { mode: 0o600 });
|
|
52
|
+
}
|
|
53
|
+
this.masterKey = scryptSync(password, salt, KEY_LENGTH);
|
|
54
|
+
this.config.enabled = true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check if encryption is enabled and configured
|
|
58
|
+
*/
|
|
59
|
+
isEnabled() {
|
|
60
|
+
return this.config.enabled && this.masterKey !== null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Derive a unique key for each piece of data
|
|
64
|
+
*/
|
|
65
|
+
deriveKey(salt) {
|
|
66
|
+
if (!this.masterKey) {
|
|
67
|
+
throw new Error('Crypto not initialized. Call initialize() with a password first.');
|
|
68
|
+
}
|
|
69
|
+
return scryptSync(this.masterKey, salt, KEY_LENGTH);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Encrypt a string value
|
|
73
|
+
*/
|
|
74
|
+
encrypt(plaintext) {
|
|
75
|
+
if (!this.isEnabled()) {
|
|
76
|
+
throw new Error('Encryption not enabled');
|
|
77
|
+
}
|
|
78
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
79
|
+
const iv = randomBytes(IV_LENGTH);
|
|
80
|
+
const key = this.deriveKey(salt);
|
|
81
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
82
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
83
|
+
encrypted += cipher.final('hex');
|
|
84
|
+
const tag = cipher.getAuthTag();
|
|
85
|
+
return {
|
|
86
|
+
iv: iv.toString('hex'),
|
|
87
|
+
salt: salt.toString('hex'),
|
|
88
|
+
tag: tag.toString('hex'),
|
|
89
|
+
data: encrypted,
|
|
90
|
+
version: 1,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Decrypt an encrypted value
|
|
95
|
+
*/
|
|
96
|
+
decrypt(encrypted) {
|
|
97
|
+
if (!this.isEnabled()) {
|
|
98
|
+
throw new Error('Encryption not enabled');
|
|
99
|
+
}
|
|
100
|
+
const salt = Buffer.from(encrypted.salt, 'hex');
|
|
101
|
+
const iv = Buffer.from(encrypted.iv, 'hex');
|
|
102
|
+
const tag = Buffer.from(encrypted.tag, 'hex');
|
|
103
|
+
const key = this.deriveKey(salt);
|
|
104
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
105
|
+
decipher.setAuthTag(tag);
|
|
106
|
+
let decrypted = decipher.update(encrypted.data, 'hex', 'utf8');
|
|
107
|
+
decrypted += decipher.final('utf8');
|
|
108
|
+
return decrypted;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Encrypt if enabled, otherwise return plaintext
|
|
112
|
+
*/
|
|
113
|
+
maybeEncrypt(value) {
|
|
114
|
+
if (!this.isEnabled()) {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
const encrypted = this.encrypt(value);
|
|
118
|
+
return `ENC:${JSON.stringify(encrypted)}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Decrypt if encrypted, otherwise return as-is
|
|
122
|
+
*/
|
|
123
|
+
maybeDecrypt(value) {
|
|
124
|
+
if (!value.startsWith('ENC:')) {
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
if (!this.isEnabled()) {
|
|
128
|
+
// Can't decrypt without key
|
|
129
|
+
return '[ENCRYPTED - KEY REQUIRED]';
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
const encrypted = JSON.parse(value.slice(4));
|
|
133
|
+
return this.decrypt(encrypted);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return '[DECRYPT FAILED]';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Generate a secure random key for API tokens, etc.
|
|
141
|
+
*/
|
|
142
|
+
static generateSecureToken(length = 32) {
|
|
143
|
+
return randomBytes(length).toString('hex');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Hash a value for comparison (one-way)
|
|
147
|
+
*/
|
|
148
|
+
static hash(value, algorithm = 'sha256') {
|
|
149
|
+
return createHash(algorithm).update(value).digest('hex');
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Verify a hash
|
|
153
|
+
*/
|
|
154
|
+
static verifyHash(value, hash, algorithm = 'sha256') {
|
|
155
|
+
const computed = WyrmCrypto.hash(value, algorithm);
|
|
156
|
+
// Constant-time comparison
|
|
157
|
+
if (computed.length !== hash.length)
|
|
158
|
+
return false;
|
|
159
|
+
let result = 0;
|
|
160
|
+
for (let i = 0; i < computed.length; i++) {
|
|
161
|
+
result |= computed.charCodeAt(i) ^ hash.charCodeAt(i);
|
|
162
|
+
}
|
|
163
|
+
return result === 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Singleton instance
|
|
167
|
+
let cryptoInstance = null;
|
|
168
|
+
export function getCrypto() {
|
|
169
|
+
if (!cryptoInstance) {
|
|
170
|
+
cryptoInstance = new WyrmCrypto();
|
|
171
|
+
}
|
|
172
|
+
return cryptoInstance;
|
|
173
|
+
}
|
|
174
|
+
export function initializeCrypto(password) {
|
|
175
|
+
cryptoInstance = new WyrmCrypto({ enabled: true });
|
|
176
|
+
cryptoInstance.initialize(password);
|
|
177
|
+
return cryptoInstance;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=crypto.js.map
|