Skip to content

Commit 6a462e9

Browse files
committed
feat: add deploy site public paths
1 parent 2ec8036 commit 6a462e9

43 files changed

Lines changed: 1805 additions & 107 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,10 @@ The SDK is the canonical kernel — a single typed client with a `CredentialsPro
115115
- **All bytes ride through CAS.** The plan request body never carries inline bytes — only `ContentRef` objects. When the normalized spec exceeds 5 MB JSON, the SDK uploads the manifest itself as a CAS object and references it (`manifest_ref` escape hatch — no body-size cliff).
116116
- **Manifest adapters live in the Node SDK.** `loadDeployManifest(path)` and `normalizeDeployManifest(input)` accept the agent/CLI/MCP JSON shape (`project_id`, `{data,encoding}`, `{path}`, migration `sql_path` / `sql_file`) and return an SDK-native `ReleaseSpec` plus optional `idempotencyKey`. CLI/MCP should call these helpers instead of reimplementing adapter logic.
117117
- **Strict/no-op validation lives in the SDK.** `Deploy.validateSpec` rejects unknown raw `ReleaseSpec` fields before normalization can drop them, and rejects project/base-only or empty nested specs with `MANIFEST_EMPTY` before hashing, uploading, or planning. The Node manifest adapter is strict too, so agent JSON typos such as `subdomain` or `sqlPath` do not become partial deploys.
118-
- **Replace vs patch semantics per resource.** `site.replace` = "this is the whole site" (files absent are removed in the new release); `site.patch.put` / `patch.delete` = surgical updates. Same for `functions`. Secrets are value-free declarations: `secrets.require` asserts keys already exist, and `secrets.delete` removes keys at activation. Set secret values out-of-band through the secrets API. `subdomains` use `set` / `add` / `remove`. `routes` is `undefined | null | { replace: RouteSpec[] }`: omitted/null carries forward base routes, `replace: []` clears dynamic routes, and route entries target materialized functions with `{ type: "function", name }`. Top-level absence = leave untouched.
118+
- **Replace vs patch semantics per resource.** `site.replace` = "this is the whole site" (files absent are removed in the new release); `site.patch.put` / `patch.delete` = surgical updates; `site.public_paths` is the direct browser reachability table for static assets. Explicit mode uses a complete map such as `{ "/events": { asset: "events.html", cache_class: "html" } }`, so `/events` serves the release asset `events.html` while `/events.html` is not public unless separately declared. `mode: "implicit"` restores filename-derived reachability and can widen access. Public-path-only site specs are deployable. Same replace/patch split for `functions`. Secrets are value-free declarations: `secrets.require` asserts keys already exist, and `secrets.delete` removes keys at activation. Set secret values out-of-band through the secrets API. `subdomains` use `set` / `add` / `remove`. `routes` is `undefined | null | { replace: RouteSpec[] }`: omitted/null carries forward base routes, `replace: []` clears dynamic routes, and route entries target materialized functions with `{ type: "function", name }` or exact method-aware static aliases with `{ type: "static", file: "events.html" }`, where `file` is a release asset path, not a public path, URL, CAS hash, rewrite, or redirect. Prefer `site.public_paths` for ordinary clean static URLs. Top-level absence = leave untouched.
119119
- **Structured warnings.** Plan responses include `warnings: WarningEntry[]`. `deploy.apply` emits `plan.warnings` and aborts before upload/commit when a warning requires confirmation (including `MISSING_REQUIRED_SECRET`) unless the caller explicitly passes `allowWarnings`.
120120
- **Server-authoritative dry-runs.** `deploy.plan(spec, { dryRun: true })` calls `POST /deploy/v2/plans?dry_run=true`; the gateway returns the v2 flat plan envelope without creating plan or operation rows, so `plan_id` and `operation_id` are `null` and the response cannot be uploaded or committed.
121-
- **Release observability.** `getRelease({ project, releaseId, siteLimit? })`, `getActiveRelease({ project, siteLimit? })`, and `diff({ project, from, to, limit? })` are typed apikey reads over `/deploy/v2/releases*`. `active` means the current-live target; inventories expose materialized routes and warnings when returned; release-to-release diffs expose `migrations.applied_between_releases`, not plan migration buckets. Secret diffs expose keys only; route diffs expose `added` / `removed` / `changed`.
121+
- **Release observability.** `getRelease({ project, releaseId, siteLimit? })`, `getActiveRelease({ project, siteLimit? })`, and `diff({ project, from, to, limit? })` are typed apikey reads over `/deploy/v2/releases*`. `active` means the current-live target; inventories expose materialized routes, `static_public_paths` when returned, and warnings when returned. `site.paths` is the release static asset inventory; `static_public_paths[]` is the browser reachability inventory with `public_path`, `asset_path`, `reachability_authority`, `direct`, cache class, and content type. Release-to-release diffs expose `migrations.applied_between_releases`, not plan migration buckets. Secret diffs expose keys only; route diffs expose `added` / `removed` / `changed`.
122122
- **Server-authoritative manifest digest.** The gateway returns the canonical digest in the plan response. The SDK no longer requires byte-for-byte canonicalize agreement — `canonicalize.ts` is now a UX helper only.
123123
- **Convenience shims.** `sites.deployDir` is a Node-only wrapper that uses `fileSetFromDir(dir)` and delegates to `deploy.apply`; its event callback emits only unified `DeployEvent` shapes.
124124
- **MCP/CLI surface.** `deploy` and `deploy_resume` MCP tools (in `src/tools/deploy.ts` and `src/tools/deploy-resume.ts`) expose the primitive directly; `deploy_release_get` / `deploy_release_active` / `deploy_release_diff` expose release observability reads. CLI subcommands `run402 deploy apply`, `run402 deploy resume`, and `run402 deploy release <get|active|diff>` (in `cli/lib/deploy-v2.mjs`) mirror them. Use a v2 `ReleaseSpec` through `deploy` / `deploy apply`.
@@ -128,7 +128,7 @@ The SDK is the canonical kernel — a single typed client with a `CredentialsPro
128128
- **`namespaces/ci.ts`**`/ci/v1/*` SDK surface: `createBinding`, `listBindings`, `getBinding`, `revokeBinding`, `exchangeToken`, plus canonical delegation builders (`buildCiDelegationStatement`, `buildCiDelegationResourceUri`) and validators.
129129
- **`ci-credentials.ts`** — isomorphic CI-session credential providers. `githubActionsCredentials({ projectId })` requests the GitHub OIDC subject token, exchanges it through `ci.exchangeToken`, caches the Run402 session until `expires_in - refreshBeforeSeconds`, and marks credentials with `CI_SESSION_CREDENTIALS`.
130130
- **`node/ci.ts`** — Node-only `signCiDelegation(values, opts?)`; reads the local allowance and signs the canonical SIWX delegation for `/ci/v1/bindings`. Default delegation chain id is `eip155:84532` unless overridden.
131-
- **Deploy integration is credential-driven.** `Deploy` detects the CI credential marker internally. Do not add public `ci` options, `r.ci.deployApply`, or broad CI deploy wrapper tools without a new design. CI deploys allow only `project`, `database`, `functions`, `site`, absent/current `base`, and `routes` authorized by the binding's `route_scopes`; every `spec.secrets` shape (including value-free `require`/`delete`), subdomains, checks, unknown top-level fields, non-current base, and `manifest_ref` are rejected before upload/plan. Gateway planning enforces route diffs and returns `CI_ROUTE_SCOPE_DENIED` for out-of-scope route declarations.
131+
- **Deploy integration is credential-driven.** `Deploy` detects the CI credential marker internally. Do not add public `ci` options, `r.ci.deployApply`, or broad CI deploy wrapper tools without a new design. CI deploys allow only `project`, `database`, `functions`, the complete `site` resource including `site.public_paths`, absent/current `base`, and `routes` authorized by the binding's `route_scopes`; every `spec.secrets` shape (including value-free `require`/`delete`), subdomains, checks, unknown top-level fields, non-current base, and `manifest_ref` are rejected before upload/plan. Gateway planning enforces route diffs and nested public-path validation/authorization, returning canonical errors such as `CI_ROUTE_SCOPE_DENIED` for out-of-scope route declarations.
132132
- **CLI DX.** `run402 ci link github` creates a deploy-scoped binding and generated workflow that calls `run402 deploy apply --manifest <manifest> --project <project>`. Repeatable `--route-scope <pattern>` delegates exact public paths such as `/admin` or final-wildcard prefixes such as `/api/*`; no scopes means no CI route authority. `run402 ci list` and `run402 ci revoke` manage bindings. V1 intentionally omits raw subject/wildcard/event/PR-deploy flags and requires GitHub repository-id binding.
133133

