drafted 1.12.6 → 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 +57 -11
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -200,7 +200,9 @@ IMPORTANT: Any URL containing /f/{uuid} is a Drafted frame link — ALWAYS use r
200
200
  }, {
201
201
  // Initialize instructions: the agent-identity contract, so an agent learns its own
202
202
  // tab name + the right way to read it WITHOUT having to "think to" call a tool.
203
- instructions: `SESSION IDENTITY — read this first: you run as a NAMED session tab visible to the user on their Drafted surface. Your session has a human-readable name (a Greek term, e.g. "Nous") — that name is how the user matches YOUR window to the tab they see, so identify yourself by it when it matters which agent you are. Read it from get_org (response field "session.name") or from the whoami tool; call whoami to refresh after a reconnect.`,
203
+ instructions: `SESSION IDENTITY — read this first: you run as a NAMED session tab visible to the user on their Drafted surface. Your session has a human-readable name (a Greek term, e.g. "Nous") — that name is how the user matches YOUR window to the tab they see, so identify yourself by it when it matters which agent you are. Read it from get_org (response field "session.name") or from the whoami tool; call whoami to refresh after a reconnect.
204
+
205
+ DUAL REGISTRATION IS NORMAL: a separate "Drafted" remote connector (https://drafted.live/mcp, managed by claude.ai) may appear alongside this local stdio server in MCP listings. It is NOT a duplicate and NOT broken — it serves claude.ai web/mobile/Cowork, where a local stdio process cannot run. Never advise removing it; a "needs authentication" state on it is fixed by signing in from claude.ai and does not affect this stdio session.`,
204
206
  });
205
207
 
206
208
  const layerKeys = Object.keys(LAYERS);
