drafted 1.19.24 → 1.19.26

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/mcp/server.mjs CHANGED
@@ -633,6 +633,11 @@ registerAppResource(
633
633
 
634
634
  const AUTH_FILE = process.env.DRAFTED_AUTH_FILE || join(homedir(), '.drafted', 'auth.json');
635
635
  const PENDING_AUTH_FILE = process.env.DRAFTED_PENDING_AUTH_FILE || `${AUTH_FILE}.pending`;
636
+ // The DESKTOP app's own browser session (desktop/src-tauri/src/main.rs writes it beside
637
+ // auth.json). Never read for a credential here — only to answer "is the app signed in?",
638
+ // which is a different question from "is this MCP signed in?" and the one that decides
639
+ // whether an agent can reach the human at all.
640
+ const DESKTOP_SESSION_FILE = join(dirname(AUTH_FILE), 'desktop.json');
636
641
 
637
642
  // Active-project persistence (stdio only): see mcp/active-project-store.mjs.
638
643
  // Persisted so a `system restart mode=mcp` / skill-edit pass that re-spawns the
@@ -2218,7 +2223,60 @@ if (!isRemote) tool('auth', 'Sign in to Drafted.\n\n`action=get_link` ALWAYS ret
2218
2223
  });
2219
2224
  if (res.ok) {
2220
2225
  const me = await res.json();
2221
- return ok({ status: 'already_authenticated', userId: me.userId, email: me.userEmail, org: me.currentOrg?.name });
2226
+ // "Signed in" has to mean THIS MACHINE is signed in, not "this process
2227
+ // happens to hold a session". The two came apart the first time anything
2228
+ // deleted ~/.drafted/auth.json underneath a running MCP (the desktop's
2229
+ // credential burn does exactly that, by design): the in-process session
2230
+ // stayed valid, so login answered already_authenticated — truthfully, and
2231
+ // uselessly — while every OTHER reader on the machine stayed signed out.
2232
+ // The desktop's focus listener is one of those readers, so an agent asking
2233
+ // for a human reached nobody and the tool that exists to fix it reported
2234
+ // success.
2235
+ let restoredAuthFile = false;
2236
+ if (!getBootstrapSessionId()) {
2237
+ try {
2238
+ persistAuthSession({ sessionId: existing, userId: me.userId, orgId: me.currentOrg?.id });
2239
+ restoredAuthFile = true;
2240
+ } catch { /* read-only home: report the truth below rather than throwing */ }
2241
+ }
2242
+ // The DESKTOP's own browser credential is a separate file, and an agent
2243
+ // cannot mint one — it is the origin='browser' class that may approve
2244
+ // actions, which is exactly what the two-credential split protects. But
2245
+ // "cannot mint it" is not "cannot ask for it": opening the app's sign-in
2246
+ // window is this tool's documented job, and the window usually completes
2247
+ // with no interaction at all because the shared cookie store still holds a
2248
+ // valid login. Short-circuiting on the agent session skipped that entirely,
2249
+ // so `login` reported success while the app stayed signed out and every
2250
+ // agent that pinged the human reached nobody.
2251
+ let openedDesktopSignin = false;
2252
+ if (desktopAppBinary() && !existsSync(DESKTOP_SESSION_FILE)) {
2253
+ openedDesktopSignin = await launchDesktopSignin();
2254
+ if (openedDesktopSignin) {
2255
+ // Give the app a moment to re-capture the cookie so the caller can act
2256
+ // on a true answer rather than an optimistic one.
2257
+ for (let i = 0; i < 20 && !existsSync(DESKTOP_SESSION_FILE); i++) {
2258
+ await new Promise((r) => setTimeout(r, 500));
2259
+ }
2260
+ }
2261
+ }
2262
+ const desktopSignedIn = !desktopAppBinary() ? null : existsSync(DESKTOP_SESSION_FILE);
2263
+ return ok({
2264
+ status: 'already_authenticated',
2265
+ userId: me.userId,
2266
+ email: me.userEmail,
2267
+ org: me.currentOrg?.name,
2268
+ ...(restoredAuthFile ? {
2269
+ restoredAuthFile: true,
2270
+ note: 'This session was signed in but ~/.drafted/auth.json was missing, so other readers on this machine (the desktop app, the CLI) were not. Rewrote it from the live session.',
2271
+ } : {}),
2272
+ ...(desktopSignedIn === null ? {} : { desktopSignedIn }),
2273
+ ...(openedDesktopSignin ? {
2274
+ openedDesktopSignin: true,
2275
+ desktopNote: desktopSignedIn
2276
+ ? 'The desktop app was signed out; opened its sign-in window and it completed on its own from the existing browser session.'
2277
+ : 'The desktop app is signed out and its sign-in window is open — it needs one approval there. Until then it holds no session, so an agent asking for attention cannot reach this machine.',
2278
+ } : {}),
2279
+ });
2222
2280
  }
2223
2281
  } catch { /* session invalid, proceed with login */ }
2224
2282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.19.24",
3
+ "version": "1.19.26",
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": [
@@ -0,0 +1,45 @@
1
+ /**
2
+ * ONE definition of the root-URL grammar, shared by the server, the client
3
+ * router and the tests:
4
+ *
5
+ * /o/<org>[/<folder chain>]/wiki|skills|tasks|projects[/<path>]
6
+ *
7
+ * The regex is a literal copy of the four root routes in server/server.mjs. It
8
+ * must stay that way: the client router uses it to decide what it may handle
9
+ * without a round trip, so any divergence renders a different page than the URL
10
+ * names.
11
+ *
12
+ * The source form carries ZERO BACKSLASHES on purpose. This function is
13
+ * embedded into client JS via `String(parseRootPath)` inside a template literal
14
+ * (see AGENTS.md's escape trap: a `\/` written inside a template literal
15
+ * collapses to `/`). Interpolating a runtime string is safe either way, but a
16
+ * body with no backslashes cannot be broken by a later copy-paste into a
17
+ * literal region.
18
+ */
19
+
20
+ export const ROOTS = ['wiki', 'skills', 'tasks', 'projects'];
21
+
22
+ /**
23
+ * @param {string} pathname
24
+ * @returns {{orgParam: string, chain: string, root: string, path: string}|null}
25
+ * `null` means "not a root URL" — the caller must let the server decide.
26
+ * `chain` is decoded ('' = the org itself); `path` is left RAW/encoded,
27
+ * exactly as the server's `req.params[2]` is, so both halves agree.
28
+ */
29
+ export function parseRootPath(pathname) {
30
+ var re = new RegExp('^/o/([^/]+)/(?:(.+?)/)?(wiki|skills|tasks|projects)(?:/(.*))?$');
31
+ var m = re.exec(String(pathname || ''));
32
+ if (!m) return null;
33
+ var path = m[4] || '';
34
+ while (path.length && path.charAt(path.length - 1) === '/') path = path.slice(0, -1);
35
+ // The projects root takes NO sub-path on the server, because
36
+ // /o/<org>/projects/<slug> is the PROJECT CANVAS. Claiming it here would make
37
+ // the client router swallow every project link and render the folder list.
38
+ if (m[3] === 'projects' && path) return null;
39
+ return {
40
+ orgParam: decodeURIComponent(m[1]),
41
+ chain: m[2] ? decodeURIComponent(m[2]) : '',
42
+ root: m[3],
43
+ path: path,
44
+ };
45
+ }