134134
### Shared Core (`core/src/`)

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ run402 sites deploy-dir ./dist --project prj_… > result.json 2> events.log
124124

125125
### Same-origin web routes — static site + function ingress
126126

127-
Deploy-v2 routes are release resources: they activate atomically with the site, functions, migrations, secrets, and subdomains in the same `deploy apply`. Lead with a small route table: ordinary static files do not need routes, API prefixes should use narrow methods, and a static route target is for an exact public path to one deployed file.
127+
Deploy-v2 routes and static public paths are release resources: they activate atomically with the site, functions, migrations, secrets, and subdomains in the same `deploy apply`. Release static asset paths such as `events.html` are distinct from browser-visible public static paths such as `/events`. Use `site.public_paths` for ordinary clean static URLs; keep routes for function ingress and exact, method-aware static aliases.
128128

129129
```json
130130
{
@@ -133,6 +133,12 @@ Deploy-v2 routes are release resources: they activate atomically with the site,
133133
"replace": {
134134
"index.html": { "data": "<!doctype html><main id='app'></main><script>fetch('/api/hello')</script>" },
135135
"events.html": { "data": "<!doctype html><h1>Events</h1>" }
136+
},
137+
"public_paths": {
138+
"mode": "explicit",
139+
"replace": {
140+
"/events": { "asset": "events.html", "cache_class": "html" }
141+
}
136142
}
137143
},
138144
"functions": {
@@ -152,20 +158,21 @@ Deploy-v2 routes are release resources: they activate atomically with the site,
152158
"routes": {
153159
"replace": [
154160
{ "pattern": "/api/*", "methods": ["GET", "POST", "OPTIONS"], "target": { "type": "function", "name": "api" } },
155-
{ "pattern": "/login", "methods": ["POST"], "target": { "type": "function", "name": "login" } },
156-
{ "pattern": "/events", "methods": ["GET", "HEAD"], "target": { "type": "static", "file": "events.html" } }
161+
{ "pattern": "/login", "methods": ["POST"], "target": { "type": "function", "name": "login" } }
157162
]
158163
}
159164
}
160165
```
161166