@@ -804,7 +806,7 @@ async function cloneSession() {
804
806
  // instead of churning a fresh session every process. (Org is per-request, so a
805
807
  // shared child session does NOT cause cross-agent org clobbering.)
806
808
  const agentKey = (process.env.DRAFTED_AGENT_NAME || '').trim() || bootstrapId;
807
- const res = await fetch(`${getServerUrl()}/auth/session/clone`, {
809
+ const res = await serverFetch(`${getServerUrl()}/auth/session/clone`, {
808
810
  method: 'POST',
809
811
  headers: { 'Content-Type': 'application/json', Cookie: `gc_session=${bootstrapId}` },
810
812
  body: JSON.stringify({ agentKey }),
@@ -836,7 +838,7 @@ async function restoreBoundOrg(clonedOrgId) {
836
838
  const want = sess.boundOrgId;
837
839
  if (!want || want === clonedOrgId) return;
838
840
  try {
839
- const res = await fetch(`${getServerUrl()}/auth/switch-org`, {
841
+ const res = await serverFetch(`${getServerUrl()}/auth/switch-org`, {
840
842
  method: 'POST',
841
843
  headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
842
844
  body: JSON.stringify({ orgId: want }),
@@ -866,6 +868,37 @@ const MIME_MAP = {
866
868
  };
867
869
  function mimeFromExt(ext) { return MIME_MAP[ext?.toLowerCase()] || 'application/octet-stream'; }
868
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
+
869
902
  async function api(method, path, body, extraHeaders = {}, _retried = false) {
870
903
  await ensureSession();
871
904
  const pid = getState().projectId;
@@ -897,7 +930,7 @@ async function api(method, path, body, extraHeaders = {}, _retried = false) {
897
930
  opts.body = JSON.stringify(body);
898
931
  }
899
932
 
900
- const res = await fetch(url, opts);
933
+ const res = await serverFetch(url, opts);
901
934
  const text = await res.text();
902
935
 
903
936
  // Session expired after server restart, or a browser approval just completed
@@ -1397,7 +1430,7 @@ async function consumePendingDeviceCode() {
1397
1430
  if (!pending?.deviceCode) return false;
1398
1431
 
1399
1432
  try {
1400
- const res = await fetch(`${getServerUrl()}/auth/device/token`, {
1433
+ const res = await serverFetch(`${getServerUrl()}/auth/device/token`, {
1401
1434
  method: 'POST',
1402
1435
  headers: { 'Content-Type': 'application/json' },
1403
1436
  body: JSON.stringify({ deviceCode: pending.deviceCode }),
@@ -1433,7 +1466,7 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1433
1466
  }, async ({ action }) => {
1434
1467
  try {
1435
1468
  if (action === 'get_link') {
1436
- const codeRes = await fetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
1469
+ const codeRes = await serverFetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
1437
1470
  if (!codeRes.ok) throw new Error(`Failed to start device authorization (HTTP ${codeRes.status})`);
1438
1471
  const data = await codeRes.json();
1439
1472
  persistPendingDeviceCode(data);
@@ -1447,7 +1480,7 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1447
1480
  const existing = getState().sessionId || getBootstrapSessionId();
1448
1481
  if (existing) {
1449
1482
  try {
1450
- const res = await fetch(`${getServerUrl()}/auth/me`, {
1483
+ const res = await serverFetch(`${getServerUrl()}/auth/me`, {
1451
1484
  headers: { Cookie: `gc_session=${existing}` },
1452
1485
  });
1453
1486
  if (res.ok) {
@@ -1465,7 +1498,7 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1465
1498
  ({ deviceCode, verificationUrl, expiresIn } = pending);
1466
1499
  reusingPending = true;
1467
1500
  } else {
1468
- const codeRes = await fetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
1501
+ const codeRes = await serverFetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
1469
1502
  if (!codeRes.ok) throw new Error(`Failed to start device authorization (HTTP ${codeRes.status})`);
1470
1503
  ({ deviceCode, verificationUrl, expiresIn } = await codeRes.json());
1471
1504
  }
@@ -1489,7 +1522,7 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1489
1522
  const deadline = Date.now() + (expiresIn * 1000);
1490
1523
  while (Date.now() < deadline) {
1491
1524
  await new Promise(r => setTimeout(r, 4000));
1492
- const res = await fetch(`${getServerUrl()}/auth/device/token`, {
1525
+ const res = await serverFetch(`${getServerUrl()}/auth/device/token`, {
1493
1526
  method: 'POST',
1494
1527
  headers: { 'Content-Type': 'application/json' },
1495
1528
  body: JSON.stringify({ deviceCode }),
@@ -1540,11 +1573,20 @@ async function sessionSurfaceBlock() {
1540
1573
  // No WS ack yet — best-effort identity from /auth/me so callers still get a userId/org.
1541
1574
  const cookieSid = sessionId || getBootstrapSessionId();
1542
1575
  let me = null;
1576
+ let unreachable = false;
1577
+ let unreachableWhy = null;
1543
1578
  if (cookieSid) {
1544
1579
  try {
1545
- 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}` } });
1546
1581
  if (res.ok) me = await res.json();
1547
- } catch { /* not yet authenticated */ }
1582
+ } catch (e) {
1583
+ // Transport failure, NOT a sign-out. Conflating the two made agents tell
1584
+ // users "you're signed out" during a network blip and start needless
1585
+ // re-logins — surface the distinction instead. serverFetch already
1586
+ // translates TLS-interception codes into the actual fix.
1587
+ unreachable = true;
1588
+ if (String(e?.message || '').startsWith('TLS interception')) unreachableWhy = e.message;
1589
+ }
1548
1590
  }
1549
1591
  return {
1550
1592
  sessionId: cookieSid,
@@ -1556,6 +1598,10 @@ async function sessionSurfaceBlock() {
1556
1598
  color: null,
1557
1599
  surfaced: false,
1558
1600
  alive: false,
1601
+ ...(unreachable ? {
1602
+ serverUnreachable: true,
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.`,
1604
+ } : {}),
1559
1605
  };
1560
1606
  }
1561
1607
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.12.6",
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": [