drafted 1.25.8 → 1.25.10

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 +33 -4
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -1634,7 +1634,14 @@ async function resolveProjectRef(ref, orgHint = null) {
1634
1634
  if (!ref) return null;
1635
1635
  const cacheKey = orgHint ? `${orgHint}|${ref}` : ref;
1636
1636
  const hit = projectRefCache.get(cacheKey) || (!orgHint && projectRefCache.get(ref));
1637
- if (hit) return hit;
1637
+ // Keep the BARE-ref key in step with a scoped hit. The fs route resolves the
1638
+ // path's project twice: once with the path's org as the hint (the guard, and
1639
+ // the state it records), once without one — and that second pass is what scopes
1640
+ // the call. The unhinted pass reads `ref`, which every resolve in any org
1641
+ // rewrites, so without this the two passes could name different orgs' projects:
1642
+ // with `architecture` in two orgs, read home → read other → read home answered
1643
+ // `project "architecture" is not in the addressed org` for a correct path.
1644
+ if (hit) { projectRefCache.set(ref, hit); return hit; }
1638
1645
  // Scope the lookup to the org the path named. Without a hint the session's
1639
1646
  // org applies — which is right for `fs ls /projects` but WRONG for a path
1640
1647
  // that addressed another org, and that mismatch is what made a project
@@ -1647,6 +1654,7 @@ async function resolveProjectRef(ref, orgHint = null) {
1647
1654
  rows.find(x => String(x.slug || '').toLowerCase() === want) ||
1648
1655
  rows.find(x => String(x.name || '').toLowerCase() === want);
1649
1656
  let p = pick(list);
1657
+ const fromHintOrg = !!p;
1650
1658
  // A miss is worth one unscoped look before "not found": the hint may be stale
1651
1659
  // (a project that moved org), and an id passed as projectId= carries no org at
1652
1660
  // all — only a UUID can name a project in an org the session is not inside.
@@ -1659,7 +1667,15 @@ async function resolveProjectRef(ref, orgHint = null) {
1659
1667
  // `folder` rides along so a caller can rebuild the project's printed address
1660
1668
  // (/o/<org>/projects/<folder>/<slug>) without a second lookup.
1661
1669
  const meta = { id: p.id, slug: p.slug || null, name: p.name || null, orgId: p.orgId || null, orgSlug: p.orgSlug || null, folder: p.folder || '' };
1662
- projectRefCache.set(cacheKey, meta);
1670
+ // `<org>|<ref>` means "this ref IN THAT ORG", so only an answer that came out of
1671
+ // that org's own listing may be stored under it. A cross-org fallback answer
1672
+ // stored there is re-served to every later path naming that org, and the org
1673
+ // guard then rejects a project the path never asked for: fs read/write on
1674
+ // /o/<org-slug>/projects/<name>/... answered "project <name> is not in org
1675
+ // <slug>" after one miss (a mkdir of a name another org owned) cached a foreign
1676
+ // project under the slug, while the same path spelled with the org UUID — a
1677
+ // different key — still resolved. Measured on prod 1.25.9.
1678
+ if (fromHintOrg || !orgHint) projectRefCache.set(cacheKey, meta);
1663
1679
  projectRefCache.set(ref, meta);
1664
1680
  projectRefCache.set(p.id, meta);
1665
1681
  return meta;
@@ -3830,8 +3846,21 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
3830
3846
  // (/o/<org>/projects/eng/x). The chain wins when both are given.
3831
3847
  const projectFolder = folder || (parts.length === 2 ? parts[0] : null);
3832
3848
  const projectRef = parts[parts.length - 1];
3833
- const existing = await resolveProjectRef(projectRef, orgFromPath || org || null).catch(() => null);
3834
- if (existing) return err(new Error(`project already exists: ${projectRef}`));
3849
+ // "Already exists" means IN THE ORG THIS CREATE LANDS IN, and nothing wider.
3850
+ // resolveProjectRef scopes its first lookup to the org (correct) but then
3851
+ // takes one unscoped look before answering "not found" — right for resolving
3852
+ // an id or a project that changed org, WRONG as an existence test: it made a
3853
+ // same-named project in ANY org make the name uncreatable everywhere. Measured
3854
+ // on prod 2026-10-26: Flow2's 'architecture' blocked
3855
+ // /o/causeway-1781450760/projects/architecture. So ask the same org-identity
3856
+ // question the project-path guard asks a few blocks below (pathOrgMatches),
3857
+ // which also makes the answer independent of projectRefCache — an earlier
3858
+ // path in this session can have cached a cross-org answer under this org's key.
3859
+ const targetOrg = orgFromPath || org || null;
3860
+ const existing = await resolveProjectRef(projectRef, targetOrg).catch(() => null);
3861
+ if (existing && (!targetOrg || await pathOrgMatches(targetOrg, existing))) {
3862
+ return err(new Error(`project already exists: ${projectRef}`));
3863
+ }
3835
3864
  const projectName = projectRef.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
3836
3865
  const created = await api('POST', '/api/projects', { name: projectName, ...(projectFolder ? { folder: projectFolder } : {}) }, orgHeader);
3837
3866
  if (!created?.id) return err(new Error('project create returned no id'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.25.8",
3
+ "version": "1.25.10",
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
  "bin": {