162-
Omit `routes` or pass `routes: null` to carry forward base routes. Use `routes: { "replace": [] }` to clear the route table. Route entries are an ordered `replace` list, not a path-keyed map. Function targets use `{ "type": "function", "name": "<materialized function name>" }`. Static route targets use exact patterns only, methods `["GET"]` or `["GET","HEAD"]`, and `{ "type": "static", "file": "events.html" }` with a relative deployed file path, no leading slash, wildcard, directory shorthand, query, or fragment. Direct `/functions/v1/:name` calls remain API-key protected; browser-routed paths are public same-origin ingress.
167+
`site.public_paths.mode: "explicit"` means only the complete `public_paths.replace` table is directly reachable as static URLs. In the example, `/events` serves the release asset `events.html`, while `/events.html` is not public unless separately declared. `mode: "implicit"` restores filename-derived public reachability and can widen access, so review gateway warnings before confirming it.
168+
169+
Omit `routes` or pass `routes: null` to carry forward base routes. Use `routes: { "replace": [] }` to clear the route table. Route entries are an ordered `replace` list, not a path-keyed map. Function targets use `{ "type": "function", "name": "<materialized function name>" }`. Static route targets use exact patterns only, methods `["GET"]` or `["GET","HEAD"]`, and `{ "pattern": "/events", "methods": ["GET","HEAD"], "target": { "type": "static", "file": "events.html" } }` where `file` is a release static asset path, not a public path, URL, CAS hash, rewrite, or redirect. Use static route targets for method-aware aliases such as static `GET /login` plus function `POST /login`; in explicit public path mode the backing asset can stay private by filename. Direct `/functions/v1/:name` calls remain API-key protected; browser-routed paths are public same-origin ingress.
163170

