What's new.
Reverse-chronological list of meaningful changes to the Artifact Publisher service and its adapters. Patch fixes that don't change behavior are folded into the next release line.
Republishing now updates instantly FIX
Republishing over an existing artifact, or changing its password/expiry, could take up to 60 seconds to show the new content — cache invalidation was silently failing on every surface.
- Root cause: the invalidation call only works from inside a Next.js App Router request, but every publish/gate-update/delete path runs as a standalone Vercel Function — so it was a no-op every time, and the artifact page relied entirely on passing its 60-second cache window.
- Fix: added a small internal endpoint that runs in the right context to invalidate the cache for real, and routed publish, gate updates, and delete — across web, Slack, CLI, MCP, and the macOS app — through it.
- 13 new tests cover the fix, including that a real publish or delete actually triggers it.
Typography refresh & mobile viewport fix DESIGN
A light visual pass across the publish, docs, and changelog pages.
- New typeface: headings and body now set in Outfit, which carries more presence at display sizes. Monospace stays JetBrains Mono.
- Mobile fix: full-height sections use
100dvhinstead of100vh, so the layout no longer jumps when iOS Safari's address bar shows and hides. - Manage-tab status badges squared off slightly for a more intentional look.
- Under the hood: deduplicated the HMAC token + base64url helpers behind the access gate and dropped some dead host-detection code — no behavior change, ~80 fewer lines to maintain.
Duplicate-name protection on publish NEW
Publishing a file or paste whose name matches an existing artifact now prompts for confirmation instead of silently overwriting.
- Web UI — inline confirm: if the resolved slug already exists, a compact "already exists — replace it?" row appears beneath the Publish button with Cancel and Update options — no page navigation or modal. Covers both the Upload and Paste tabs.
- Chrome extension: the same check runs in the popup before the page is captured. A dedicated confirm screen shows the conflicting slug with Update and Cancel buttons.
- Low-latency: the existing-slug list is fetched in the background as soon as the UI is ready and cached for the session, so the check adds no perceptible delay. A new
POST /api/cli/listendpoint serves the extension's bearer-token auth. The cache is updated after each successful publish.
Chrome extension: publish any page with one click NEW
A new browser adapter joins the roster. Install the extension, sign in once with Slack, and every page on the web is one click away from being a gated artifact.
- One-click publish: click the extension icon, choose Private or Public, hit Publish — the artifact URL and auto-generated password appear immediately in the popup with copy buttons.
- Live DOM capture: captures
document.documentElement.outerHTMLat publish time, so JavaScript-rendered content is preserved exactly as it appears in the browser. - Same auth as the CLI: uses the Slack OAuth device-code flow — a user code appears in the popup, you complete sign-in in a browser tab, and the token is stored locally. Re-auth is only needed if the token is revoked server-side.
- Private by default: consistent with every other surface, Private is the default visibility — an auto-generated password gates the artifact unless you explicitly choose Public.
- Available as an unlisted Chrome Web Store extension — install via this link, no Developer Mode required. Extension source lives in
chrome-extension/.
Publisher UI polish & bug fix: share-at-publish now emails recipients FIX
A design review pass on the web publisher surfaced one real bug and a handful of copy inconsistencies.
- Bug: share-at-publish never sent email invites. Entering an email in the Share with field created a magic link but never emailed it. Fixed — email addresses now automatically trigger an invite (names still just mint a link for you to forward).
- SLUG → TITLE on the Upload tab, matching the Paste tab. Both fields set the URL slug; Title is the more meaningful label.
- Hint copy tightened throughout: PASSWORD, TITLE, and SHARE WITH labels are shorter, single-line, and consistent across both tabs.
Copy password with URL, email it, post it to Slack NEW
Three small wins for sharing gated artifacts from the web publisher.
- Copy button includes the password. The result box's Copy button now writes
URL\nPassword: xxxx-xxxx-xxxxto the clipboard — one paste into a message and the recipient has both. - Invite emails include the password. When you share at publish time with an email address and check Send email, the magic-link email now includes the artifact password so the recipient doesn't need a second message.
- Post to a Slack channel on publish. A new optional Slack Channel field (on both the Upload and Paste tabs) posts a brief message — URL + password — to any channel the bot is in, the moment publish completes. Leave it blank to skip.
Every publish is password-protected by default NEW
You asked, we shipped: artifacts are no longer public unless you say so. Every publish now starts with a password — keep the suggested one, type your own, or explicitly opt out to publish publicly. Nothing changes for artifacts you've already published; the new default applies going forward.
- Web: the password toggle starts on with a generated password pre-filled and visible — copy it before you publish. Uncheck to publish publicly.
- Slack
/publish: the modal opens pre-filled with a generated password, and your confirmation DM repeats it so it's never lost when the modal closes. Clear the field to publish publicly. - Claude (MCP): omit
passwordand one is generated and returned with the URL — Claude relays it to you. Passpublic: truefor an ungated artifact. - API / macOS app:
POST /api/publishauto-generates when no password is sent and returns it in the response (generatedPasswords, plus a plain-text warning line for older clients). An emptypasswordis the explicit public opt-out. - Republishing keeps your password: pushing a new version of an existing artifact doesn't rotate the password you already shared.
- Rotation only: passwords can be changed from Manage or
update_gate, but no longer removed — going public is a publish-time decision. Generated passwords are the shareablexxxx-xxxx-xxxxform (no look-alike characters), stored only as scrypt hashes like always.
Paste to publish NEW
Publish without making a file first. The web UI has a new Paste tab — drop in HTML or Markdown straight from your editor or a Claude conversation, add an optional title, and publish. The content type is detected automatically, and the same password, expiry, and sharing controls apply.
- Web: Paste tab next to the upload form, with the same gating controls.
- API:
POST /api/publishnow accepts a rawcontentstring per file (with an optionalcontentTypeor filename hint) alongside the existing base64 upload. - Same 3 MB limit and password rules as file uploads.
Share at publish time NEW
No more publish → Manage → Share round-trip. Add recipients in the same action as publishing and get their private links back with the result.
- Web: a "Share with" field on both the upload and Paste forms — enter emails or names (comma-separate several) and each recipient's magic link appears alongside the published URL, ready to copy.
- API:
POST /api/publishaccepts arecipientslist and returns the minted links assharesin the response. - Links behave exactly like ones created from the Manage panel: individually revocable, view-audited, and if a share can't be created the publish itself still succeeds with a warning.
Know the moment a client first opens your link NEW
When a per-recipient share link is opened for the first time, the publisher gets a Slack DM — "👀 jane@client.com just opened acme-pitch" — closing the loop on whether a deliverable actually landed.
- First view only; repeat opens stay in the existing view timeline.
- Internal teammate views never trigger it, same as they never count in the audit.
- Delivery is fire-and-forget: a notification hiccup can never slow down or block the recipient's page.
Test suite expanded: 657 → 798 tests INFRA
The two remaining low-coverage HTTP handlers — the Slack /publish slash-command shell and the MCP transport shell — are now at full line coverage, with the MCP tests driving the real protocol end to end behind an ephemeral server. Also fixed a flaky session-token test that failed ~25% of runs due to a base64 padding quirk (production verification was always correct).
Stronger gate passwords FIX
Gate passwords now require a minimum of 8 characters, enforced consistently across every surface that sets one — web upload, the web Manage edit panel, the Slack publish modal (inline error), and the MCP publish_artifact / update_gate tools. A backstop in the hashing layer means an under-strength password can never be stored, even by a caller that skips the check. Existing passwords are unaffected.
View history for shared links NEW
Per-recipient links now keep a recent view timeline, not just a count and last-viewed time — so you can see the cadence of how a client has been opening a deliverable.
- Web: once a link has been opened, a History toggle in the Manage → Share panel expands its recent opens, shown as relative times.
- Claude (MCP):
list_sharesnow includesrecent_viewstimestamps for each link that has been opened. - The timeline keeps the 50 most recent views. For privacy we record timestamps only — never IP addresses.
Email a share link to the recipient NEW
When you create a share for an email recipient, you can now optionally have the magic link emailed to them — no more copy-paste into a separate message.
- Opt-in per share: tick "Email the link" on the Share panel and the recipient gets the link directly.
- Fail-soft: if email isn't configured (or delivery fails), the share is still created and the link is still returned to copy and send yourself — emailing never blocks creating the share.
- Plain and transactional: a short line plus the link, no tracking. Delivery uses Resend and is inert until an API key and a verified sending domain are configured.
Share artifacts from Slack NEW
Per-recipient sharing is now available straight from the /publish slash command — no need to switch to the web Manage tab or Claude.
/publish share <slug> <email-or-name>: mint a recipient's magic link (ownership-checked against your own artifacts) and get the link DM'd back to send./publish access <slug>(aliasshares): list every recipient with status (valid / expired / revoked), view count, and last-viewed date.
Revocation stays where it already lives (the web Manage panel or the MCP revoke_share tool) and takes effect on the next request, as before.
Manage sharing from Claude NEW
Per-recipient sharing is now available through the MCP adapter, not just the web Manage tab — ask Claude to share, list, or revoke access in any conversation.
share_artifact: create a recipient's magic link (with optional expiry) and get the link back to send.list_shares: see who has access and whether they've opened it — Claude renders it as a per-recipient summary.revoke_share: cut off one recipient's link immediately; others are unaffected.
Docs now cover the full access model (public / password / expiry / per-recipient) end to end.
Share artifacts with specific people NEW
Per-recipient access for client-facing artifacts — alongside the existing shared password and expiry.
- Private links per recipient: from the Manage tab's new Share panel, add a recipient (email or name) and get a unique link. Each link can have its own expiry.
- Real revocation: revoke a link and it stops working on the next request — not just for new visitors. Unlike the shared password, access is checked server-side per request.
- See who's viewed it: each link tracks whether it's been opened, how many times, and when — so you know if a client has actually seen the deliverable.
- Internal WWT teammates still bypass the gate by signing in with Slack, and those views aren't counted against a recipient.
Slack sign-in loop fixed across subdomains FIX
- Session now spans both hosts: the
apssession cookie is minted onpublish.*(the Slack OAuth callback) but checked onartifacts.*(where gated artifacts are served). WithCOOKIE_DOMAINnow live, the cookie is scoped to the parent domain so a single sign-in unlocks both — no more redirect loop after authenticating with Slack. - Sign-out clears legacy sessions too: sessions minted before the cross-subdomain cookie shipped were host-only and survived sign-out. Sign-out now clears both the host-only and domain-scoped cookies, so logging out fully ends the session.
Password gate hardened against KV failures FIX
- Retry on write failure: the Redis gate write now retries once (200ms delay) before giving up, handling transient KV blips without failing the publish.
- Hard error on unprotected publish: if a password or expiry was requested but the gate write still failed after retry, the publish now returns an error instead of silently succeeding — the artifact is live but unprotected, so the user is told to retry rather than discovering later that the gate was never applied.
Password gate clarity and publish warnings FIX
- Password field hint: the label now reads "external visitors only · WWT members exempt" so publishers understand that signed-in WWT team members always bypass the gate — the password applies to clients and external visitors only.
- Publish warnings surfaced: if the server returns warnings after a publish (e.g. the gate metadata write to Redis failed), the UI now shows them in red instead of silently discarding them. Previously a failed password-gate write would leave the artifact live but unprotected with no indication to the publisher.
Security, performance, and observability hardening FIX
Several bugs caught by new test coverage, plus backend performance work and improved error visibility.
- Sign-out cookie fix: clearing the session cookie now includes the correct
Domain=attribute whenCOOKIE_DOMAINis set, so cross-subdomain sign-out actually works instead of leaving a stale cookie. - Open-redirect hardening in MCP OAuth:
redirect_uriis now validated to requirehttps://or a loopbackhttp://scheme, preventing a potential redirect to arbitrary URLs. - Manage tab performance: artifact metadata in the Manage tab and the MCP
list_artifactstool now fetches all records in a single Redismgetinstead of N sequential calls — eliminates the N+1 KV pattern under load. - Artifact listing performance:
listUserArtifactsreduced from 1 tree fetch + N sequential blob fetches to 1 tree fetch + all blobs in parallel. - Publish error logging: async publish failures in the Slack
/publishmodal now emit aconsole.errorentry visible in Vercel function logs, not just in the DM to the submitter.
Test suite expanded: 437 → 521 tests INFRA
Added 84 tests across 14 new test files, achieving full coverage of all API handlers and library modules. New coverage includes: the Slack interactive handler (waitUntil async path + files.info fallback), MCP OAuth callback branches, the Manage tab list API, the artifacts-index route, Slack modal helpers, signout cookie domain scoping, and open-redirect guards. All four exported MCP tool functions (publishArtifactTool, listArtifactsTool, deleteArtifactTool, updateGateTool) are now directly tested as pure functions.
Password protection for artifacts SHIPPED
Any artifact can now be password-protected at publish time. Viewers hit a prompt before the page loads — useful for drafts or anything not ready for wide distribution. Available across all four surfaces: web upload, Slack /publish, MCP publish_artifact tool, and the macOS app. Passwords are stored as scrypt hashes; the plain-text value never touches the database.
Manage tab SHIPPED
The web UI at publish.creative-technology.digital now has a Manage tab. See all your published artifacts in one place, update the password or expiry on any of them after the fact, or delete ones you no longer need. Badge icons surface which artifacts are password-protected, expiring, or from a preview deploy.
Expiring artifacts SHIPPED
Set a time-to-live when publishing — 1, 7, or 30 days — and the artifact is automatically taken offline when the window closes. Expiry is also editable from the Manage tab without republishing. The MCP publish_artifact and update_gate tools accept expires_in_days for the same control programmatically.
macOS app replaces the drag-drop droplet SHIPPED
The original Finder droplet was rewritten as a native SwiftUI app. Click the app icon and a small window opens with a file picker, optional slug field, and a Publish button — same flow as the Slack /publish modal. Drop a file on the app icon and the window opens with that file pre-selected. The status line and result panel are in-window now, not as macOS notifications.
- Reinstall:
curl -fsSL https://publish.creative-technology.digital/install.sh | bash - The installer now compiles with
swiftc(Xcode CLT) instead ofosacompile.
Comprehensive test suite + report-only CI INFRA
201 tests across vitest unit, mocked-integration, race, and preview-deploy e2e tiers. Covers slugify / extract-title / render-index / slack-verify (pure logic), identity-store + commit-artifact + slack-auth + github-app-auth + render-markdown (mocked), and all API handlers reachable without stream-based dependencies. GitHub Actions workflow runs on every push (currently report-only).
Visible Docs page + redesigned UI DESIGN
Public /docs page added, covering all four adapters. The publish UI was rebuilt in an editorial-technical aesthetic: slate near-monochrome with a single electric-cyan accent for action, theme-aware (light by default, dark via prefers-color-scheme), Inter + JetBrains Mono via Google Fonts. Replaces the original Slack-purple chrome with WWT Creative Technology framing.
MCP server renamed to wwtd-artifacts SHIPPED
The server identifies itself as wwtd-artifacts in the Claude Integrations panel (was the generic artifact-publisher). Suggested install command updated to claude mcp add wwtd-artifacts --url …/api/mcp. Existing installs continue to work — the local slug is user-chosen.
Slack OAuth fixed: users.identity + users.info FIX
Slack tightened auth.test for identity.basic-scoped user tokens, which broke the identity lookup. Replaced with two-step users.identity → users.info via the bot token, with a fallback to the display name when the bot lookup misses. Restored sign-in across all surfaces.
Several smaller fixes FIX
- /undefined/ slug bug: pre-rename KV records (
githubLogin/githubId) now coalesce tousername/userIdon read. - Defensive slugify on identity: display names with spaces no longer break the URL path (
Scott Cullum→scott-cullum/<slug>). - KV env vars trimmed:
vercel env pulltrailing newlines no longer crash the Upstash client. - Stale "GitHub" wording in
/api/publisherror messages and the MCP auth-required text — replaced with Slack-correct copy.
Slack-identity pivot: no GitHub account needed to publish SHIPPED
All four adapters now authenticate via Slack OAuth (WWT workspace). The service holds a single GitHub App installation that signs commits on your behalf. Teammates no longer need a GitHub account or org membership to publish. Identity fields in KV were renamed (githubLogin → username) with back-compat for pre-pivot records.
Web upload UI (Phase 4) SHIPPED
Browser-based upload form at publish.creative-technology.digital. Sign in with Slack, drag (or pick) an .html or .md file, optional custom slug, click Publish. Result panel shows the live URL with Open and Copy buttons.
Slack /publish slash command (Phase 3) SHIPPED
Type /publish in any channel or DM in the WWT workspace. A file-upload modal opens; submit and the bot DMs you the live URL when the deploy finishes (~30s). No separate sign-in step — your Slack handle is verified by the request signature on every call.
Remote MCP server (Phase 2) SHIPPED
Streamable HTTP MCP transport at /api/mcp exposing a single publish_artifact tool. PKCE OAuth flow through Slack. Works in Claude Code, Claude Desktop, and claude.ai web. RFC 7591 dynamic client registration so Claude Code can self-register.
Hosted publishing service (Phase 0) SHIPPED
Initial scaffold: POST /api/publish backend that owns slug stamping, the index page, the GitHub commit, and deploy polling. Upstash Redis for identity storage. Replaces the per-implementation duplication that lived in the original macOS droplet's publish.sh.