Skip to content

Commit 3a9b847

Browse files
tombeckenhamclaude
andauthored
feat: rewrite as TanStack Start app — a clearer HyperFrames-on-Cloudflare example (#10)
* feat: rewrite as TanStack Start app with TanStack AI generation Full-stack rewrite of the template on TanStack Start (Vite + @cloudflare/vite-plugin), replacing the hand-rolled Worker router and static preview page: - src/server.ts custom entry: Start fetch handler + RenderContainer DO export (wrangler main now points here) - /api/render, /api/preview, /r/<key> ported to Start server routes; asset fetches use the request origin (dev assets binding is same-origin-only) - /api/generate rebuilt on TanStack AI: chat() + @tanstack/ai-openrouter streams the composition to the browser over SSE; BYOK key travels in forwardedProps via a custom useChat fetcher and is never persisted - lint + self-heal moved to a client-driven loop: lintComposition server function (@hyperframes/core/lint) after each assistant turn, fix requests sent as follow-up chat turns (max 2, temp 0.3) - React UI (routes/index.tsx + components) reproduces the original preview page, adds live streaming view of the generated HTML - scripts/build.mjs now runs via npm prepare-assets before vite dev/build instead of wrangler build.command * chore: commit worker-configuration.d.ts per Cloudflare guidance * chore: commit generated routeTree.gen.ts * fix: harden AI endpoints, surface errors honestly, add vitest coverage Review fixes on top of the TanStack Start rewrite: - Gate custom-HTML /api/render behind ENABLE_AI_GEN so public demos can't run arbitrary HTML through the render container; the bundled "click Render" path stays open - Keep the BYOK key in React state only, never sessionStorage — generated compositions execute same-origin in the player iframe and could read origin storage; security note updated to match - Drop empty preview_image_url that breaks the Deploy to Cloudflare button (re-fix of 02b8a69) - Surface server error messages: the useChat fetcher throws the JSON error body instead of the library's bare status line, RenderControls parses the error envelope, and a lint server-fn failure warns instead of faking success - Wrap the R2 put with contextual logging; serve only renders/* keys from /r/$; validate message roles and clamp durationSec - Extract pure encoding helpers (native Uint8Array.toBase64 with a chunked fallback for Node 22) and add 16 vitest tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: log in with OpenRouter via OAuth PKCE instead of pasting a key Adds a "Log in with OpenRouter" button that runs OpenRouter's OAuth PKCE flow entirely client-side (src/lib/openrouter-oauth.ts): redirect to openrouter.ai/auth with an S256 code challenge, then exchange the one-time callback code for a runtime API key scoped to this app. No client secret, no server involvement — the key stays in React memory just like the pasted-key flow. Manual key entry remains as a fallback behind a toggle. The PKCE code verifier is the one sanctioned sessionStorage use: it only exists across the redirect round-trip and is deleted (and ?code stripped from the URL) before any generated composition can run. Includes unit tests for the pure helpers (base64url, RFC 7636 appendix B challenge vector, auth URL construction) and README updates. * Moved exaples to above prompt * docs: credit the spend-capped key, not memory placement, as the real protection Review feedback on #10: memory-only key storage removes the trivial storage read but is not an isolation boundary — same-origin generated code can still reach window.parent and recover the key. Tighten the code comment, UI security note, and README so adopters don't over-trust where the key is stored. * fix: address remaining review findings on #10 — durationSec clamp, OAuth remount safety - Clamp durationSec to 120s. It's only a prompt hint, but an absurd value could steer the model toward a very long composition that inflates render time/cost on the deployer's account. - Memoize the OAuth callback exchange for the page load. The one-time code is consumed synchronously before the async key exchange resolves, so a repeat effect run (React StrictMode double-effect, or an unmount/ remount mid-exchange) previously found the code gone and silently dropped the minted key with no recovery. Repeat calls now await the same promise and receive the key. Corrects the AiPanel comment that claimed the second run was a no-op, and adds a regression test. * fix: commit composition-manifest.json so a clean checkout typechecks Review blocker on #10: 'npm run typecheck' failed from a fresh clone with TS2307 because src/lib/server/assets.ts imports the manifest, which was generated by prepare-assets and gitignored. Commit it — matching routeTree.gen.ts and worker-configuration.d.ts — so bare 'tsc --noEmit' works with no build step. prepare-assets still rewrites it on every dev/build, so composition changes show up as a git diff to commit alongside them. * ci: typecheck, tests, and manifest-freshness check on PRs Review feedback on #10: the repo had no PR checks, so a typecheck that only passed after a build step went unnoticed. Runs from a clean checkout: npm ci, typecheck (no build first — the committed manifest must suffice), vitest, then regenerates the manifest and fails on any diff so composition changes can't land without it. * ci: harden workflow per zizmor — pin actions by SHA, least privilege All --pedantic findings fixed: checkout and setup-node pinned to the commit SHAs of their latest releases (v7.0.1 / v7.0.0), read-only permissions block, persist-credentials off, per-ref concurrency with cancel-in-progress, and a job name. * ci: run on Node 24 (current LTS) --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b2ecc18 commit 3a9b847

37 files changed

Lines changed: 20509 additions & 2471 deletions

‎.github/workflows/ci.yml‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
check:
17+
name: Typecheck, test, manifest freshness
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21+
with:
22+
persist-credentials: false
23+
24+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
25+
with:
26+
node-version: 24
27+
cache: npm
28+
29+
- run: npm ci
30+
31+
# Deliberately from a clean checkout, before any build step — the
32+
# committed composition-manifest.json must be enough for tsc to pass.
33+
- run: npm run typecheck
34+
35+
- run: npm test
36+
37+
# The manifest is committed but regenerated by prepare-assets; fail if
38+
# a composition change landed without its manifest diff.
39+
- run: npm run prepare-assets
40+
- run: git diff --exit-code src/composition-manifest.json

‎.gitignore‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ dist
99
.env
1010
.env.local
1111
.env.*.local
12-
worker-configuration.d.ts
1312

14-
# Build artifacts (regenerated by scripts/build.mjs on wrangler dev/deploy)
15-
src/composition-manifest.json
13+
# Build artifacts (regenerated by scripts/build.mjs before vite dev/build).
14+
# src/composition-manifest.json is committed so a clean checkout typechecks;
15+
# prepare-assets rewrites it, so commit the diff when composition files change.
1616
public/_bundled/
1717
public/_hyperframes/
18+
19+
# TanStack Start generated files
20+
.tanstack
21+
.nitro
22+
.output

‎README.md‎

Lines changed: 72 additions & 48 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)