164171
Matching is exact or final-prefix-wildcard only. `/admin` and `/admin/` are exact trailing-slash equivalents; `/admin/*` matches children but not `/admin`, `/admin/`, `/admin.css`, or `/administrator`, so deploy both `/admin` and `/admin/*` for a routed section root. Query strings are ignored for matching and preserved in the handler's full public `req.url`. Exact routes beat prefix routes; longest prefix wins; method-compatible dynamic routes beat static assets. A `POST /login` route can coexist with static `GET /login` HTML. Unsafe method mismatch returns `405`, and matched dynamic route failures fail closed instead of falling back to static files.
165172

166173
Routed functions use the Node 22 Fetch Request -> Response contract: `export default async function handler(req) { ... }`. `req.method` is the browser method, and `req.url` is the full public URL on managed subdomains, deployment hosts, and verified custom domains. Derive OAuth callbacks from it, for example `new URL("/admin/oauth/google/callback", new URL(req.url).origin)`. Append multiple cookies with `headers.append("Set-Cookie", value)`; redirects, cookies, and query strings are preserved. The raw `run402.routed_http.v1` envelope is internal; do not write route handlers against it.
167174

168-
Avoid routing every static file, broad method lists by default, wildcard static route targets, leading-slash static files, directory shorthand, and one-static-route-target-per-page tables that exhaust route limits. Also watch wildcard function routes that shadow static assets. Warning codes to handle include `STATIC_ALIAS_SHADOWS_STATIC_PATH`, `STATIC_ALIAS_RELATIVE_ASSET_RISK`, `STATIC_ALIAS_DUPLICATE_CANONICAL_URL`, `STATIC_ALIAS_EXTENSIONLESS_NON_HTML`, and `STATIC_ALIAS_TABLE_NEAR_LIMIT`.
175+
Avoid routing every static file, broad method lists by default, wildcard static route targets, leading-slash static files, directory shorthand, and one-static-route-target-per-page tables that exhaust route limits. Also watch wildcard function routes that shadow direct public static paths. Warning codes to handle include `STATIC_ALIAS_SHADOWS_STATIC_PATH`, `STATIC_ALIAS_RELATIVE_ASSET_RISK`, `STATIC_ALIAS_DUPLICATE_CANONICAL_URL`, `STATIC_ALIAS_EXTENSIONLESS_NON_HTML`, and `STATIC_ALIAS_TABLE_NEAR_LIMIT`; inspect active routes, `static_public_paths`, and resolve diagnostics to distinguish the route pattern from the backing `asset_path`.
169176

170177
Diagnose public URLs with the URL-first CLI or MCP/SDK equivalents:
171178

