Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions docs/contributing/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,24 @@ confirmed non-production runtimes; see `packages/worker/src/app-base-url.ts` and

- `PACKAGE_APP_BASE_URL` — the **apex** origin of the package-app domain that
hosted package apps are served from. Production sets `https://kodyapps.dev` in
`packages/worker/wrangler.jsonc`, and the deploy derives a Workers
`custom_domain` route for the apex (which provisions its DNS and certificate)
plus a wildcard **zone route** (`*.<apex-host>/*`) for per-user subdomains —
Cloudflare custom domains cannot be wildcards, and zone routes do not create
DNS records, so production CI ensures the proxied wildcard DNS record
separately (see [setup-manifest.md](./setup-manifest.md)). Each owner's apps
are addressed at `https://{username}.<apex-host>/packages/{kodyId}/...`; the
apex itself serves only redirects (legacy `/@user/packages/...` paths to the
owning subdomain, `/` to the app origin). It **must be a separate registrable
domain** from `APP_BASE_URL`: that is what makes author-supplied package code
cross-site, so the `SameSite=Lax` `kody_session` cookie never reaches it.
Production origin validation also requires `APP_BASE_URL` so this relationship
can be checked at runtime. Production returns `500` for package-app requests
when this value is missing, invalid, equal to `APP_BASE_URL`, or on the same
registrable domain; it never falls back to inline serving. Preview, tests, and
E2E may leave it unset and keep serving package apps inline on the app origin
at `/@{username}/packages/*`.
`packages/worker/wrangler.jsonc`, and the deploy publishes **zone routes** for
the apex (`<apex-host>/*`) and the per-user wildcard (`*.<apex-host>/*`) on
the runtime Worker — never a custom domain in this zone (replacing a zone's
route table detaches its custom domains and deletes their DNS records). Zone
routes do not create DNS records, so production CI ensures proxied placeholder
records for both names separately (see
[setup-manifest.md](./setup-manifest.md)). Each owner's apps are addressed at
`https://{username}.<apex-host>/packages/{kodyId}/...`; the apex itself serves
only redirects (legacy `/@user/packages/...` paths to the owning subdomain,
`/` to the app origin). It **must be a separate registrable domain** from
`APP_BASE_URL`: that is what makes author-supplied package code cross-site, so
the `SameSite=Lax` `kody_session` cookie never reaches it. Production origin
validation also requires `APP_BASE_URL` so this relationship can be checked at
runtime. Production returns `500` for package-app requests when this value is
missing, invalid, equal to `APP_BASE_URL`, or on the same registrable domain;
it never falls back to inline serving. Preview, tests, and E2E may leave it
unset and keep serving package apps inline on the app origin at
`/@{username}/packages/*`.

