Skip to content

Commit d85cb48

Browse files
authored
blog: securing notebooks (#617)
* feat(blog): draft security post * toss the dynamic param * prep for publish * blog: dynamic cover media from frontmatter, scale down hero without media - Add coverVideo to BlogPostFrontmatter type and parser - Move hardcoded tada.mp4 into nteract-2.0.mdx frontmatter as coverVideo - Hero section renders video/image from latest post, or nothing - Smaller headline when no cover media is present - Security post prose updates * clean up more * blog: inline CSP callout as prose line * blog: correct daemon diagram to show GET-only blob server The SocketDiagram claimed "NO HTTP SERVER" which is wrong — runtimed exposes a read-only blob store on 127.0.0.1 with an OS-assigned port. Redraw as two planes (Unix socket control, HTTP GET data), add CSP allowlist to the iframe diagram, and tighten the local-only + Tauri sections to match. * slim down * blog: lead security post with olive-garden demo, swap diagram for Figma SVG Restructure the "Every output is isolated" section so the side-by-side nteract vs JupyterLab screenshots run before the technical explanation. Replace the inline SVG diagram with the polished Figma export served from /public, drop the widget postMessage video into the placeholder that was waiting for it, and update the blog test to expect both posts. * blog: tighten trust-signature paragraph * blog: add cover video + OG image, polish diagram and security prose - Add coverVideo + ogImage frontmatter wiring - Dark-transparent postMessage label in iframe isolation SVG so the dashed origin boundary bleeds through; light-gray text for contrast on the dark page - Merge "What about agents?" into the control-socket paragraph (SSH analogy + MCP agents folded in) - Fold the standalone "secrets only on your machine" paragraph into the control-socket block to tighten Your data, your machine - Swap stale Tauri "allowlist" → "capabilities" - Note CSP allows unsafe-inline/eval inside the opaque iframe - "rotates every run" → "picked fresh each time the daemon starts" - Blob store example: "Parquet, Arrow, output rendering assets" (drops the "plugin" framing) - Drop the widget video placeholder, inline the postMessage demo * blog: fix nested <p> hydration warning in BlogInlineCTA Collapse the component's outer <p> into its <div className="not-prose">. MDX was wrapping the component's children in a <p>, which landed inside the <a>, producing a <p>-inside-<p> hydration error. The typography classes move to the <div> with no visual change.
1 parent 494ddb0 commit d85cb48

13 files changed

Lines changed: 539 additions & 30 deletions

File tree

app/(blog)/blog/[slug]/page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,20 @@ type BlogPostPageProps = {
1313
}>;
1414
};
1515

16+
const isDev = process.env.NODE_ENV === "development";
17+
1618
export const dynamicParams = false;
1719

1820
export async function generateStaticParams() {
19-
const slugs = await getAllSlugs();
21+
const slugs = await getAllSlugs({ includeUnpublished: isDev });
2022
return slugs.map((slug) => ({ slug }));
2123
}
2224

