drafted 1.14.6 → 1.14.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.
package/cli/drafted.mjs CHANGED
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  import { program } from 'commander';
11
- import { spawn, execSync } from 'child_process';
12
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'fs';
11
+ import { spawn, execSync, execFileSync } from 'child_process';
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync, chmodSync } from 'fs';
13
13
  import { join, dirname, basename, resolve } from 'path';
14
14
  import { homedir, tmpdir, platform } from 'os';
15
15
  import { fileURLToPath } from 'url';
@@ -38,9 +38,9 @@ const PACKAGE_VERSION = (() => {
38
38
  }
39
39
  })();
40
40
 
41
- // Ensure state directory exists
41
+ // Ensure state directory exists (0700 — holds the session token in auth.json)
42
42
  if (!existsSync(DEFAULT_STATE_DIR)) {
43
- mkdirSync(DEFAULT_STATE_DIR, { recursive: true });
43
+ mkdirSync(DEFAULT_STATE_DIR, { recursive: true, mode: 0o700 });
44
44
  }
45
45
 
46
46
  // Helper: Read projects
@@ -203,9 +203,13 @@ function readAuth() {
203
203
 
204
204
  function writeAuth(authData) {
205
205
  if (!existsSync(DEFAULT_STATE_DIR)) {
206
- mkdirSync(DEFAULT_STATE_DIR, { recursive: true });
206
+ mkdirSync(DEFAULT_STATE_DIR, { recursive: true, mode: 0o700 });
207
207
  }
208
- writeFileSync(DEFAULT_AUTH_FILE, JSON.stringify(authData, null, 2));
208
+ // SECURITY: auth.json holds the long-lived Drafted session token. Restrict it to the
209
+ // owner (0600) so no other local user on a shared host can read it. writeFileSync's mode
210
+ // only applies on create, so chmod after to also fix a pre-existing world-readable file.
211
+ writeFileSync(DEFAULT_AUTH_FILE, JSON.stringify(authData, null, 2), { mode: 0o600 });
212
+ try { chmodSync(DEFAULT_AUTH_FILE, 0o600); } catch { /* best effort */ }
209
213
  }
210
214
 
211
215
  function clearAuth() {
@@ -422,12 +426,15 @@ program
422
426
  console.log('');
423
427
  console.log(' If the browser doesn\'t open, visit the URL above manually.');
424
428
 
425
- // Open browser
429
+ // Open browser. SECURITY: verificationUrl comes from the server — pass it as an argv
430
+ // arg (execFileSync), never interpolated into a shell string, so a compromised/MITM'd
431
+ // server response can't inject `$(...)`/backticks that run on this host.
426
432
  try {
427
- const openCmd = process.platform === 'darwin' ? 'open'
428
- : process.platform === 'win32' ? 'start'
429
- : 'xdg-open';
430
- execSync(`${openCmd} "${verificationUrl}"`, { stdio: 'ignore' });
433
+ if (process.platform === 'win32') {
434
+ execFileSync('cmd', ['/c', 'start', '', verificationUrl], { stdio: 'ignore' });
435
+ } else {
436
+ execFileSync(process.platform === 'darwin' ? 'open' : 'xdg-open', [verificationUrl], { stdio: 'ignore' });
437
+ }
431
438
  } catch {
432
439
  // Browser open failed — user will use the URL manually
433
440
  }
@@ -1584,7 +1591,12 @@ async function syncOneSkill(ref, outDir, org) {
1584
1591
  mkdirSync(dirname(full), { recursive: true });
1585
1592
  writeFileSync(full, content);
1586
1593
  }
1587
- return { ref, slug, hash, status: 'ok' };
1594
+ // createdBy + forkedFrom decide whether this skill's setup may auto-run: shell authored by
1595
+ // someone else must not run unattended on sync. forkedFrom is load-bearing — the CLI/MCP
1596
+ // AUTO-forks a global/other-org skill on use, which copies the source author's setup but
1597
+ // stamps createdBy = the forking (victim) user, so createdBy alone would mislabel foreign
1598
+ // setup as self-authored. A forked skill is never treated as self-authored.
1599
+ return { ref, slug, hash, status: 'ok', createdBy: skill.createdBy ?? null, forkedFrom: skill.forkedFrom ?? null };
1588
1600
  }
1589
1601
 
1590
1602
  const skillCmd = program.command('skill').description('Skill library operations');
@@ -1594,10 +1606,11 @@ skillCmd
1594
1606
  .option('--ref <ref>', 'skill ref slug[@hash] (repeatable)', (v, acc) => { acc.push(v); return acc; }, [])
1595
1607
  .requiredOption('--out <dir>', 'output directory for bundles')
1596
1608
  .option('--org <org>', 'resolve against this Drafted org (id or name); scopes per-request without switching the session')
1597
- .option('--no-setup', 'do not run each skill\'s setup after materializing (default: run setup so source-only skills are runnable)')
1609
+ .option('--no-setup', 'do not run each skill\'s setup after materializing (default: run setup only for skills you authored)')
1610
+ .option('--allow-untrusted-setup', 'also run setup for skills authored by someone else (SECURITY: runs their shell on this host — only for skills you trust)')
1598
1611
  .option('--format <fmt>', 'output format: json or text', 'text')
1599
1612
  .action(async (opts) => {
1600
- requireLogin();
1613
+ const auth = requireLogin();
1601
1614
  const refs = opts.ref || [];
1602
1615
  mkdirSync(opts.out, { recursive: true });
1603
1616
  const results = [];
@@ -1611,9 +1624,22 @@ skillCmd
1611
1624
  // failure is reported but the materialized source stays put (sync is still
1612
1625
  // `ok` — the bytes are there, just not built).
1613
1626
  if (r.status === 'ok' && opts.setup !== false) {
1614
- const s = runSkillSetup(join(opts.out, r.slug));
1615
- r.setup = s.skipped ? 'none' : (s.ok ? 'ok' : 'failed');
1616
- if (!s.ok && !s.skipped) r.setupError = s.failed ? `${s.failed}: ${s.error}` : s.error;
1627
+ // SECURITY: setup commands are arbitrary shell (execSync). Auto-run them ONLY for
1628
+ // skills the current user authored self-consent, like your own repo. Skills
1629
+ // authored by another org member, a fork, or an imported bundle must NOT run their
1630
+ // shell unattended (Causeway auto-syncs pinned skills), or a teammate's skill is
1631
+ // RCE on this host. `--allow-untrusted-setup` is the explicit opt-in.
1632
+ // Self-authored = created by me AND not a fork (a fork carries another author's
1633
+ // setup under my createdBy — see syncOneSkill). Forks/other-authored require the
1634
+ // explicit opt-in.
1635
+ const selfAuthored = r.createdBy && auth?.userId && r.createdBy === auth.userId && !r.forkedFrom;
1636
+ if (selfAuthored || opts.allowUntrustedSetup) {
1637
+ const s = runSkillSetup(join(opts.out, r.slug));
1638
+ r.setup = s.skipped ? 'none' : (s.ok ? 'ok' : 'failed');
1639
+ if (!s.ok && !s.skipped) r.setupError = s.failed ? `${s.failed}: ${s.error}` : s.error;
1640
+ } else {
1641
+ r.setup = 'skipped-untrusted';
1642
+ }
1617
1643
  }
1618
1644
  results.push(r);
1619
1645
  if (r.status !== 'ok') allOk = false;
package/mcp/server.mjs CHANGED
@@ -1693,9 +1693,11 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. On a local install the DESKTOP
1693
1693
  console.error(`\n[MCP] Sign in at: ${verificationUrl}\n${qrText ? qrText + '\n' : ''}[MCP] Waiting for approval...`);
1694
1694
 
1695
1695
  if (!reusingPending) {
1696
- const { exec } = await import('child_process');
1697
- const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
1698
- exec(`${cmd} ${JSON.stringify(verificationUrl)}`);
1696
+ // SECURITY: verificationUrl is server-supplied pass as an argv arg (execFile, no
1697
+ // shell) so a compromised/MITM'd server response can't inject shell into this host.
1698
+ const { execFile } = await import('child_process');
1699
+ if (process.platform === 'win32') execFile('cmd', ['/c', 'start', '', verificationUrl]);
1700
+ else execFile(process.platform === 'darwin' ? 'open' : 'xdg-open', [verificationUrl]);
1699
1701
  }
1700
1702
 
1701
1703
  const deadline = Date.now() + (expiresIn * 1000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.14.6",
3
+ "version": "1.14.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": [