@synkro-sh/cli 1.6.99 → 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;
@@ -10668,6 +10706,7 @@ function captureClaudeSetupToken() {
10668
10706
  const isMac = platform3() === "darwin";
10669
10707
  const bin = "script";
10670
10708
  const args2 = isMac ? ["-q", tmpFile, "claude", "setup-token"] : ["-qec", "claude setup-token", tmpFile];
10709
+ const OAUTH_HINT = 'The browser approval did not return a token. This usually means claude.ai rejected the OAuth request (its "Authorization failed \u2014 Unsupported media type" page), most often because a browser extension (Grammarly, ad/script blockers, AI-assistant toolbars) stripped the request headers, or you approved on the wrong Claude account. Fix: retry in a clean incognito window with extensions disabled, and approve on the Claude account you want the cloud workers to use.';
10671
10710
  return new Promise((resolve4, reject) => {
10672
10711
  const proc = nodeSpawn(bin, args2, {
10673
10712
  stdio: "inherit",
@@ -10687,7 +10726,7 @@ function captureClaudeSetupToken() {
10687
10726
  } catch {
10688
10727
  }
10689
10728
  if (code !== 0) {
10690
- reject(new Error(`claude setup-token exited with code ${code}`));
10729
+ reject(new Error(`claude setup-token exited with code ${code}. ${OAUTH_HINT}`));
10691
10730
  return;
10692
10731
  }
10693
10732
  const TOKEN_YELLOW = "2;255;193;7";
@@ -10731,7 +10770,8 @@ function captureClaudeSetupToken() {
10731
10770
  i += 1;
10732
10771
  }
10733
10772
  if (!token) {
10734
- reject(new Error(`Captured no yellow token text from claude setup-token output (raw=${raw.length}b \u2014 is the terminal emitting color?)`));
10773
+ const reason = raw.length < 200 ? OAUTH_HINT : `is the terminal emitting color? (captured ${raw.length}b but no token-yellow run)`;
10774
+ reject(new Error(`Captured no setup token from claude setup-token output. ${reason}`));
10735
10775
  return;
10736
10776
  }
10737
10777
  resolve4(token);
@@ -11410,7 +11450,7 @@ function writeConfigEnv(opts) {
11410
11450
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
11411
11451
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
11412
11452
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
11413
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.99")}`
11453
+ `SYNKRO_VERSION=${shellQuoteSingle("1.7.0")}`
11414
11454
  ];
11415
11455
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
11416
11456
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -15473,7 +15513,7 @@ var args = process.argv.slice(2);
15473
15513
  var cmd = args[0] || "";
15474
15514
  var subArgs = args.slice(1);
15475
15515
  function printVersion() {
15476
- console.log("1.6.99");
15516
+ console.log("1.7.0");
15477
15517
  }
15478
15518
  function printHelp2() {
15479
15519
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents