feat(package-apps): per-user subdomains on the package-app domain - #1395
Conversation
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>
|
Warning Review limit reached
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 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe 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. ChangesPer-user package-app hosting
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
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>
|
🔎 Preview deployed: https://kody-pr-1395.kody-a99.workers.dev Worker: Mocks:
|
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
tools/ci/resource-utils.node.test.ts (2)
928-946: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAssert 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
nameandtype=AAAAfilters 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
encodeURIComponentto 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 winAdd coverage for a package-app host that is not the registrable zone.
Both route assertions derive the expected
zone_namefrom the package-app hostname itself. This equality only holds whenPACKAGE_APP_BASE_URLis already a registrable domain. The production code setszone_namefromreadPackageAppZoneName(...), which returns the registrable domain. A fixture such asapps.kodyapps.devwould producepattern: '*.apps.kodyapps.dev/*'withzone_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 liftCollapse the duplicated Cloudflare client into the existing one.
cloudflareRootApiRequestOnceandcloudflareRootApiRequestrepeatcloudflareApiRequestOnceandcloudflareApiRequest(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
accountIdoptional 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
📒 Files selected for processing (43)
docs/contributing/architecture/authentication.mddocs/contributing/architecture/request-lifecycle.mddocs/contributing/decisions/0017-per-user-package-app-subdomains.mddocs/contributing/decisions/index.mddocs/contributing/environment-variables.mddocs/contributing/security.mddocs/contributing/setup-manifest.mddocs/guides/package-authoring.mddocs/use/packages.mddocs/use/search.mdpackages/shared/src/public-urls.tspackages/worker/client/markdown-view.node.test.tspackages/worker/client/markdown-view.tsxpackages/worker/client/routes/account.tsxpackages/worker/client/routes/login.tsxpackages/worker/src/app-base-url.node.test.tspackages/worker/src/app-base-url.tspackages/worker/src/app/handlers/account-profile.node.test.tspackages/worker/src/app/handlers/auth-handler.node.test.tspackages/worker/src/app/handlers/package-app.tspackages/worker/src/app/package-app-handoff.node.test.tspackages/worker/src/app/package-app-origin.tspackages/worker/src/app/package-app-origin.workers.test.tspackages/worker/src/app/package-app-session.tspackages/worker/src/identity/generated-username.tspackages/worker/src/identity/username.node.test.tspackages/worker/src/identity/username.tspackages/worker/src/mcp/capabilities/packages/package-app-fetch.node.test.tspackages/worker/src/mcp/capabilities/packages/package-app-fetch.tspackages/worker/src/mcp/capabilities/packages/publish-external-push.node.test.tspackages/worker/src/mcp/capabilities/packages/publish-external-push.tspackages/worker/src/mcp/tools/package-search-identity.node.test.tspackages/worker/src/mcp/tools/package-search-identity.tspackages/worker/src/mcp/tools/search-detail.node.test.tspackages/worker/src/mcp/tools/search-detail.tspackages/worker/src/mcp/tools/search-entity-plugins/package.tspackages/worker/src/mcp/tools/search-format-helpers.tspackages/worker/src/package-runtime/package-app-serve.tspackages/worker/src/package-runtime/package-app-synthetic.tspackages/worker/src/package-runtime/package-app.tstools/ci/production-resources.tstools/ci/resource-utils.node.test.tstools/ci/resource-utils.ts
| 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." |
There was a problem hiding this comment.
🎯 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 toa-z.packages/worker/client/routes/login.tsx#L714-L715: change both character classes toa-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.
…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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
- 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>
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 (1)
tools/ci/resource-utils.ts (1)
383-469: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winRestrict retries for the wildcard DNS
POST.
cloudflareRootApiRequestretriesPOSTafter timeouts, connection errors, and 5xx responses. If Cloudflare creates the record before the response is lost, the retry repeats the create. Cloudflare may return error81057, which can make provisioning fail after the record exists. Restrict automatic retries to idempotent methods, or reconcile the record before retrying an ambiguousPOST.🤖 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
📒 Files selected for processing (10)
docs/contributing/architecture/authentication.mddocs/contributing/decisions/0017-per-user-package-app-subdomains.mddocs/contributing/environment-variables.mddocs/contributing/security.mdpackages/worker/src/app-base-url.node.test.tspackages/worker/src/app-base-url.tspackages/worker/src/app/package-app-origin.tspackages/worker/src/app/package-app-origin.workers.test.tstools/ci/resource-utils.node.test.tstools/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
…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>
* 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>

Intent
One user's package apps must never share a browser origin with another user's: shared-origin cookies (
kody_pkg_sessiononkodyapps.dev) let package A reach package B's session-authorized endpoints from the browser, the exact gap called out as "out of scope" indocs/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
kodyapps.dev/@{username}/packages/{kodyId}tohttps://{username}.kodyapps.dev/packages/{kodyId}— the username lives in the hostname, so the/@{username}path prefix disappears on the package-app domain (routing simplification)./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.__Host-kody_pkg_sessionon secure requests: browsers refuse anyDomain=-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)._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.*.kodyapps.dev/*) alongside the apex custom domain, andproduction-resources ensureidempotently provisions the proxied wildcard AAAA100::DNS record (zone routes do not create DNS)./packages/...mount paths on every host, matching the existing/@...rule.hostedUrlemission (search, publish,package_app_fetch,packageContext) and search identity parsing understand the subdomain shape; inline (non-production) serving keeps the path-based mount.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.devzone before the first deploy; submittingkodyapps.devto 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.__Host-/noDomain, 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:bebcc4b1Classification: extends — the package-app origin-isolation contract changes shape (per-user subdomains,
__Host-cookie, new mount path); no new primitives.Primitives touched
package-apps/packages/{kodyId}, newpackageContextpathspackage-runtimePackageAppPath.mount, subdomain path parsingapp-ui__Host-session cookie; markdown link rule; two-tier DNS-label usernamesmcp-serverSystem 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:#fffBefore / after
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).Summary by CodeRabbit
New Features
Security
Bug Fixes
Documentation
Validation