feat(apex): add marketing worker at agent-paste.sh#1
Conversation
Per ADR 0014, the apex domain serves the marketing surface plus agent-discoverable /llms.txt and /agents.md, and 308-redirects every authenticated product path to app.agent-paste.sh. This worker holds no state, sets no cookies, and has no bindings besides static assets. Wires the worker into deploy-preview.mjs, the PR preview script (with apex_url emitted to the PR comment), and the hosted smoke script so the existing CI/CD pipeline picks it up on the next merge. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Important Review skippedThis PR was authored by the user configured for CodeRabbit reviews. CodeRabbit does not review PRs authored by this user. It's recommended to use a dedicated user account to post CodeRabbit review feedback. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThis PR adds apps/apex: a TypeScript Cloudflare Worker that serves public marketing and agent-discoverable content (/, /llms.txt, /agents.md, /robots.txt, /sitemap.xml), renders a styled homepage, redirects product/auth routes to app.agent-paste.sh with 308s, falls back to static asset serving via an ASSETS binding, includes Vitest integration tests, and integrates apex into preview/production deployment scripts, the PR-preview workflow, and smoke tests. Sequence Diagram: Apex Worker Request HandlingsequenceDiagram
participant Client
participant handleRequest
participant routeApex
participant productRedirect
participant ASSETS
participant Response
Client->>handleRequest: HTTP Request
handleRequest->>routeApex: routeApex(request)
alt routeApex returns Response
routeApex->>Response: HTML/text/xml response (with SECURITY_HEADERS)
else routeApex returns null
routeApex-->>handleRequest: null
handleRequest->>productRedirect: productRedirect(new URL(...))
alt productRedirect returns location
productRedirect-->>handleRequest: redirect URL
handleRequest->>Response: 308 Redirect (Location header)
else no product redirect
alt env.ASSETS is configured
handleRequest->>ASSETS: env.ASSETS.fetch(request)
ASSETS->>Response: asset Response (or 404)
else no ASSETS
handleRequest->>Response: 404 plain text "not_found"
end
end
end
Response->>Client: HTTP response
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/apex/src/routes.ts`:
- Around line 11-16: The SECURITY_HEADERS constant is missing browser hardening
for scripts and framing; update the SECURITY_HEADERS object to include a strict
Content-Security-Policy (content-security-policy) and frame protection header
(x-frame-options). Add a CSP that at minimum restricts scripts to 'self' and
disallows unsafe-inline/unsafe-eval (for example: default-src 'self'; script-src
'self'; object-src 'none'; style-src 'self' 'unsafe-hashes' or trusted CDNs as
needed) and add x-frame-options: DENY (or SAMEORIGIN if embedding is required);
ensure the header names match existing casing and HeadersInit usage so the new
keys are included wherever SECURITY_HEADERS is used.
In `@scripts/smoke-hosted.mjs`:
- Around line 77-98: In smokeApex, add assertions to validate the full
redirect/cookie contract: for non-home endpoints (llms, agents, dashboard)
assert that response.headers.get("set-cookie") is falsy to ensure they never set
cookies; and for the dashboard redirect test (redirect variable) perform the
request with a sample query string (e.g., /dashboard?foo=bar) and assert the
returned Location header preserves that query (location includes "?foo=bar" or
equivalent), while keeping the existing equality check for the host/path; update
the fetch calls for /llms.txt, /agents.md and the /dashboard redirect case in
function smokeApex accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: fd3c71ec-312e-4e40-b761-f38f554d120a
⛔ Files ignored due to path filters (5)
apps/apex/public/favicon.svgis excluded by!**/*.svgapps/apex/public/fonts/HankenGrotesk-Variable.woff2is excluded by!**/*.woff2apps/apex/public/fonts/JetBrainsMono-Medium.woff2is excluded by!**/*.woff2apps/apex/public/fonts/JetBrainsMono-Regular.woff2is excluded by!**/*.woff2pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (19)
.github/workflows/pr-preview.ymlREADME.mdapps/apex/README.mdapps/apex/package.jsonapps/apex/src/agents.tsapps/apex/src/copy.tsapps/apex/src/index.test.tsapps/apex/src/index.tsapps/apex/src/llms.tsapps/apex/src/page.tsapps/apex/src/redirects.tsapps/apex/src/routes.tsapps/apex/src/styles.tsapps/apex/tsconfig.jsonapps/apex/wrangler.jsoncpnpm-workspace.yamlscripts/deploy-pr-preview.mjsscripts/deploy-preview.mjsscripts/smoke-hosted.mjs
Single-column layout: wordmark, headline, lead, terminal transcript showing `npx agent-paste publish` returning a durable URL, primary CTA, and a minimal footer. Removed the showcase/manifest/CTA sections and infrastructure-link footer; kept a subtle /agents.md pointer for crawlers. Transcript now renders origin + id together so the result reads as a URL, matching the lead copy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add Content-Security-Policy and X-Frame-Options: DENY to apex responses to harden the marketing surface. CSP allows 'self' plus 'unsafe-inline' for the inline style and bootstrap script blocks. Expand smoke-hosted apex assertions: no-cookie checks on llms.txt, agents.md, and the dashboard redirect, plus a query-preserving redirect assertion. Addresses CodeRabbit review. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/apex/src/index.test.ts (3)
88-92:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd no-cookie assertion to match redirect test pattern.
The
/dashboardredirect test (line 85) checksset-cookieis null, but this query-preserved redirect test does not. For consistency and given the critical "no cookies" requirement, all redirect tests should verify the header.🛡️ Suggested addition
const response = await get("/login?return_to=%2Fartifacts%2Fart_1"); expect(response.status).toBe(308); expect(response.headers.get("location")).toBe("https://app.agent-paste.sh/login?return_to=%2Fartifacts%2Fart_1"); + expect(response.headers.get("set-cookie")).toBeNull(); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/apex/src/index.test.ts` around lines 88 - 92, The test "preserves query strings on product redirects" fails to assert that no cookies are set; update this test (the it block with description "preserves query strings on product redirects") to include an assertion that response.headers.get("set-cookie") is null (e.g., expect(response.headers.get("set-cookie")).toBeNull()) immediately after obtaining response so it matches the `/dashboard` redirect test pattern and enforces the no-cookie requirement.
139-145:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winExpand blanket no-cookie test to match comprehensive intent.
The test is named "never sets cookies on any apex response" but only checks a subset of paths. Other tests exercise redirects (
/login,/artifacts/*,/r/*), 404 responses, asset delegation, and 405 errors without verifyingset-cookie. Given the critical "no cookies" requirement per ADR 0014, include examples of each response type in this blanket assertion.🛡️ Suggested expansion
it("never sets cookies on any apex response", async () => { - const paths = ["/", "/llms.txt", "/agents.md", "/robots.txt", "/sitemap.xml", "/dashboard"]; + const paths = [ + "/", + "/llms.txt", + "/agents.md", + "/robots.txt", + "/sitemap.xml", + "/dashboard", + "/login", + "/artifacts/art_01HZ8K2X9NPQR3VW7TYBE5MCDF", + "/r/token-abc", + "/no-such-page", + ]; for (const path of paths) { const response = await get(path); expect(response.headers.get("set-cookie"), `cookie on ${path}`).toBeNull(); } });Note: Asset and 405 responses are harder to test in this loop (require custom env/method), but the expanded list covers public routes, all redirect types, and 404.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/apex/src/index.test.ts` around lines 139 - 145, The test "never sets cookies on any apex response" only checks a few paths; update the paths array used in the test (the one iterated with for (const path of paths) and the get(path) call) to include representative examples of all response types mentioned in the suite: public pages ("/", "/llms.txt", "/agents.md", "/robots.txt", "/sitemap.xml", "/dashboard"), redirect endpoints (e.g. "/login", "/artifacts/some-id", "/r/some-short"), a 404 path (e.g. "/non-existent-path"), and an asset/405 example if feasible; ensure each response returned by get(path) is asserted with expect(response.headers.get("set-cookie")).toBeNull() so every response type covered by ADR 0014 is validated.
40-40: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueConsider more robust word-boundary regex (optional).
The current regex
/["\s,]inter["\s,]/requires both leading and trailing boundary characters, which might miss edge cases likefont-family:inter(no space) or "inter" at string edges. A word-boundary regex like/\binter\b/iwould be more comprehensive.However, given this is a defensive check and the style guide is enforced upstream, the current regex likely catches real violations in typical HTML/CSS contexts (e.g.,
"inter",in font stacks).♻️ Alternative regex
- expect(body).not.toMatch(/["\s,]inter["\s,]/); + expect(body).not.toMatch(/\binter\b/i);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/apex/src/index.test.ts` at line 40, The test uses a fragile regex `/["\s,]inter["\s,]/` to assert "inter" isn't present; replace it with a word-boundary, case-insensitive pattern such as `/\binter\b/i` so matches cover edge cases like font-family:inter or "inter" at string boundaries—update the expect assertion that currently calls expect(body).not.toMatch(/["\s,]inter["\s,]/) to use the new `/\binter\b/i` pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/apex/src/page.ts`:
- Around line 36-43: The hero is only rendering the primary CTA; restore the
secondary "View on GitHub" CTA by adding a second anchor after the existing
primary anchor that uses HERO.secondary.href and HERO.secondary.label (escaped
with esc like the primary), e.g. an anchor with the ghost/secondary classes
(e.g. "button button-ghost" or "button button-secondary") and any necessary aria
attributes; locate the hero section where HERO and esc are used (the existing
primary anchor) and mirror that pattern to render the secondary CTA.
---
Outside diff comments:
In `@apps/apex/src/index.test.ts`:
- Around line 88-92: The test "preserves query strings on product redirects"
fails to assert that no cookies are set; update this test (the it block with
description "preserves query strings on product redirects") to include an
assertion that response.headers.get("set-cookie") is null (e.g.,
expect(response.headers.get("set-cookie")).toBeNull()) immediately after
obtaining response so it matches the `/dashboard` redirect test pattern and
enforces the no-cookie requirement.
- Around line 139-145: The test "never sets cookies on any apex response" only
checks a few paths; update the paths array used in the test (the one iterated
with for (const path of paths) and the get(path) call) to include representative
examples of all response types mentioned in the suite: public pages ("/",
"/llms.txt", "/agents.md", "/robots.txt", "/sitemap.xml", "/dashboard"),
redirect endpoints (e.g. "/login", "/artifacts/some-id", "/r/some-short"), a 404
path (e.g. "/non-existent-path"), and an asset/405 example if feasible; ensure
each response returned by get(path) is asserted with
expect(response.headers.get("set-cookie")).toBeNull() so every response type
covered by ADR 0014 is validated.
- Line 40: The test uses a fragile regex `/["\s,]inter["\s,]/` to assert "inter"
isn't present; replace it with a word-boundary, case-insensitive pattern such as
`/\binter\b/i` so matches cover edge cases like font-family:inter or "inter" at
string boundaries—update the expect assertion that currently calls
expect(body).not.toMatch(/["\s,]inter["\s,]/) to use the new `/\binter\b/i`
pattern.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 90388072-3f24-4cf4-9fed-b9e2c2f3add9
📒 Files selected for processing (6)
apps/apex/src/agents.tsapps/apex/src/copy.tsapps/apex/src/index.test.tsapps/apex/src/llms.tsapps/apex/src/page.tsapps/apex/src/styles.ts
| <section class="hero"> | ||
| <h1 class="hero-headline">${esc(HERO.headline)}<span class="hero-headline-stop">.</span></h1> | ||
| <p class="hero-lead">${esc(HERO.lead)}</p> | ||
|
|
||
| <pre class="transcript" aria-label="Example agent-paste session">${renderTranscript(TRANSCRIPT)}</pre> | ||
|
|
||
| <a class="button button-primary button-lg" href="${esc(HERO.primary.href)}">${esc(HERO.primary.label)}</a> | ||
| </section> |
There was a problem hiding this comment.
Restore the required secondary “View on GitHub” hero CTA.
The hero currently renders only the primary action, but the stated PR objective requires both the primary CTA and a ghost “View on GitHub” link.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/apex/src/page.ts` around lines 36 - 43, The hero is only rendering the
primary CTA; restore the secondary "View on GitHub" CTA by adding a second
anchor after the existing primary anchor that uses HERO.secondary.href and
HERO.secondary.label (escaped with esc like the primary), e.g. an anchor with
the ghost/secondary classes (e.g. "button button-ghost" or "button
button-secondary") and any necessary aria attributes; locate the hero section
where HERO and esc are used (the existing primary anchor) and mirror that
pattern to render the secondary CTA.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/apex/src/index.test.ts (1)
34-35:⚠️ Potential issue | 🟠 Major | ⚡ Quick winTest expectation contradicts the required “View on GitHub” CTA.
Line 34 and Line 35 assert the homepage must not include GitHub/text, which conflicts with the PR objective requiring a ghost “View on GitHub” link. This will enforce the wrong behavior.
Suggested test fix
- expect(body).not.toContain("github.com"); - expect(body).not.toContain("View on GitHub"); + expect(body).toContain("github.com"); + expect(body).toContain("View on GitHub");🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/apex/src/index.test.ts` around lines 34 - 35, The two assertions using the variable body (expect(body).not.toContain("github.com") and expect(body).not.toContain("View on GitHub")) contradict the PR requirement to include a ghost "View on GitHub" CTA; update the test in index.test.ts to assert the presence of that CTA instead (e.g., change to expect(body).toContain("View on GitHub") and/or expect(body).toContain("github.com") so the test verifies the link is rendered), and if the CTA uses a specific CSS marker or href pattern, assert that marker/href appears in body to make the check robust.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@apps/apex/src/index.test.ts`:
- Around line 34-35: The two assertions using the variable body
(expect(body).not.toContain("github.com") and expect(body).not.toContain("View
on GitHub")) contradict the PR requirement to include a ghost "View on GitHub"
CTA; update the test in index.test.ts to assert the presence of that CTA instead
(e.g., change to expect(body).toContain("View on GitHub") and/or
expect(body).toContain("github.com") so the test verifies the link is rendered),
and if the CTA uses a specific CSS marker or href pattern, assert that
marker/href appears in body to make the check robust.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e9921a60-2d24-4d2e-9f27-ecf4f03d6d96
📒 Files selected for processing (3)
apps/apex/src/index.test.tsapps/apex/src/routes.tsscripts/smoke-hosted.mjs
Wraps `lefthook install` so it does not fail CI when the platform binary is absent. Also widens supportedArchitectures so pnpm keeps cross-platform optional deps in the lockfile. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Neon project only has `production` and `preview` branches; there is no `main`. PR preview should diverge from `preview`, which is the right staging baseline. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/install-hooks.mjs`:
- Around line 10-12: The warning message inside the failure branch of the
lefthook install check (the if (result.status !== 0) block) is misleading;
update the console.warn in scripts/install-hooks.mjs to clearly state that
installation failed and hooks will not be installed or modified (do not imply
any uninstall occurred). Locate the console.warn call that currently logs
"[prepare] lefthook install failed; skipping (hooks will be uninstalled)." and
replace its message with a concise clarification such as indicating installation
failed and existing hooks remain unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 91c03ed4-b191-4ee7-8e09-717bda84a828
📒 Files selected for processing (3)
package.jsonpnpm-workspace.yamlscripts/install-hooks.mjs
| if (result.status !== 0) { | ||
| console.warn("[prepare] lefthook install failed; skipping (hooks will be uninstalled)."); | ||
| } |
There was a problem hiding this comment.
Clarify the warning message.
The message "hooks will be uninstalled" is misleading—hooks are simply not being installed, not being actively uninstalled. This could confuse developers into thinking previously installed hooks are being removed.
Suggested fix
if (result.status !== 0) {
- console.warn("[prepare] lefthook install failed; skipping (hooks will be uninstalled).");
+ console.warn("[prepare] lefthook install failed; continuing without git hooks.");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (result.status !== 0) { | |
| console.warn("[prepare] lefthook install failed; skipping (hooks will be uninstalled)."); | |
| } | |
| if (result.status !== 0) { | |
| console.warn("[prepare] lefthook install failed; continuing without git hooks."); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install-hooks.mjs` around lines 10 - 12, The warning message inside
the failure branch of the lefthook install check (the if (result.status !== 0)
block) is misleading; update the console.warn in scripts/install-hooks.mjs to
clearly state that installation failed and hooks will not be installed or
modified (do not imply any uninstall occurred). Locate the console.warn call
that currently logs "[prepare] lefthook install failed; skipping (hooks will be
uninstalled)." and replace its message with a concise clarification such as
indicating installation failed and existing hooks remain unchanged.
The Neon primary branch was renamed to `main`, matching the workflow's original intent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pnpm's isolated nodeLinker does not place wrangler on PATH, so direct spawn fails with ENOENT in CI. Routing through `pnpm exec` resolves the binary from the workspace store. Also adds apex to cleanup-pr-preview since the worker now exists. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The lockfile didn't track @cloudflare/workerd-* or lefthook-* platform optional deps, so Linux CI runners couldn't resolve workerd-linux-64 when wrangler loaded miniflare. Declare the full matrix as root optionalDependencies and drop `current` from supportedArchitectures so pnpm includes every variant in the lockfile. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts: # pnpm-workspace.yaml
|
agent-paste PR preview is ready. API: https://agent-paste-api-pr-1.isaac-a46.workers.dev |
|
agent-paste PR preview resources were cleaned up. |
…ting (#9) * fix(api,upload): replay idempotent mutations before rate-limit accounting Adds a SELECT-only peekIdempotentReplay helper to @agent-paste/commands and uses it in both upload mutation routes to short-circuit known-good retries before either rate-limit binding is touched. Hosted smoke now hammers POST /v1/upload-sessions with unique idempotency keys to confirm 429 with the rate_limited_actor envelope on preview targets. * docs(status): close out item #1 and renumber backlog PR #9 finished the rate-limit work, so mark ADR 0039/0064 done, drop the backlog entry, and shift items #2-#11 down one slot. Updates the phase partition note to match the new numbering. * debug: dump content fetch context on failure Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: drop temporary debug print from smoke-hosted Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(smoke): run rate-limit probe after purge tests; reconcile backlog The new assertActorRateLimitFires helper exhausts the actor's 60/min budget by hammering an upload mutation. The next assertion, assertBytesPurgedAfterExpiry, republishes through the same user API key and was hitting rate_limited_actor at the publish step. Reorder so the rate-limit probe runs last; bytes-after-delete and -after-expiry now run on a fresh budget. Also reconcile docs/ops/project-status.md: PR #6 (CSP) and PR #8 (bytes-after-delete/expiry) closed two backlog items without updating this doc. Moved both into Recently Completed and renumbered. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(smoke): fire rate-limit probe in parallel waves Sequential 120 attempts at ~2/sec spread across multiple Cloudflare worker isolates means no single isolate ever sees the 60-in-60s threshold. Replace the serial loop with four parallel waves of 80 requests each. The burst forces a single isolate's counter past the limit before the trailing window slides. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* chore(branches): close obsolete t3code branches Backlog item #7 in docs/ops/project-status.md asked to review and merge t3code/7bcd4587 and t3code/5b6355f9. Both branches no longer exist on origin (verified via git fetch and gh api repos/:owner/:repo/branches); the underlying commits are unreachable. The only Apex/front-end work that ever landed from the t3code family was PR #1 (the marketing worker scaffold). Remove the stale references from project-status.md and move the item to Recently Completed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs: clarify former backlog item #7 reference --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Snapshot now points at ad85175 (#46); record the operator lockdown (#45) and web loader-wiring (#46) merges in Recently Completed; strike backlog #4 as done; update the Phase 3 (~55%), web.md, ADR 0055 rows. Remaining Phase 3 code work is CLI login (#5) and smoke:web (#6), both gated on the WorkOS/Access click-ops in backlog item #1. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…loy (#50) Record that agent-paste-web-preview is live at app.preview.agent-paste.sh with a verified WorkOS login flow, the GET /v1/web/admin/lockdowns list endpoint (#48) shipped, and the CLOUDFLARE_ENV build/deploy mechanism (#49). Update the snapshot main pointer, backlog items #1 and #6, the web.md / ADR 0033 / 0040 / 0059 rows, and the lockdown deferred-follow-up note. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…st-login provisioning, /v tests Cursor Bugbot (blocking, Medium) + first-pass review findings: - **Revision-link retry duplicates (Bugbot).** createAndMintAccessLink's salted-key retry ran for both make_public and create_revision_link. Share links dedupe on create (findActiveShareLink), so the retry is idempotent; revision links do NOT dedupe, so a create-ok/mint-fail retry inserted a second revision link for the same revision. Scope the retry to `type:'share'` only; revision links return the original failure. Test asserts create_revision_link does not retry. - **/v first-login provisioning (finding #1).** /v is a standalone handoff route, not under _authed, so it never provisioned the workspace member. A brand-new user signing in directly via a private link had a valid token but no member row, so the owner-scoped artifact read missed and showed the empty state. Provision via webSessionQuery in the loader before the artifact read. - **/v route tests (finding #4).** New apps/web/test/v-route.test.tsx pins the gate: unauthed → redirect + no artifact read; authed → provision-then-read in order; chromeless viewer renders; redirect path works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ic, unified scopes (#525) * feat(publish): make publish content-only and private-first; one private_url, explicit make_public Publish and add_revision are now content-only on every surface (CLI, MCP, REST). They take no visibility input and return exactly one link, `private_url`, the login-walled clean viewer at `/v/<artifactId>` for a Workspace Member. There is no `share` input and no `shared` output anywhere; the misleading viewer_url/ shared flip is gone. Going public is a separate, explicit, revocable step: `make_public` (MCP) / `agent-paste make-public` (CLI), with `revoke_access_link` as the go-private verb. The `/artifacts/<id>` console is never returned to an agent or user. Private Link is a plain `_authed` route, not an access_links row: nothing to sign, nothing to leak, nothing to revoke. The public `/al/<publicId>` resolve gate is untouched, so existing Share Links keep resolving. A new shared ArtifactLiveViewer backs both the console and the clean `/v` viewer. make_public now reuses an Artifact's one active (non-revoked, unexpired) Share Link instead of minting a duplicate, so an Artifact has at most one live Share Link — making the "mints or reuses the one Share Link" contract true in code, not just in copy. revision links are never deduped. Unify the scope vocabulary: one set `read`/`publish`/`admin` shared verbatim by API and MCP. The old MCP-only `write`/`share` names are gone, and the mcpScopesToApiScopes/apiScopesToMcpScopes translation layer is deleted (MCP scopes are the member's stored API scopes verbatim, per ADR 0079). The four agent access-link routes require `publish` (managing your own Artifact's public access is content authority); `admin` is dashboard-only and no MCP tool needs it. web.accessLinks.lockdown.* stay `admin`. Supersedes ADR 0085 (publish-returns-one-viewer-url, which made the link flip and surfaced the lie); amends ADR 0084 (output shape) and ADR 0079 (scope vocabulary unified). Adds ADR 0086. Specs are updated as the source of truth (api/web/cli/ephemeral-publish, CONTEXT vocab, docs/mcp.md, apex agent docs). OpenAPI goldens regenerated. Early-alpha break: CLI and MCP ship in lockstep; the deployed MCP server `instructions` text still teaches `share:true` and updates on deploy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(web): clean viewer header links to /dashboard, not the banned /artifacts console The /v clean viewer's own header Wordmark linked to /artifacts/$artifactId — the dashboard console page that is explicitly never to be handed to a user. Point it at /dashboard (brand home) instead. The console stays reachable only from the dashboard's own artifact list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(mcp): make_public self-heals when a replayed create points mint at a revoked link If a client reuses the same idempotency key across a revoke (make_public → revoke_access_link → make_public with the same key), the create step replays the now-revoked link from its idempotency record and mint fails on the dead link. Real MCP clients use monotonic JSON-RPC ids and the CLI uses random UUIDs so neither collides, but the failure mode is sharp: a revoke must never lock you out of going public again. createAndMintAccessLink now retries once on a salted idempotency key when the first create→mint fails. The salted create runs the command fresh, so for a share link it reuses the artifact's active link (the DB-layer findActiveShareLink path) or mints a new one. The happy path never retries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: state the Private Link permanence guarantee explicitly We kept tripping over what the link guarantees, so write it down where the specs are the source of truth. The Private Link (`/v/<artifactId>`) is permanent and stable: derived only from the Artifact id with no token/signature/expiry, and add_revision republishes into the same id, so it never changes across revisions and live-updates to the latest Published Revision. It is always private (member only; publish never grants public access) and stops resolving only when the Artifact is deleted or swept by Auto Deletion — the expires_at in the publish response is the Artifact's content lifetime, not a link expiry. Going public is the separate, revocable Share Link. Documented in CONTEXT.md (Private Link + Share Link vocab), docs/specs/api.md (publish behavior), and ADR 0086 (decision trail). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(mcp,web): address Cursor review — revision-link retry dup, /v first-login provisioning, /v tests Cursor Bugbot (blocking, Medium) + first-pass review findings: - **Revision-link retry duplicates (Bugbot).** createAndMintAccessLink's salted-key retry ran for both make_public and create_revision_link. Share links dedupe on create (findActiveShareLink), so the retry is idempotent; revision links do NOT dedupe, so a create-ok/mint-fail retry inserted a second revision link for the same revision. Scope the retry to `type:'share'` only; revision links return the original failure. Test asserts create_revision_link does not retry. - **/v first-login provisioning (finding #1).** /v is a standalone handoff route, not under _authed, so it never provisioned the workspace member. A brand-new user signing in directly via a private link had a valid token but no member row, so the owner-scoped artifact read missed and showed the empty state. Provision via webSessionQuery in the loader before the artifact read. - **/v route tests (finding #4).** New apps/web/test/v-route.test.tsx pins the gate: unauthed → redirect + no artifact read; authed → provision-then-read in order; chromeless viewer renders; redirect path works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
apps/apexCloudflare Worker serving the marketing surface atagent-paste.shplus the agent-discoverable/llms.txtand/agents.mdfiles. Per ADR 0014, every authenticated product path (/dashboard,/artifacts,/keys,/audit,/settings,/admin,/al/,/r/,/login,/logout,/auth/) is 308-redirected toapp.agent-paste.shwith query strings preserved. No cookies, no bindings besides static assets, no DB.docs/specs/style-guide.md("Quiet Confidence"): Hanken Grotesk + JetBrains Mono self-hosted, emerald-teal accent, hero with primaryGet an API keyCTA, the canonicalnpx agent-paste publish ./reportblock with copy affordance, and the §5.11 identifier component (mono, tinted, click-to-copy with 700ms accent flash) rendering the sample Artifact ID. Honorsprefers-color-schemeandprefers-reduced-motion.scripts/deploy-preview.mjsdeploys it in production/preview order,scripts/deploy-pr-preview.mjsbuilds anapex.jsonconfig with the assets binding and deploys toagent-paste-apex-pr-{N}.workers.dev, the PR-comment step now includes the apex URL, andscripts/smoke-hosted.mjsasserts/returns 200 HTML,/llms.txttext/plain,/dashboard308 to the app domain, and noset-cookie.Test plan
pnpm --filter @agent-paste/apex typecheckclean.pnpm --filter @agent-paste/apex test— 14/14 vitest tests pass (HTML render, content types, 308 targets, query preservation, no-cookie on every response, assets fallthrough, 405 on non-GET, §11 banned-token absence).pnpm --filter @agent-paste/apex lintclean.pnpm verifyat the root — 45/45 turbo tasks pass.pr-preview.ymlposts anapex_urland the rendered apex is visible at that URL.https://agent-paste.sh/returns the rendered page andcurl -sI https://agent-paste.sh/dashboardreturns308 -> https://app.agent-paste.sh/dashboard.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores