Skip to content

feat(v2): rich artifacts in pod inspector — paste Notion/Drive/Figma/GitHub/Zoom#266

Merged
samxu01 merged 1 commit intomainfrom
feat/v2-rich-artifacts-samxu
May 3, 2026
Merged

feat(v2): rich artifacts in pod inspector — paste Notion/Drive/Figma/GitHub/Zoom#266
samxu01 merged 1 commit intomainfrom
feat/v2-rich-artifacts-samxu

Conversation

@samxu01
Copy link
Copy Markdown
Contributor

@samxu01 samxu01 commented May 2, 2026

Reopens #265 from samxu01 account.

Summary

  • Extend ExternalLink enum with 13 new URL kinds: notion, google_doc/sheet/slides/drive, figma, zoom, gmail, github_pr/issue/repo, youtube, loom, other_link. Existing chat-bridge types untouched.
  • Add detectLinkType(url) host/path matcher and deriveLinkName(url) so the v2 add-link flow is paste-and-go (one URL field, no type picker).
  • Relax POST /api/pods/external-link from owner-only to any pod member — multi-member pods could not add links before.
  • Inspector "+ Add" toggle in the Artifacts section, per-type icon glyph (N, GD, FG, PR, …) and humanized kind labels.
  • Reject javascript: / data: / file: / non-URL strings server-side with 400; inspector wraps <a href> in safeHref() for defense-in-depth on legacy rows.

Why

The Inspector's Artifacts section already existed and was reading ExternalLink, but the type enum was a chat-bridge artifact (Discord/Telegram/WeChat/GroupMe). Empty in every demo pod. With ~80 lines of additive backend + UI work the YC demo gets real Notion/Drive/Figma/GitHub URLs in a designed surface — same substrate, same endpoints.

Linked context: ADR-011 shell-first GTM track; YC demo beat 2 ("project memory is in the room with the agents") gates on artifacts being visible.

What I did NOT do

  • No new tables, no migration. Enum is purely additive.
  • No frontend test file (no existing inspector tests to extend; not a new pattern to introduce on a one-shot UI).

Tests

  • backend/__tests__/unit/routes/pods.external-links.test.js — 27 cases (was 8).
  • backend/__tests__/unit/models/ExternalLink.test.js — 17 cases (was 2).
  • 44/44 passing locally.

Test plan

  • Deploy to dev: gh workflow run deploy-dev.yml --ref feat/v2-rich-artifacts-samxu
  • Open Sam's YC Sprint pod on app-dev.commonly.me/v2, click "+ Add" in the Artifacts section
  • Paste a Notion URL — confirm "N" icon and "Notion" sub-label
  • Paste a Google Doc URL — confirm "GD" + "Google Doc"
  • Paste a Figma file URL — confirm "F" + "Figma"
  • Paste a GitHub PR URL — confirm "PR" + "GitHub PR"
  • Paste `javascript:alert(1)` — confirm 400 with the "URL must be http or https" message inline
  • As a non-owner pod member, paste a URL — confirm 201 (was 403 before)
  • Click an artifact → detail view → "Open" — confirm it opens in a new tab

🤖 Generated with Claude Code

Comment thread backend/routes/pods.ts
Comment on lines +99 to +118
// Reject anything that isn't a real http(s) URL — guards against `javascript:`
// or `data:` schemes ending up in an <a href> in the inspector. WeChat QR-code
// links are exempt because their primary surface is qrCodePath, not href.
const isSafeHttpUrl = (rawUrl: string): boolean => {
try {
const u = new URL(rawUrl);
return u.protocol === 'http:' || u.protocol === 'https:';
} catch {
return false;
}
};

// URL → ExternalLinkType. Used when the client passes type='auto' (or omits
// type with a URL present) so the v2 inspector "+ Add" flow is paste-and-go.
// Match the most specific host first; everything unknown falls back to
// 'other_link'. Keep this in sync with the enum in models/ExternalLink.ts.
const detectLinkType = (rawUrl: string): string => {
if (!rawUrl) return 'other_link';
let host = '';
let pathname = '';
Comment thread backend/routes/pods.ts
}
if (host === 'youtube.com' || host.endsWith('.youtube.com') || host === 'youtu.be') return 'youtube';
if (host === 'loom.com' || host.endsWith('.loom.com')) return 'loom';
if (host.includes('discord.com') || host.includes('discord.gg')) return 'discord';
Comment thread backend/routes/pods.ts
if (host === 'loom.com' || host.endsWith('.loom.com')) return 'loom';
if (host.includes('discord.com') || host.includes('discord.gg')) return 'discord';
if (host === 't.me' || host.endsWith('.telegram.org')) return 'telegram';
if (host.includes('groupme.com')) return 'groupme';
Comment thread backend/routes/pods.ts
if (url && !isSafeHttpUrl(url)) return res.status(400).json({ message: 'URL must be http or https' });
const name = (rawName && rawName.trim()) || (url ? deriveLinkName(url) : '');
if (!name) return res.status(400).json({ message: 'Missing name' });
const pod = await Pod.findById(podId) as { createdBy?: { toString: () => string }; members?: Array<{ toString: () => string }>; externalLinks?: unknown[]; save: () => Promise<void> } | null;
…GitHub/Zoom

Extend ExternalLink enum with notion, google_doc/sheet/slides/drive,
figma, zoom, gmail, github_pr/issue/repo, youtube, loom, other_link.
Existing chat-bridge types untouched.

Add detectLinkType(url) host/path matcher and deriveLinkName(url) so
the v2 add-link flow is paste-and-go (one URL field, no type picker).

Relax POST /api/pods/external-link from owner-only to any pod member —
multi-member pods could not add links before.

Inspector "+ Add" toggle in Artifacts section, per-type icon glyph
(N, GD, FG, PR, …) and humanized kind labels.

Reject javascript:/data:/file:/non-URL strings server-side with 400;
inspector wraps <a href> in safeHref() for defense-in-depth on legacy
rows.

Tests: 44/44 — 27 route, 17 model. Detect cases for Notion (apex +
subdomain), Figma, Zoom, Drive, youtu.be, Loom, fallback. 4 unsafe-URL
rejection cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@samxu01 samxu01 force-pushed the feat/v2-rich-artifacts-samxu branch from 88bf7ec to 9ffbc75 Compare May 3, 2026 05:58
@samxu01 samxu01 merged commit 9ffbc75 into main May 3, 2026
8 checks passed
@samxu01 samxu01 deleted the feat/v2-rich-artifacts-samxu branch May 3, 2026 06:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants