@synkro-sh/cli 1.6.69 → 1.6.71

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
@@ -6425,6 +6425,50 @@ function loadMcpJwt(): string {
6425
6425
  try { return readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch { return ''; }
6426
6426
  }
6427
6427
 
6428
+ let _cfgCache: Record<string, string> | null = null;
6429
+ function cfgVal(key: string): string {
6430
+ if (process.env[key]) return process.env[key] as string;
6431
+ if (!_cfgCache) {
6432
+ _cfgCache = {};
6433
+ try {
6434
+ const txt = readFileSync(join(HOME, '.synkro', 'config.env'), 'utf-8');
6435
+ for (const line of txt.split('\n')) {
6436
+ const mm = line.match(/^([A-Z0-9_]+)=['"]?(.*?)['"]?\s*$/);
6437
+ if (mm) _cfgCache[mm[1]] = mm[2];
6438
+ }
6439
+ } catch {}
6440
+ }
6441
+ return _cfgCache[key] || '';
6442
+ }
6443
+ function deployIsCloud(): boolean { return cfgVal('SYNKRO_DEPLOY_LOCATION') === 'cloud'; }
6444
+
6445
+ // Cloud MCP gate: in cloud mode the local server (:18931) isn't running, so the
6446
+ // gate asks the org policy directly (api /api/mcp/gate, authed by .mcp-jwt).
6447
+ // Fail-open by contract — any error or non-block verdict allows the call.
6448
+ async function cloudMcpGate(payload: any, harness: string): Promise<string> {
6449
+ try {
6450
+ const toolName = String(payload.tool_name || payload.tool || '');
6451
+ const parts = toolName.split('__');
6452
+ const server = (parts.length >= 3 && parts[0] === 'mcp') ? parts[1] : '';
6453
+ if (!server) return failOpen(harness);
6454
+ const base = (cfgVal('SYNKRO_GATEWAY_URL') || 'https://api.synkro.sh').replace(/\/+$/, '');
6455
+ const resp = await fetch(base + '/api/mcp/gate', {
6456
+ method: 'POST',
6457
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() },
6458
+ body: JSON.stringify({ server: server, tool: parts.slice(2).join('__') }),
6459
+ signal: AbortSignal.timeout(5000),
6460
+ });
6461
+ if (!resp.ok) return failOpen(harness);
6462
+ const d = await resp.json() as any;
6463
+ if (d && d.decision === 'block') {
6464
+ const reason = String(d.reason || ('MCP server "' + server + '" blocked by policy'));
6465
+ if (harness === 'cursor') return JSON.stringify({ permission: 'deny', user_message: reason });
6466
+ return JSON.stringify({ systemMessage: '[synkro:cloud:mcp] ' + reason, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason, additionalContext: reason } });
6467
+ }
6468
+ return failOpen(harness);
6469
+ } catch { return failOpen(harness); }
6470
+ }
6471
+
6428
6472
  async function readStdin(): Promise<string> {
6429
6473
  const chunks: Buffer[] = [];
6430
6474
  for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
@@ -6484,6 +6528,10 @@ export async function runStub(surface: string, opts: StubOpts = {}): Promise<voi
6484
6528
  if (!input.trim()) { out(failOpen(harness)); return; }
6485
6529
  const payload = JSON.parse(input);
6486
6530
 
6531
+ // Cloud MCP enforcement: the gate can't reach the local server, so decide
6532
+ // against the org policy directly. Fail-open inside cloudMcpGate.
6533
+ if (surface === 'mcp-gate' && deployIsCloud()) { out(await cloudMcpGate(payload, harness)); return; }
6534
+
6487
6535
  const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots : [];
6488
6536
  const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
6489
6537
  const sessionId = String(payload.session_id || payload.conversation_id || '');
@@ -9639,11 +9687,12 @@ function captureClaudeSetupToken() {
9639
9687
  reject(new Error(`claude setup-token exited with code ${code}`));
9640
9688
  return;
9641
9689
  }
9690
+ const stripToToken = (s) => s.replace(/\x1B\[[0-9;?]*[a-zA-Z]/g, "").replace(/\x1B\][^\x07]*\x07/g, "").replace(/[\s\x00-\x1F\x7F]/g, "");
9642
9691
  const yellowRe = /\x1B\[38;2;255;193;7m([^\x1B]*)/g;
9643
9692
  let yellow = "";
9644
9693
  let m;
9645
9694
  while ((m = yellowRe.exec(raw)) !== null) yellow += m[1];
9646
- const yMatch = yellow.replace(/\s/g, "").match(/sk-ant-oat01-[A-Za-z0-9_-]+/);
9695
+ const yToken = stripToToken(yellow);
9647
9696
  let lineMatch = "";
9648
9697
  const clean = raw.replace(/\x1B\[[0-9;?]*[a-zA-Z]/g, "").replace(/\x1B\][^\x07]*\x07/g, "");
9649
9698
  const lines = clean.split("\n").map((l) => l.trim());
@@ -9655,13 +9704,13 @@ function captureClaudeSetupToken() {
9655
9704
  lineMatch = tok;
9656
9705
  break;
9657
9706
  }
9658
- const candidates = [yMatch ? yMatch[0] : "", lineMatch].filter((t) => /^sk-ant-oat01-[A-Za-z0-9_-]{20,}$/.test(t));
9659
- const best = candidates.sort((a, b) => b.length - a.length)[0];
9660
- if (!best) {
9661
- reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
9707
+ const valid = (t) => /^sk-ant-oat01-[A-Za-z0-9_-]{40,}$/.test(t);
9708
+ const token = valid(yToken) ? yToken : valid(lineMatch) ? lineMatch : "";
9709
+ if (!token) {
9710
+ reject(new Error(`Could not capture a full token from claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b, yToken=${yToken.length}b)`));
9662
9711
  return;
9663
9712
  }
9664
- resolve4(best);
9713
+ resolve4(token);
9665
9714
  });
9666
9715
  });
9667
9716
  }
@@ -10005,6 +10054,7 @@ var init_setupGithub = __esm({
10005
10054
  var install_exports = {};
10006
10055
  __export(install_exports, {
10007
10056
  detectGitRepo: () => detectGitRepo2,
10057
+ getOrMintCloudToken: () => getOrMintCloudToken,
10008
10058
  installCommand: () => installCommand,
10009
10059
  parseArgs: () => parseArgs,
10010
10060
  reconcileDeployLocation: () => reconcileDeployLocation,
@@ -10336,7 +10386,7 @@ function writeConfigEnv(opts) {
10336
10386
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10337
10387
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10338
10388
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10339
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.69")}`
10389
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.71")}`
10340
10390
  ];
10341
10391
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10342
10392
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -10356,6 +10406,38 @@ function writeConfigEnv(opts) {
10356
10406
  writeFileSync8(CONFIG_PATH2, lines.join("\n"), "utf-8");
10357
10407
  chmodSync2(CONFIG_PATH2, 384);
10358
10408
  }
10409
+ function jwtExpired(token) {
10410
+ try {
10411
+ const p = JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf-8"));
10412
+ if (!p?.exp) return true;
10413
+ return Date.now() > p.exp * 1e3 - 5 * 60 * 1e3;
10414
+ } catch {
10415
+ return true;
10416
+ }
10417
+ }
10418
+ async function getOrMintCloudToken(gatewayUrl) {
10419
+ assertGatewayAllowed(gatewayUrl);
10420
+ let stored = "";
10421
+ try {
10422
+ stored = readFileSync10(CLOUD_JWT_PATH, "utf-8").trim();
10423
+ } catch {
10424
+ }
10425
+ if (stored && !jwtExpired(stored)) return stored;
10426
+ await refreshToken().catch(() => false);
10427
+ const jwt2 = getAccessToken();
10428
+ if (!jwt2) throw new Error("Not authenticated \u2014 run `synkro login`");
10429
+ const resp = await fetch(`${gatewayUrl}/api/v1/cli/cloud-token`, {
10430
+ method: "POST",
10431
+ headers: { Authorization: `Bearer ${jwt2}`, "Content-Type": "application/json" }
10432
+ });
10433
+ if (!resp.ok) {
10434
+ const t = await resp.text().catch(() => "");
10435
+ throw new Error(`cloud-token mint failed (${resp.status}): ${t.slice(0, 200)}`);
10436
+ }
10437
+ const { token } = await resp.json();
10438
+ writeFileSync8(CLOUD_JWT_PATH, token + "\n", { mode: 384 });
10439
+ return token;
10440
+ }
10359
10441
  async function provisionCloudContainer(opts) {
10360
10442
  let setupToken;
10361
10443
  try {
@@ -10382,10 +10464,16 @@ async function provisionCloudContainer(opts) {
10382
10464
  } catch {
10383
10465
  }
10384
10466
  }
10467
+ let cloudJwt = opts.jwt;
10468
+ try {
10469
+ cloudJwt = await getOrMintCloudToken(opts.gatewayUrl);
10470
+ } catch (e) {
10471
+ console.warn(` (cloud token unavailable, using session token: ${e.message})`);
10472
+ }
10385
10473
  try {
10386
10474
  const resp = await fetch(`${opts.gatewayUrl}/api/v1/cli/cloud-provision`, {
10387
10475
  method: "POST",
10388
- headers: { "Authorization": `Bearer ${opts.jwt}`, "Content-Type": "application/json" },
10476
+ headers: { "Authorization": `Bearer ${cloudJwt}`, "Content-Type": "application/json" },
10389
10477
  body: JSON.stringify({
10390
10478
  org_id: opts.orgId,
10391
10479
  user_id: opts.userId,
@@ -11823,7 +11911,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
11823
11911
  }
11824
11912
  return { sessions: totalSessions, messages: totalMessages };
11825
11913
  }
11826
- var SYNKRO_DIR5, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR;
11914
+ var SYNKRO_DIR5, HOOKS_DIR, BIN_DIR, CONFIG_PATH2, MCP_STDIO_PROXY_SRC, OFFSETS_DIR, CLOUD_JWT_PATH;
11827
11915
  var init_install = __esm({
11828
11916
  "cli/commands/install.ts"() {
11829
11917
  "use strict";
@@ -11957,6 +12045,7 @@ rl.on('line', async (line) => {
11957
12045
  });
11958
12046
  `;
11959
12047
  OFFSETS_DIR = join10(SYNKRO_DIR5, ".transcript-offsets");
12048
+ CLOUD_JWT_PATH = join10(SYNKRO_DIR5, ".cloud-jwt");
11960
12049
  }
11961
12050
  });
11962
12051
 
@@ -13930,7 +14019,7 @@ var args = process.argv.slice(2);
13930
14019
  var cmd = args[0] || "";
13931
14020
  var subArgs = args.slice(1);
13932
14021
  function printVersion() {
13933
- console.log("1.6.69");
14022
+ console.log("1.6.71");
13934
14023
  }
13935
14024
  function printHelp2() {
13936
14025
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents