drafted 1.12.7 → 1.12.8

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.
Files changed (2) hide show
  1. package/mcp/server.mjs +46 -12
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -806,7 +806,7 @@ async function cloneSession() {
806
806
  // instead of churning a fresh session every process. (Org is per-request, so a
807
807
  // shared child session does NOT cause cross-agent org clobbering.)
808
808
  const agentKey = (process.env.DRAFTED_AGENT_NAME || '').trim() || bootstrapId;
809
- const res = await fetch(`${getServerUrl()}/auth/session/clone`, {
809
+ const res = await serverFetch(`${getServerUrl()}/auth/session/clone`, {
810
810
  method: 'POST',
811
811
  headers: { 'Content-Type': 'application/json', Cookie: `gc_session=${bootstrapId}` },
812
812
  body: JSON.stringify({ agentKey }),
@@ -838,7 +838,7 @@ async function restoreBoundOrg(clonedOrgId) {
838
838
  const want = sess.boundOrgId;
839
839
  if (!want || want === clonedOrgId) return;
840
840
  try {
841
- const res = await fetch(`${getServerUrl()}/auth/switch-org`, {
841
+ const res = await serverFetch(`${getServerUrl()}/auth/switch-org`, {
842
842
  method: 'POST',
843
843
  headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
844
844
  body: JSON.stringify({ orgId: want }),
@@ -868,6 +868,37 @@ const MIME_MAP = {
868
868
  };
869
869
  function mimeFromExt(ext) { return MIME_MAP[ext?.toLowerCase()] || 'application/octet-stream'; }
870
870
 
871
+ // TLS-trust failures from antivirus/corporate HTTPS interception (Norton,
872
+ // Zscaler, ...) surface from undici as a bare "fetch failed" — agents then tell
873
+ // users "network error / you're offline" for a day (real incident, 2026-07-02).
874
+ // Node does not use the OS certificate store, so the interceptor's re-signed
875
+ // chain is untrusted. Translate the code into the actual fix.
876
+ const TLS_TRUST_CODES = new Set([
877
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'SELF_SIGNED_CERT_IN_CHAIN',
878
+ 'DEPTH_ZERO_SELF_SIGNED_CERT', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
879
+ 'UNABLE_TO_GET_ISSUER_CERT', 'CERT_UNTRUSTED',
880
+ ]);
881
+ function enrichFetchError(error, url) {
882
+ const code = String(error?.cause?.code || error?.code || '').toUpperCase();
883
+ if (!TLS_TRUST_CODES.has(code)) return error;
884
+ const e = new Error(
885
+ `TLS interception detected reaching ${url} (${code}): antivirus or a corporate proxy ` +
886
+ `(Norton, Zscaler, Netskope, ...) is re-signing HTTPS, and Node does not use the OS ` +
887
+ `certificate store, so it rejects the interceptor's certificate. This is NOT a Drafted ` +
888
+ `outage and NOT a sign-out. Fixes, best first: (1) exclude drafted.live and *.drafted.live ` +
889
+ `from the interceptor's HTTPS/SSL scanning; (2) export the interceptor's root certificate ` +
890
+ `to a PEM file and set NODE_EXTRA_CA_CERTS=<path> in this MCP server's env; (3) on Node ` +
891
+ `>= 22.15, set NODE_OPTIONS=--use-system-ca so Node trusts the OS store. NEVER set ` +
892
+ `NODE_TLS_REJECT_UNAUTHORIZED=0 (disables all TLS verification).`
893
+ );
894
+ e.cause = error;
895
+ return e;
896
+ }
897
+ async function serverFetch(url, opts) {
898
+ try { return await fetch(url, opts); }
899
+ catch (e) { throw enrichFetchError(e, url); }
900
+ }
901
+
871
902
  async function api(method, path, body, extraHeaders = {}, _retried = false) {
872
903
  await ensureSession();
873
904
  const pid = getState().projectId;
@@ -899,7 +930,7 @@ async function api(method, path, body, extraHeaders = {}, _retried = false) {
899
930
  opts.body = JSON.stringify(body);
900
931
  }
901
932
 
902
- const res = await fetch(url, opts);
933
+ const res = await serverFetch(url, opts);
903
934
  const text = await res.text();
904
935
 
905
936
  // Session expired after server restart, or a browser approval just completed
@@ -1399,7 +1430,7 @@ async function consumePendingDeviceCode() {
1399
1430
  if (!pending?.deviceCode) return false;
1400
1431
 
1401
1432
  try {
1402
- const res = await fetch(`${getServerUrl()}/auth/device/token`, {
1433
+ const res = await serverFetch(`${getServerUrl()}/auth/device/token`, {
1403
1434
  method: 'POST',
1404
1435
  headers: { 'Content-Type': 'application/json' },
1405
1436
  body: JSON.stringify({ deviceCode: pending.deviceCode }),
@@ -1435,7 +1466,7 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1435
1466
  }, async ({ action }) => {
1436
1467
  try {
1437
1468
  if (action === 'get_link') {
1438
- const codeRes = await fetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
1469
+ const codeRes = await serverFetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
1439
1470
  if (!codeRes.ok) throw new Error(`Failed to start device authorization (HTTP ${codeRes.status})`);
1440
1471
  const data = await codeRes.json();
1441
1472
  persistPendingDeviceCode(data);
@@ -1449,7 +1480,7 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1449
1480
  const existing = getState().sessionId || getBootstrapSessionId();
1450
1481
  if (existing) {
1451
1482
  try {
1452
- const res = await fetch(`${getServerUrl()}/auth/me`, {
1483
+ const res = await serverFetch(`${getServerUrl()}/auth/me`, {
1453
1484
  headers: { Cookie: `gc_session=${existing}` },
1454
1485
  });
1455
1486
  if (res.ok) {
@@ -1467,7 +1498,7 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1467
1498
  ({ deviceCode, verificationUrl, expiresIn } = pending);
1468
1499
  reusingPending = true;
1469
1500
  } else {
1470
- const codeRes = await fetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
1501
+ const codeRes = await serverFetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
1471
1502
  if (!codeRes.ok) throw new Error(`Failed to start device authorization (HTTP ${codeRes.status})`);
1472
1503
  ({ deviceCode, verificationUrl, expiresIn } = await codeRes.json());
1473
1504
  }
@@ -1491,7 +1522,7 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1491
1522
  const deadline = Date.now() + (expiresIn * 1000);
1492
1523
  while (Date.now() < deadline) {
1493
1524
  await new Promise(r => setTimeout(r, 4000));
1494
- const res = await fetch(`${getServerUrl()}/auth/device/token`, {
1525
+ const res = await serverFetch(`${getServerUrl()}/auth/device/token`, {
1495
1526
  method: 'POST',
1496
1527
  headers: { 'Content-Type': 'application/json' },
1497
1528
  body: JSON.stringify({ deviceCode }),
@@ -1543,15 +1574,18 @@ async function sessionSurfaceBlock() {
1543
1574
  const cookieSid = sessionId || getBootstrapSessionId();
1544
1575
  let me = null;
1545
1576
  let unreachable = false;
1577
+ let unreachableWhy = null;
1546
1578
  if (cookieSid) {
1547
1579
  try {
1548
- const res = await fetch(`${getServerUrl()}/auth/me`, { headers: { Cookie: `gc_session=${cookieSid}` } });
1580
+ const res = await serverFetch(`${getServerUrl()}/auth/me`, { headers: { Cookie: `gc_session=${cookieSid}` } });
1549
1581
  if (res.ok) me = await res.json();
1550
- } catch {
1582
+ } catch (e) {
1551
1583
  // Transport failure, NOT a sign-out. Conflating the two made agents tell
1552
1584
  // users "you're signed out" during a network blip and start needless
1553
- // re-logins — surface the distinction instead.
1585
+ // re-logins — surface the distinction instead. serverFetch already
1586
+ // translates TLS-interception codes into the actual fix.
1554
1587
  unreachable = true;
1588
+ if (String(e?.message || '').startsWith('TLS interception')) unreachableWhy = e.message;
1555
1589
  }
1556
1590
  }
1557
1591
  return {
@@ -1566,7 +1600,7 @@ async function sessionSurfaceBlock() {
1566
1600
  alive: false,
1567
1601
  ...(unreachable ? {
1568
1602
  serverUnreachable: true,
1569
- note: `Could not reach ${getServerUrl()} — identity UNKNOWN, not signed out. This is a network/transport failure: do not tell the user they are logged out and do not start a new login; retry when connectivity is back.`,
1603
+ note: unreachableWhy || `Could not reach ${getServerUrl()} — identity UNKNOWN, not signed out. This is a network/transport failure: do not tell the user they are logged out and do not start a new login; retry when connectivity is back.`,
1570
1604
  } : {}),
1571
1605
  };
1572
1606
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.12.7",
3
+ "version": "1.12.8",
4
4
  "description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
5
5
  "type": "module",
6
6
  "files": [