`npm run dev` runs the **production** Wrangler environment, so the committed
production value reaches local dev too; `getPackageAppBaseUrl` ignores an
Expand Down
64 changes: 34 additions & 30 deletions docs/contributing/setup-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,33 +102,36 @@ This project uses the following resources:
Workers AI binding options.
- Second registrable domain for hosted package apps
- Production: `kodyapps.dev` (zone in the same Cloudflare account, on
Cloudflare nameservers), attached to the production Worker as a Workers
**custom domain**, which provisions the apex DNS record and edge
certificate. Per-user package apps are served from
`https://{username}.kodyapps.dev/packages/{kodyId}/...`; the apex stays
attached for legacy redirects (`/@user/packages/...` → per-user subdomain,
`/` → app origin).
- **Wildcard DNS (one-time per zone).** Zone routes do not create DNS records.
Production CI (`tools/ci/production-resources.ts ensure`) idempotently
ensures a proxied wildcard record in the package-app zone: `*.kodyapps.dev`
→ AAAA `100::` (orange-cloud proxied). The deploy token needs **DNS:Edit**
on that zone (in addition to Workers deploy permissions). Forks must create
the zone and either run `ensure` or add the record manually before the first
per-user-subdomain deploy.
- **Wildcard zone route.** `writeGeneratedWranglerConfig`
(`tools/ci/resource-utils.ts`) also publishes
`{ pattern: "*.kodyapps.dev/*", zone_name: "kodyapps.dev" }` alongside the
apex `custom_domain` route. Cloudflare custom domains cannot be wildcards,
so per-user hosts use a zone route instead. Cloudflare Universal SSL covers
one wildcard label (`*.kodyapps.dev`), which is enough for
`{username}.kodyapps.dev`.
Cloudflare nameservers), served by the runtime Worker via **zone routes**
plus proxied placeholder DNS records — deliberately **not** a Workers custom
domain. Per-user package apps are served from
`https://{username}.kodyapps.dev/packages/{kodyId}/...`; the apex serves
legacy redirects (`/@user/packages/...` → per-user subdomain, `/` → app
origin).
- **No custom domain in this zone (incident-tested).** The deploy publishes
this zone's Worker route table, and replacing a zone's routes detaches any
Workers custom domain in that zone and deletes its DNS record — that took
`kodyapps.dev` down on 2026-08-11. Custom domains stay reserved for the
app-origin zones, whose route tables the deploy never publishes.
- **DNS records (idempotent, per deploy).** Zone routes do not create DNS
records. Production CI (`tools/ci/production-resources.ts ensure`)
idempotently ensures proxied records for both names in the package-app zone:
`kodyapps.dev` and `*.kodyapps.dev` → AAAA `100::` (orange-cloud proxied).
The deploy token needs **DNS:Edit** on that zone (in addition to Workers
deploy permissions). Forks must create the zone and either run `ensure` or
add the records manually before the first per-user-subdomain deploy.
- **Zone routes.** `tools/ci/runtime-worker-config.ts` publishes
`{ pattern: "kodyapps.dev/*", zone_name: "kodyapps.dev" }` and
`{ pattern: "*.kodyapps.dev/*", zone_name: "kodyapps.dev" }` on the runtime
Worker. Cloudflare Universal SSL covers one wildcard label
(`*.kodyapps.dev`), which is enough for `{username}.kodyapps.dev`.
- The attach happens on deploy, but the routes are **generated, not
committed**: `writeGeneratedWranglerConfig` derives one `custom_domain`
route per base-URL var (`APP_BASE_URL`, `APP_LEGACY_HOSTS`, and
`PACKAGE_APP_BASE_URL`) plus the wildcard zone route while writing
`packages/worker/wrangler-production.generated.json`. Those vars are the
single source of truth for both the hosts the Worker routes on and the
domains the deploy attaches, so the two cannot drift.
route per app-origin var (`APP_BASE_URL` and `APP_LEGACY_HOSTS`) while
writing `packages/worker/wrangler-production.generated.json`; the
package-app zone routes are generated into the runtime Worker config. Those
vars are the single source of truth for both the hosts the Workers route on
and the domains the deploy attaches, so the two cannot drift.
- **`routes` replaces the Worker's whole route set — it does not add to it.**
Omitting a previously attached custom domain detaches that origin and
deletes its DNS record. The generator therefore always lists the app origin
Expand Down Expand Up @@ -327,11 +330,12 @@ automatically:
- `PACKAGE_APP_BASE_URL` (Wrangler `var`; required in production and optional
for confirmed local/preview/test runtimes; origin for hosted package apps.
Production sets `https://kodyapps.dev` in `packages/worker/wrangler.jsonc`,
and the deploy attaches that apex as a Workers custom domain plus a wildcard
zone route (`*.kodyapps.dev/*`) from the generated config (see the Cloudflare
resources list above). Per-user apps use `{username}.kodyapps.dev` subdomains;
production CI ensures the proxied wildcard DNS record. Must be a **separate
registrable domain** from `APP_BASE_URL` — see
and the deploy publishes apex and wildcard zone routes (`kodyapps.dev/*`,
`*.kodyapps.dev/*`) on the runtime Worker (see the Cloudflare resources list
above — never a custom domain in this zone). Per-user apps use
`{username}.kodyapps.dev` subdomains; production CI ensures the proxied apex
and wildcard DNS records. Must be a **separate registrable domain** from
`APP_BASE_URL` — see
[Hosted package app origin isolation](./security.md#hosted-package-app-origin-isolation).
Local dev ignores any value it cannot serve itself, and preview/test leave it
unset, so those keep serving package apps inline on the app origin. Point it
Expand Down
4 changes: 2 additions & 2 deletions tools/ci/production-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
ensureArtifactsAccountEventSubscription,
ensureCloudflareQueue,
ensureEmailSendingEventSubscription,
ensurePackageAppWildcardDnsRecord,
ensurePackageAppDnsRecords,
ensureR2Bucket,
fail,
isValidBareHostname,
Expand Down Expand Up @@ -643,7 +643,7 @@ async function ensureProductionResources(options: CliOptions) {
})

if (bindings.packageAppHostname) {
await ensurePackageAppWildcardDnsRecord({
await ensurePackageAppDnsRecords({
accountId: accountId ?? 'dry-run-account',
apiToken: apiToken ?? 'dry-run-token',
packageAppHostname: bindings.packageAppHostname,
Expand Down
75 changes: 58 additions & 17 deletions tools/ci/resource-utils.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ensureArtifactsAccountEventSubscription,
ensureCloudflareQueue,
ensureEmailSendingEventSubscription,
ensurePackageAppWildcardDnsRecord,
ensurePackageAppDnsRecords,
isR2BucketAlreadyExistsOutput,
isRetryableCloudflareApiError,
isWranglerNotFoundOutput,
Expand Down Expand Up @@ -869,7 +869,7 @@ test('ensureArtifactsAccountEventSubscription creates account-level lifecycle su
)
})

test('ensurePackageAppWildcardDnsRecord creates a proxied wildcard AAAA record', async () => {
test('ensurePackageAppDnsRecords creates proxied apex and wildcard AAAA records', async () => {
consoleError.mockImplementation(() => {})
const fetcher = vi
.fn<typeof fetch>()
Expand All @@ -879,12 +879,20 @@ test('ensurePackageAppWildcardDnsRecord creates a proxied wildcard AAAA record',
result: [{ id: 'zone-kodyapps', name: 'kodyapps.dev' }],
}),
)
.mockResolvedValueOnce(Response.json({ success: true, result: [] }))
.mockResolvedValueOnce(
Response.json({
success: true,
result: [],
result: {
id: 'dns-apex',
type: 'AAAA',
name: 'kodyapps.dev',
content: '100::',
proxied: true,
},
}),
)
.mockResolvedValueOnce(Response.json({ success: true, result: [] }))
.mockResolvedValueOnce(
Response.json({
success: true,
Expand All @@ -898,7 +906,7 @@ test('ensurePackageAppWildcardDnsRecord creates a proxied wildcard AAAA record',
}),
)

await ensurePackageAppWildcardDnsRecord({
await ensurePackageAppDnsRecords({
accountId: 'account-1',
apiToken: 'token-1',
packageAppHostname: 'kodyapps.dev',
Expand All @@ -911,16 +919,35 @@ test('ensurePackageAppWildcardDnsRecord creates a proxied wildcard AAAA record',
'https://api.cloudflare.com/client/v4/zones?name=kodyapps.dev&account.id=account-1&status=active',
expect.objectContaining({ method: 'GET' }),
)
// The list query is name-only on purpose: a type filter would hide
// conflicting A/CNAME records at the wildcard name.
// The list queries are name-only on purpose: a type filter would hide
// conflicting A/CNAME records at the same name.
expect(fetcher).toHaveBeenNthCalledWith(
2,
`https://api.cloudflare.com/client/v4/zones/zone-kodyapps/dns_records?name=${encodeURIComponent('*.kodyapps.dev')}`,
'https://api.cloudflare.com/client/v4/zones/zone-kodyapps/dns_records?name=kodyapps.dev',
expect.objectContaining({ method: 'GET' }),
)
expect(fetcher).toHaveBeenNthCalledWith(
3,
'https://api.cloudflare.com/client/v4/zones/zone-kodyapps/dns_records',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
type: 'AAAA',
name: 'kodyapps.dev',
content: '100::',
proxied: true,
ttl: 1,
}),
}),
)
expect(fetcher).toHaveBeenNthCalledWith(
4,
`https://api.cloudflare.com/client/v4/zones/zone-kodyapps/dns_records?name=${encodeURIComponent('*.kodyapps.dev')}`,
expect.objectContaining({ method: 'GET' }),
)
expect(fetcher).toHaveBeenNthCalledWith(
5,
'https://api.cloudflare.com/client/v4/zones/zone-kodyapps/dns_records',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
Expand All @@ -934,7 +961,7 @@ test('ensurePackageAppWildcardDnsRecord creates a proxied wildcard AAAA record',
)
})

test('ensurePackageAppWildcardDnsRecord fails on a conflicting record of another type', async () => {
test('ensurePackageAppDnsRecords fails on a conflicting record of another type', async () => {
consoleError.mockImplementation(() => {})
const exit = vi.spyOn(process, 'exit').mockImplementation((() => {
throw new Error('process.exit called')
Expand All @@ -954,7 +981,7 @@ test('ensurePackageAppWildcardDnsRecord fails on a conflicting record of another
{
id: 'dns-conflicting',
type: 'CNAME',
name: '*.kodyapps.dev',
name: 'kodyapps.dev',
content: 'somewhere-else.example',
proxied: false,
},
Expand All @@ -963,7 +990,7 @@ test('ensurePackageAppWildcardDnsRecord fails on a conflicting record of another
)

await expect(
ensurePackageAppWildcardDnsRecord({
ensurePackageAppDnsRecords({
accountId: 'account-1',
apiToken: 'token-1',
packageAppHostname: 'kodyapps.dev',
Expand All @@ -978,7 +1005,7 @@ test('ensurePackageAppWildcardDnsRecord fails on a conflicting record of another
exit.mockRestore()
})

test('ensurePackageAppWildcardDnsRecord fails on a conflict even when the required record exists', async () => {
test('ensurePackageAppDnsRecords fails on a conflict even when the required record exists', async () => {
consoleError.mockImplementation(() => {})
const exit = vi.spyOn(process, 'exit').mockImplementation((() => {
throw new Error('process.exit called')
Expand All @@ -998,14 +1025,14 @@ test('ensurePackageAppWildcardDnsRecord fails on a conflict even when the requir
{
id: 'dns-required',
type: 'AAAA',
name: '*.kodyapps.dev',
name: 'kodyapps.dev',
content: '100::',
proxied: true,
},
{
id: 'dns-stray',
type: 'A',
name: '*.kodyapps.dev',
name: 'kodyapps.dev',
content: '192.0.2.1',
proxied: false,
},
Expand All @@ -1014,7 +1041,7 @@ test('ensurePackageAppWildcardDnsRecord fails on a conflict even when the requir
)

await expect(
ensurePackageAppWildcardDnsRecord({
ensurePackageAppDnsRecords({
accountId: 'account-1',
apiToken: 'token-1',
packageAppHostname: 'kodyapps.dev',
Expand All @@ -1028,7 +1055,7 @@ test('ensurePackageAppWildcardDnsRecord fails on a conflict even when the requir
exit.mockRestore()
})

test('ensurePackageAppWildcardDnsRecord reuses an existing proxied wildcard record', async () => {
test('ensurePackageAppDnsRecords reuses existing proxied apex and wildcard records', async () => {
consoleError.mockImplementation(() => {})
const fetcher = vi
.fn<typeof fetch>()
Expand All @@ -1038,6 +1065,20 @@ test('ensurePackageAppWildcardDnsRecord reuses an existing proxied wildcard reco
result: [{ id: 'zone-kodyapps', name: 'kodyapps.dev' }],
}),
)
.mockResolvedValueOnce(
Response.json({
success: true,
result: [
{
id: 'dns-existing-apex',
type: 'AAAA',
name: 'kodyapps.dev',
content: '100::',
proxied: true,
},
],
}),
)
.mockResolvedValueOnce(
Response.json({
success: true,
Expand All @@ -1053,15 +1094,15 @@ test('ensurePackageAppWildcardDnsRecord reuses an existing proxied wildcard reco
}),
)

await ensurePackageAppWildcardDnsRecord({
await ensurePackageAppDnsRecords({
accountId: 'account-1',
apiToken: 'token-1',
packageAppHostname: 'kodyapps.dev',
dryRun: false,
fetcher,
})

expect(fetcher).toHaveBeenCalledTimes(2)
expect(fetcher).toHaveBeenCalledTimes(3)
})

test('writeGeneratedWranglerConfig rejects invalid environment asset config', async () => {
Expand Down
Loading
Loading