Skip to content

feat(package-apps): per-user subdomains on the package-app domain - #1395

Merged
kentcdodds merged 7 commits into
mainfrom
cursor/per-user-package-app-subdomains-1636
Aug 11, 2026
Merged

kentcdodds merged 7 commits into
mainfrom
cursor/per-user-package-app-subdomains-1636

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Aug 11, 2026 •

Copy link
Copy Markdown
Owner

Intent

One user's package apps must never share a browser origin with another user's: shared-origin cookies (kody_pkg_session on kodyapps.dev) let package A reach package B's session-authorized endpoints from the browser, the exact gap called out as "out of scope" in docs/contributing/security.md. This PR closes it by giving every user their own subdomain on the package-app domain and hardening the session cookie against cross-subdomain tossing.

Summary

  • Hosted package apps move from kodyapps.dev/@{username}/packages/{kodyId} to https://{username}.kodyapps.dev/packages/{kodyId} — the username lives in the hostname, so the /@{username} path prefix disappears on the package-app domain (routing simplification).
  • The bare package-app origin serves no package code: / redirects to the app origin, legacy path URLs redirect (302/307) to the owning user's subdomain, everything else fails closed — including hostnames under the domain that are not a valid username label.
  • The package-app session cookie becomes __Host-kody_pkg_session on secure requests: browsers refuse any Domain=-carrying variant under that name, so a sibling subdomain cannot toss or shadow it (defense that works before any Public Suffix List entry). Plain-HTTP local dev falls back to the unprefixed name. Serving requires session username == subdomain label == owner (fixation defense).
  • Usernames get two-tier validation: new/changed usernames must be strict DNS labels (no underscores; generated usernames map _ to -), while recognition of stored usernames stays lenient so existing underscore accounts keep display names, public lookup, and inbound email routing (Bugbot catch). Legacy-username owners get a 409 rename prompt at the package-app entry instead of a redirect to an unservable hostname, and hosted-URL emission falls back to the path-based shape for them.
  • Deploy tooling publishes a wildcard zone route (*.kodyapps.dev/*) alongside the apex custom domain, and production-resources ensure idempotently provisions the proxied wildcard AAAA 100:: DNS record (zone routes do not create DNS).
  • Untrusted markdown link safety also refuses /packages/... mount paths on every host, matching the existing /@... rule.
  • hostedUrl emission (search, publish, package_app_fetch, packageContext) and search identity parsing understand the subdomain shape; inline (non-production) serving keeps the path-based mount.
  • Decision record docs/contributing/decisions/0017-per-user-package-app-subdomains.md; security/architecture/authoring docs rewritten for the new model.

Operational follow-ups (not in this PR): the deploy token needs DNS:Edit on the kodyapps.dev zone before the first deploy; submitting kodyapps.dev to the Public Suffix List is documented defense-in-depth.

Testing

  • npm run validate — green (format, lint, typecheck, 2043 node+workers unit tests across 603 files, Playwright E2E, MCP E2E, backup/status builds, primitives, migrations, deploy-guardrails, docs checks). One unrelated flaky passkeys E2E passed on retry.
  • New/updated coverage: end-to-end workers test of the app-origin → subdomain handoff (cookie flags incl. __Host-/no Domain, replay refusal, cross-user subdomain 404, invalid-label fail-closed, legacy apex redirect, apex/subdomain first-party 404s, legacy-underscore-owner 409); host classification unit tests; subdomain search-identity parsing; two-tier username validation; wildcard route generation and idempotent DNS ensure; markdown /packages/ link refusal.

System changes

System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ 4db2ab37 · Head: bebcc4b1

Classification: extends — the package-app origin-isolation contract changes shape (per-user subdomains, __Host- cookie, new mount path); no new primitives.

Primitives touched

Primitive Group Impact
package-apps assistant extends — per-user subdomain mount /packages/{kodyId}, new packageContext paths
package-runtime runtime extends — PackageAppPath.mount, subdomain path parsing
app-ui surfaces extends — handoff redirects to per-user subdomain; __Host- session cookie; markdown link rule; two-tier DNS-label usernames
mcp-server surfaces composes — hostedUrl emission and search identity accept the subdomain shape

System map

The app origin authenticates the owner and hands off to the owner's package-app subdomain, which exchanges the token for a host-only __Host- cookie before serving package code.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	appUi["app-ui<br/>Browser app"]:::extended
	packageApps["package-apps<br/>Package apps"]:::extended
	packageRuntime["package-runtime<br/>Package runtime"]:::extended
	mcpServer["mcp-server<br/>MCP endpoint"]:::touched
	appUi -->|"302 + handoff token to {username}.kodyapps.dev"| packageApps
	packageApps -->|"__Host-kody_pkg_session, username must match subdomain"| packageRuntime
	mcpServer -->|"hostedUrl / search identity use subdomain URLs"| packageApps
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

before: https://kodyapps.dev/@alice/packages/notes/report   (one shared origin for all users)
after:  https://alice.kodyapps.dev/packages/notes/report    (origin per user; apex only redirects)

before: Set-Cookie: kody_pkg_session=...        (host-only by convention)
after:  Set-Cookie: __Host-kody_pkg_session=... (host-only enforced by the browser)

routes before: kodyapps.dev (custom domain)
routes after:  kodyapps.dev (custom domain) + *.kodyapps.dev/* (zone route, wildcard AAAA 100:: proxied)

Invariants

Strengthens per-user isolation: browser origin state (cookies, storage, document) is now partitioned per user on the package-app domain, and serving still requires session-username == subdomain == owner. Same-user package↔package origin sharing remains a documented, deliberately deferred residual (decision 0017).

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Production package apps now use dedicated per-user subdomain URLs.
    • Legacy and inline links redirect to canonical hosted URLs.
    • Search recognizes and validates hosted package-app URLs.
    • Production infrastructure supports wildcard routing and DNS.
  • Security

    • Improved session handoff and secure cookie protections.
    • Unsafe package links are blocked across hosted and inline paths.
    • Routing validates domains, ownership, and usernames.
  • Bug Fixes

    • Redirects, sessions, paths, and query strings now behave consistently.
  • Documentation

    • Updated setup, security, routing, authoring, search, and package guidance.
  • Validation

    • New usernames now allow only letters, numbers, and hyphens.

Every user's hosted package apps move from the shared package-app origin
(kodyapps.dev/@user/packages/id) to that user's own subdomain
(user.kodyapps.dev/packages/id), so browser state (cookies, storage,
document access) never crosses accounts. The bare package-app origin now
only redirects: legacy path URLs to the owning subdomain, the root to the
app origin; everything else fails closed, including hostnames under the
domain that are not a valid username label.

The package-app session cookie gains the __Host- prefix on secure
requests, so browsers refuse any Domain-wide variant a sibling subdomain
could toss, independent of a future Public Suffix List entry. Untrusted
markdown link safety now also refuses /packages/... mount paths on every
host, matching the existing /@... rule.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d48ec5a5-2fdb-4125-b7b9-6c440778eaa1

📥 Commits

Reviewing files that changed from the base of the PR and between 4ebdb63 and 8780b9c.

📒 Files selected for processing (3)
  • packages/worker/src/identity/platform-account-creation.ts
  • tools/ci/resource-utils.node.test.ts
  • tools/ci/resource-utils.ts
📝 Walkthrough

Walkthrough

The PR changes production package apps from shared-origin paths to per-user subdomains. It adds host-aware routing, host-only sessions, canonical URL builders, DNS-safe username validation, wildcard DNS provisioning, link-safety checks, and updated documentation.

Changes

Per-user package-app hosting

Layer / File(s) Summary
URL and runtime contracts
packages/shared/src/public-urls.ts, packages/worker/src/package-runtime/*, packages/worker/src/mcp/capabilities/packages/package-app-fetch.ts
Adds mount-aware paths, subdomain URL builders, hosted URL resolution, and explicit runtime path types.
Host routing and package sessions
packages/worker/src/app-base-url.ts, packages/worker/src/app/package-app-origin.ts, packages/worker/src/app/package-app-session.ts, packages/worker/src/identity/*, packages/worker/client/routes/*
Classifies package-app hosts, redirects legacy and app-origin requests, uses host-only __Host- cookies over HTTPS, and disallows underscores in new usernames.
URL consumers and link safety
packages/worker/src/mcp/tools/*, packages/worker/src/mcp/capabilities/packages/*, packages/worker/client/markdown-view.*
Search, publishing, and package fetching use canonical subdomain URLs. Markdown filtering rejects package-app mount paths, including encoded variants.
Production routes and wildcard DNS
tools/ci/resource-utils.ts, tools/ci/production-resources.ts, tools/ci/*.test.ts
Adds registrable-zone validation, apex and wildcard routes, Cloudflare wildcard DNS provisioning, and tests.
Architecture and setup documentation
docs/contributing/*, docs/guides/package-authoring.md, docs/use/*
Documents routing, handoff credentials, cookie behavior, URL forms, security controls, DNS setup, and package authoring behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant AppOrigin
  participant PackageAppOrigin
  participant PackageAppSession
  participant PackageRuntime
  App->>AppOrigin: request package app
  AppOrigin->>PackageAppOrigin: redirect to owner subdomain
  PackageAppOrigin->>PackageAppSession: exchange handoff token
  PackageAppSession->>PackageAppOrigin: set host-only package session
  PackageAppOrigin->>PackageRuntime: serve owner-scoped package path
Loading

Possibly related PRs

Suggested reviewers: kody-bot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: per-user subdomains for package apps.
Description check ✅ Passed The description includes the required Intent, Summary, Testing, and System changes sections with detailed, relevant information.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/per-user-package-app-subdomains-1636

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

cursoragent and others added 3 commits August 11, 2026 21:57
Every user now owns {username}.<package-app domain>, so usernames must be
valid DNS labels. New and changed usernames reject underscores; generated
usernames (email local parts, provider handles) map underscores to
hyphens. Client-side input patterns and the requirements copy match.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
The generated production wrangler config publishes a zone route
(*.kodyapps.dev/*) alongside the apex custom domain — Cloudflare custom
domains cannot be wildcards. Zone routes do not create DNS records, so
production-resources ensure now idempotently provisions a proxied
wildcard AAAA 100:: record in the package-app zone, failing with an
actionable message when the zone is missing or the record conflicts.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Rewrites the origin-isolation section of security.md around the per-user
subdomain model (three controls, fixation defense, PSL follow-up, and the
deliberately deferred same-owner package isolation), updates the handoff
and request-lifecycle architecture docs, env var semantics, authoring and
usage docs for the new mount contract, and records decision 0017.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds marked this pull request as ready for review August 11, 2026 22:08
@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1395.kody-a99.workers.dev

Worker: kody-pr-1395
D1: kody-pr-1395-db
KV: kody-pr-1395-oauth-kv

Mocks:

Comment thread packages/worker/src/identity/username.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
tools/ci/resource-utils.node.test.ts (2)

928-946: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert the DNS list request as well.

The test verifies call 1 (zone lookup) and call 3 (record creation). It does not verify call 2. Call 2 carries the name and type=AAAA filters that make the existence check correct. If that query loses its filters, the test still passes and the helper can create a duplicate record.

♻️ Proposed addition
 	expect(fetcher).toHaveBeenNthCalledWith(
 		1,
 		'https://api.cloudflare.com/client/v4/zones?name=kodyapps.dev&account.id=account-1&status=active',
 		expect.objectContaining({ method: 'GET' }),
 	)
+	expect(fetcher).toHaveBeenNthCalledWith(
+		2,
+		'https://api.cloudflare.com/client/v4/zones/zone-kodyapps/dns_records?name=*.kodyapps.dev&type=AAAA',
+		expect.objectContaining({ method: 'GET' }),
+	)
 	expect(fetcher).toHaveBeenNthCalledWith(

Note: the helper applies encodeURIComponent to the record name, so adjust the expected string to the encoded form.

🤖 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 `@tools/ci/resource-utils.node.test.ts` around lines 928 - 946, Extend the test
around the existing fetcher assertions to verify call 2, the DNS record list
request, including the encoded wildcard record name and the AAAA type filter in
its URL. Keep the existing call 1 zone lookup and call 3 record-creation
assertions unchanged.

143-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for a package-app host that is not the registrable zone.

Both route assertions derive the expected zone_name from the package-app hostname itself. This equality only holds when PACKAGE_APP_BASE_URL is already a registrable domain. The production code sets zone_name from readPackageAppZoneName(...), which returns the registrable domain. A fixture such as apps.kodyapps.dev would produce pattern: '*.apps.kodyapps.dev/*' with zone_name: 'kodyapps.dev', and that path is currently untested. The same gap exists in the legacy-host assertions at Lines 318-339.

Add one case with a subdomain package-app host to pin the derivation.

🤖 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 `@tools/ci/resource-utils.node.test.ts` around lines 143 - 153, Add a test case
in the resource configuration coverage using a subdomain package-app base URL
such as apps.kodyapps.dev, and assert that wildcard route patterns use that
hostname while zone_name uses the registrable domain kodyapps.dev. Apply the
same fixture and expectations to the legacy-host assertions near the existing
route checks, preserving current expectations for registrable-domain hosts.
tools/ci/resource-utils.ts (1)

383-467: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Collapse the duplicated Cloudflare client into the existing one.

cloudflareRootApiRequestOnce and cloudflareRootApiRequest repeat cloudflareApiRequestOnce and cloudflareApiRequest (Lines 360-381) almost line for line. The retry loop at Lines 448-467 differs from Lines 360-381 only in the callee name. The single functional difference is the missing /accounts/{accountId} path prefix.

Two implementations of the same retry, timeout, and envelope-validation logic will drift. A future fix to one loop will not reach the other.

Make accountId optional on the existing input type and prefix the path only when it is present. Then delete the root variants and the duplicated type.

🤖 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 `@tools/ci/resource-utils.ts` around lines 383 - 467, The existing Cloudflare
client should handle both account-scoped and root API requests instead of
maintaining duplicate implementations. Add optional accountId support to the
existing Cloudflare request input, prefix the request pathname with
/accounts/{accountId} only when provided, then remove
CloudflareRootApiRequestInput, cloudflareRootApiRequestOnce, and
cloudflareRootApiRequest while updating their callers to use
cloudflareApiRequest.
🤖 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 `@docs/contributing/environment-variables.md`:
- Around line 117-121: Update the PACKAGE_APP_BASE_URL documentation to describe
the per-user route as a wildcard zone route using the pattern *.<apex-host>/*,
while retaining the apex custom_domain route. Clarify that production CI
separately provisions wildcard DNS because zone routes do not create DNS
records.

In `@docs/contributing/security.md`:
- Around line 176-178: Update the redirect-cycle description in the security
documentation to state that a per-user package-app subdomain redirects `/` to
the app origin, while redirects within the same subdomain occur only when
removing a handoff token. Remove the conflicting claim that subdomains redirect
only within themselves.
- Around line 123-126: Update the package-app isolation design to reject
cross-host mutating requests before claiming isolation is complete, using Origin
or Sec-Fetch-Site validation, or PSL separation. In
docs/contributing/security.md:123-126, :150-159, and :228-234;
docs/contributing/architecture/authentication.md:408-419; and
docs/contributing/decisions/0017-per-user-package-app-subdomains.md:63-65,
revise the isolation claims to describe this mitigation and ensure they state
the protection is active before asserting sibling subdomains are isolated.

In `@packages/worker/client/routes/account.tsx`:
- Around line 738-739: Update both username input patterns in
packages/worker/client/routes/account.tsx (lines 738-739) and
packages/worker/client/routes/login.tsx (lines 714-715) to use lowercase-only
character classes, replacing uppercase-inclusive ranges with a-z in both classes
at each site. Keep the existing length and start/end constraints unchanged.

In `@packages/worker/src/app-base-url.ts`:
- Around line 97-104: Update parsePackageAppRequestHost to reject package-app
hostnames ending with a trailing DNS dot, including both apex and user-subdomain
forms, by classifying them as unrecognized-subdomain rather than allowing host
isolation to fall through to first-party routing. Add regression coverage for
trailing-dot apex and user-subdomain inputs while preserving existing
exact-origin and valid-subdomain behavior.

In `@tools/ci/resource-utils.ts`:
- Around line 568-592: The wildcard DNS lookup in tools/ci/resource-utils.ts
lines 568-592 must query all record types by removing the type=AAAA filter from
the pathname, allowing the conflicting filter to detect A and CNAME records.
Update tools/ci/resource-utils.node.test.ts lines 928-946 to assert the second
fetcher call uses the corrected URL and add coverage for a CNAME at
*.kodyapps.dev that exercises the conflict branch.

---

Nitpick comments:
In `@tools/ci/resource-utils.node.test.ts`:
- Around line 928-946: Extend the test around the existing fetcher assertions to
verify call 2, the DNS record list request, including the encoded wildcard
record name and the AAAA type filter in its URL. Keep the existing call 1 zone
lookup and call 3 record-creation assertions unchanged.
- Around line 143-153: Add a test case in the resource configuration coverage
using a subdomain package-app base URL such as apps.kodyapps.dev, and assert
that wildcard route patterns use that hostname while zone_name uses the
registrable domain kodyapps.dev. Apply the same fixture and expectations to the
legacy-host assertions near the existing route checks, preserving current
expectations for registrable-domain hosts.

In `@tools/ci/resource-utils.ts`:
- Around line 383-467: The existing Cloudflare client should handle both
account-scoped and root API requests instead of maintaining duplicate
implementations. Add optional accountId support to the existing Cloudflare
request input, prefix the request pathname with /accounts/{accountId} only when
provided, then remove CloudflareRootApiRequestInput,
cloudflareRootApiRequestOnce, and cloudflareRootApiRequest while updating their
callers to use cloudflareApiRequest.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a3ed66fb-8f67-4a5a-b034-4a5ba7816993

📥 Commits

Reviewing files that changed from the base of the PR and between 43275c5 and c79c255.

📒 Files selected for processing (43)
  • docs/contributing/architecture/authentication.md
  • docs/contributing/architecture/request-lifecycle.md
  • docs/contributing/decisions/0017-per-user-package-app-subdomains.md
  • docs/contributing/decisions/index.md
  • docs/contributing/environment-variables.md
  • docs/contributing/security.md
  • docs/contributing/setup-manifest.md
  • docs/guides/package-authoring.md
  • docs/use/packages.md
  • docs/use/search.md
  • packages/shared/src/public-urls.ts
  • packages/worker/client/markdown-view.node.test.ts
  • packages/worker/client/markdown-view.tsx
  • packages/worker/client/routes/account.tsx
  • packages/worker/client/routes/login.tsx
  • packages/worker/src/app-base-url.node.test.ts
  • packages/worker/src/app-base-url.ts
  • packages/worker/src/app/handlers/account-profile.node.test.ts
  • packages/worker/src/app/handlers/auth-handler.node.test.ts
  • packages/worker/src/app/handlers/package-app.ts
  • packages/worker/src/app/package-app-handoff.node.test.ts
  • packages/worker/src/app/package-app-origin.ts
  • packages/worker/src/app/package-app-origin.workers.test.ts
  • packages/worker/src/app/package-app-session.ts
  • packages/worker/src/identity/generated-username.ts
  • packages/worker/src/identity/username.node.test.ts
  • packages/worker/src/identity/username.ts
  • packages/worker/src/mcp/capabilities/packages/package-app-fetch.node.test.ts
  • packages/worker/src/mcp/capabilities/packages/package-app-fetch.ts
  • packages/worker/src/mcp/capabilities/packages/publish-external-push.node.test.ts
  • packages/worker/src/mcp/capabilities/packages/publish-external-push.ts
  • packages/worker/src/mcp/tools/package-search-identity.node.test.ts
  • packages/worker/src/mcp/tools/package-search-identity.ts
  • packages/worker/src/mcp/tools/search-detail.node.test.ts
  • packages/worker/src/mcp/tools/search-detail.ts
  • packages/worker/src/mcp/tools/search-entity-plugins/package.ts
  • packages/worker/src/mcp/tools/search-format-helpers.ts
  • packages/worker/src/package-runtime/package-app-serve.ts
  • packages/worker/src/package-runtime/package-app-synthetic.ts
  • packages/worker/src/package-runtime/package-app.ts
  • tools/ci/production-resources.ts
  • tools/ci/resource-utils.node.test.ts
  • tools/ci/resource-utils.ts

Comment thread docs/contributing/environment-variables.md Outdated
Comment thread docs/contributing/security.md
Comment thread docs/contributing/security.md
Comment on lines +738 to +739
pattern="[A-Za-z0-9][A-Za-z0-9-]{1,30}[A-Za-z0-9]"
title="Use 3 to 32 letters, numbers, and hyphens. Start and end with a letter or number."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match the lowercase-only username contract.

Both client patterns accept uppercase letters. getUsernameFormatValidationError rejects them. Browser validation passes, then the server rejects the submitted username.

  • packages/worker/client/routes/account.tsx#L738-L739: change both character classes to a-z.
  • packages/worker/client/routes/login.tsx#L714-L715: change both character classes to a-z.
📍 Affects 2 files
  • packages/worker/client/routes/account.tsx#L738-L739 (this comment)
  • packages/worker/client/routes/login.tsx#L714-L715
🤖 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 `@packages/worker/client/routes/account.tsx` around lines 738 - 739, Update
both username input patterns in packages/worker/client/routes/account.tsx (lines
738-739) and packages/worker/client/routes/login.tsx (lines 714-715) to use
lowercase-only character classes, replacing uppercase-inclusive ranges with a-z
in both classes at each site. Keep the existing length and start/end constraints
unchanged.

Comment thread packages/worker/src/app-base-url.ts
Comment thread tools/ci/resource-utils.ts Outdated
…ounts

Bugbot caught that the strict DNS-label pattern was also the recognition
gate for stored usernames, so existing underscore accounts would have
lost display names, public user lookup, and inbound email routing — not
just hosted subdomains. Recognition (getUsernameFormatValidationError)
stays lenient for the legacy shape; the strict DNS-label check applies
to new/changed usernames, subdomain host parsing, and subdomain URL
emission (which now falls back to the path-based shape for legacy
names). The app-origin package-app entry answers legacy-username owners
with a 409 rename prompt instead of redirecting to an unservable
hostname. Also applies formatter fixes the earlier commits missed.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bebcc4b. Configure here.

Comment thread packages/worker/src/identity/username.ts
- Trailing-dot package-app hosts (kodyapps.dev.) now fail closed as
  unrecognized instead of falling through to first-party routing —
  URL.hostname preserves the trailing DNS dot.
- Mutating requests on a user subdomain require any Origin header to
  match the subdomain: siblings stay same-site until the PSL entry, so a
  SameSite=Lax cookie would attach to a cross-subdomain mutation from a
  browser holding sessions for two accounts. CORS blocks the read, not
  the side effect; now the request is rejected before package code runs.
- The wildcard DNS conflict check lists all record types at the wildcard
  name, so a conflicting A/CNAME produces the actionable error instead
  of an opaque create failure; tests cover the list URL and the CNAME
  conflict branch.
- Docs: the per-user route is a wildcard zone route (not a custom
  domain); the subdomain root redirect is named in the redirect-cycle
  invariant; the same-site mutation defense and its residual (GET until
  PSL) are documented in security.md, authentication.md, and 0017.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (1)
tools/ci/resource-utils.ts (1)

383-469: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Restrict retries for the wildcard DNS POST.

cloudflareRootApiRequest retries POST after timeouts, connection errors, and 5xx responses. If Cloudflare creates the record before the response is lost, the retry repeats the create. Cloudflare may return error 81057, which can make provisioning fail after the record exists. Restrict automatic retries to idempotent methods, or reconcile the record before retrying an ambiguous POST.

🤖 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 `@tools/ci/resource-utils.ts` around lines 383 - 469, Update
cloudflareRootApiRequest to avoid automatic retries for POST requests,
especially the wildcard DNS create operation, while preserving retries for
idempotent methods and existing retryable error handling. Use input.method to
gate the retry path before logging and waiting, so an ambiguous POST is
propagated immediately rather than repeated.
🤖 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 `@tools/ci/resource-utils.ts`:
- Around line 570-578: Update the wildcard DNS record handling around
isPackageAppWildcardDnsRecord so conflicting records are computed before
selecting or accepting existing. Exclude only records accepted by
isPackageAppWildcardDnsRecord from conflicting, and ensure any A, CNAME, or
non-required AAAA record triggers the conflict path even when a valid proxied
AAAA record is also present. Add coverage for a response containing both the
accepted and conflicting record types.

---

Outside diff comments:
In `@tools/ci/resource-utils.ts`:
- Around line 383-469: Update cloudflareRootApiRequest to avoid automatic
retries for POST requests, especially the wildcard DNS create operation, while
preserving retries for idempotent methods and existing retryable error handling.
Use input.method to gate the retry path before logging and waiting, so an
ambiguous POST is propagated immediately rather than repeated.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 99921eb7-f526-4fd2-875c-8471805c0c5b

📥 Commits

Reviewing files that changed from the base of the PR and between bebcc4b and 4ebdb63.

📒 Files selected for processing (10)
  • docs/contributing/architecture/authentication.md
  • docs/contributing/decisions/0017-per-user-package-app-subdomains.md
  • docs/contributing/environment-variables.md
  • docs/contributing/security.md
  • packages/worker/src/app-base-url.node.test.ts
  • packages/worker/src/app-base-url.ts
  • packages/worker/src/app/package-app-origin.ts
  • packages/worker/src/app/package-app-origin.workers.test.ts
  • tools/ci/resource-utils.node.test.ts
  • tools/ci/resource-utils.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/worker/src/app-base-url.node.test.ts
  • docs/contributing/decisions/0017-per-user-package-app-subdomains.md
  • docs/contributing/architecture/authentication.md
  • docs/contributing/environment-variables.md
  • tools/ci/resource-utils.node.test.ts
  • packages/worker/src/app-base-url.ts
  • docs/contributing/security.md
  • packages/worker/src/app/package-app-origin.ts
  • packages/worker/src/app/package-app-origin.workers.test.ts

Comment thread tools/ci/resource-utils.ts
…T retries

- createPlatformAccount validates new usernames with the strict DNS-label
  check (Bugbot): platform accounts bypass only the reserved-list
  restriction, not the format rules, so they can own a subdomain.
- The wildcard DNS ensure reports a conflicting record even when the
  required proxied AAAA also exists, instead of accepting and leaving
  resolution ambiguous (CodeRabbit).
- The root Cloudflare API client no longer auto-retries non-idempotent
  methods: a create whose response was lost may have succeeded, and
  repeating it fails on the duplicate; ensure-style callers reconcile on
  the next run (CodeRabbit).

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds merged commit e2cd8a3 into main Aug 11, 2026
10 checks passed
@kentcdodds
kentcdodds deleted the cursor/per-user-package-app-subdomains-1636 branch August 11, 2026 22:56
kentcdodds added a commit that referenced this pull request Aug 12, 2026
* chore(identity): remove the legacy underscore-username tier

Production had exactly one underscore username (debs_obrien); it was
renamed by hand on 2026-08-12 (users.username, email_inbox_addresses,
saved_packages names -> debs-obrien), so the two-tier validation shipped
in #1395/#1396 no longer guards anyone. Username validation is one strict
DNS-label rule everywhere again: the lenient recognition pattern, the
separate DNS-safe validator, the 409 rename prompt on the package-app
entry, and the hosted-URL path-mount fallback for non-DNS-safe usernames
are all removed. The shared dnsSafeUsernamePattern stays as the single
source of truth (also validating wildcard-routed subdomain labels).

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

* docs(0017): drop the obsolete rename-before-hosting consequence

The sole underscore account was already migrated; CodeRabbit caught the
stale Consequences bullet still requiring renames before hosting.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

* fix(preview): SQLite-backed DO namespaces for the mock cloudflare worker

Cloudflare now rejects creating key-value backed Durable Object
namespaces (error 10099). Every PR preview deploys a fresh
kody-pr-<n>-mock-cloudflare script that runs these migrations from
scratch, so preview resource deploys started failing repo-wide. Existing
long-lived scripts already applied tags v1/v2 and are unaffected.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
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