drafted 1.14.6 → 1.14.7
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 +37 -17
- package/mcp/server.mjs +5 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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,9 @@ async function syncOneSkill(ref, outDir, org) {
|
|
|
1584
1591
|
mkdirSync(dirname(full), { recursive: true });
|
|
1585
1592
|
writeFileSync(full, content);
|
|
1586
1593
|
}
|
|
1587
|
-
|
|
1594
|
+
// createdBy lets the caller decide whether to auto-run this skill's setup: shell authored
|
|
1595
|
+
// by someone else must not run unattended on sync (RCE via a teammate's/forked skill).
|
|
1596
|
+
return { ref, slug, hash, status: 'ok', createdBy: skill.createdBy ?? null };
|
|
1588
1597
|
}
|
|
1589
1598
|
|
|
1590
1599
|
const skillCmd = program.command('skill').description('Skill library operations');
|
|
@@ -1594,10 +1603,11 @@ skillCmd
|
|
|
1594
1603
|
.option('--ref <ref>', 'skill ref slug[@hash] (repeatable)', (v, acc) => { acc.push(v); return acc; }, [])
|
|
1595
1604
|
.requiredOption('--out <dir>', 'output directory for bundles')
|
|
1596
1605
|
.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
|
|
1606
|
+
.option('--no-setup', 'do not run each skill\'s setup after materializing (default: run setup only for skills you authored)')
|
|
1607
|
+
.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
1608
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1599
1609
|
.action(async (opts) => {
|
|
1600
|
-
requireLogin();
|
|
1610
|
+
const auth = requireLogin();
|
|
1601
1611
|
const refs = opts.ref || [];
|
|
1602
1612
|
mkdirSync(opts.out, { recursive: true });
|
|
1603
1613
|
const results = [];
|
|
@@ -1611,9 +1621,19 @@ skillCmd
|
|
|
1611
1621
|
// failure is reported but the materialized source stays put (sync is still
|
|
1612
1622
|
// `ok` — the bytes are there, just not built).
|
|
1613
1623
|
if (r.status === 'ok' && opts.setup !== false) {
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1624
|
+
// SECURITY: setup commands are arbitrary shell (execSync). Auto-run them ONLY for
|
|
1625
|
+
// skills the current user authored — self-consent, like your own repo. Skills
|
|
1626
|
+
// authored by another org member, a fork, or an imported bundle must NOT run their
|
|
1627
|
+
// shell unattended (Causeway auto-syncs pinned skills), or a teammate's skill is
|
|
1628
|
+
// RCE on this host. `--allow-untrusted-setup` is the explicit opt-in.
|
|
1629
|
+
const selfAuthored = r.createdBy && auth?.userId && r.createdBy === auth.userId;
|
|
1630
|
+
if (selfAuthored || opts.allowUntrustedSetup) {
|
|
1631
|
+
const s = runSkillSetup(join(opts.out, r.slug));
|
|
1632
|
+
r.setup = s.skipped ? 'none' : (s.ok ? 'ok' : 'failed');
|
|
1633
|
+
if (!s.ok && !s.skipped) r.setupError = s.failed ? `${s.failed}: ${s.error}` : s.error;
|
|
1634
|
+
} else {
|
|
1635
|
+
r.setup = 'skipped-untrusted';
|
|
1636
|
+
}
|
|
1617
1637
|
}
|
|
1618
1638
|
results.push(r);
|
|
1619
1639
|
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
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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.
|
|
3
|
+
"version": "1.14.7",
|
|
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": [
|