@vaultcompass/vault-guard 1.0.6 → 1.1.1
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/dist/cli.js +1 -1
- package/dist/commands/check.js +2 -34
- package/dist/commands/proxy-sse.d.ts +19 -0
- package/dist/commands/proxy-sse.js +52 -0
- package/dist/commands/proxy.js +37 -4
- package/dist/commands/scan.d.ts +1 -1
- package/dist/commands/scan.js +4 -2
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -115,7 +115,7 @@ function buildCli() {
|
|
|
115
115
|
// Check command
|
|
116
116
|
program
|
|
117
117
|
.command('check')
|
|
118
|
-
.description('
|
|
118
|
+
.description('Scan files with config and baselines')
|
|
119
119
|
.argument('[files...]', 'Files to check')
|
|
120
120
|
.action(async (files) => {
|
|
121
121
|
const exitCode = await (0, check_1.checkCommand)(files);
|
package/dist/commands/check.js
CHANGED
|
@@ -1,39 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.checkCommand = checkCommand;
|
|
7
|
-
const
|
|
8
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
-
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const scan_utils_1 = require("../utils/scan-utils");
|
|
4
|
+
const scan_1 = require("./scan");
|
|
11
5
|
async function checkCommand(files) {
|
|
12
|
-
|
|
13
|
-
const filesToCheck = files.length > 0 ? files : ['.'];
|
|
14
|
-
console.log(chalk_1.default.blue('✅ Quick check\n'));
|
|
15
|
-
// Use async scanning logic with same safeguards as scan command
|
|
16
|
-
const results = await (0, scan_utils_1.scanFilesAsync)(filesToCheck, scanner, {
|
|
17
|
-
verbose: false,
|
|
18
|
-
skipBinary: true,
|
|
19
|
-
progress: false
|
|
20
|
-
});
|
|
21
|
-
if (results.length === 0) {
|
|
22
|
-
console.log(chalk_1.default.green.bold('✅ Clean:'), chalk_1.default.white('No secrets found\n'));
|
|
23
|
-
return 0; // Success exit code
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
const totalSecrets = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
27
|
-
console.log(chalk_1.default.red.bold('🔴 BLOCKED:'), chalk_1.default.white(`Found ${totalSecrets} secret${totalSecrets > 1 ? 's' : ''}\n`));
|
|
28
|
-
// Display simplified results
|
|
29
|
-
for (const { file, matches } of results) {
|
|
30
|
-
const relativePath = relativePathCwd(file);
|
|
31
|
-
console.log(chalk_1.default.red('🔴'), chalk_1.default.white(`${relativePath}: ${matches.length} secret${matches.length > 1 ? 's' : ''}`));
|
|
32
|
-
}
|
|
33
|
-
console.log('');
|
|
34
|
-
return 1; // Error exit code (secrets found)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function relativePathCwd(filePath) {
|
|
38
|
-
return path_1.default.relative(process.cwd(), filePath);
|
|
6
|
+
return (0, scan_1.scanCommand)(files.length > 0 ? files : '.', 'text', false);
|
|
39
7
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SseUsage {
|
|
2
|
+
inputTokens: number;
|
|
3
|
+
outputTokens: number;
|
|
4
|
+
model: string | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Extract token usage from a buffered Anthropic SSE stream.
|
|
8
|
+
*
|
|
9
|
+
* Pure and synchronous — no HTTP dependencies — so it can be unit-tested
|
|
10
|
+
* without spinning up a server. Call after the full response has been teed.
|
|
11
|
+
*
|
|
12
|
+
* Anthropic usage token delivery:
|
|
13
|
+
* message_start → message.usage.input_tokens (and initial output_tokens)
|
|
14
|
+
* message_delta → usage.output_tokens (cumulative; last one wins)
|
|
15
|
+
* message_stop → no usage fields
|
|
16
|
+
*
|
|
17
|
+
* Unknown or malformed `data:` lines are silently skipped.
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseAnthropicSseUsage(raw: string, fallbackModel: string | null): SseUsage;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseAnthropicSseUsage = parseAnthropicSseUsage;
|
|
4
|
+
/**
|
|
5
|
+
* Extract token usage from a buffered Anthropic SSE stream.
|
|
6
|
+
*
|
|
7
|
+
* Pure and synchronous — no HTTP dependencies — so it can be unit-tested
|
|
8
|
+
* without spinning up a server. Call after the full response has been teed.
|
|
9
|
+
*
|
|
10
|
+
* Anthropic usage token delivery:
|
|
11
|
+
* message_start → message.usage.input_tokens (and initial output_tokens)
|
|
12
|
+
* message_delta → usage.output_tokens (cumulative; last one wins)
|
|
13
|
+
* message_stop → no usage fields
|
|
14
|
+
*
|
|
15
|
+
* Unknown or malformed `data:` lines are silently skipped.
|
|
16
|
+
*/
|
|
17
|
+
function parseAnthropicSseUsage(raw, fallbackModel) {
|
|
18
|
+
let inputTokens = 0;
|
|
19
|
+
let outputTokens = 0;
|
|
20
|
+
let model = fallbackModel;
|
|
21
|
+
for (const line of raw.split('\n')) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed.startsWith('data:'))
|
|
24
|
+
continue;
|
|
25
|
+
const payload = trimmed.slice('data:'.length).trim();
|
|
26
|
+
if (!payload || payload === '[DONE]')
|
|
27
|
+
continue;
|
|
28
|
+
let evt;
|
|
29
|
+
try {
|
|
30
|
+
evt = JSON.parse(payload);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (evt.type === 'message_start' && evt.message) {
|
|
36
|
+
if (typeof evt.message.model === 'string')
|
|
37
|
+
model = evt.message.model;
|
|
38
|
+
const u = evt.message.usage ?? {};
|
|
39
|
+
if (typeof u.input_tokens === 'number')
|
|
40
|
+
inputTokens = u.input_tokens;
|
|
41
|
+
if (typeof u.output_tokens === 'number')
|
|
42
|
+
outputTokens = u.output_tokens;
|
|
43
|
+
}
|
|
44
|
+
else if (evt.type === 'message_delta' && evt.usage) {
|
|
45
|
+
if (typeof evt.usage.input_tokens === 'number')
|
|
46
|
+
inputTokens = evt.usage.input_tokens;
|
|
47
|
+
if (typeof evt.usage.output_tokens === 'number')
|
|
48
|
+
outputTokens = evt.usage.output_tokens;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { inputTokens, outputTokens, model };
|
|
52
|
+
}
|
package/dist/commands/proxy.js
CHANGED
|
@@ -41,6 +41,7 @@ const http = __importStar(require("http"));
|
|
|
41
41
|
const https = __importStar(require("https"));
|
|
42
42
|
const path_1 = __importDefault(require("path"));
|
|
43
43
|
const vault_guard_telemetry_1 = require("@vaultcompass/vault-guard-telemetry");
|
|
44
|
+
const proxy_sse_1 = require("./proxy-sse");
|
|
44
45
|
/**
|
|
45
46
|
* Bound inbound request buffering. 32 MB is a generous ceiling for
|
|
46
47
|
* Anthropic-shaped JSON bodies (multi-megabyte system prompts, large
|
|
@@ -266,15 +267,47 @@ async function handleRequest(req, res, store, options, takeRateSlot) {
|
|
|
266
267
|
const headers = { ...pres.headers };
|
|
267
268
|
res.writeHead(pres.statusCode ?? 502, headers);
|
|
268
269
|
if (stream) {
|
|
270
|
+
// Pipe to client immediately (bounded by the OS pipe, not us). Tee a
|
|
271
|
+
// bounded copy purely to parse SSE usage events after the stream ends.
|
|
272
|
+
// On tee overflow the client still receives the full stream; we record
|
|
273
|
+
// a distinct source so missing usage is visible in telemetry.
|
|
269
274
|
pres.pipe(res);
|
|
275
|
+
const teeChunks = [];
|
|
276
|
+
let teeLen = 0;
|
|
277
|
+
let teeAbandoned = false;
|
|
278
|
+
pres.on('data', chunk => {
|
|
279
|
+
if (teeAbandoned)
|
|
280
|
+
return;
|
|
281
|
+
const b = chunk;
|
|
282
|
+
teeLen += b.length;
|
|
283
|
+
if (teeLen > MAX_TEE_BYTES) {
|
|
284
|
+
teeAbandoned = true;
|
|
285
|
+
teeChunks.length = 0;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
teeChunks.push(b);
|
|
289
|
+
});
|
|
270
290
|
pres.on('end', () => {
|
|
291
|
+
const model = typeof bodyJson.model === 'string' ? bodyJson.model : null;
|
|
292
|
+
if (teeAbandoned) {
|
|
293
|
+
store.recordUsage({
|
|
294
|
+
provider: 'anthropic',
|
|
295
|
+
model,
|
|
296
|
+
cwd,
|
|
297
|
+
inputTokens: 0,
|
|
298
|
+
outputTokens: 0,
|
|
299
|
+
source: 'proxy-stream-overflow',
|
|
300
|
+
});
|
|
301
|
+
resolve();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const usage = (0, proxy_sse_1.parseAnthropicSseUsage)(Buffer.concat(teeChunks).toString('utf8'), model);
|
|
271
305
|
store.recordUsage({
|
|
272
306
|
provider: 'anthropic',
|
|
273
|
-
model:
|
|
307
|
+
model: usage.model,
|
|
274
308
|
cwd,
|
|
275
|
-
inputTokens:
|
|
276
|
-
outputTokens:
|
|
277
|
-
estCostUsd: 0,
|
|
309
|
+
inputTokens: usage.inputTokens,
|
|
310
|
+
outputTokens: usage.outputTokens,
|
|
278
311
|
source: 'proxy-stream',
|
|
279
312
|
});
|
|
280
313
|
resolve();
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type OutputFormat = 'text' | 'json' | 'sarif';
|
|
2
|
-
export declare function scanCommand(targetPath: string, format?: OutputFormat, staged?: boolean): Promise<number>;
|
|
2
|
+
export declare function scanCommand(targetPath: string | string[], format?: OutputFormat, staged?: boolean): Promise<number>;
|
package/dist/commands/scan.js
CHANGED
|
@@ -9,6 +9,8 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
9
9
|
const scan_utils_1 = require("../utils/scan-utils");
|
|
10
10
|
async function scanCommand(targetPath, format = 'text', staged = false) {
|
|
11
11
|
const cwd = process.cwd();
|
|
12
|
+
const targetPaths = Array.isArray(targetPath) ? targetPath : [targetPath];
|
|
13
|
+
const targetLabel = targetPaths.length === 1 ? targetPaths[0] : `${targetPaths.length} paths`;
|
|
12
14
|
let config;
|
|
13
15
|
try {
|
|
14
16
|
config = (0, vault_guard_core_1.loadConfig)(cwd);
|
|
@@ -54,7 +56,7 @@ async function scanCommand(targetPath, format = 'text', staged = false) {
|
|
|
54
56
|
'you have audited every pattern.\n'));
|
|
55
57
|
}
|
|
56
58
|
if (format === 'text' && !staged) {
|
|
57
|
-
console.log(chalk_1.default.blue('🔍 Scanning'), chalk_1.default.cyan(
|
|
59
|
+
console.log(chalk_1.default.blue('🔍 Scanning'), chalk_1.default.cyan(targetLabel));
|
|
58
60
|
}
|
|
59
61
|
const stats = { filesScanned: 0, bytesScanned: 0 };
|
|
60
62
|
const t0 = Date.now();
|
|
@@ -96,7 +98,7 @@ async function scanCommand(targetPath, format = 'text', staged = false) {
|
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
100
|
else {
|
|
99
|
-
results = await (0, scan_utils_1.scanFilesAsync)(
|
|
101
|
+
results = await (0, scan_utils_1.scanFilesAsync)(targetPaths, scanner, {
|
|
100
102
|
verbose: format === 'text',
|
|
101
103
|
skipBinary: true,
|
|
102
104
|
progress: format === 'text',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaultcompass/vault-guard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Block secrets at commit and in CI. Pre-commit hooks, SARIF output, and fast staged-file scans for AI-native workflows.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"chalk": "^4.1.2",
|
|
38
38
|
"commander": "^12.0.0",
|
|
39
|
-
"@vaultcompass/vault-guard-core": "1.
|
|
40
|
-
"@vaultcompass/vault-guard-telemetry": "1.
|
|
39
|
+
"@vaultcompass/vault-guard-core": "1.1.1",
|
|
40
|
+
"@vaultcompass/vault-guard-telemetry": "1.1.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/jest": "^30.0.0",
|
|
44
|
-
"@types/node": "^25.9.
|
|
44
|
+
"@types/node": "^25.9.3",
|
|
45
45
|
"jest": "^30.4.2",
|
|
46
46
|
"ts-jest": "^29.4.11",
|
|
47
47
|
"typescript": "^5.3.3"
|