@@ -175,9 +182,9 @@ run402 deploy resolve --project prj_123 --url https://example.com/events?utm=x#h
175182
run402 deploy resolve --project prj_123 --host example.com --path /events --method GET
176183
```
177184

178-
`deploy_diagnose_url` and `r.deploy.resolve({ project, url, method: "GET" })` return `would_serve`, `diagnostic_status`, `match`, normalized request data, warnings, full resolution JSON, and next steps. Today the public resolve contract is authoritative for host/static/SPAfallback diagnostics, not complete route introspection unless the gateway returns future route context. Known `match` literals are `host_missing`, `manifest_missing`, `path_error`, `none`, `static_exact`, `static_index`, `spa_fallback`, and `spa_fallback_missing`; preserve unknown future strings. `result` is the diagnostic body status, not the HTTP status of the SDK call, so host misses can still be successful CLI/MCP/SDK calls with `would_serve: false`. Do not treat resolve/diagnose as a fetch, cache purge, or cache-policy oracle; do not hard-code `cache_policy` strings. Branch on `cache_class` when present and preserve unknown cache classes.
185+
`deploy_diagnose_url` and `r.deploy.resolve({ project, url, method: "GET" })` return `would_serve`, `diagnostic_status`, `match`, normalized request data, warnings, full resolution JSON, and next steps. When returned, `asset_path`, `reachability_authority`, and `direct` explain which release asset backs the public URL and whether reachability came from implicit file-path mode, explicit `site.public_paths`, or a route-only static alias. Today the public resolve contract is authoritative for host/static/SPAfallback diagnostics, not complete route introspection unless the gateway returns future route context. Known `match` literals are `host_missing`, `manifest_missing`, `path_error`, `none`, `static_exact`, `static_index`, `spa_fallback`, and `spa_fallback_missing`; preserve unknown future strings. `result` is the diagnostic body status, not the HTTP status of the SDK call, so host misses can still be successful CLI/MCP/SDK calls with `would_serve: false`. Do not treat resolve/diagnose as a fetch, cache purge, or cache-policy oracle; do not hard-code `cache_policy` strings. Branch on `cache_class` when present and preserve unknown cache classes.
179186

180-
Release observability exposes stable asset identity. Inventories include `release_generation`, `static_manifest_sha256`, and nullable `static_manifest_metadata` (`file_count`, `total_bytes`, `cache_classes`, `cache_class_sources`, `spa_fallback`); `static_manifest_metadata: null` means unavailable, not zero. Plan and release diffs expose `static_assets` counters: unchanged/changed/added/removed, `newly_uploaded_cas_bytes`, `reused_cas_bytes`, `deployment_copy_bytes_eliminated`, `legacy_immutable_warnings`, `previous_immutable_failures`, and `cas_authorization_failures`.
187+
Release observability exposes stable asset identity and public reachability. Inventories include `release_generation`, `static_manifest_sha256`, nullable `static_manifest_metadata` (`file_count`, `total_bytes`, `cache_classes`, `cache_class_sources`, `spa_fallback`), and `static_public_paths[]` when returned. `site.paths` lists release static assets; `static_public_paths[]` lists browser-visible public paths with `public_path`, `asset_path`, `reachability_authority`, `direct`, cache class, and content type. Plan and release diffs expose `static_assets` counters: unchanged/changed/added/removed, `newly_uploaded_cas_bytes`, `reused_cas_bytes`, `deployment_copy_bytes_eliminated`, `legacy_immutable_warnings`, `previous_immutable_failures`, and `cas_authorization_failures`.
181188

182189
Runtime route failure codes to branch on: `ROUTE_MANIFEST_LOAD_FAILED` (manifest/propagation), `ROUTED_INVOKE_WORKER_SECRET_MISSING` (custom-domain Worker secret), `ROUTED_INVOKE_AUTH_FAILED` (internal invoke signature), `ROUTED_ROUTE_STALE` (selected route failed release revalidation), `ROUTE_METHOD_NOT_ALLOWED` (method mismatch), and `ROUTED_RESPONSE_TOO_LARGE` (body over 6 MiB).
183190

0 commit comments

Comments
 (0)