@synkro-sh/cli 1.6.100 → 1.7.0

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 CHANGED
@@ -1050,16 +1050,18 @@ export function log(msg: string): void {
1050
1050
  // \u2500\u2500\u2500 JWT Management \u2500\u2500\u2500
1051
1051
 
1052
1052
  export function loadJwt(): string | null {
1053
- // Cloud mode: prefer the long-lived (1yr) MCP token. It's org+user scoped and
1054
- // accepted on the whole cloud-hook surface (grade/submit, cwe-rules, hook/*,
1055
- // cli/*), so grading survives sessions far longer than the ~5-min WorkOS token.
1056
- // jwtIsExpired sees a year of runway, so ensureFreshJwt never needs to refresh.
1057
- if (process.env.SYNKRO_STORAGE_MODE === 'cloud') {
1058
- try {
1059
- const mcp = readFileSync(MCP_JWT_PATH, 'utf-8').trim();
1060
- if (mcp) return mcp;
1061
- } catch { /* no mcp token on disk \u2014 fall back to the login token below */ }
1062
- }
1053
+ // Prefer the long-lived (1yr) Synkro MCP token in BOTH cloud and local mode.
1054
+ // It's org+user scoped and accepted across the hook surface (grade/submit,
1055
+ // cwe-rules, hook/*, cli/*), so a single 'synkro login' keeps grading + CWE
1056
+ // detection alive for a year with NO refresh. The alternative \u2014 the ~5-min
1057
+ // WorkOS access_token \u2014 rides a rotating refresh chain that dies with the
1058
+ // WorkOS session (invalid_grant "session has already ended"), which silently
1059
+ // disabled CWE findings while editGuard/cveScan (which don't need this token)
1060
+ // kept working. jwtIsExpired sees a year of runway, so we never hit refreshJwt.
1061
+ try {
1062
+ const mcp = readFileSync(MCP_JWT_PATH, 'utf-8').trim();
1063
+ if (mcp) return mcp;
1064
+ } catch { /* no mcp token on disk \u2014 fall back to the login token below */ }
1063
1065
  try {
1064
1066
  if (!existsSync(CREDS_PATH)) return null;
1065
1067
  const creds = JSON.parse(readFileSync(CREDS_PATH, 'utf-8'));
@@ -5126,7 +5128,9 @@ async function main() {
5126
5128
  const findings = Array.isArray(cweResp?.findings) ? cweResp.findings : [];
5127
5129
  if (cweResp?.action === 'deny' && findings.length > 0) {
5128
5130
  const activeCweIds = findings
5129
- .filter((f: any) => f.mode === 'blocking' || f.mode === 'ask')
5131
+ // CWE is a separate, always-ask grader a detected weakness is ALWAYS
5132
+ // surfaced regardless of the rule's stored mode (fix/blocking/ask).
5133
+ // Only path exemptions suppress it (matches the local-route branch).
5130
5134
  .map((f: any) => f.cwe)
5131
5135
  .filter((id: string) => !exemptedCwes.has(id.toUpperCase()));
5132
5136
 
@@ -5297,6 +5301,40 @@ async function main() {
5297
5301
  let anyFailed = false;
5298
5302
 
5299
5303
  for (const gradeResp of gradeResponses) {
5304
+ // The CWE grader emits findings, but its wrapper VARIES per call:
5305
+ // {"findings":[...]} | <findings>[...]</findings> | a bare [...] array.
5306
+ // Extract findings from any of them. Without this, parseVerdict sees no
5307
+ // <synkro-verdict> wrapper and defaults to ok:true, silently dropping every
5308
+ // detected CWE (a format mismatch). The <synkro-verdict> XML path below is
5309
+ // the fallback for the legacy format.
5310
+ let jsonFindings: any[] | null = null;
5311
+ try {
5312
+ const tagM = gradeResp.match(/<findings>([\s\S]*?)<\/findings>/);
5313
+ const src = tagM ? tagM[1] : gradeResp;
5314
+ const objM = src.match(/\{[\s\S]*"findings"[\s\S]*\}/);
5315
+ const arrM = src.match(/\[[\s\S]*\]/);
5316
+ if (objM) { const p = JSON.parse(objM[0]); if (Array.isArray(p.findings)) jsonFindings = p.findings; }
5317
+ if (!jsonFindings && arrM) { const p = JSON.parse(arrM[0]); if (Array.isArray(p)) jsonFindings = p; }
5318
+ } catch { /* not parseable JSON — fall through to XML parsing */ }
5319
+ if (jsonFindings) {
5320
+ if (jsonFindings.length > 0) {
5321
+ anyFailed = true;
5322
+ for (const f of jsonFindings.slice(0, 8)) {
5323
+ const id = String(f.cwe || f.rule_id || '').trim().replace(/^cwe-/i, 'CWE-');
5324
+ // Validate the shape (CWE-<number>) before using it as an object key —
5325
+ // the grade response is model output, so reject anything that isn't a
5326
+ // real CWE id (also blocks __proto__/constructor key injection).
5327
+ if (!/^CWE-\d+$/.test(id)) continue;
5328
+ if (!cweIds.includes(id)) cweIds.push(id);
5329
+ if (f.detail && !fixes[id]) fixes[id] = String(f.detail);
5330
+ if (f.code_snippet && !snippets[id]) snippets[id] = String(f.code_snippet);
5331
+ if (!mergedReason && (f.summary || f.detail)) mergedReason = String(f.summary || f.detail);
5332
+ if (!mergedSeverity && f.severity) mergedSeverity = String(f.severity);
5333
+ if (!mergedCategory && f.category) mergedCategory = String(f.category);
5334
+ }
5335
+ }
5336
+ continue;
5337
+ }
5300
5338
  const v = parseVerdict(gradeResp);
5301
5339
  if (!v.ok) {
5302
5340
  anyFailed = true;
@@ -11412,7 +11450,7 @@ function writeConfigEnv(opts) {
11412
11450
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
11413
11451
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
11414
11452
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
11415
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.100")}`
11453
+ `SYNKRO_VERSION=${shellQuoteSingle("1.7.0")}`
11416
11454
  ];
11417
11455
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
11418
11456
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -15475,7 +15513,7 @@ var args = process.argv.slice(2);
15475
15513
  var cmd = args[0] || "";
15476
15514
  var subArgs = args.slice(1);
15477
15515
  function printVersion() {
15478
- console.log("1.6.100");
15516
+ console.log("1.7.0");
15479
15517
  }
15480
15518
  function printHelp2() {
15481
15519
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents