@synkro-sh/cli 1.4.59 → 1.4.61
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 +121 -67
- package/dist/bootstrap.js.map +1 -1
- package/package.json +2 -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 {
|
|
@@ -901,69 +911,96 @@ function decodeJwtExp(jwt: string): number {
|
|
|
901
911
|
}
|
|
902
912
|
|
|
903
913
|
export async function refreshJwt(jwt: string): Promise<string> {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
if (!rt) return jwt;
|
|
914
|
+
const creds = JSON.parse(readFileSync(CREDS_PATH, 'utf-8'));
|
|
915
|
+
const rt = creds.refresh_token;
|
|
916
|
+
if (!rt) return jwt;
|
|
908
917
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const newRt = data.refresh_token || rt;
|
|
920
|
-
const existing = (() => {
|
|
921
|
-
try { return JSON.parse(readFileSync(CREDS_PATH, 'utf-8')); } catch { return {}; }
|
|
922
|
-
})();
|
|
923
|
-
const updated = { ...existing, access_token: newAt, refresh_token: newRt };
|
|
924
|
-
const tmp = CREDS_PATH + '.synkro.tmp';
|
|
925
|
-
writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
926
|
-
renameSync(tmp, CREDS_PATH);
|
|
927
|
-
return newAt;
|
|
928
|
-
} catch {
|
|
918
|
+
const resp = await fetch(GATEWAY_URL + '/api/auth/refresh', {
|
|
919
|
+
method: 'POST',
|
|
920
|
+
headers: { 'Content-Type': 'application/json' },
|
|
921
|
+
body: JSON.stringify({ refresh_token: rt }),
|
|
922
|
+
signal: AbortSignal.timeout(4000),
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
if (!resp.ok) {
|
|
926
|
+
log('refresh failed: HTTP ' + resp.status);
|
|
929
927
|
return jwt;
|
|
930
928
|
}
|
|
929
|
+
|
|
930
|
+
const data = await resp.json() as any;
|
|
931
|
+
const newAt = data.access_token;
|
|
932
|
+
if (!newAt) return jwt;
|
|
933
|
+
|
|
934
|
+
const newRt = data.refresh_token || rt;
|
|
935
|
+
const existing = (() => {
|
|
936
|
+
try { return JSON.parse(readFileSync(CREDS_PATH, 'utf-8')); } catch { return {}; }
|
|
937
|
+
})();
|
|
938
|
+
const updated = { ...existing, access_token: newAt, refresh_token: newRt };
|
|
939
|
+
const tmp = CREDS_PATH + '.synkro.tmp';
|
|
940
|
+
writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
941
|
+
renameSync(tmp, CREDS_PATH);
|
|
942
|
+
return newAt;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function jwtIsExpired(jwt: string): boolean {
|
|
946
|
+
const exp = decodeJwtExp(jwt);
|
|
947
|
+
return exp - Math.floor(Date.now() / 1000) < 60;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function lockIsStale(lockfile: string, maxAgeMs = 8000): boolean {
|
|
951
|
+
try {
|
|
952
|
+
const stat = require('node:fs').statSync(lockfile);
|
|
953
|
+
return Date.now() - stat.mtimeMs > maxAgeMs;
|
|
954
|
+
} catch {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
931
957
|
}
|
|
932
958
|
|
|
933
959
|
export async function ensureFreshJwt(jwt: string): Promise<string> {
|
|
934
960
|
if (!jwt) return jwt;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
961
|
+
if (!jwtIsExpired(jwt)) return jwt;
|
|
962
|
+
|
|
963
|
+
const lockfile = CREDS_PATH + '.lock';
|
|
964
|
+
|
|
965
|
+
// Clean stale lock left by a killed hook
|
|
966
|
+
if (existsSync(lockfile) && lockIsStale(lockfile)) {
|
|
967
|
+
try { unlinkSync(lockfile); } catch {}
|
|
968
|
+
}
|
|
938
969
|
|
|
939
|
-
|
|
940
|
-
let acquired = false;
|
|
970
|
+
let fd = -1;
|
|
941
971
|
try {
|
|
942
|
-
|
|
943
|
-
acquired = true;
|
|
972
|
+
fd = openSync(lockfile, FS_CONSTANTS.O_WRONLY | FS_CONSTANTS.O_CREAT | FS_CONSTANTS.O_EXCL, 0o644);
|
|
944
973
|
} catch {
|
|
945
|
-
// Another process is refreshing \u2014 wait
|
|
946
|
-
for (let i = 0; i <
|
|
974
|
+
// Another process is refreshing \u2014 wait for it
|
|
975
|
+
for (let i = 0; i < 8; i++) {
|
|
947
976
|
await new Promise(r => setTimeout(r, 500));
|
|
948
|
-
if (!existsSync(
|
|
977
|
+
if (!existsSync(lockfile)) break;
|
|
949
978
|
}
|
|
950
|
-
//
|
|
979
|
+
// Stale lock after full wait \u2014 force-remove
|
|
980
|
+
if (existsSync(lockfile) && lockIsStale(lockfile)) {
|
|
981
|
+
try { unlinkSync(lockfile); } catch {}
|
|
982
|
+
}
|
|
983
|
+
// Re-read \u2014 the winner should have written a fresh JWT
|
|
951
984
|
const fresh = loadJwt();
|
|
952
|
-
|
|
985
|
+
if (fresh && !jwtIsExpired(fresh)) return fresh;
|
|
986
|
+
// Winner's refresh failed \u2014 try to acquire lock ourselves
|
|
987
|
+
try {
|
|
988
|
+
fd = openSync(lockfile, FS_CONSTANTS.O_WRONLY | FS_CONSTANTS.O_CREAT | FS_CONSTANTS.O_EXCL, 0o644);
|
|
989
|
+
} catch {
|
|
990
|
+
return fresh || jwt;
|
|
991
|
+
}
|
|
953
992
|
}
|
|
954
993
|
|
|
955
994
|
try {
|
|
956
|
-
// Re-check \u2014 another hook may have
|
|
995
|
+
// Re-check \u2014 another hook may have written fresh creds while we waited for lock
|
|
957
996
|
const freshJwt = loadJwt();
|
|
958
|
-
if (freshJwt)
|
|
959
|
-
const freshExp = decodeJwtExp(freshJwt);
|
|
960
|
-
if (freshExp - Math.floor(Date.now() / 1000) >= 60) return freshJwt;
|
|
961
|
-
}
|
|
997
|
+
if (freshJwt && !jwtIsExpired(freshJwt)) return freshJwt;
|
|
962
998
|
return await refreshJwt(jwt);
|
|
999
|
+
} catch {
|
|
1000
|
+
return jwt;
|
|
963
1001
|
} finally {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
}
|
|
1002
|
+
try { closeSync(fd); } catch {}
|
|
1003
|
+
try { unlinkSync(lockfile); } catch {}
|
|
967
1004
|
}
|
|
968
1005
|
}
|
|
969
1006
|
|
|
@@ -1014,7 +1051,7 @@ export interface HookConfig {
|
|
|
1014
1051
|
silent: boolean;
|
|
1015
1052
|
policyName: string;
|
|
1016
1053
|
rules: Rule[];
|
|
1017
|
-
scanExemptions: string
|
|
1054
|
+
scanExemptions: Array<{ path: string; cwe_id: string }>;
|
|
1018
1055
|
}
|
|
1019
1056
|
|
|
1020
1057
|
export async function loadConfig(jwt: string, query?: string): Promise<HookConfig> {
|
|
@@ -1038,7 +1075,9 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1038
1075
|
config.silent = data.silent_mode === true || data.silent_mode === 'true';
|
|
1039
1076
|
config.policyName = data.active_policy_name || '';
|
|
1040
1077
|
if (Array.isArray(data.scan_exemptions)) {
|
|
1041
|
-
config.scanExemptions = data.scan_exemptions
|
|
1078
|
+
config.scanExemptions = data.scan_exemptions
|
|
1079
|
+
.filter((e: any) => e && typeof e.path === 'string')
|
|
1080
|
+
.map((e: any) => ({ path: e.path, cwe_id: e.cwe_id || '' }));
|
|
1042
1081
|
}
|
|
1043
1082
|
if (Array.isArray(data.rules)) {
|
|
1044
1083
|
config.rules = data.rules
|
|
@@ -1283,14 +1322,15 @@ export function ruleMode(ruleId: string, rules: Rule[]): 'blocking' | 'audit' {
|
|
|
1283
1322
|
|
|
1284
1323
|
// \u2500\u2500\u2500 Content Reconstruction \u2500\u2500\u2500
|
|
1285
1324
|
|
|
1286
|
-
export function reconstructContent(toolName: string, toolInput: any, filePath: string): string {
|
|
1325
|
+
export function reconstructContent(toolName: string, toolInput: any, filePath: string, cwd?: string): string {
|
|
1326
|
+
const canRead = filePath && (!cwd || isPathUnder(filePath, cwd));
|
|
1287
1327
|
switch (toolName) {
|
|
1288
1328
|
case 'Write':
|
|
1289
1329
|
return toolInput.content || '';
|
|
1290
1330
|
case 'Edit': {
|
|
1291
1331
|
let content = '';
|
|
1292
1332
|
try {
|
|
1293
|
-
if (
|
|
1333
|
+
if (canRead && existsSync(filePath)) {
|
|
1294
1334
|
content = readFileSync(filePath, 'utf-8').slice(0, 65536);
|
|
1295
1335
|
}
|
|
1296
1336
|
} catch {}
|
|
@@ -1304,7 +1344,7 @@ export function reconstructContent(toolName: string, toolInput: any, filePath: s
|
|
|
1304
1344
|
case 'MultiEdit': {
|
|
1305
1345
|
let content = '';
|
|
1306
1346
|
try {
|
|
1307
|
-
if (
|
|
1347
|
+
if (canRead && existsSync(filePath)) {
|
|
1308
1348
|
content = readFileSync(filePath, 'utf-8').slice(0, 65536);
|
|
1309
1349
|
}
|
|
1310
1350
|
} catch {}
|
|
@@ -1589,7 +1629,7 @@ export function outputEmpty(): void {
|
|
|
1589
1629
|
EDIT_PRECHECK_TS = `#!/usr/bin/env bun
|
|
1590
1630
|
import {
|
|
1591
1631
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
1592
|
-
parseVerdict, dispatchCapture, ruleMode, reconstructContent, postWithRetry,
|
|
1632
|
+
parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
1593
1633
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, log,
|
|
1594
1634
|
outputJson, outputEmpty, GATEWAY_URL,
|
|
1595
1635
|
type HookConfig, type Rule,
|
|
@@ -1631,7 +1671,7 @@ async function main() {
|
|
|
1631
1671
|
jwt = await ensureFreshJwt(jwt);
|
|
1632
1672
|
|
|
1633
1673
|
// Reconstruct proposed content
|
|
1634
|
-
const proposed = reconstructContent(toolName, toolInput, filePath);
|
|
1674
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1635
1675
|
if (!proposed) { outputEmpty(); return; }
|
|
1636
1676
|
|
|
1637
1677
|
// Build diff field
|
|
@@ -1645,7 +1685,7 @@ async function main() {
|
|
|
1645
1685
|
|
|
1646
1686
|
// Read file before edit for cloud payload
|
|
1647
1687
|
let fileBefore = '';
|
|
1648
|
-
if (toolName !== 'Write' && filePath && existsSync(filePath)) {
|
|
1688
|
+
if (toolName !== 'Write' && filePath && isPathUnder(filePath, cwd || '.') && existsSync(filePath)) {
|
|
1649
1689
|
try { fileBefore = readFileSync(filePath, 'utf-8').slice(0, 65536); } catch {}
|
|
1650
1690
|
}
|
|
1651
1691
|
|
|
@@ -1835,17 +1875,19 @@ async function main() {
|
|
|
1835
1875
|
jwt = await ensureFreshJwt(jwt);
|
|
1836
1876
|
|
|
1837
1877
|
// Reconstruct proposed content
|
|
1838
|
-
const proposed = reconstructContent(toolName, toolInput, filePath);
|
|
1878
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1839
1879
|
if (!proposed) { outputEmpty(); return; }
|
|
1840
1880
|
|
|
1841
1881
|
const config = await loadConfig(jwt);
|
|
1842
1882
|
const rt = await cweRoute(config);
|
|
1843
1883
|
|
|
1844
|
-
//
|
|
1845
|
-
|
|
1846
|
-
|
|
1884
|
+
// Build set of exempted CWE IDs for this file path
|
|
1885
|
+
const exemptedCwes = new Set<string>();
|
|
1886
|
+
for (const ex of config.scanExemptions) {
|
|
1887
|
+
if (ex.cwe_id && filePath.includes(ex.path)) {
|
|
1888
|
+
exemptedCwes.add(ex.cwe_id.toUpperCase());
|
|
1889
|
+
}
|
|
1847
1890
|
}
|
|
1848
|
-
|
|
1849
1891
|
if (config.silent) {
|
|
1850
1892
|
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan] ' + fileShort + ' \\u2192 skipped (silent mode)' });
|
|
1851
1893
|
return;
|
|
@@ -1891,15 +1933,23 @@ async function main() {
|
|
|
1891
1933
|
const verdict = parseVerdict(gradeResp);
|
|
1892
1934
|
|
|
1893
1935
|
if (!verdict.ok) {
|
|
1894
|
-
// Extract all CWE rule_ids from the raw response
|
|
1895
1936
|
const ruleIdMatches = gradeResp.match(/<rule_id>([^<]+)<\\/rule_id>/g) || [];
|
|
1896
1937
|
const cweIds: string[] = [];
|
|
1897
1938
|
for (const match of ruleIdMatches.slice(0, 5)) {
|
|
1898
1939
|
const id = match.replace(/<\\/?rule_id>/g, '').trim().replace(/^cwe-/, 'CWE-');
|
|
1899
1940
|
if (id && !cweIds.includes(id)) cweIds.push(id);
|
|
1900
1941
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1942
|
+
|
|
1943
|
+
// Filter out exempted CWEs for this file
|
|
1944
|
+
const activeCweIds = cweIds.filter(id => !exemptedCwes.has(id.toUpperCase()));
|
|
1945
|
+
|
|
1946
|
+
if (activeCweIds.length === 0) {
|
|
1947
|
+
outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \\u2192 clean (exempted: ' + cweIds.join(', ') + ')' });
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
const displayIds = activeCweIds.slice(0, 3).join(', ');
|
|
1952
|
+
const count = activeCweIds.length;
|
|
1903
1953
|
const label = count === 1 ? 'match' : 'matches';
|
|
1904
1954
|
const cweMsg = cweTag + ' ' + fileShort + ' \\u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
1905
1955
|
const denyDetail = '[' + displayIds + '] ' + (verdict.reason || 'code weakness detected');
|
|
@@ -1985,7 +2035,7 @@ async function main() {
|
|
|
1985
2035
|
const cveTag = '[synkro:' + rt + ':cveScan]';
|
|
1986
2036
|
|
|
1987
2037
|
// Reconstruct proposed content
|
|
1988
|
-
const proposed = reconstructContent(toolName, toolInput, filePath);
|
|
2038
|
+
const proposed = reconstructContent(toolName, toolInput, filePath, cwd);
|
|
1989
2039
|
if (!proposed) {
|
|
1990
2040
|
outputJson({ systemMessage: cveTag + ' ' + fileShort + ' \\u2192 skip (no content)' });
|
|
1991
2041
|
return;
|
|
@@ -2420,7 +2470,7 @@ main();
|
|
|
2420
2470
|
STOP_SUMMARY_TS = `#!/usr/bin/env bun
|
|
2421
2471
|
import {
|
|
2422
2472
|
loadJwt, detectRepo, loadConfig, tag, readStdin, aggregateUsage,
|
|
2423
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
2473
|
+
outputJson, outputEmpty, appendLocalTelemetry, GATEWAY_URL,
|
|
2424
2474
|
} from './_synkro-common.ts';
|
|
2425
2475
|
|
|
2426
2476
|
async function main() {
|
|
@@ -2459,6 +2509,7 @@ async function main() {
|
|
|
2459
2509
|
...(gitRepo ? { repo: gitRepo } : {}),
|
|
2460
2510
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
2461
2511
|
};
|
|
2512
|
+
appendLocalTelemetry(usageBody);
|
|
2462
2513
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2463
2514
|
method: 'POST',
|
|
2464
2515
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -2570,7 +2621,7 @@ main();
|
|
|
2570
2621
|
BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
|
|
2571
2622
|
import {
|
|
2572
2623
|
loadJwt, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
|
|
2573
|
-
outputEmpty, GATEWAY_URL,
|
|
2624
|
+
outputEmpty, appendLocalTelemetry, GATEWAY_URL,
|
|
2574
2625
|
} from './_synkro-common.ts';
|
|
2575
2626
|
|
|
2576
2627
|
async function main() {
|
|
@@ -2611,6 +2662,8 @@ async function main() {
|
|
|
2611
2662
|
command_hash: cmdHash,
|
|
2612
2663
|
};
|
|
2613
2664
|
|
|
2665
|
+
appendLocalTelemetry(body);
|
|
2666
|
+
|
|
2614
2667
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2615
2668
|
method: 'POST',
|
|
2616
2669
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -2628,7 +2681,7 @@ main();
|
|
|
2628
2681
|
`;
|
|
2629
2682
|
TRANSCRIPT_SYNC_TS = `#!/usr/bin/env bun
|
|
2630
2683
|
import {
|
|
2631
|
-
loadJwt, detectRepo, readStdin, aggregateUsage,
|
|
2684
|
+
loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
|
|
2632
2685
|
outputEmpty, GATEWAY_URL,
|
|
2633
2686
|
} from './_synkro-common.ts';
|
|
2634
2687
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
@@ -2671,6 +2724,7 @@ async function main() {
|
|
|
2671
2724
|
},
|
|
2672
2725
|
session_id: sessionId,
|
|
2673
2726
|
};
|
|
2727
|
+
appendLocalTelemetry(usageBody);
|
|
2674
2728
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
2675
2729
|
method: 'POST',
|
|
2676
2730
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -5113,7 +5167,7 @@ function writeConfigEnv(opts) {
|
|
|
5113
5167
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
5114
5168
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
5115
5169
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
5116
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
5170
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.61")}`
|
|
5117
5171
|
];
|
|
5118
5172
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
5119
5173
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|