@synkro-sh/cli 1.4.59 → 1.4.60
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/bootstrap.js +60 -36
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -834,10 +834,11 @@ var init_hookScriptsTs = __esm({
|
|
|
834
834
|
"use strict";
|
|
835
835
|
SYNKRO_COMMON_TS = `
|
|
836
836
|
// Shared Synkro hook utilities \u2014 imported by all hook scripts.
|
|
837
|
-
import { readFileSync, writeFileSync, appendFileSync, mkdirSync,
|
|
838
|
-
import { join, dirname, basename, extname } from 'node:path';
|
|
837
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, renameSync, openSync, closeSync, unlinkSync } from 'node:fs';
|
|
838
|
+
import { join, dirname, basename, extname, resolve as resolvePath } from 'node:path';
|
|
839
839
|
import { homedir } from 'node:os';
|
|
840
840
|
import { execSync, spawn } from 'node:child_process';
|
|
841
|
+
import { constants as FS_CONSTANTS } from 'node:fs';
|
|
841
842
|
|
|
842
843
|
// \u2500\u2500\u2500 Config \u2500\u2500\u2500
|
|
843
844
|
|
|
@@ -868,6 +869,15 @@ export const GATEWAY_URL = process.env.SYNKRO_GATEWAY_URL || 'https://api.synkro
|
|
|
868
869
|
export const CREDS_PATH = process.env.SYNKRO_CREDENTIALS_PATH || join(HOME, '.synkro', 'credentials.json');
|
|
869
870
|
const LAST_PROMPT_FILE = join(HOME, '.synkro', '.last-prompt');
|
|
870
871
|
|
|
872
|
+
// \u2500\u2500\u2500 Path Validation \u2500\u2500\u2500
|
|
873
|
+
|
|
874
|
+
export function isPathUnder(filePath: string, cwd: string): boolean {
|
|
875
|
+
if (!filePath || !cwd) return false;
|
|
876
|
+
const resolved = resolvePath(filePath);
|
|
877
|
+
const base = resolvePath(cwd);
|
|
878
|
+
return resolved.startsWith(base + '/') || resolved === base;
|
|
879
|
+
}
|
|
880
|
+
|
|
871
881
|
// \u2500\u2500\u2500 Logging \u2500\u2500\u2500
|
|
872
882
|
|
|
873
883
|
export function log(msg: string): void {
|
|
@@ -936,24 +946,22 @@ export async function ensureFreshJwt(jwt: string): Promise<string> {
|
|
|
936
946
|
const now = Math.floor(Date.now() / 1000);
|
|
937
947
|
if (exp - now >= 60) return jwt;
|
|
938
948
|
|
|
939
|
-
const
|
|
940
|
-
let
|
|
949
|
+
const lockfile = CREDS_PATH + '.lock';
|
|
950
|
+
let fd = -1;
|
|
941
951
|
try {
|
|
942
|
-
|
|
943
|
-
acquired = true;
|
|
952
|
+
fd = openSync(lockfile, FS_CONSTANTS.O_WRONLY | FS_CONSTANTS.O_CREAT | FS_CONSTANTS.O_EXCL, 0o644);
|
|
944
953
|
} catch {
|
|
945
|
-
// Another process
|
|
946
|
-
for (let i = 0; i <
|
|
947
|
-
await new Promise(r => setTimeout(r,
|
|
948
|
-
if (!existsSync(
|
|
954
|
+
// Another process holds the lock \u2014 wait for it to finish and re-read
|
|
955
|
+
for (let i = 0; i < 10; i++) {
|
|
956
|
+
await new Promise(r => setTimeout(r, 300));
|
|
957
|
+
if (!existsSync(lockfile)) break;
|
|
949
958
|
}
|
|
950
|
-
// Re-read creds
|
|
951
959
|
const fresh = loadJwt();
|
|
952
960
|
return fresh || jwt;
|
|
953
961
|
}
|
|
954
962
|
|
|
955
963
|
try {
|
|
956
|
-
// Re-check \u2014 another hook may have just refreshed
|
|
964
|
+
// Re-check \u2014 another hook may have just refreshed while we waited
|
|
957
965
|
const freshJwt = loadJwt();
|
|
958
966
|
if (freshJwt) {
|
|
959
967
|
const freshExp = decodeJwtExp(freshJwt);
|
|
@@ -961,9 +969,8 @@ export async function ensureFreshJwt(jwt: string): Promise<string> {
|
|
|
961
969
|
}
|
|
962
970
|
return await refreshJwt(jwt);
|
|
963
971
|
} finally {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
}
|
|
972
|
+
try { closeSync(fd); } catch {}
|
|
973
|
+
try { unlinkSync(lockfile); } catch {}
|
|
967
974
|
}
|
|
968
975
|
}
|
|
969
976
|
|
|
@@ -1014,7 +1021,7 @@ export interface HookConfig {
|
|
|
1014
1021
|
silent: boolean;
|
|
1015
1022
|
policyName: string;
|
|
1016
1023
|
rules: Rule[];
|
|
1017
|
-
scanExemptions: string
|
|
1024
|
+
scanExemptions: Array<{ path: string; cwe_id: string }>;
|
|
1018
1025
|
}
|
|
1019
1026
|
|
|
1020
1027
|
export async function loadConfig(jwt: string, query?: string): Promise<HookConfig> {
|
|
@@ -1038,7 +1045,9 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1038
1045
|
config.silent = data.silent_mode === true || data.silent_mode === 'true';
|
|
1039
1046
|
config.policyName = data.active_policy_name || '';
|
|
1040
1047
|
if (Array.isArray(data.scan_exemptions)) {
|
|
1041
|
-
config.scanExemptions = data.scan_exemptions
|
|
1048
|
+
config.scanExemptions = data.scan_exemptions
|
|
1049
|
+
.filter((e: any) => e && typeof e.path === 'string')
|
|
1050
|
+
.map((e: any) => ({ path: e.path, cwe_id: e.cwe_id || '' }));
|
|
1042
1051
|
}
|
|
1043
1052
|
if (Array.isArray(data.rules)) {
|
|
1044
1053
|
config.rules = data.rules
|
|
@@ -1283,14 +1292,15 @@ export function ruleMode(ruleId: string, rules: Rule[]): 'blocking' | 'audit' {
|
|
|
1283
1292
|
|
|
1284
1293
|
// \u2500\u2500\u2500 Content Reconstruction \u2500\u2500\u2500
|
|
1285
1294
|
|
|
1286
|
-
export function reconstructContent(toolName: string, toolInput: any, filePath: string): string {
|
|
1295
|
+
export function reconstructContent(toolName: string, toolInput: any, filePath: string, cwd?: string): string {
|
|
1296
|
+
const canRead = filePath && (!cwd || isPathUnder(filePath, cwd));
|
|
1287
1297
|
switch (toolName) {
|
|
1288
1298
|
case 'Write':
|
|
1289
1299
|
return toolInput.content || '';
|
|
1290
1300
|
case 'Edit': {
|
|
1291
1301
|
let content = '';
|
|
1292
1302
|
try {
|
|
1293
|
-
if (
|
|
1303
|
+
if (canRead && existsSync(filePath)) {
|
|
1294
1304
|
content = readFileSync(filePath, 'utf-8').slice(0, 65536);
|
|
1295
1305
|
}
|
|
1296
1306
|
} catch {}
|
|
@@ -1304,7 +1314,7 @@ export function reconstructContent(toolName: string, toolInput: any, filePath: s
|
|
|
1304
1314
|
case 'MultiEdit': {
|
|
1305
1315
|
let content = '';
|
|
1306
1316
|
try {
|
|
1307
|
-
if (
|
|
1317
|
+
if (canRead && existsSync(filePath)) {
|
|
1308
1318
|
content = readFileSync(filePath, 'utf-8').slice(0, 65536);
|
|
1309
1319
|
}
|
|
1310
1320
|
} catch {}
|
|
@@ -1589,7 +1599,7 @@ export function outputEmpty(): void {
|
|
|
1589
1599
|
EDIT_PRECHECK_TS = `#!/usr/bin/env bun
|
|
1590
1600
|
import {
|
|
1591
1601
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
1592
|
-
parseVerdict, dispatchCapture, ruleMode, reconstructContent, postWithRetry,
|
|
1602
|
+
parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
1593
1603
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, log,
|
|
1594
1604
|
outputJson, outputEmpty, GATEWAY_URL,
|
|
1595
1605
|
type HookConfig, type Rule,
|
|
@@ -1631,7 +1641,7 @@ async function main() {
|
|
|
1631
1641
|
jwt = await ensureFreshJwt(jwt);
|
|
1632
1642
|
|
|
1633
1643
|
// Reconstruct proposed content
|
|
1634
|
-
const proposed = reconstructContent(toolName, toolInput, filePath);
|
|
1644
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1635
1645
|
if (!proposed) { outputEmpty(); return; }
|
|
1636
1646
|
|
|
1637
1647
|
// Build diff field
|
|
@@ -1645,7 +1655,7 @@ async function main() {
|
|
|
1645
1655
|
|
|
1646
1656
|
// Read file before edit for cloud payload
|
|
1647
1657
|
let fileBefore = '';
|
|
1648
|
-
if (toolName !== 'Write' && filePath && existsSync(filePath)) {
|
|
1658
|
+
if (toolName !== 'Write' && filePath && isPathUnder(filePath, cwd || '.') && existsSync(filePath)) {
|
|
1649
1659
|
try { fileBefore = readFileSync(filePath, 'utf-8').slice(0, 65536); } catch {}
|
|
1650
1660
|
}
|
|
1651
1661
|
|
|
@@ -1835,17 +1845,19 @@ async function main() {
|
|
|
1835
1845
|
jwt = await ensureFreshJwt(jwt);
|
|
1836
1846
|
|
|
1837
1847
|
// Reconstruct proposed content
|
|
1838
|
-
const proposed = reconstructContent(toolName, toolInput, filePath);
|
|
1848
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1839
1849
|
if (!proposed) { outputEmpty(); return; }
|
|
1840
1850
|
|
|
1841
1851
|
const config = await loadConfig(jwt);
|
|
1842
1852
|
const rt = await cweRoute(config);
|
|
1843
1853
|
|
|
1844
|
-
//
|
|
1845
|
-
|
|
1846
|
-
|
|
1854
|
+
// Build set of exempted CWE IDs for this file path
|
|
1855
|
+
const exemptedCwes = new Set<string>();
|
|
1856
|
+
for (const ex of config.scanExemptions) {
|
|
1857
|
+
if (ex.cwe_id && filePath.includes(ex.path)) {
|
|
1858
|
+
exemptedCwes.add(ex.cwe_id.toUpperCase());
|
|
1859
|
+
}
|
|
1847
1860
|
}
|
|
1848
|
-
|
|
1849
1861
|
if (config.silent) {
|
|
1850
1862
|
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan] ' + fileShort + ' \\u2192 skipped (silent mode)' });
|
|
1851
1863
|
return;
|
|
@@ -1891,15 +1903,23 @@ async function main() {
|
|
|
1891
1903
|
const verdict = parseVerdict(gradeResp);
|
|
1892
1904
|
|
|
1893
1905
|
if (!verdict.ok) {
|
|
1894
|
-
// Extract all CWE rule_ids from the raw response
|
|
1895
1906
|
const ruleIdMatches = gradeResp.match(/<rule_id>([^<]+)<\\/rule_id>/g) || [];
|
|
1896
1907
|
const cweIds: string[] = [];
|
|
1897
1908
|
for (const match of ruleIdMatches.slice(0, 5)) {
|
|
1898
1909
|
const id = match.replace(/<\\/?rule_id>/g, '').trim().replace(/^cwe-/, 'CWE-');
|
|
1899
1910
|
if (id && !cweIds.includes(id)) cweIds.push(id);
|
|
1900
1911
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1912
|
+
|
|
1913
|
+
// Filter out exempted CWEs for this file
|
|
1914
|
+
const activeCweIds = cweIds.filter(id => !exemptedCwes.has(id.toUpperCase()));
|
|
1915
|
+
|
|
1916
|
+
if (activeCweIds.length === 0) {
|
|
1917
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \\u2192 clean (exempted: ' + cweIds.join(', ') + ')' });
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
const displayIds = activeCweIds.slice(0, 3).join(', ');
|
|
1922
|
+
const count = activeCweIds.length;
|
|
1903
1923
|
const label = count === 1 ? 'match' : 'matches';
|
|
1904
1924
|
const cweMsg = cweTag + ' ' + fileShort + ' \\u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
1905
1925
|
const denyDetail = '[' + displayIds + '] ' + (verdict.reason || 'code weakness detected');
|
|
@@ -1985,7 +2005,7 @@ async function main() {
|
|
|
1985
2005
|
const cveTag = '[synkro:' + rt + ':cveScan]';
|
|
1986
2006
|
|
|
1987
2007
|
// Reconstruct proposed content
|
|
1988
|
-
const proposed = reconstructContent(toolName, toolInput, filePath);
|
|
2008
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1989
2009
|
if (!proposed) {
|
|
1990
2010
|
outputJson({ systemMessage: cveTag + ' ' + fileShort + ' \\u2192 skip (no content)' });
|
|
1991
2011
|
return;
|
|
@@ -2420,7 +2440,7 @@ main();
|
|
|
2420
2440
|
STOP_SUMMARY_TS = `#!/usr/bin/env bun
|
|
2421
2441
|
import {
|
|
2422
2442
|
loadJwt, detectRepo, loadConfig, tag, readStdin, aggregateUsage,
|
|
2423
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
2443
|
+
outputJson, outputEmpty, appendLocalTelemetry, GATEWAY_URL,
|
|
2424
2444
|
} from './_synkro-common.ts';
|
|
2425
2445
|
|
|
2426
2446
|
async function main() {
|
|
@@ -2459,6 +2479,7 @@ async function main() {
|
|
|
2459
2479
|
...(gitRepo ? { repo: gitRepo } : {}),
|
|
2460
2480
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
2461
2481
|
};
|
|
2482
|
+
appendLocalTelemetry(usageBody);
|
|
2462
2483
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2463
2484
|
method: 'POST',
|
|
2464
2485
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -2570,7 +2591,7 @@ main();
|
|
|
2570
2591
|
BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
|
|
2571
2592
|
import {
|
|
2572
2593
|
loadJwt, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
|
|
2573
|
-
outputEmpty, GATEWAY_URL,
|
|
2594
|
+
outputEmpty, appendLocalTelemetry, GATEWAY_URL,
|
|
2574
2595
|
} from './_synkro-common.ts';
|
|
2575
2596
|
|
|
2576
2597
|
async function main() {
|
|
@@ -2611,6 +2632,8 @@ async function main() {
|
|
|
2611
2632
|
command_hash: cmdHash,
|
|
2612
2633
|
};
|
|
2613
2634
|
|
|
2635
|
+
appendLocalTelemetry(body);
|
|
2636
|
+
|
|
2614
2637
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2615
2638
|
method: 'POST',
|
|
2616
2639
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -2628,7 +2651,7 @@ main();
|
|
|
2628
2651
|
`;
|
|
2629
2652
|
TRANSCRIPT_SYNC_TS = `#!/usr/bin/env bun
|
|
2630
2653
|
import {
|
|
2631
|
-
loadJwt, detectRepo, readStdin, aggregateUsage,
|
|
2654
|
+
loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
|
|
2632
2655
|
outputEmpty, GATEWAY_URL,
|
|
2633
2656
|
} from './_synkro-common.ts';
|
|
2634
2657
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
@@ -2671,6 +2694,7 @@ async function main() {
|
|
|
2671
2694
|
},
|
|
2672
2695
|
session_id: sessionId,
|
|
2673
2696
|
};
|
|
2697
|
+
appendLocalTelemetry(usageBody);
|
|
2674
2698
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2675
2699
|
method: 'POST',
|
|
2676
2700
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -5113,7 +5137,7 @@ function writeConfigEnv(opts) {
|
|
|
5113
5137
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
5114
5138
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
5115
5139
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
5116
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
5140
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.60")}`
|
|
5117
5141
|
];
|
|
5118
5142
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
5119
5143
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|