tlc-claude-code 1.4.9 → 1.5.2
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.md +23 -0
- package/CODING-STANDARDS.md +408 -0
- package/bin/install.js +2 -0
- package/dashboard/dist/components/QualityGatePane.d.ts +38 -0
- package/dashboard/dist/components/QualityGatePane.js +31 -0
- package/dashboard/dist/components/QualityGatePane.test.d.ts +1 -0
- package/dashboard/dist/components/QualityGatePane.test.js +147 -0
- package/dashboard/dist/components/orchestration/AgentCard.d.ts +26 -0
- package/dashboard/dist/components/orchestration/AgentCard.js +60 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.js +63 -0
- package/dashboard/dist/components/orchestration/AgentControls.d.ts +11 -0
- package/dashboard/dist/components/orchestration/AgentControls.js +20 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.js +52 -0
- package/dashboard/dist/components/orchestration/AgentDetail.d.ts +35 -0
- package/dashboard/dist/components/orchestration/AgentDetail.js +37 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.js +79 -0
- package/dashboard/dist/components/orchestration/AgentList.d.ts +31 -0
- package/dashboard/dist/components/orchestration/AgentList.js +47 -0
- package/dashboard/dist/components/orchestration/AgentList.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentList.test.js +64 -0
- package/dashboard/dist/components/orchestration/CostMeter.d.ts +11 -0
- package/dashboard/dist/components/orchestration/CostMeter.js +28 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.js +50 -0
- package/dashboard/dist/components/orchestration/ModelSelector.d.ts +20 -0
- package/dashboard/dist/components/orchestration/ModelSelector.js +12 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.js +56 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.d.ts +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.js +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.js +56 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.d.ts +11 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.js +37 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.js +52 -0
- package/dashboard/dist/components/orchestration/index.d.ts +8 -0
- package/dashboard/dist/components/orchestration/index.js +8 -0
- package/package.json +1 -1
- package/server/lib/access-control.js +352 -0
- package/server/lib/access-control.test.js +322 -0
- package/server/lib/agents-cancel-command.js +139 -0
- package/server/lib/agents-cancel-command.test.js +180 -0
- package/server/lib/agents-get-command.js +159 -0
- package/server/lib/agents-get-command.test.js +167 -0
- package/server/lib/agents-list-command.js +150 -0
- package/server/lib/agents-list-command.test.js +149 -0
- package/server/lib/agents-logs-command.js +126 -0
- package/server/lib/agents-logs-command.test.js +198 -0
- package/server/lib/agents-retry-command.js +117 -0
- package/server/lib/agents-retry-command.test.js +192 -0
- package/server/lib/budget-limits.js +222 -0
- package/server/lib/budget-limits.test.js +214 -0
- package/server/lib/code-generator.js +291 -0
- package/server/lib/code-generator.test.js +307 -0
- package/server/lib/cost-command.js +290 -0
- package/server/lib/cost-command.test.js +202 -0
- package/server/lib/cost-optimizer.js +404 -0
- package/server/lib/cost-optimizer.test.js +232 -0
- package/server/lib/cost-projections.js +302 -0
- package/server/lib/cost-projections.test.js +217 -0
- package/server/lib/cost-reports.js +277 -0
- package/server/lib/cost-reports.test.js +254 -0
- package/server/lib/cost-tracker.js +216 -0
- package/server/lib/cost-tracker.test.js +302 -0
- package/server/lib/crypto-patterns.js +433 -0
- package/server/lib/crypto-patterns.test.js +346 -0
- package/server/lib/design-command.js +385 -0
- package/server/lib/design-command.test.js +249 -0
- package/server/lib/design-parser.js +237 -0
- package/server/lib/design-parser.test.js +290 -0
- package/server/lib/gemini-vision.js +377 -0
- package/server/lib/gemini-vision.test.js +282 -0
- package/server/lib/input-validator.js +360 -0
- package/server/lib/input-validator.test.js +295 -0
- package/server/lib/litellm-client.js +232 -0
- package/server/lib/litellm-client.test.js +267 -0
- package/server/lib/litellm-command.js +291 -0
- package/server/lib/litellm-command.test.js +260 -0
- package/server/lib/litellm-config.js +273 -0
- package/server/lib/litellm-config.test.js +212 -0
- package/server/lib/model-pricing.js +189 -0
- package/server/lib/model-pricing.test.js +178 -0
- package/server/lib/models-command.js +223 -0
- package/server/lib/models-command.test.js +193 -0
- package/server/lib/optimize-command.js +197 -0
- package/server/lib/optimize-command.test.js +193 -0
- package/server/lib/orchestration-integration.js +206 -0
- package/server/lib/orchestration-integration.test.js +235 -0
- package/server/lib/output-encoder.js +308 -0
- package/server/lib/output-encoder.test.js +312 -0
- package/server/lib/quality-evaluator.js +396 -0
- package/server/lib/quality-evaluator.test.js +337 -0
- package/server/lib/quality-gate-command.js +340 -0
- package/server/lib/quality-gate-command.test.js +321 -0
- package/server/lib/quality-gate-scorer.js +378 -0
- package/server/lib/quality-gate-scorer.test.js +376 -0
- package/server/lib/quality-history.js +265 -0
- package/server/lib/quality-history.test.js +359 -0
- package/server/lib/quality-presets.js +288 -0
- package/server/lib/quality-presets.test.js +269 -0
- package/server/lib/quality-retry.js +323 -0
- package/server/lib/quality-retry.test.js +325 -0
- package/server/lib/quality-thresholds.js +255 -0
- package/server/lib/quality-thresholds.test.js +237 -0
- package/server/lib/secure-auth.js +333 -0
- package/server/lib/secure-auth.test.js +288 -0
- package/server/lib/secure-code-command.js +540 -0
- package/server/lib/secure-code-command.test.js +309 -0
- package/server/lib/secure-errors.js +521 -0
- package/server/lib/secure-errors.test.js +298 -0
- package/server/lib/vision-command.js +372 -0
- package/server/lib/vision-command.test.js +255 -0
- package/server/lib/visual-command.js +350 -0
- package/server/lib/visual-command.test.js +256 -0
- package/server/lib/visual-testing.js +315 -0
- package/server/lib/visual-testing.test.js +357 -0
- package/server/package-lock.json +2 -2
- package/server/package.json +1 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const { describe, it, beforeEach, mock } = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const {
|
|
4
|
+
wrapWithOrchestration,
|
|
5
|
+
createAgentForCommand,
|
|
6
|
+
trackCommandCost,
|
|
7
|
+
applyQualityGate,
|
|
8
|
+
OrchestrationIntegration,
|
|
9
|
+
} = require('./orchestration-integration.js');
|
|
10
|
+
|
|
11
|
+
describe('orchestration-integration', () => {
|
|
12
|
+
describe('OrchestrationIntegration', () => {
|
|
13
|
+
let integration;
|
|
14
|
+
let mockRegistry;
|
|
15
|
+
let mockCostTracker;
|
|
16
|
+
let mockQualityGate;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mockRegistry = {
|
|
20
|
+
agents: [],
|
|
21
|
+
createAgent: (data) => {
|
|
22
|
+
const agent = { id: `agent-${Date.now()}`, ...data };
|
|
23
|
+
mockRegistry.agents.push(agent);
|
|
24
|
+
return agent;
|
|
25
|
+
},
|
|
26
|
+
getAgent: (id) => mockRegistry.agents.find(a => a.id === id),
|
|
27
|
+
updateAgent: (id, updates) => {
|
|
28
|
+
const agent = mockRegistry.agents.find(a => a.id === id);
|
|
29
|
+
if (agent) Object.assign(agent, updates);
|
|
30
|
+
return agent;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
mockCostTracker = {
|
|
35
|
+
costs: [],
|
|
36
|
+
track: (cost) => mockCostTracker.costs.push(cost),
|
|
37
|
+
getTotal: () => mockCostTracker.costs.reduce((s, c) => s + c.amount, 0),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
mockQualityGate = {
|
|
41
|
+
evaluate: async (output) => ({ pass: output.quality > 70, score: output.quality }),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
integration = new OrchestrationIntegration({
|
|
45
|
+
registry: mockRegistry,
|
|
46
|
+
costTracker: mockCostTracker,
|
|
47
|
+
qualityGate: mockQualityGate,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('build command', () => {
|
|
52
|
+
it('creates agents', async () => {
|
|
53
|
+
const result = await integration.wrapCommand('build', async () => ({
|
|
54
|
+
success: true,
|
|
55
|
+
output: 'Built successfully',
|
|
56
|
+
quality: 85,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
assert.ok(mockRegistry.agents.length > 0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('tracks cost', async () => {
|
|
63
|
+
await integration.wrapCommand('build', async () => ({
|
|
64
|
+
success: true,
|
|
65
|
+
cost: 0.15,
|
|
66
|
+
quality: 85,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
assert.ok(mockCostTracker.costs.length > 0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('review command', () => {
|
|
74
|
+
it('uses multi-model', async () => {
|
|
75
|
+
let modelsUsed = [];
|
|
76
|
+
const result = await integration.wrapCommand('review', async (ctx) => {
|
|
77
|
+
modelsUsed.push(ctx.model || 'default');
|
|
78
|
+
return { success: true, quality: 90 };
|
|
79
|
+
}, { multiModel: true, models: ['claude', 'gpt-4'] });
|
|
80
|
+
|
|
81
|
+
assert.ok(result.success);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('applies consensus', async () => {
|
|
85
|
+
const results = [
|
|
86
|
+
{ score: 80, issues: ['a', 'b'] },
|
|
87
|
+
{ score: 85, issues: ['b', 'c'] },
|
|
88
|
+
];
|
|
89
|
+
const consensus = integration.applyConsensus(results);
|
|
90
|
+
assert.ok(consensus.score >= 80);
|
|
91
|
+
assert.ok(consensus.issues.includes('b')); // Common issue
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('refactor command', () => {
|
|
96
|
+
it('uses quality gate', async () => {
|
|
97
|
+
let qualityChecked = false;
|
|
98
|
+
const customQuality = {
|
|
99
|
+
evaluate: async (output) => {
|
|
100
|
+
qualityChecked = true;
|
|
101
|
+
return { pass: true, score: 90 };
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const int = new OrchestrationIntegration({
|
|
106
|
+
registry: mockRegistry,
|
|
107
|
+
costTracker: mockCostTracker,
|
|
108
|
+
qualityGate: customQuality,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await int.wrapCommand('refactor', async () => ({
|
|
112
|
+
success: true,
|
|
113
|
+
quality: 90,
|
|
114
|
+
}), { useQualityGate: true });
|
|
115
|
+
|
|
116
|
+
assert.ok(qualityChecked);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('agents appear in registry', async () => {
|
|
121
|
+
await integration.wrapCommand('test', async () => ({ success: true, quality: 85 }));
|
|
122
|
+
assert.ok(mockRegistry.agents.length > 0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('costs appear in tracker', async () => {
|
|
126
|
+
await integration.wrapCommand('test', async () => ({
|
|
127
|
+
success: true,
|
|
128
|
+
cost: 0.25,
|
|
129
|
+
quality: 85,
|
|
130
|
+
}));
|
|
131
|
+
assert.ok(mockCostTracker.costs.some(c => c.amount === 0.25));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('quality scores recorded', async () => {
|
|
135
|
+
await integration.wrapCommand('test', async () => ({
|
|
136
|
+
success: true,
|
|
137
|
+
quality: 92,
|
|
138
|
+
}));
|
|
139
|
+
const agent = mockRegistry.agents[0];
|
|
140
|
+
assert.ok(agent.quality?.score === 92 || agent.qualityScore === 92);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('existing behavior preserved', async () => {
|
|
144
|
+
const original = async () => ({ custom: 'data', success: true, quality: 85 });
|
|
145
|
+
const result = await integration.wrapCommand('test', original);
|
|
146
|
+
assert.strictEqual(result.custom, 'data');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('graceful fallback on error', async () => {
|
|
150
|
+
const brokenRegistry = {
|
|
151
|
+
createAgent: () => { throw new Error('Registry error'); },
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const int = new OrchestrationIntegration({
|
|
155
|
+
registry: brokenRegistry,
|
|
156
|
+
costTracker: mockCostTracker,
|
|
157
|
+
qualityGate: mockQualityGate,
|
|
158
|
+
fallbackOnError: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Should not throw, should fall back
|
|
162
|
+
const result = await int.wrapCommand('test', async () => ({
|
|
163
|
+
success: true,
|
|
164
|
+
quality: 85,
|
|
165
|
+
}));
|
|
166
|
+
assert.ok(result.success);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('wrapWithOrchestration', () => {
|
|
171
|
+
it('wraps function with tracking', async () => {
|
|
172
|
+
let tracked = false;
|
|
173
|
+
const wrapped = wrapWithOrchestration(
|
|
174
|
+
async () => ({ result: 'ok' }),
|
|
175
|
+
{ onComplete: () => { tracked = true; } }
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const result = await wrapped();
|
|
179
|
+
assert.strictEqual(result.result, 'ok');
|
|
180
|
+
assert.ok(tracked);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('passes context through', async () => {
|
|
184
|
+
const wrapped = wrapWithOrchestration(
|
|
185
|
+
async (ctx) => ({ model: ctx.model }),
|
|
186
|
+
{}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const result = await wrapped({ model: 'claude' });
|
|
190
|
+
assert.strictEqual(result.model, 'claude');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('createAgentForCommand', () => {
|
|
195
|
+
it('creates agent with command name', () => {
|
|
196
|
+
const agent = createAgentForCommand('build', { model: 'claude' });
|
|
197
|
+
assert.strictEqual(agent.command, 'build');
|
|
198
|
+
assert.strictEqual(agent.model, 'claude');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('sets initial status', () => {
|
|
202
|
+
const agent = createAgentForCommand('test', {});
|
|
203
|
+
assert.strictEqual(agent.status, 'queued');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('trackCommandCost', () => {
|
|
208
|
+
it('records cost with command', () => {
|
|
209
|
+
const tracker = { costs: [], track: (c) => tracker.costs.push(c) };
|
|
210
|
+
trackCommandCost(tracker, 'build', 0.15);
|
|
211
|
+
assert.strictEqual(tracker.costs[0].command, 'build');
|
|
212
|
+
assert.strictEqual(tracker.costs[0].amount, 0.15);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('applyQualityGate', () => {
|
|
217
|
+
it('passes for high quality', async () => {
|
|
218
|
+
const gate = { evaluate: async () => ({ pass: true, score: 90 }) };
|
|
219
|
+
const result = await applyQualityGate(gate, { output: 'test' });
|
|
220
|
+
assert.ok(result.pass);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('fails for low quality', async () => {
|
|
224
|
+
const gate = { evaluate: async () => ({ pass: false, score: 50 }) };
|
|
225
|
+
const result = await applyQualityGate(gate, { output: 'test' });
|
|
226
|
+
assert.strictEqual(result.pass, false);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('returns score', async () => {
|
|
230
|
+
const gate = { evaluate: async () => ({ pass: true, score: 85 }) };
|
|
231
|
+
const result = await applyQualityGate(gate, { output: 'test' });
|
|
232
|
+
assert.strictEqual(result.score, 85);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Encoder Module
|
|
3
|
+
*
|
|
4
|
+
* Context-aware output encoding for XSS prevention
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create an output encoder
|
|
11
|
+
* @param {Object} options - Encoder options
|
|
12
|
+
* @returns {Object} Encoder instance
|
|
13
|
+
*/
|
|
14
|
+
function createOutputEncoder(options = {}) {
|
|
15
|
+
return {
|
|
16
|
+
contexts: options.contexts || ['html', 'js', 'url', 'css', 'attribute'],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Encode string for HTML context
|
|
22
|
+
* @param {string} input - Input to encode
|
|
23
|
+
* @returns {string} Encoded string
|
|
24
|
+
*/
|
|
25
|
+
function encodeHtml(input) {
|
|
26
|
+
if (input === null || input === undefined) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return String(input)
|
|
31
|
+
.replace(/&/g, '&')
|
|
32
|
+
.replace(/</g, '<')
|
|
33
|
+
.replace(/>/g, '>')
|
|
34
|
+
.replace(/"/g, '"')
|
|
35
|
+
.replace(/'/g, ''');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Encode string for JavaScript context
|
|
40
|
+
* @param {string} input - Input to encode
|
|
41
|
+
* @param {Object} options - Encoding options
|
|
42
|
+
* @returns {string} Encoded string
|
|
43
|
+
*/
|
|
44
|
+
function encodeJs(input, options = {}) {
|
|
45
|
+
if (input === null || input === undefined) {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let result = String(input)
|
|
50
|
+
.replace(/\\/g, '\\\\')
|
|
51
|
+
.replace(/'/g, "\\'")
|
|
52
|
+
.replace(/"/g, '\\"')
|
|
53
|
+
.replace(/\n/g, '\\n')
|
|
54
|
+
.replace(/\r/g, '\\r')
|
|
55
|
+
.replace(/\t/g, '\\t')
|
|
56
|
+
.replace(/<\/script>/gi, '<\\/script>');
|
|
57
|
+
|
|
58
|
+
if (options.unicodeEscape) {
|
|
59
|
+
result = result.replace(/[\u0080-\uffff]/g, (char) => {
|
|
60
|
+
return '\\u' + ('0000' + char.charCodeAt(0).toString(16)).slice(-4);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Encode string for URL context
|
|
69
|
+
* @param {string} input - Input to encode
|
|
70
|
+
* @returns {string} Encoded string
|
|
71
|
+
*/
|
|
72
|
+
function encodeUrl(input) {
|
|
73
|
+
if (input === null || input === undefined) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return encodeURIComponent(String(input));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Encode string for CSS context
|
|
82
|
+
* @param {string} input - Input to encode
|
|
83
|
+
* @returns {string} Encoded string
|
|
84
|
+
*/
|
|
85
|
+
function encodeCss(input) {
|
|
86
|
+
if (input === null || input === undefined) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let result = String(input)
|
|
91
|
+
.replace(/\\/g, '\\\\')
|
|
92
|
+
.replace(/</g, '\\3c ')
|
|
93
|
+
.replace(/>/g, '\\3e ')
|
|
94
|
+
.replace(/expression\s*\(/gi, '')
|
|
95
|
+
.replace(/url\s*\(\s*javascript:/gi, 'url(blocked:');
|
|
96
|
+
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Encode string for HTML attribute context
|
|
102
|
+
* @param {string} input - Input to encode
|
|
103
|
+
* @param {Object} options - Encoding options
|
|
104
|
+
* @returns {string} Encoded string
|
|
105
|
+
*/
|
|
106
|
+
function encodeAttribute(input, options = {}) {
|
|
107
|
+
if (input === null || input === undefined) {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let result = String(input);
|
|
112
|
+
|
|
113
|
+
// Remove event handlers
|
|
114
|
+
result = result.replace(/on\w+\s*=/gi, '');
|
|
115
|
+
|
|
116
|
+
if (options.quoted === false) {
|
|
117
|
+
// Unquoted attributes need more escaping
|
|
118
|
+
result = result.replace(/[\s"'=<>`]/g, (char) => {
|
|
119
|
+
return '&#' + char.charCodeAt(0) + ';';
|
|
120
|
+
});
|
|
121
|
+
} else if (options.quoteChar === "'") {
|
|
122
|
+
result = result.replace(/'/g, ''');
|
|
123
|
+
} else {
|
|
124
|
+
result = result.replace(/"/g, '"');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generate Content Security Policy header
|
|
132
|
+
* @param {Object} options - CSP options
|
|
133
|
+
* @returns {string} CSP header value
|
|
134
|
+
*/
|
|
135
|
+
function generateCspHeader(options = {}) {
|
|
136
|
+
const directives = [];
|
|
137
|
+
|
|
138
|
+
// Always include default-src
|
|
139
|
+
directives.push("default-src 'self'");
|
|
140
|
+
|
|
141
|
+
if (options.scriptSrc) {
|
|
142
|
+
directives.push(`script-src ${options.scriptSrc.join(' ')}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.styleSrc) {
|
|
146
|
+
directives.push(`style-src ${options.styleSrc.join(' ')}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (options.imgSrc) {
|
|
150
|
+
directives.push(`img-src ${options.imgSrc.join(' ')}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (options.useNonce && options.nonce) {
|
|
154
|
+
// Replace or add script-src with nonce
|
|
155
|
+
const nonceValue = `'nonce-${options.nonce}'`;
|
|
156
|
+
const existingScriptIdx = directives.findIndex(d => d.startsWith('script-src'));
|
|
157
|
+
if (existingScriptIdx >= 0) {
|
|
158
|
+
directives[existingScriptIdx] += ` ${nonceValue}`;
|
|
159
|
+
} else {
|
|
160
|
+
directives.push(`script-src ${nonceValue}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (options.reportUri) {
|
|
165
|
+
directives.push(`report-uri ${options.reportUri}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const headerValue = directives.join('; ');
|
|
169
|
+
|
|
170
|
+
// For reportOnly mode, include in the returned string
|
|
171
|
+
if (options.reportOnly) {
|
|
172
|
+
return headerValue + ' /* Report-Only */';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return headerValue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Generate Subresource Integrity hash
|
|
180
|
+
* @param {string} content - Content to hash
|
|
181
|
+
* @param {Object} options - Hashing options
|
|
182
|
+
* @returns {string} SRI hash
|
|
183
|
+
*/
|
|
184
|
+
function generateSriHash(content, options = {}) {
|
|
185
|
+
const algorithm = options.algorithm || 'sha384';
|
|
186
|
+
const hash = crypto.createHash(algorithm).update(content).digest('base64');
|
|
187
|
+
|
|
188
|
+
return `${algorithm}-${hash}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Generate encoding code
|
|
193
|
+
* @param {Object} options - Generation options
|
|
194
|
+
* @returns {string} Generated code
|
|
195
|
+
*/
|
|
196
|
+
function generateEncodingCode(options = {}) {
|
|
197
|
+
const { context, language = 'javascript' } = options;
|
|
198
|
+
|
|
199
|
+
const generators = {
|
|
200
|
+
javascript: {
|
|
201
|
+
html: () => `
|
|
202
|
+
function encodeHtml(input) {
|
|
203
|
+
if (input === null || input === undefined) return '';
|
|
204
|
+
return String(input)
|
|
205
|
+
.replace(/&/g, '&')
|
|
206
|
+
.replace(/</g, '<')
|
|
207
|
+
.replace(/>/g, '>')
|
|
208
|
+
.replace(/"/g, '"')
|
|
209
|
+
.replace(/'/g, ''');
|
|
210
|
+
}`,
|
|
211
|
+
js: () => `
|
|
212
|
+
function encodeJs(input) {
|
|
213
|
+
if (input === null || input === undefined) return '';
|
|
214
|
+
return String(input)
|
|
215
|
+
.replace(/\\\\/g, '\\\\\\\\')
|
|
216
|
+
.replace(/'/g, "\\\\'")
|
|
217
|
+
.replace(/"/g, '\\\\"')
|
|
218
|
+
.replace(/\\n/g, '\\\\n');
|
|
219
|
+
}`,
|
|
220
|
+
url: () => `
|
|
221
|
+
function encodeUrl(input) {
|
|
222
|
+
if (input === null || input === undefined) return '';
|
|
223
|
+
return encodeURIComponent(String(input));
|
|
224
|
+
}`,
|
|
225
|
+
css: () => `
|
|
226
|
+
function encodeCss(input) {
|
|
227
|
+
if (input === null || input === undefined) return '';
|
|
228
|
+
return String(input)
|
|
229
|
+
.replace(/\\\\/g, '\\\\\\\\')
|
|
230
|
+
.replace(/</g, '\\\\3c ')
|
|
231
|
+
.replace(/>/g, '\\\\3e ');
|
|
232
|
+
}`,
|
|
233
|
+
},
|
|
234
|
+
typescript: {
|
|
235
|
+
html: () => `
|
|
236
|
+
function encodeHtml(input: string | null | undefined): string {
|
|
237
|
+
if (input === null || input === undefined) return '';
|
|
238
|
+
return String(input)
|
|
239
|
+
.replace(/&/g, '&')
|
|
240
|
+
.replace(/</g, '<')
|
|
241
|
+
.replace(/>/g, '>')
|
|
242
|
+
.replace(/"/g, '"')
|
|
243
|
+
.replace(/'/g, ''');
|
|
244
|
+
}`,
|
|
245
|
+
js: () => `
|
|
246
|
+
function encodeJs(input: string | null | undefined): string {
|
|
247
|
+
if (input === null || input === undefined) return '';
|
|
248
|
+
return String(input)
|
|
249
|
+
.replace(/\\\\/g, '\\\\\\\\')
|
|
250
|
+
.replace(/'/g, "\\\\'")
|
|
251
|
+
.replace(/"/g, '\\\\"');
|
|
252
|
+
}`,
|
|
253
|
+
url: () => `
|
|
254
|
+
function encodeUrl(input: string | null | undefined): string {
|
|
255
|
+
if (input === null || input === undefined) return '';
|
|
256
|
+
return encodeURIComponent(String(input));
|
|
257
|
+
}`,
|
|
258
|
+
css: () => `
|
|
259
|
+
function encodeCss(input: string | null | undefined): string {
|
|
260
|
+
if (input === null || input === undefined) return '';
|
|
261
|
+
return String(input).replace(/</g, '\\\\3c ');
|
|
262
|
+
}`,
|
|
263
|
+
},
|
|
264
|
+
python: {
|
|
265
|
+
html: () => `
|
|
266
|
+
import html
|
|
267
|
+
|
|
268
|
+
def encode_html(input_str):
|
|
269
|
+
if input_str is None:
|
|
270
|
+
return ''
|
|
271
|
+
return html.escape(str(input_str))`,
|
|
272
|
+
js: () => `
|
|
273
|
+
def encode_js(input_str):
|
|
274
|
+
if input_str is None:
|
|
275
|
+
return ''
|
|
276
|
+
return str(input_str).replace('\\\\', '\\\\\\\\').replace("'", "\\\\'")`,
|
|
277
|
+
url: () => `
|
|
278
|
+
from urllib.parse import quote
|
|
279
|
+
|
|
280
|
+
def encode_url(input_str):
|
|
281
|
+
if input_str is None:
|
|
282
|
+
return ''
|
|
283
|
+
return quote(str(input_str))`,
|
|
284
|
+
css: () => `
|
|
285
|
+
def encode_css(input_str):
|
|
286
|
+
if input_str is None:
|
|
287
|
+
return ''
|
|
288
|
+
return str(input_str).replace('<', '\\\\3c ')`,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const lang = generators[language] || generators.javascript;
|
|
293
|
+
const gen = lang[context] || lang.html;
|
|
294
|
+
|
|
295
|
+
return gen();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
module.exports = {
|
|
299
|
+
createOutputEncoder,
|
|
300
|
+
encodeHtml,
|
|
301
|
+
encodeJs,
|
|
302
|
+
encodeUrl,
|
|
303
|
+
encodeCss,
|
|
304
|
+
encodeAttribute,
|
|
305
|
+
generateCspHeader,
|
|
306
|
+
generateSriHash,
|
|
307
|
+
generateEncodingCode,
|
|
308
|
+
};
|