2325
export async function generateMetadata({
2426
params,
2527
}: BlogPostPageProps): Promise<Metadata> {
2628
const { slug } = await params;
27-
const post = await getPostBySlug(slug);
29+
const post = await getPostBySlug(slug, { includeUnpublished: isDev });
2830

2931
if (!post) {
3032
return {};
@@ -58,7 +60,7 @@ export async function generateMetadata({
5860

5961
export default async function BlogPostPage({ params }: BlogPostPageProps) {
6062
const { slug } = await params;
61-
const post = await getPostBySlug(slug);
63+
const post = await getPostBySlug(slug, { includeUnpublished: isDev });
6264

6365
if (!post) {
6466
notFound();

app/(blog)/blog/page.tsx

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ export default async function BlogPage() {
6262
</div>
6363

6464
<Link href={`/blog/${latest.slug}`} className="group block">
65-
<h1 className="mb-6 font-headline text-6xl font-bold leading-[0.9] tracking-tighter text-on-surface transition-colors group-hover:text-secondary md:text-8xl">
65+
<h1
66+
className={`mb-6 font-headline font-bold leading-[0.9] tracking-tighter text-on-surface transition-colors group-hover:text-secondary ${latest.coverVideo || latest.coverImage ? "text-6xl md:text-8xl" : "text-4xl md:text-5xl"}`}
67+
>
6668
{latest.title}
6769
</h1>
6870

@@ -79,7 +81,9 @@ export default async function BlogPage() {
7981
const rest2 = latest.description.slice(dot + 1).trim();
8082
return (
8183
<div className="mb-6 max-w-2xl space-y-2">
82-
<p className="font-headline text-2xl font-semibold tracking-tight text-on-surface/80 md:text-3xl">
84+
<p
85+
className={`font-headline font-semibold tracking-tight text-on-surface/80 ${latest.coverVideo || latest.coverImage ? "text-2xl md:text-3xl" : "text-lg md:text-xl"}`}
86+
>
8387
{lead}
8488
</p>
8589
{rest2 && (
@@ -99,17 +103,33 @@ export default async function BlogPage() {
99103
</div>
100104
</Link>
101105

102-
{/* Hero preview */}
103-
<Link href={`/blog/${latest.slug}`} className="mt-10 block overflow-hidden">
104-
<video
105-
src="https://pub-d6c6294d12e242e7acb5f8d1eaf78e06.r2.dev/tada.mp4"
106-
autoPlay
107-
muted
108-
loop
109-
playsInline
110-
className="w-full"
111-
/>
112-
</Link>
106+
{/* Hero preview — driven by coverVideo / coverImage frontmatter */}
107+
{latest.coverVideo ? (
108+
<Link
109+
href={`/blog/${latest.slug}`}
110+
className="mt-10 block overflow-hidden"
111+
>
112+
<video
113+
src={latest.coverVideo}
114+
autoPlay
115+
muted
116+
loop
117+
playsInline
118+
className="w-full"
119+
/>
120+
</Link>
121+
) : latest.coverImage ? (
122+
<Link
123+
href={`/blog/${latest.slug}`}
124+
className="mt-10 block overflow-hidden"
125+
>
126+
<img
127+
src={latest.coverImage}
128+
alt={latest.title}
129+
className="w-full"
130+
/>
131+
</Link>
132+
) : null}
113133
</>
114134
) : (
115135
<div className="bg-surface-container-low px-6 py-10 text-on-surface-variant">
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { LightboxImage } from "./lightbox-image";
2+
3+
export function IframeIsolationDiagram() {
4+
return (
5+
<div className="not-prose my-12 flex justify-center">
6+
<LightboxImage
7+
src="/iframe-isolation.svg"
8+
alt="Diagram showing iframe isolation: parent window with Tauri APIs separated from an isolated blob: iframe by an opaque-origin boundary, with postMessage / JSON-RPC 2.0 as the only bridge."
9+
className="w-full max-w-[700px]"
10+
/>
11+
</div>
12+
);
13+
}

components/blog/inline-cta.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { ReactNode } from "react";
2+
3+
type BlogInlineCTAProps = {
4+
href: string;
5+
children: ReactNode;
6+
/** Optional lead-in phrase rendered before the link */
7+
lead?: string;
8+
};
9+
10+
export function BlogInlineCTA({ href, children, lead }: BlogInlineCTAProps) {
11+
const isExternal =
12+
href.startsWith("http://") || href.startsWith("https://");
13+
14+
return (
15+
<div className="not-prose font-body text-[0.95rem] leading-relaxed text-on-surface-variant/75">
16+
{lead ? <span>{lead} </span> : null}
17+
<a
18+
href={href}
19+
{...(isExternal
20+
? { target: "_blank", rel: "noopener noreferrer" }
21+
: {})}
22+
className="group inline-flex items-baseline gap-1.5 text-[#cbb5f4] underline decoration-[#a993d1]/40 decoration-from-font underline-offset-[5px] transition-colors hover:decoration-[#cbb5f4]"
23+
>
24+
{children}
25+
<span
26+
aria-hidden
27+
className="inline-block translate-y-[1px] transition-transform group-hover:translate-x-0.5"
28+
>
29+
30+
</span>
31+
</a>
32+
</div>
33+
);
34+
}

components/blog/socket-diagram.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Two-surface architecture:
3+
* - Control plane: Unix socket (0600) ← App, agents
4+
* - Data plane: GET-only HTTP blob store on 127.0.0.1:<ephemeral> → iframes
5+
* Both bind locally only; nothing is reachable off-host.
6+
*/
7+
export function SocketDiagram() {
8+
return (
9+
<div className="not-prose my-12 flex justify-center">
10+
<svg
11+
width="720"
12+
height="340"
13+
viewBox="0 0 720 340"
14+
fill="none"
15+
className="w-full max-w-[720px]"
16+
>
17+
<title>
18+
runtimed exposes two local-only surfaces: a Unix socket for control
19+
and a GET-only HTTP blob store for binary output data
20+
</title>
21+
<style>{`
22+
.sd-label { fill: #e5e5e5; font-family: 'Space Grotesk', system-ui, sans-serif; font-size: 13px; font-weight: 600; }
23+
.sd-sub { fill: #ababab; font-family: 'JetBrains Mono', monospace; font-size: 9px; letter-spacing: 0.15em; text-transform: uppercase; }
24+
.sd-mono { fill: #ababab; font-family: 'JetBrains Mono', monospace; font-size: 10px; }
25+
.sd-purple { fill: #a993d1; font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.05em; }
26+
.sd-teal { fill: #7dd3c4; font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.05em; }
27+
.sd-check { fill: #86efac; font-family: 'JetBrains Mono', monospace; font-size: 10px; }
28+
.sd-circle { fill: none; stroke: #a993d1; stroke-width: 2; }
29+
.sd-circle-fill { fill: #a993d1; opacity: 0.1; }
30+
.sd-iframe { fill: none; stroke: #7dd3c4; stroke-width: 2; }
31+
.sd-iframe-fill { fill: #7dd3c4; opacity: 0.08; }
32+
.sd-ctrl-line { stroke: #a993d1; stroke-width: 1.5; stroke-dasharray: 4 4; opacity: 0.55; }
33+
.sd-data-line { stroke: #7dd3c4; stroke-width: 1.5; opacity: 0.55; }
34+
.sd-hex { fill: #0e0e0e; stroke: #a993d1; stroke-width: 2; }
35+
.sd-hex-fill { fill: #a993d1; opacity: 0.15; }
36+
.sd-divider { stroke: #484848; stroke-width: 1; stroke-dasharray: 2 4; opacity: 0.5; }
37+
@keyframes sd-pulse { 0%, 100% { opacity: 0.25; } 50% { opacity: 0.8; } }
38+
.sd-pulse { animation: sd-pulse 2.5s ease-in-out infinite; }
39+
`}</style>
40+
41+
{/* ── Daemon (center hexagon) ── */}
42+
<g transform="translate(360, 150)">
43+
<polygon points="0,-44 38,-22 38,22 0,44 -38,22 -38,-22" className="sd-hex-fill" />
44+
<polygon points="0,-44 38,-22 38,22 0,44 -38,22 -38,-22" className="sd-hex" />
45+
<text y="-2" textAnchor="middle" className="sd-label" style={{ fontSize: "11px" }}>runtimed</text>
46+
<text y="14" textAnchor="middle" className="sd-sub">daemon</text>
47+
</g>
48+
49+
{/* ── Control-plane lines (left) ── */}
50+
<line x1="130" y1="80" x2="322" y2="128" className="sd-ctrl-line" />
51+
<line x1="130" y1="220" x2="322" y2="172" className="sd-ctrl-line" />
52+
53+
{/* Pulse dots */}
54+
<circle r="2.5" fill="#a993d1" className="sd-pulse">
55+
<animateMotion dur="2.2s" repeatCount="indefinite" path="M 130 80 L 322 128" />
56+
</circle>
57+
<circle r="2.5" fill="#a993d1" className="sd-pulse" style={{ animationDelay: "0.9s" }}>
58+
<animateMotion dur="2.6s" repeatCount="indefinite" path="M 130 220 L 322 172" />
59+
</circle>
60+
61+
<text x="220" y="116" textAnchor="middle" className="sd-sub" transform="rotate(-12, 220, 116)">unix socket</text>
62+
63+
{/* ── App (upper left) ── */}
64+
<g transform="translate(100, 75)">
65+
<circle r="30" className="sd-circle-fill" />
66+
<circle r="30" className="sd-circle" />
67+
<rect x="-12" y="-9" width="24" height="16" rx="2" stroke="#a993d1" strokeWidth="1.5" fill="none" />
68+
<line x1="-12" y1="-2" x2="12" y2="-2" stroke="#a993d1" strokeWidth="1" />
69+
<circle cx="-8" cy="-6" r="1" fill="#a993d1" />
70+
<text y="-42" textAnchor="middle" className="sd-label">nteract App</text>
71+
</g>
72+
73+
{/* ── Agents (lower left) ── */}
74+
<g transform="translate(100, 220)">
75+
<circle r="30" className="sd-circle-fill" />
76+
<circle r="30" className="sd-circle" />
77+
<rect x="-8" y="-10" width="16" height="12" rx="2" stroke="#a993d1" strokeWidth="1.5" fill="none" />
78+
<circle cx="-3" cy="-4" r="1.4" fill="#a993d1" />
79+
<circle cx="3" cy="-4" r="1.4" fill="#a993d1" />
80+
<path d="M -4 8 L 4 8" stroke="#a993d1" strokeWidth="1.5" />
81+
<text y="48" textAnchor="middle" className="sd-label">MCP agents</text>
82+
<text y="62" textAnchor="middle" className="sd-sub">claude · codex · warp</text>
83+
</g>
84+
85+
{/* ── Data-plane line (right) ── */}
86+
<line x1="400" y1="150" x2="560" y2="150" className="sd-data-line" />
87+
<circle r="2.5" fill="#7dd3c4" className="sd-pulse" style={{ animationDelay: "0.3s" }}>
88+
<animateMotion dur="2.4s" repeatCount="indefinite" path="M 400 150 L 560 150" />
89+
</circle>
90+
<text x="480" y="142" textAnchor="middle" className="sd-sub" style={{ letterSpacing: "0.1em" }}>http get</text>
91+
92+
{/* ── Iframe (right) ── */}
93+
<g transform="translate(605, 150)">
94+
<rect x="-45" y="-34" width="90" height="68" rx="3" className="sd-iframe-fill" />
95+
<rect x="-45" y="-34" width="90" height="68" rx="3" className="sd-iframe" />
96+
<line x1="-45" y1="-22" x2="45" y2="-22" stroke="#7dd3c4" strokeWidth="1" opacity="0.5" />
97+
<circle cx="-38" cy="-28" r="1.5" fill="#7dd3c4" opacity="0.8" />
98+
<circle cx="-32" cy="-28" r="1.5" fill="#7dd3c4" opacity="0.8" />
99+
<circle cx="-26" cy="-28" r="1.5" fill="#7dd3c4" opacity="0.8" />
100+
{/* output glyph */}
101+
<path d="M -20 0 L -8 -12 L 6 4 L 20 -10" stroke="#7dd3c4" strokeWidth="1.5" fill="none" opacity="0.7" />
102+
<circle cx="-20" cy="0" r="2" fill="#7dd3c4" />
103+
<circle cx="-8" cy="-12" r="2" fill="#7dd3c4" />
104+
<circle cx="6" cy="4" r="2" fill="#7dd3c4" />
105+
<circle cx="20" cy="-10" r="2" fill="#7dd3c4" />
106+
<text y="-52" textAnchor="middle" className="sd-label">Output iframes</text>
107+
<text y="52" textAnchor="middle" className="sd-sub">blob: origin</text>
108+
</g>
109+
110+
{/* ── Legend divider ── */}
111+
<line x1="30" y1="272" x2="690" y2="272" className="sd-divider" />
112+
113+
{/* ── Legend: Control plane ── */}
114+
<g transform="translate(60, 296)">
115+
<rect x="0" y="-8" width="12" height="4" fill="#a993d1" opacity="0.55" />
116+
<rect x="16" y="-8" width="4" height="4" fill="#a993d1" opacity="0.55" />
117+
<rect x="24" y="-8" width="4" height="4" fill="#a993d1" opacity="0.55" />
118+
<text x="40" y="-3" className="sd-purple">CONTROL</text>
119+
<text x="40" y="14" className="sd-mono">unix socket · chmod 0600 · owner-only</text>
120+
</g>
121+
122+
{/* ── Legend: Data plane ── */}
123+
<g transform="translate(400, 296)">
124+
<rect x="0" y="-8" width="28" height="4" fill="#7dd3c4" opacity="0.55" />
125+
<text x="40" y="-3" className="sd-teal">DATA</text>
126+
<text x="40" y="14" className="sd-mono">127.0.0.1:&lt;random&gt; · GET /blob/&#123;sha256&#125;</text>
127+
</g>
128+
</svg>
129+
</div>
130+
);
131+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Side-by-side comparison of Electron vs Tauri security models.
3+
* Electron: Node.js flows into renderer. Tauri: wall between webview and native.
4+
*/
5+
export function TauriComparison() {
6+
return (
7+
<div className="not-prose my-12 flex justify-center">
8+
<svg
9+
width="700"
10+
height="280"
11+
viewBox="0 0 700 280"
12+
fill="none"
13+
className="w-full max-w-[700px]"
14+
>
15+
<title>Comparison of Electron and Tauri security models</title>
16+
<style>{`
17+
.tc-label { fill: #e5e5e5; font-family: 'Space Grotesk', system-ui, sans-serif; font-size: 14px; font-weight: 600; }
18+
.tc-sublabel { fill: #ababab; font-family: 'JetBrains Mono', monospace; font-size: 9px; letter-spacing: 0.15em; text-transform: uppercase; }
19+
.tc-item { fill: #ababab; font-family: 'JetBrains Mono', monospace; font-size: 10px; }
20+
.tc-danger { fill: #ef4444; font-family: 'JetBrains Mono', monospace; font-size: 10px; }
21+
.tc-safe { fill: #22c55e; font-family: 'JetBrains Mono', monospace; font-size: 10px; }
22+
.tc-accent { fill: #a993d1; font-family: 'JetBrains Mono', monospace; font-size: 10px; }
23+
.tc-box { fill: none; stroke: #484848; stroke-width: 1.5; rx: 6; }
24+
.tc-box-fill { fill: #484848; opacity: 0.05; rx: 6; }
25+
.tc-box-good { fill: none; stroke: #a993d1; stroke-width: 1.5; rx: 6; }
26+
.tc-box-good-fill { fill: #a993d1; opacity: 0.05; rx: 6; }
27+
.tc-flow { stroke: #ef4444; stroke-width: 1.5; opacity: 0.5; marker-end: url(#tc-arrow-red); }
28+
.tc-wall { stroke: #22c55e; stroke-width: 2.5; opacity: 0.6; }
29+
.tc-divider { stroke: #333333; stroke-width: 1; stroke-dasharray: 4 4; }
30+
`}</style>
31+
32+
<defs>
33+
<marker id="tc-arrow-red" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
34+
<path d="M 0 0 L 8 3 L 0 6" fill="none" stroke="#ef4444" strokeWidth="1" />
35+
</marker>
36+
</defs>
37+
38+
{/* ── Divider ── */}
39+
<line x1="350" y1="10" x2="350" y2="270" className="tc-divider" />
40+
41+
{/* ════════════════ ELECTRON (left) ════════════════ */}
42+
<text x="175" y="28" textAnchor="middle" className="tc-label" style={{ opacity: 0.5 }}>Electron</text>
43+
44+
{/* Renderer box */}
45+
<rect x="30" y="50" width="140" height="120" className="tc-box-fill" />
46+
<rect x="30" y="50" width="140" height="120" className="tc-box" />
47+
<text x="100" y="72" textAnchor="middle" className="tc-sublabel">renderer</text>
48+
<text x="50" y="95" className="tc-item">Chromium</text>
49+
<text x="50" y="115" className="tc-danger">Node.js ⚠</text>
50+
<text x="50" y="135" className="tc-danger">require()</text>
51+
<text x="50" y="155" className="tc-danger">child_process</text>
52+
53+
{/* Node.js backend box */}
54+
<rect x="190" y="50" width="140" height="120" className="tc-box-fill" />
55+
<rect x="190" y="50" width="140" height="120" className="tc-box" />
56+
<text x="260" y="72" textAnchor="middle" className="tc-sublabel">main process</text>
57+
<text x="210" y="95" className="tc-item">Node.js</text>
58+
<text x="210" y="115" className="tc-item">Full fs access</text>
59+
<text x="210" y="135" className="tc-item">Shell commands</text>
60+
<text x="210" y="155" className="tc-item">No restrictions</text>
61+
62+
{/* Flow arrow — Node.js leaks into renderer */}
63+
<line x1="190" y1="110" x2="170" y2="110" className="tc-flow" />
64+
65+
{/* Electron verdict */}
66+
<text x="175" y="200" textAnchor="middle" className="tc-danger" style={{ fontSize: "9px", letterSpacing: "0.15em" }}>FULL NATIVE ACCESS FROM RENDERER</text>
67+
<text x="175" y="218" textAnchor="middle" className="tc-item" style={{ fontSize: "9px", opacity: 0.5 }}>One nodeIntegration: true away</text>
68+
<text x="175" y="236" textAnchor="middle" className="tc-item" style={{ fontSize: "9px", opacity: 0.5 }}>from compromise</text>
69+
70+
{/* ════════════════ TAURI (right) ════════════════ */}
71+
<text x="525" y="28" textAnchor="middle" className="tc-label">Tauri</text>
72+
73+
{/* Webview box */}
74+
<rect x="380" y="50" width="140" height="120" className="tc-box-good-fill" />
75+
<rect x="380" y="50" width="140" height="120" className="tc-box-good" />
76+
<text x="450" y="72" textAnchor="middle" className="tc-sublabel">webview</text>
77+
<text x="400" y="95" className="tc-safe">Native webview</text>
78+
<text x="400" y="115" className="tc-safe">No Node.js</text>
79+
<text x="400" y="135" className="tc-safe">No require()</text>
80+
<text x="400" y="155" className="tc-safe">No fs, no shell</text>
81+
82+
{/* Capability wall */}
83+
<line x1="535" y1="52" x2="535" y2="168" className="tc-wall" />
84+
<text x="535" y="180" textAnchor="middle" className="tc-sublabel" style={{ fill: "#22c55e", fontSize: "7px" }}>CAPABILITY</text>
85+
<text x="535" y="190" textAnchor="middle" className="tc-sublabel" style={{ fill: "#22c55e", fontSize: "7px" }}>BOUNDARY</text>
86+
87+
{/* Rust backend box */}
88+
<rect x="550" y="50" width="130" height="120" className="tc-box-good-fill" />
89+
<rect x="550" y="50" width="130" height="120" className="tc-box-good" />
90+
<text x="615" y="72" textAnchor="middle" className="tc-sublabel">rust backend</text>
91+
<text x="570" y="95" className="tc-accent">Explicit grants</text>
92+
<text x="570" y="115" className="tc-accent">Per-capability</text>
93+
<text x="570" y="135" className="tc-accent">Allowlist only</text>
94+
<text x="570" y="155" className="tc-accent">Type-safe IPC</text>
95+
96+
{/* Tauri verdict */}
97+
<text x="525" y="218" textAnchor="middle" className="tc-safe" style={{ fontSize: "9px", letterSpacing: "0.15em" }}>WEBVIEW IS A DEAD END</text>
98+
<text x="525" y="236" textAnchor="middle" className="tc-item" style={{ fontSize: "9px", opacity: 0.5 }}>Nothing to hijack</text>
99+
</svg>
100+
</div>
101+
);
102+
}

0 commit comments

Comments
 (0)