@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
- package/.claude/skills/urlcode-operations/SKILL.md +108 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/CONTRIBUTING.md +30 -2
- package/README.md +195 -255
- package/ROADMAP.md +143 -15
- package/SECURITY.md +31 -9
- package/dist/BUILD-MANIFEST.json +72 -47
- package/dist/adapters.js +4 -23
- package/dist/agent-lists.js +1 -1
- package/dist/agents-guide.js +113 -0
- package/dist/authoring-files.js +60 -0
- package/dist/authoring.js +11 -1
- package/dist/aws.js +4 -3
- package/dist/build-cloudflare.js +11 -24
- package/dist/build-static.js +134 -0
- package/dist/bulk.js +37 -0
- package/dist/capabilities.js +262 -0
- package/dist/capability-query.js +71 -0
- package/dist/catalog.js +105 -0
- package/dist/cli.js +165 -34
- package/dist/client-address.js +1 -1
- package/dist/compliance-rules/baseline.js +9 -17
- package/dist/compliance-rules/privacy.js +7 -18
- package/dist/compliance-rules/shared.js +0 -2
- package/dist/compliance-rules/strict.js +5 -5
- package/dist/compliance.js +6 -8
- package/dist/conditions.js +88 -0
- package/dist/config.js +69 -6
- package/dist/context.js +155 -0
- package/dist/ecosystem-cli.js +88 -0
- package/dist/egress.js +98 -0
- package/dist/examples.js +92 -0
- package/dist/explain-cli.js +64 -0
- package/dist/explain.js +131 -0
- package/dist/extensions.js +231 -0
- package/dist/function-sources.js +49 -5
- package/dist/function-worker.js +3 -1
- package/dist/functions.js +84 -13
- package/dist/guest-api.js +29 -3
- package/dist/index.js +40 -6
- package/dist/init-with.js +165 -0
- package/dist/interchange-cli.js +42 -0
- package/dist/interchange.js +189 -0
- package/dist/manifest.js +109 -0
- package/dist/match.js +2 -2
- package/dist/mcp-authoring.js +147 -0
- package/dist/mcp.js +97 -0
- package/dist/observability.js +7 -21
- package/dist/operator-host.js +29 -0
- package/dist/plugins.js +12 -0
- package/dist/policies/agents.js +2 -2
- package/dist/policies/cache.js +8 -3
- package/dist/policies/compression.js +2 -1
- package/dist/policies/security.js +0 -0
- package/dist/policies.js +1 -1
- package/dist/policy.js +56 -15
- package/dist/prerender.js +100 -41
- package/dist/project-tests.js +3 -3
- package/dist/provider-verification.js +92 -0
- package/dist/proxy.js +44 -0
- package/dist/readiness.js +34 -11
- package/dist/recipes.js +41 -0
- package/dist/route-diff.js +106 -0
- package/dist/router.js +45 -7
- package/dist/runtime.js +164 -64
- package/dist/sandbox.js +48 -0
- package/dist/scaffold.js +0 -0
- package/dist/schema-query.js +62 -0
- package/dist/scripts/operational-drills.js +12 -54
- package/dist/server.js +3 -29
- package/dist/signals.js +24 -0
- package/dist/site.js +0 -0
- package/dist/tooling.js +96 -0
- package/dist/trusted-functions.js +210 -0
- package/dist/types/adapters.d.ts +7 -4
- package/dist/types/agent-lists.d.ts +0 -1
- package/dist/types/agents-guide.d.ts +17 -0
- package/dist/types/authoring-files.d.ts +10 -0
- package/dist/types/aws.d.ts +3 -1
- package/dist/types/build-cloudflare.d.ts +1 -0
- package/dist/types/build-static.d.ts +43 -0
- package/dist/types/bulk.d.ts +27 -0
- package/dist/types/capabilities.d.ts +64 -0
- package/dist/types/capability-query.d.ts +24 -0
- package/dist/types/catalog.d.ts +65 -0
- package/dist/types/client-address.d.ts +0 -1
- package/dist/types/compliance-rules/baseline.d.ts +1 -9
- package/dist/types/compliance-rules/privacy.d.ts +1 -4
- package/dist/types/compliance-rules/shared.d.ts +0 -2
- package/dist/types/compliance-rules/strict.d.ts +0 -5
- package/dist/types/compliance.d.ts +0 -3
- package/dist/types/conditions.d.ts +19 -0
- package/dist/types/config.d.ts +21 -2
- package/dist/types/context.d.ts +66 -0
- package/dist/types/ecosystem-cli.d.ts +17 -0
- package/dist/types/egress.d.ts +46 -0
- package/dist/types/examples.d.ts +50 -0
- package/dist/types/explain-cli.d.ts +11 -0
- package/dist/types/explain.d.ts +95 -0
- package/dist/types/extensions.d.ts +177 -0
- package/dist/types/function-sources.d.ts +9 -0
- package/dist/types/functions.d.ts +48 -5
- package/dist/types/guest-api.d.ts +1 -0
- package/dist/types/index.d.ts +36 -6
- package/dist/types/init-with.d.ts +30 -0
- package/dist/types/interchange-cli.d.ts +16 -0
- package/dist/types/interchange.d.ts +42 -0
- package/dist/types/manifest.d.ts +79 -0
- package/dist/types/match.d.ts +1 -0
- package/dist/types/mcp-authoring.d.ts +92 -0
- package/dist/types/mcp.d.ts +12 -0
- package/dist/types/observability.d.ts +3 -14
- package/dist/types/operator-host.d.ts +8 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/policies/agents.d.ts +0 -2
- package/dist/types/policies/compression.d.ts +2 -0
- package/dist/types/policies/security.d.ts +0 -1
- package/dist/types/policy.d.ts +15 -4
- package/dist/types/project-tests.d.ts +3 -2
- package/dist/types/provider-verification.d.ts +53 -0
- package/dist/types/proxy.d.ts +21 -0
- package/dist/types/readiness.d.ts +10 -3
- package/dist/types/recipes.d.ts +30 -0
- package/dist/types/route-diff.d.ts +27 -0
- package/dist/types/router.d.ts +2 -1
- package/dist/types/runtime.d.ts +11 -27
- package/dist/types/sandbox.d.ts +12 -0
- package/dist/types/scaffold.d.ts +0 -2
- package/dist/types/schema-query.d.ts +12 -0
- package/dist/types/server.d.ts +1 -4
- package/dist/types/signals.d.ts +25 -0
- package/dist/types/site.d.ts +0 -1
- package/dist/types/tooling.d.ts +115 -0
- package/dist/types/trusted-functions.d.ts +29 -0
- package/dist/types/types.d.ts +71 -7
- package/dist/types/typescript-authoring.d.ts +12 -0
- package/dist/types/vercel.d.ts +3 -1
- package/dist/types/verify-deployment.d.ts +47 -0
- package/dist/types.js +37 -5
- package/dist/typescript-authoring.js +142 -0
- package/dist/vercel.js +4 -3
- package/dist/verify-deployment.js +270 -0
- package/docs/AI-AUTHORING.md +232 -15
- package/docs/AWS.md +4 -4
- package/docs/BEST-PRACTICES.md +3 -2
- package/docs/BULK.md +79 -0
- package/docs/CAPABILITIES.md +192 -0
- package/docs/CAPACITY.md +129 -32
- package/docs/CI.md +142 -0
- package/docs/CLOUDFLARE.md +1 -2
- package/docs/COMPLIANCE.md +6 -9
- package/docs/CONDITIONS.md +74 -0
- package/docs/DEPLOYMENT-CHECKS.md +108 -0
- package/docs/EGRESS.md +125 -0
- package/docs/EXTENSIONS.md +398 -0
- package/docs/FRAMEWORK.md +198 -0
- package/docs/FUNCTION-SECURITY.md +129 -32
- package/docs/INSTALL.md +45 -12
- package/docs/INTERCHANGE.md +134 -0
- package/docs/LOAD-TESTING.md +4 -4
- package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
- package/docs/MIDDLEWARE.md +29 -16
- package/docs/MONITORING.md +2 -19
- package/docs/NEXT-PHASE-PLAN.md +98 -0
- package/docs/NEXT-STEPS.md +634 -0
- package/docs/OBSERVABILITY.md +11 -18
- package/docs/OPEN-DECISIONS.md +212 -0
- package/docs/OPERATIONAL-PROOF.md +30 -31
- package/docs/OPERATIONS.md +29 -35
- package/docs/PLUGINS.md +37 -0
- package/docs/POLICIES.md +23 -309
- package/docs/PRERENDER.md +41 -1
- package/docs/PROJECT-DIRECTION.md +75 -8
- package/docs/PROVIDER-VERIFICATION.md +84 -0
- package/docs/READINESS.md +21 -1
- package/docs/README.md +87 -34
- package/docs/RECIPES.md +99 -0
- package/docs/RELEASE-READINESS.md +57 -35
- package/docs/RELEASE-SECURITY.md +116 -7
- package/docs/RESILIENCE.md +16 -15
- package/docs/ROUTING.md +8 -10
- package/docs/SANDBOX-REVIEW.md +19 -6
- package/docs/SCAFFOLDING.md +0 -2
- package/docs/SECURITY-AUDIT.md +41 -1
- package/docs/SPECIFICATION.md +150 -29
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
- package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
- package/docs/SPIKE-CORE-LAYERING.md +337 -0
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
- package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
- package/docs/SPIKE-EXTENSIONS.md +6 -0
- package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
- package/docs/SPIKE-MONOREPO.md +322 -0
- package/docs/STANDARDS.md +150 -142
- package/docs/STARTERS.md +21 -1
- package/docs/STATIC.md +94 -0
- package/docs/TOOLING.md +295 -0
- package/docs/TUNNELS.md +0 -3
- package/docs/TYPESCRIPT-AUTHORING.md +82 -0
- package/docs/TYPESCRIPT.md +25 -4
- package/docs/USABILITY-REVIEW.md +129 -0
- package/docs/VERCEL.md +4 -5
- package/docs/VERSION-ALIGNMENT.md +205 -0
- package/docs/YAML-GUIDE.md +15 -479
- package/docs/YAML-REFERENCE.md +143 -22
- package/docs/policies/agents.md +1 -1
- package/docs/policies/cache.md +13 -0
- package/docs/policies/contract.md +52 -0
- package/docs/policies/hardened.md +56 -0
- package/docs/policies/interoperability.md +169 -0
- package/docs/policies/operations.md +45 -0
- package/docs/yaml/assets.md +36 -0
- package/docs/yaml/conditions.md +20 -0
- package/docs/yaml/functions.md +160 -0
- package/docs/yaml/middleware.md +29 -0
- package/docs/yaml/organization.md +74 -0
- package/docs/yaml/policies.md +37 -0
- package/docs/yaml/redirects.md +64 -0
- package/docs/yaml/responses.md +57 -0
- package/docs/yaml/site.md +24 -0
- package/examples/assets/example.yaml +17 -0
- package/examples/aws/example.yaml +20 -0
- package/examples/cloudflare/example.yaml +19 -0
- package/examples/compliance/example.yaml +11 -0
- package/examples/conditions/README.md +12 -0
- package/examples/conditions/example.yaml +19 -0
- package/examples/conditions/tests/requests.json +13 -0
- package/examples/conditions/urlcode.yaml +24 -0
- package/examples/cookbook/README.md +8 -4
- package/examples/cookbook/example.yaml +17 -0
- package/examples/cookbook/functions/catalog.mjs +3 -0
- package/examples/cookbook/functions/fail.mjs +4 -0
- package/examples/cookbook/functions/items.mjs +3 -0
- package/examples/cookbook/functions/profile.mjs +3 -0
- package/examples/cookbook/functions/resource.mjs +3 -0
- package/examples/cookbook/functions/status.mjs +3 -0
- package/examples/cookbook/middleware/auth.mjs +48 -0
- package/examples/cookbook/middleware/body.mjs +15 -0
- package/examples/cookbook/middleware/bucket.mjs +29 -0
- package/examples/cookbook/middleware/cors.mjs +21 -0
- package/examples/cookbook/middleware/debug.mjs +13 -0
- package/examples/cookbook/middleware/envelope.mjs +11 -0
- package/examples/cookbook/middleware/errors.mjs +11 -0
- package/examples/cookbook/middleware/etag.mjs +18 -0
- package/examples/cookbook/middleware/locale.mjs +20 -0
- package/examples/cookbook/middleware/maintenance.mjs +10 -0
- package/examples/cookbook/middleware/methods.mjs +15 -0
- package/examples/cookbook/middleware/negotiate.mjs +20 -0
- package/examples/cookbook/middleware/referer.mjs +12 -0
- package/examples/cookbook/middleware/request-id.mjs +16 -0
- package/examples/cookbook/route-index.json +676 -0
- package/examples/cookbook/routes/middleware.yaml +126 -0
- package/examples/cookbook/tests/requests.json +526 -0
- package/examples/cookbook/urlcode.yaml +1 -0
- package/examples/egress/README.md +22 -0
- package/examples/egress/example.yaml +19 -0
- package/examples/egress/urlcode.yaml +19 -0
- package/examples/extensions/README.md +7 -0
- package/examples/extensions/example.yaml +21 -0
- package/examples/extensions/urlcode.yaml +25 -0
- package/examples/monitoring/example.yaml +8 -0
- package/examples/prerender/README.md +2 -2
- package/examples/prerender/example.yaml +16 -0
- package/examples/provider-conformance/README.md +12 -0
- package/examples/provider-conformance/example.yaml +14 -0
- package/examples/provider-conformance/urlcode.yaml +34 -0
- package/examples/tunnel/example.yaml +8 -0
- package/examples/vercel/example.yaml +19 -0
- package/llms-full.txt +3084 -0
- package/llms.txt +61 -21
- package/package.json +36 -7
- package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
- package/recipes/authenticated-json-api/README.md +51 -0
- package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
- package/recipes/authenticated-json-api/recipe.yaml +34 -0
- package/recipes/authenticated-json-api/tests/requests.json +39 -0
- package/recipes/authenticated-json-api/urlcode.yaml +12 -0
- package/recipes/contact-form/README.md +25 -0
- package/recipes/contact-form/functions/contact.mjs +17 -0
- package/recipes/contact-form/recipe.yaml +33 -0
- package/recipes/contact-form/tests/requests.json +47 -0
- package/recipes/contact-form/urlcode.yaml +18 -0
- package/recipes/cors-api/README.md +16 -0
- package/recipes/cors-api/functions/items.mjs +3 -0
- package/recipes/cors-api/middleware/cors.mjs +21 -0
- package/recipes/cors-api/recipe.yaml +26 -0
- package/recipes/cors-api/tests/requests.json +65 -0
- package/recipes/cors-api/urlcode.yaml +12 -0
- package/recipes/health-page/README.md +13 -0
- package/recipes/health-page/recipe.yaml +23 -0
- package/recipes/health-page/tests/requests.json +36 -0
- package/recipes/health-page/urlcode.yaml +19 -0
- package/recipes/json-api/README.md +6 -0
- package/recipes/json-api/functions/echo.mjs +3 -0
- package/recipes/json-api/recipe.yaml +25 -0
- package/recipes/json-api/tests/requests.json +34 -0
- package/recipes/json-api/urlcode.yaml +12 -0
- package/recipes/middleware/README.md +34 -0
- package/recipes/middleware/functions/catalog.mjs +3 -0
- package/recipes/middleware/functions/fail.mjs +4 -0
- package/recipes/middleware/functions/items.mjs +3 -0
- package/recipes/middleware/functions/profile.mjs +3 -0
- package/recipes/middleware/functions/resource.mjs +3 -0
- package/recipes/middleware/functions/status.mjs +3 -0
- package/recipes/middleware/middleware/auth.mjs +48 -0
- package/recipes/middleware/middleware/body.mjs +15 -0
- package/recipes/middleware/middleware/bucket.mjs +29 -0
- package/recipes/middleware/middleware/cors.mjs +21 -0
- package/recipes/middleware/middleware/debug.mjs +13 -0
- package/recipes/middleware/middleware/envelope.mjs +11 -0
- package/recipes/middleware/middleware/errors.mjs +11 -0
- package/recipes/middleware/middleware/etag.mjs +18 -0
- package/recipes/middleware/middleware/locale.mjs +20 -0
- package/recipes/middleware/middleware/maintenance.mjs +10 -0
- package/recipes/middleware/middleware/methods.mjs +15 -0
- package/recipes/middleware/middleware/negotiate.mjs +20 -0
- package/recipes/middleware/middleware/referer.mjs +12 -0
- package/recipes/middleware/middleware/request-id.mjs +16 -0
- package/recipes/middleware/public/guide.txt +1 -0
- package/recipes/middleware/recipe.yaml +50 -0
- package/recipes/middleware/tests/requests.json +528 -0
- package/recipes/middleware/urlcode.yaml +127 -0
- package/recipes/protected-download/README.md +22 -0
- package/recipes/protected-download/files/report.txt +1 -0
- package/recipes/protected-download/recipe.yaml +31 -0
- package/recipes/protected-download/tests/requests.json +32 -0
- package/recipes/protected-download/urlcode.yaml +15 -0
- package/recipes/redirect/README.md +7 -0
- package/recipes/redirect/recipe.yaml +25 -0
- package/recipes/redirect/tests/requests.json +19 -0
- package/recipes/redirect/urlcode.yaml +9 -0
- package/recipes/static-plus-api/README.md +15 -0
- package/recipes/static-plus-api/functions/info.mjs +3 -0
- package/recipes/static-plus-api/public/assets/index.html +3 -0
- package/recipes/static-plus-api/public/assets/site.css +1 -0
- package/recipes/static-plus-api/public/index.html +8 -0
- package/recipes/static-plus-api/recipe.yaml +29 -0
- package/recipes/static-plus-api/tests/requests.json +56 -0
- package/recipes/static-plus-api/urlcode.yaml +23 -0
- package/recipes/typescript/README.md +8 -0
- package/recipes/typescript/functions/hello.ts +5 -0
- package/recipes/typescript/recipe.yaml +23 -0
- package/recipes/typescript/tests/requests.json +18 -0
- package/recipes/typescript/urlcode.yaml +5 -0
- package/recipes/webhook-receiver/README.md +20 -0
- package/recipes/webhook-receiver/functions/receive.mjs +16 -0
- package/recipes/webhook-receiver/recipe.yaml +27 -0
- package/recipes/webhook-receiver/tests/requests.json +59 -0
- package/recipes/webhook-receiver/urlcode.yaml +23 -0
- package/schemas/recipe.schema.json +139 -0
- package/schemas/urlcode.schema.json +659 -110
- package/skills/urlcode/SKILL.md +119 -0
- package/starters/default/.github/workflows/urlcode.yml +23 -0
- package/starters/default/.mcp.json +12 -0
- package/starters/default/AGENTS.md +79 -0
- package/starters/default/urlcode.yaml +0 -1
- package/dist/link-api.js +0 -136
- package/dist/link-cli.js +0 -141
- package/dist/link-events.js +0 -76
- package/dist/link-records.js +0 -31
- package/dist/link-store-worker.js +0 -150
- package/dist/link-store.js +0 -250
- package/dist/management-policy.js +0 -41
- package/dist/sqlite-version.js +0 -6
- package/dist/types/link-api.d.ts +0 -30
- package/dist/types/link-cli.d.ts +0 -36
- package/dist/types/link-events.d.ts +0 -27
- package/dist/types/link-records.d.ts +0 -11
- package/dist/types/link-store-worker.d.ts +0 -1
- package/dist/types/link-store.d.ts +0 -130
- package/dist/types/management-policy.d.ts +0 -9
- package/dist/types/sqlite-version.d.ts +0 -1
- package/docs/DYNAMIC-LINKS.md +0 -561
- package/docs/MANAGEMENT-SECURITY.md +0 -82
- package/examples/live-links/README.md +0 -11
- package/examples/live-links/tests/requests.json +0 -6
- package/examples/live-links/urlcode.yaml +0 -16
package/docs/POLICIES.md
CHANGED
|
@@ -40,7 +40,22 @@ routes:
|
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
`policies` at the top level sets project defaults; `routes.<path>.policies`
|
|
43
|
-
adjusts them for one route.
|
|
43
|
+
adjusts them for one route. Two route-level short forms exist. `auth`
|
|
44
|
+
expands to `policies.extensions.auth` when the project declares an auth
|
|
45
|
+
[extension](EXTENSIONS.md). `cache: {strategy, maxAge, ...}` expands to
|
|
46
|
+
`policies.cache` the same way — the compiler merges it into that route's
|
|
47
|
+
`policies` before anything else reads the project, so `routes`, `audit` and
|
|
48
|
+
`explain` see only the expanded long form, and it is refused alongside a
|
|
49
|
+
direct `policies.cache` on the same route (use one form):
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
routes:
|
|
53
|
+
/feed:
|
|
54
|
+
function: { source: functions/feed.mjs }
|
|
55
|
+
cache: { strategy: swr, maxAge: 30 } # expands to policies: { cache: {...} }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Both accept the same keys: `profile` plus one entry
|
|
44
59
|
per policy, each either an object or `false`. Unknown keys fail validation, as
|
|
45
60
|
everywhere in the project format. The
|
|
46
61
|
[field reference](YAML-REFERENCE.md) lists every accepted field with its
|
|
@@ -178,312 +193,11 @@ When any policy is declared anywhere in the project, every route is compiled
|
|
|
178
193
|
against the effective table; a route with nothing effective simply has empty
|
|
179
194
|
chains. When none is declared, no policy code runs.
|
|
180
195
|
|
|
181
|
-
##
|
|
182
|
-
|
|
183
|
-
`policies.profile: hardened` expands to the following and nothing else, so it
|
|
184
|
-
can be read in one place and overridden key by key. This is
|
|
185
|
-
`builtinProfiles.hardened` in `src/policies.ts`:
|
|
186
|
-
|
|
187
|
-
```yaml
|
|
188
|
-
policies:
|
|
189
|
-
security: { headers: oshp }
|
|
190
|
-
agents: { deny: [ai-crawlers], status: 403 }
|
|
191
|
-
throttle: { quota: 120, window: 60, partition: client, status: 429 }
|
|
192
|
-
compression: { encodings: [br, gzip], minBytes: 1024 }
|
|
193
|
-
cache: { strategy: revalidate }
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
The numbers are starting points chosen to be safe for a single small instance;
|
|
197
|
-
they are not tuned for any workload and not a security assessment of your
|
|
198
|
-
deployment. Note what the profile implies per target: on Vercel and AWS it
|
|
199
|
-
refuses activation as written, because `throttle.partition: client` is
|
|
200
|
-
refused there; set `throttle: false` (or `partition: route`) at the project
|
|
201
|
-
or route level. `compression` is delegated to the platform. On Cloudflare
|
|
202
|
-
only `agents` and `security` survive and `compression` is delegated, so the
|
|
203
|
-
profile must also drop `throttle` and `cache` there.
|
|
204
|
-
There is no `strict` profile: anything stricter is a per-project decision.
|
|
205
|
-
|
|
206
|
-
## The policy contract in TypeScript
|
|
207
|
-
|
|
208
|
-
The policies are modules of one shape, `PolicyModule<Config, State>` in
|
|
209
|
-
`src/types.ts`: `targets`, `compile`, `onRequest`, `onResponse`, optional
|
|
210
|
-
`onError`, `describe` and `close`. `@jimhoyd/urlcode/policies` exports that type with
|
|
211
|
-
`PolicyRequest`, `PolicyContext`, `PolicyChain`, `PolicyShared` and
|
|
212
|
-
`PolicyRegistry` (the five built-ins keyed by name), and the declarations ship
|
|
213
|
-
with the package. A per-policy configuration is typed as the YAML it accepts,
|
|
214
|
-
and a `profiles` layer may hold a partial one:
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
import { registry, targets, type PolicyRegistry, type PolicyRequest } from '@jimhoyd/urlcode/policies';
|
|
218
|
-
|
|
219
|
-
const throttle: PolicyRegistry['throttle'] = registry.throttle; // PolicyModule<ThrottleConfig, ThrottleState>
|
|
220
|
-
const support = throttle.targets({ quota: 120, window: 60, partition: 'client', status: 429 }); // per-target support for this config
|
|
221
|
-
console.log(support.vercel, support.cloudflare);
|
|
222
|
-
function inspect(request: PolicyRequest): void { console.log(request.route, request.client, targets); }
|
|
223
|
-
```
|
|
196
|
+
## Further pages
|
|
224
197
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
should be blocked. Ways to express your own:
|
|
232
|
-
|
|
233
|
-
- **Custom profiles.** Define any number under `profiles` and select one per
|
|
234
|
-
project or per route. Profiles are plain data and travel with the YAML.
|
|
235
|
-
- **Per-route overrides.** Any key can be tightened, replaced or set to
|
|
236
|
-
`false` on a route.
|
|
237
|
-
- **Own agent lists.** `agents.deny` and `agents.allow` accept bundled list
|
|
238
|
-
names and project-relative `.json` files in the same schema, so a list you
|
|
239
|
-
do not want to redistribute stays yours. `denyPatterns`/`allowPatterns`
|
|
240
|
-
take a bounded, linear-time pattern subset. See [agents](policies/agents.md).
|
|
241
|
-
- **Header by header.** `security.set` adds or overrides a header and wins over
|
|
242
|
-
the profile, YAML `response.headers` and handler output; `security.unset`
|
|
243
|
-
drops one the profile would emit. Headers the runtime or a handler owns
|
|
244
|
-
(`content-type`, `cache-control`, `set-cookie`, `etag`, `location`, and the
|
|
245
|
-
rest listed in `src/policies/security.ts`) cannot be `set`.
|
|
246
|
-
See [security](policies/security.md).
|
|
247
|
-
- **Explicit cache fields.** A strategy sets defaults; `maxAge`,
|
|
248
|
-
`staleWhileRevalidate`, `staleIfError`, `cdnMaxAge`, `originTtl`, `vary`,
|
|
249
|
-
`statuses`, `maxBytes` and `maxEntries` override what it implies.
|
|
250
|
-
See [cache](policies/cache.md).
|
|
251
|
-
- **Plugins.** Verified-bot checks, shared-store throttling, purge endpoints
|
|
252
|
-
and anything vendor-specific are host code an operator passes in;
|
|
253
|
-
see [plugins](PLUGINS.md).
|
|
254
|
-
|
|
255
|
-
## Client identity and `--trusted-proxies`
|
|
256
|
-
|
|
257
|
-
`throttle` partitions by `client`. On the self-hosted server the client is the
|
|
258
|
-
socket peer unless `urlcode serve --trusted-proxies 10.0.0.0/8,fd00::/8`
|
|
259
|
-
names the addresses allowed to speak for a client. Then `X-Forwarded-For` is
|
|
260
|
-
walked from the right, skipping trusted hops, and the first untrusted address is
|
|
261
|
-
the client; a chain made only of trusted proxies yields its leftmost entry, and
|
|
262
|
-
a malformed entry is skipped. A forwarded header from a peer
|
|
263
|
-
outside the trusted set is ignored, as is a request carrying more than one
|
|
264
|
-
`X-Forwarded-For` field. Ranges are IPv4 or IPv6 CIDRs (at most 256);
|
|
265
|
-
IPv4-mapped IPv6 peers match IPv4 ranges. `startServer({ trustedProxies })`
|
|
266
|
-
takes the same list.
|
|
267
|
-
|
|
268
|
-
A request whose client cannot be resolved (an adapter without a peer, an
|
|
269
|
-
embedding caller that passes none) shares one bucket rather than being exempt,
|
|
270
|
-
so a misconfigured proxy fails closed. The throttle summary in
|
|
271
|
-
`testPlan().policies` records this as `unresolvedClient: "shared key"`. The runtime still
|
|
272
|
-
never trusts forwarded headers for its public origin; set `--origin`
|
|
273
|
-
explicitly, as [resilience](RESILIENCE.md) already requires.
|
|
274
|
-
|
|
275
|
-
## What `routes` and `audit` report
|
|
276
|
-
|
|
277
|
-
`urlcode routes` prints the inventory with a `policies` array per route naming
|
|
278
|
-
the policies effective on it (`testPlan().inventory[].policies`) and the full
|
|
279
|
-
`policies` map. The embedding API and a plugin's `onActivate` see
|
|
280
|
-
`testPlan().policies`, a map from route pattern to each policy's summary with
|
|
281
|
-
its `target` value (`native`, `compiled` or `delegated`), the per-route
|
|
282
|
-
capability table the portability rule calls for. `urlcode audit` prints the
|
|
283
|
-
same table under `policies` and, with `--compliance`, checks the declared
|
|
284
|
-
configuration against standards-referenced rules; see
|
|
285
|
-
[compliance](COMPLIANCE.md). `urlcode doctor` lists the policy names this
|
|
286
|
-
runtime knows.
|
|
287
|
-
|
|
288
|
-
## Logging
|
|
289
|
-
|
|
290
|
-
Policies log through the runtime's request log with one-line events:
|
|
291
|
-
`{ event: 'throttle', route, outcome: 'exceeded' | 'allowed', remaining }`,
|
|
292
|
-
`{ event: 'agents', route, list, outcome: 'denied' | 'reported' }` and the
|
|
293
|
-
cache events described on the [cache page](policies/cache.md). Events name the
|
|
294
|
-
configured route pattern and the list or strategy, never a client address, a
|
|
295
|
-
User-Agent string or request text. A logging failure never changes a response.
|
|
296
|
-
|
|
297
|
-
## Hardened configuration guidance
|
|
298
|
-
|
|
299
|
-
Advice, not defaults, condensed from the spike's section 6.
|
|
300
|
-
|
|
301
|
-
1. **Network and edge first.** Volumetric protection, TLS termination and
|
|
302
|
-
per-client connection budgets stay with the provider or the reverse proxy.
|
|
303
|
-
Runtime policies are a second layer, never the first.
|
|
304
|
-
2. **Ingress to origin.** Bind privately; allow only the proxy's addresses;
|
|
305
|
-
pass `--trusted-proxies` so `client` partitioning sees the real peer.
|
|
306
|
-
3. **Request policies.** Agents before throttle: denials are cheaper than
|
|
307
|
-
counting. Start throttle in `mode: report` for a release to see real
|
|
308
|
-
quotas in the headers and logs, then switch to `enforce`.
|
|
309
|
-
4. **Allow before deny.** Keep an explicit allow for the crawlers you need
|
|
310
|
-
indexed; a broad deny without one is the common self-inflicted outage.
|
|
311
|
-
5. **Route contract.** Exact methods, `request.body` limits and `expires` on
|
|
312
|
-
campaign routes still do most of the work.
|
|
313
|
-
6. **Response policies.** Security headers on every route; compression only
|
|
314
|
-
on listed types and never on secret-bearing responses (the BREACH class of
|
|
315
|
-
attack, which is why compression is skipped where a route declares secrets
|
|
316
|
-
unless `allowWithSecrets` says otherwise); caching only with a strategy
|
|
317
|
-
whose semantics you can state, `immutable` only on content-hashed paths,
|
|
318
|
-
`no-store` everywhere else.
|
|
319
|
-
7. **Lists as pinned data.** Bundled agent lists ship with the release, so a
|
|
320
|
-
rollback rolls the list back too.
|
|
321
|
-
8. **Read the table.** Check `urlcode routes` on each target you deploy to;
|
|
322
|
-
the same YAML is refused where it cannot be enforced, and that is the
|
|
323
|
-
point.
|
|
324
|
-
|
|
325
|
-
## Interoperability
|
|
326
|
-
|
|
327
|
-
Every pair of policies, and every policy against the runtime's own responses,
|
|
328
|
-
was reviewed by reading the code and by exercising a server. The rules below
|
|
329
|
-
are the ones the current code enforces; where a combination misbehaves, the
|
|
330
|
-
rule says so and names what to do instead.
|
|
331
|
-
|
|
332
|
-
### Cannot coexist
|
|
333
|
-
|
|
334
|
-
Hard conflicts: the runtime refuses activation, or the combination produces
|
|
335
|
-
a wrong answer today.
|
|
336
|
-
|
|
337
|
-
- **Origin cache and conditional or range requests.** A request carrying
|
|
338
|
-
`If-None-Match`, `If-Modified-Since`, `If-Match`, `If-Unmodified-Since` or
|
|
339
|
-
`Range` is never answered from the origin cache: a stored entry is a full
|
|
340
|
-
`200` representation and the handler owns validators and ranges, so such
|
|
341
|
-
requests always reach it and get the handler's `304`, `412` or `206`.
|
|
342
|
-
Unconditional requests are served from the entry.
|
|
343
|
-
- **`security.unset` and a route that switches profile.** `unset` is
|
|
344
|
-
validated against the profile in effect on that route. A project-level
|
|
345
|
-
`unset` merges into a route that names a profile without that header
|
|
346
|
-
(`headers: off`, or `oshp-no-csp` with `unset: [Content-Security-Policy]`)
|
|
347
|
-
and activation fails with `unset names "...", which the off profile does
|
|
348
|
-
not emit`. Write `unset: []` on that route: a route key replaces the whole
|
|
349
|
-
array.
|
|
350
|
-
- **`security.set` of `Cache-Control`, `Content-Encoding`, `ETag`,
|
|
351
|
-
`Content-Type`, `Set-Cookie`, `Location` and the other reserved names** is
|
|
352
|
-
refused at activation (`is owned by the runtime or handler`). `Vary`,
|
|
353
|
-
`RateLimit`, `RateLimit-Policy`, `Retry-After` and `Age` are reserved for
|
|
354
|
-
the same reason: the cache and throttle policies own them, and a `set` of
|
|
355
|
-
any of the five is refused at activation with the route named. Declare
|
|
356
|
-
`vary` on the cache policy instead.
|
|
357
|
-
- **`agents.denyEmpty`, `throttle` and the generated probes.** `urlcode
|
|
358
|
-
audit`, `urlcode test` and `urlcode benchmark` send generated probes as
|
|
359
|
-
`Mozilla/5.0 (compatible; RouteProbe/0.1)`,
|
|
360
|
-
so `denyEmpty` does not fail them; a deny pattern that matches that string
|
|
361
|
-
would. A tight `throttle` fails an audit once the probes exceed `quota`
|
|
362
|
-
(the audit runs from one address, so a `static` tree with more files than
|
|
363
|
-
the quota trips it): keep the quota at least the number of generated cases
|
|
364
|
-
or run the audit in `mode: report`.
|
|
365
|
-
- **`hardened` on Vercel and AWS** is refused as written, because
|
|
366
|
-
`throttle.partition: client` cannot be honoured there; **on Cloudflare**
|
|
367
|
-
the build refuses `throttle` and `cache`. Override the offending keys with
|
|
368
|
-
`false` or `partition: route` as the profile section above describes.
|
|
369
|
-
- **`compression.encodings: [zstd]` on a Node without `zlib.zstdCompressSync`**
|
|
370
|
-
fails activation with the route named rather than serving identity.
|
|
371
|
-
|
|
372
|
-
### Coexist with defined precedence
|
|
373
|
-
|
|
374
|
-
Pairs that work, with the rule the code applies.
|
|
375
|
-
|
|
376
|
-
- **Request order is `agents`, `throttle`, cache lookup.** A denied agent is
|
|
377
|
-
never counted and never looked up; a throttled request is never looked up.
|
|
378
|
-
An agent on an `allow` list passes `agents` and is still throttled. An
|
|
379
|
-
`agents` denial and a `throttle` refusal both pass through the cache
|
|
380
|
-
policy's response hook (they carry no flight, so nothing is stored) and
|
|
381
|
-
pick up its declared `vary` names, then security headers, then
|
|
382
|
-
`Vary: Accept-Encoding` from compression; their bodies are below `minBytes`
|
|
383
|
-
so they are never encoded.
|
|
384
|
-
- **Cache hit and the response phase.** A hit skips only the cache's own
|
|
385
|
-
response hook: it still carries the client's `RateLimit` headers (the
|
|
386
|
-
request was counted), the security profile, and is compressed on the way
|
|
387
|
-
out. Stored entries are the handler's bytes and headers after YAML
|
|
388
|
-
`response.headers` and the cache policy ran, before throttle, security and
|
|
389
|
-
compression: bodies are stored uncompressed and encoded again on every hit,
|
|
390
|
-
and a `RateLimit` value is never stored. A cache hit on an asset serves the
|
|
391
|
-
same snapshot buffer, so a precompressed variant is used for `GET` and
|
|
392
|
-
reported at the variant's length for `HEAD`, exactly as when uncached.
|
|
393
|
-
- **Cache `Vary` and compression `Vary`.** The cache merges its declared
|
|
394
|
-
`vary` names first; compression appends `Accept-Encoding` without
|
|
395
|
-
duplicating it and leaves a `Vary: *` alone. The origin key uses only the
|
|
396
|
-
cache's names: because bodies are stored uncompressed, `Accept-Encoding` is
|
|
397
|
-
not part of the key and need not be.
|
|
398
|
-
- **ETags across `cache` and `compression`.** `revalidate` keeps a handler or
|
|
399
|
-
asset `ETag` and computes a strong SHA-256 tag for a `200` without one.
|
|
400
|
-
Compression then weakens a dynamically encoded body's tag (`W/"..."`) and
|
|
401
|
-
suffixes a precompressed asset's (`"...-gz"`). Revalidation works in every
|
|
402
|
-
combination: the cache's `304` compares weakly, the asset handler compares
|
|
403
|
-
weakly against the identity tag, and compression answers `304` for a
|
|
404
|
-
suffixed tag it produced. The `304` that `revalidate` produces keeps
|
|
405
|
-
`Content-Type`, so compression adds `Vary: Accept-Encoding` to it as it
|
|
406
|
-
does to the asset handler's own `304`.
|
|
407
|
-
- **Who owns `Cache-Control`.** YAML `response.headers` first, then an asset
|
|
408
|
-
handler's `cacheControl` when the cache policy is only inherited, then a
|
|
409
|
-
handler's `private` or `no-store`, then the strategy; `security.set` cannot
|
|
410
|
-
name it. A `no-store` or `private` handler answer is never stored whatever
|
|
411
|
-
the strategy says.
|
|
412
|
-
- **Security headers and everything else.** Profile headers fill gaps only:
|
|
413
|
-
YAML `response.headers`, function, asset, redirect and early-denial headers
|
|
414
|
-
keep their values. `set` overrides all of them. `Strict-Transport-Security`
|
|
415
|
-
needs an `https` origin. The profile is applied on cache hits and on early
|
|
416
|
-
denials (including a plugin short-circuit), on the self-hosted server and
|
|
417
|
-
in the Worker alike.
|
|
418
|
-
- **Compression and secrets or cookies.** A route with `secrets` or a
|
|
419
|
-
response with `Set-Cookie` is sent as identity (still with `Vary`) unless
|
|
420
|
-
`allowWithSecrets: true`. A route with `secrets` also never enters the
|
|
421
|
-
origin cache, so the two policies agree on what a secret-bearing route is.
|
|
422
|
-
- **Throttle and the 405.** The request phase runs before the method check,
|
|
423
|
-
so a request that ends as `405` was counted, and the `405` passes through
|
|
424
|
-
the response phase: it carries the `RateLimit` headers, the security
|
|
425
|
-
profile and plugin `onResponse` rewrites. Nothing stores it.
|
|
426
|
-
- **Plugins and policies.** `onRequest` runs before every policy; a
|
|
427
|
-
short-circuit result skips `agents`, `throttle` and the cache lookup
|
|
428
|
-
(nothing is counted, matched or stored) and skips the cache store and the
|
|
429
|
-
throttle headers, then receives security headers and compression, then
|
|
430
|
-
every plugin's `onResponse`. On a thrown error the policies' `onError`
|
|
431
|
-
hooks run first (in the order the policies were declared) and then plugin
|
|
432
|
-
`onError` hooks in reverse. Only the cache policy has an error hook today
|
|
433
|
-
and it never returns a fallback (`stale-if-error` is header-only); if a
|
|
434
|
-
future policy did, that fallback would pass through the response phase and
|
|
435
|
-
every plugin `onResponse`, and plugin `onError` would not run for that
|
|
436
|
-
request.
|
|
437
|
-
- **Reload.** Every reload builds a new runtime with fresh shared state:
|
|
438
|
-
throttle counters start empty and the origin cache and precompressed
|
|
439
|
-
variants are rebuilt; a client mid-window gets a fresh budget. The plugin
|
|
440
|
-
objects are the ones the operator passed and persist across reloads:
|
|
441
|
-
`onActivate` runs for the new runtime before `onClose` runs for the old,
|
|
442
|
-
so a plugin's own state (a shared-store connection, a `WeakMap`) survives a
|
|
443
|
-
reload unless its `onClose` discards it. A plugin whose `onActivate`
|
|
444
|
-
throws makes the reload fail and the old runtime keeps serving.
|
|
445
|
-
- **Merge edge cases.** A route `profile` that lacks a key the project layer
|
|
446
|
-
had leaves that key in force (layers merge, they do not replace); `false`
|
|
447
|
-
at the project level followed by an object at the route level re-enables
|
|
448
|
-
the policy with the route's object alone, so it must be complete
|
|
449
|
-
(`throttle` needs `quota` and `window`); a custom profile named like a
|
|
450
|
-
built-in shadows it everywhere.
|
|
451
|
-
- **Memory bounds add up.** Per runtime: 64 MiB of asset snapshot, up to
|
|
452
|
-
64 MiB of precompressed variants, up to 64 MiB of origin-cache bodies (a
|
|
453
|
-
stored asset references the snapshot buffer rather than copying it, but
|
|
454
|
-
is counted against the cache budget), and the throttle table at the
|
|
455
|
-
largest `maxKeys` (100,000 keys by default). Two routes serving the same
|
|
456
|
-
file compress it separately and both count. During a reload both
|
|
457
|
-
generations exist at once. [Capacity](CAPACITY.md) states the asset,
|
|
458
|
-
throttle and cache figures; the precompressed budget belongs in that table
|
|
459
|
-
too.
|
|
460
|
-
- **Cloudflare.** The artifact carries the effective `agents` and `security`
|
|
461
|
-
configuration per route, with the entries of any project list file
|
|
462
|
-
embedded under the reference as written in YAML (no filesystem path); a
|
|
463
|
-
route-level `agents: false` leaves the artifact without that key. The
|
|
464
|
-
Worker runs `agents` on the request and `security` on the response, so an
|
|
465
|
-
early denial carries the profile exactly as on the self-hosted server.
|
|
466
|
-
|
|
467
|
-
### Not covered by policies
|
|
468
|
-
|
|
469
|
-
- **Every error the runtime throws** (404 for no match, disabled route or
|
|
470
|
-
missing link, 410, 400/413/415 from body checks, 502/503/504 from the
|
|
471
|
-
sandbox or a link store) bypasses the response phase: no `Vary`, no
|
|
472
|
-
`RateLimit`, no compression, no plugin `onResponse`, on every target.
|
|
473
|
-
What they do get is the `security` policy: the matched route's effective
|
|
474
|
-
profile when the error came after routing (so a route with
|
|
475
|
-
`security: false` answers its 410 bare), otherwise the project-level
|
|
476
|
-
profile, including a host-side error such as an oversized body or shed
|
|
477
|
-
admission and the Worker's own 404. The runtime's fixed headers
|
|
478
|
-
(`Content-Type`, `Cache-Control: no-store`, `Content-Length`,
|
|
479
|
-
`X-Request-Id`, `X-Content-Type-Options`) can never be replaced by it.
|
|
480
|
-
Plugins keep `onError` for observation; a policy error hook may answer with
|
|
481
|
-
a fallback, and none does today.
|
|
482
|
-
- **The audit's probes** share one address and one `User-Agent`, so they do
|
|
483
|
-
not exercise `agents` or `throttle` the way real traffic does;
|
|
484
|
-
the policy table in `testPlan().policies` is the audit's evidence for
|
|
485
|
-
those two.
|
|
486
|
-
- **`stale-if-error` at the origin**: header-only, as the
|
|
487
|
-
[cache page](policies/cache.md) states.
|
|
488
|
-
- **Cross-instance state**: counters and the cache are per runtime on every
|
|
489
|
-
target; a shared budget or a shared cache is a plugin.
|
|
198
|
+
| Page | Sections |
|
|
199
|
+
|---|---|
|
|
200
|
+
| [The built-in `hardened` profile and hardening guidance](policies/hardened.md) | The built-in `hardened` profile; Hardened configuration guidance |
|
|
201
|
+
| [The policy contract and your own patterns](policies/contract.md) | The policy contract in TypeScript; Supplying your own patterns |
|
|
202
|
+
| [Client identity, inventory and logging](policies/operations.md) | Client identity and `--trusted-proxies`; What `routes` and `audit` report; Logging |
|
|
203
|
+
| [Interoperability between policies](policies/interoperability.md) | Interoperability |
|
package/docs/PRERENDER.md
CHANGED
|
@@ -14,7 +14,7 @@ and no capability.
|
|
|
14
14
|
## Why render ahead of time
|
|
15
15
|
|
|
16
16
|
A function response body is readable by middleware through `text()`. A native
|
|
17
|
-
`page`, `static`, `download`, `redirect
|
|
17
|
+
`page`, `static`, `download`, `redirect` or `respond` body is not — that
|
|
18
18
|
opacity is a deliberate contract, not a gap, and it is what keeps native file
|
|
19
19
|
serving free of guest code. See [middleware](MIDDLEWARE.md).
|
|
20
20
|
|
|
@@ -145,6 +145,8 @@ directory inside the serving project, keeping the render source outside it.
|
|
|
145
145
|
|
|
146
146
|
| Limit | Value | Where |
|
|
147
147
|
|---|---|---|
|
|
148
|
+
| Function modules per snapshot | 127 | source project; the render splits into passes |
|
|
149
|
+
| Function module source bytes | 1 MiB each, 4 MiB total per snapshot | source project; the render splits into passes |
|
|
148
150
|
| Function/middleware response body | 1 MiB default (`--max-response-bytes`) | render step |
|
|
149
151
|
| Rendered page bytes | 512 KiB (`maxPageBytes`) | helper |
|
|
150
152
|
| Rendered pages, total bytes | 500, 32 MiB (`maxPages`, `maxTotalBytes`) | helper |
|
|
@@ -159,6 +161,44 @@ snapshots. A large site is bounded by the generated project's memory, not by the
|
|
|
159
161
|
render step. For collections beyond these budgets, publish to an external asset
|
|
160
162
|
service and redirect; provider asset adapters are not implemented.
|
|
161
163
|
|
|
164
|
+
## Function budgets
|
|
165
|
+
|
|
166
|
+
The first two rows above are the sandbox's snapshot budgets: at most 127 guest
|
|
167
|
+
modules and 4 MiB of module source in one snapshot. They are deliberate — part
|
|
168
|
+
of what [function security](FUNCTION-SECURITY.md) promises about sandboxed guest
|
|
169
|
+
code — and the render step does not relax them for trusted generated content.
|
|
170
|
+
Serving a project that crosses either still fails at startup, naming the module
|
|
171
|
+
that crossed it:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
ConfigError: Function source limit exceeded: /pages/reference.mjs (12841 bytes)
|
|
175
|
+
brings the snapshot to 4196103 bytes, over the total limit of 4194304 bytes
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`prerenderPages` does not inherit that as a page ceiling. Before rendering it
|
|
179
|
+
measures each route's module closure, reading sources only, and packs the routes
|
|
180
|
+
into **passes** that each stay inside the budgets. It then builds one runtime per
|
|
181
|
+
pass, holding only that pass's snapshot, and renders that pass's pages. A render
|
|
182
|
+
that needs more than one pass logs `{event: 'prerender-passes', passes}`.
|
|
183
|
+
|
|
184
|
+
Nothing about the contract changes: all passes render before anything is
|
|
185
|
+
written, into one output directory that must not already exist, so a failure in
|
|
186
|
+
the last pass leaves no partial artifact — the same atomicity a single pass has.
|
|
187
|
+
Output filenames are checked for collision across passes, and `maxPages`,
|
|
188
|
+
`maxTotalBytes` and the returned fixtures count the whole render, not a pass.
|
|
189
|
+
|
|
190
|
+
Two consequences worth knowing:
|
|
191
|
+
|
|
192
|
+
- **A module shared by every page is paid for in every pass.** A template
|
|
193
|
+
middleware is counted once per pass, not once per render, so it costs bytes
|
|
194
|
+
against each pass's budget.
|
|
195
|
+
- **One route must still fit one snapshot.** A single route whose own modules
|
|
196
|
+
and their imports exceed the budgets cannot be split, and fails with the
|
|
197
|
+
collector's message. That is a route to make smaller, not a pass to add.
|
|
198
|
+
|
|
199
|
+
The [urlcode-docs showcase](https://github.com/jimhoyd-com/urlcode-docs) renders
|
|
200
|
+
62 documentation pages this way.
|
|
201
|
+
|
|
162
202
|
## Larger sites: generating the source project
|
|
163
203
|
|
|
164
204
|
The example keeps page content as reviewed literal `args` in YAML, which stays
|
|
@@ -6,12 +6,31 @@ under. It describes intent and
|
|
|
6
6
|
boundaries, not a delivery schedule; the [roadmap](../ROADMAP.md) owns sequence
|
|
7
7
|
and the [readiness register](RELEASE-READINESS.md) owns what is proven.
|
|
8
8
|
|
|
9
|
+
## Design principle: declarative-first
|
|
10
|
+
|
|
11
|
+
> Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement.
|
|
12
|
+
|
|
13
|
+
This is the default for human authors, coding agents, examples and framework
|
|
14
|
+
changes. Before writing application code, check the installed version's declarative
|
|
15
|
+
primitives, YAML configuration, policies, supported extensions and reusable recipes
|
|
16
|
+
or templates. Prefer the highest-level supported abstraction that satisfies the
|
|
17
|
+
requirement; do not recreate framework behavior in a lower-level handler.
|
|
18
|
+
|
|
19
|
+
Custom functions or middleware remain valid for application-specific behavior the
|
|
20
|
+
framework cannot express. Keep that code focused, explain the missing capability,
|
|
21
|
+
and report reusable gaps to the owning repository. Never invent YAML fields or
|
|
22
|
+
bypass target limits, sandbox isolation or operator grants to avoid custom code.
|
|
23
|
+
|
|
9
24
|
## What URLCode is
|
|
10
25
|
|
|
11
26
|
A portable runtime for programmable URL behavior. A project declares its public
|
|
12
|
-
URL surface in YAML, adds
|
|
27
|
+
URL surface in YAML, adds JavaScript only where declarative handlers are
|
|
13
28
|
not enough, and runs the same definition locally, in a container, or on operator
|
|
14
|
-
infrastructure.
|
|
29
|
+
infrastructure. That JavaScript runs trusted, in the host process, like any
|
|
30
|
+
other project code, unless the project isolates a specific route with
|
|
31
|
+
`sandbox: true` (docs/SPIKE-DEFAULT-TRUST-MODEL.md) — a judgment call the
|
|
32
|
+
project makes per route, not a default the runtime imposes on all guest code.
|
|
33
|
+
The project format is deliberately bounded so a runtime can
|
|
15
34
|
validate it, inspect it, test it and eventually carry it across hosting providers.
|
|
16
35
|
|
|
17
36
|
Git owns route definitions and code. Operators own credentials, storage and
|
|
@@ -19,12 +38,18 @@ capability grants. Application data stays in the operator's systems.
|
|
|
19
38
|
|
|
20
39
|
## What URLCode is not
|
|
21
40
|
|
|
22
|
-
- **Not a URL shortener.**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
- **Not a URL shortener.** Core is redirects, validated responses, request
|
|
42
|
+
functions, middleware, pages, static assets and downloads. Stored short
|
|
43
|
+
links are moving to a future `urlcode-dynamic-link` extension package
|
|
44
|
+
(mount-based, like `auth`/`admin`, not yet published), not a core handler.
|
|
45
|
+
- **Not a general Node web framework.** There is no framework code to write
|
|
46
|
+
for routing, validation, middleware wiring or policies — those are declared
|
|
47
|
+
in YAML and enforced by the runtime. Function/middleware code that needs
|
|
48
|
+
isolation from the host (untrusted input, an unreviewed contribution, a
|
|
49
|
+
particularly sensitive secret) opts into `sandbox: true`, which runs it
|
|
50
|
+
inside WASM isolation with no ambient filesystem, network or Node APIs.
|
|
51
|
+
Behavior that cannot be expressed in the bounded contract of a sandboxed
|
|
52
|
+
route is rejected rather than emulated.
|
|
28
53
|
- **Not a hosting account system.** There is no end-user identity, billing or
|
|
29
54
|
public account surface. Management is a private operator API.
|
|
30
55
|
- **Not a provider configuration format.** Provider infrastructure settings do
|
|
@@ -39,6 +64,48 @@ private fork or privileged capability. If an application needs something the
|
|
|
39
64
|
runtime cannot express, that is a gap in the public contract to close in the
|
|
40
65
|
open, not a reason for a special path. See the [roadmap](../ROADMAP.md).
|
|
41
66
|
|
|
67
|
+
## Why: your AI should build your application, not your framework
|
|
68
|
+
|
|
69
|
+
Coding agents are good at infrastructure, so they build it every time: routing,
|
|
70
|
+
sessions, validation, middleware, security headers, static serving, redirects,
|
|
71
|
+
webhooks, admin plumbing, deployment glue, tests. The application the person
|
|
72
|
+
asked for arrives last, and the person then owns twenty thousand lines instead
|
|
73
|
+
of two thousand. Cheap generation makes unnecessary code cheap to create and
|
|
74
|
+
expensive to keep.
|
|
75
|
+
|
|
76
|
+
URLCode's answer is the one databases gave a generation ago. Nobody asks a
|
|
77
|
+
model to write B-tree traversal; it writes `SELECT * FROM customers WHERE id = ?`
|
|
78
|
+
and the database owns the machinery. One level up, a route should read
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
/admin:
|
|
82
|
+
auth: { role: admin }
|
|
83
|
+
function: { source: functions/admin.mjs }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
and the runtime should own how. The agent describes what; URLCode owns how.
|
|
87
|
+
YAML is not the innovation and neither is the runtime. The innovation is a
|
|
88
|
+
small, deterministic vocabulary that is optimized for two readers at once: the
|
|
89
|
+
person who opens `urlcode.yaml`, and the agent that writes it.
|
|
90
|
+
|
|
91
|
+
Three tests keep this from becoming a YAML replacement for every framework:
|
|
92
|
+
|
|
93
|
+
- **The boundary test.** Is an agent repeatedly generating this code across
|
|
94
|
+
unrelated projects? If yes, it is a candidate primitive, policy, recipe or
|
|
95
|
+
extension. If no, it stays application code.
|
|
96
|
+
- **The feature test.** Does this reduce what the agent has to know, generate,
|
|
97
|
+
debug or maintain? If yes, it belongs on the roadmap. A feature that exists
|
|
98
|
+
because other web frameworks have it does not.
|
|
99
|
+
- **The evidence test.** The framework grows from measured repetition, not
|
|
100
|
+
from a list of things applications might need.
|
|
101
|
+
|
|
102
|
+
The metric that matters is the **application-specific code ratio**: of the
|
|
103
|
+
lines an agent generated, how many are the idea and how many are plumbing. A
|
|
104
|
+
traditional build might be 2,900 lines of business logic inside 18,400; the
|
|
105
|
+
same application on URLCode should be the same 2,900 inside a few thousand.
|
|
106
|
+
Until a reproducible benchmark shows that ratio, the thesis is a hypothesis,
|
|
107
|
+
and [next steps](NEXT-STEPS.md) puts the benchmark before the features.
|
|
108
|
+
|
|
42
109
|
## License
|
|
43
110
|
|
|
44
111
|
The runtime is free and open-source software under the
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Provider conformance and deployment evidence
|
|
2
|
+
|
|
3
|
+
The synthetic project in [`examples/provider-conformance`](../examples/provider-conformance)
|
|
4
|
+
exercises the common declarative runtime subset: redirect status/location, dropped
|
|
5
|
+
incoming queries, validated mapped queries, encoded path components, constant
|
|
6
|
+
responses, HEAD, method refusal, request-body limits and missing routes. It has
|
|
7
|
+
no bindings, function code, customer data or provider infrastructure configuration.
|
|
8
|
+
|
|
9
|
+
`test/provider-verification.test.ts` replays the same 12 versioned cases through
|
|
10
|
+
an actual local self-hosted HTTP server, a local Vercel Node handler, AWS payload
|
|
11
|
+
v2 and a freshly built Cloudflare artifact. This establishes local adapter
|
|
12
|
+
behavior only. It does not exercise provider ingress, deployment configuration,
|
|
13
|
+
TLS termination, production DNS, scaling or provider accounts.
|
|
14
|
+
|
|
15
|
+
## Record a deployment observation
|
|
16
|
+
|
|
17
|
+
First deploy the synthetic fixture into a disposable environment you own using
|
|
18
|
+
the [AWS](AWS.md), [Vercel](VERCEL.md) or [Cloudflare](CLOUDFLARE.md) adapter. Do not point this runner at a
|
|
19
|
+
third-party service or an unrelated production application: it sends three POST
|
|
20
|
+
requests to fixture paths, whose declared behavior has no side effects.
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { verifyProviderDeployment } from '@jimhoyd/urlcode';
|
|
24
|
+
const evidence = await verifyProviderDeployment(
|
|
25
|
+
'vercel',
|
|
26
|
+
'https://your-owned-fixture.example',
|
|
27
|
+
{ timeoutMs: 3000, gitCommit: 'your-commit-id', release: 'your-release-id' }
|
|
28
|
+
);
|
|
29
|
+
console.log(JSON.stringify(evidence, null, 2));
|
|
30
|
+
if (!evidence.pass) process.exitCode = 1;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The CLI form is `urlcode verify-provider --target vercel --origin https://owned-fixture.example [--timeout-ms 3000] [--release label] [--git-commit sha]`, where `--release` and `--git-commit` are the caller-supplied labels recorded in the report and `--timeout-ms` is the per-request deadline.
|
|
34
|
+
|
|
35
|
+
Targets are `self-hosted`, `aws`, `vercel` and `cloudflare`. The caller must supply
|
|
36
|
+
an HTTPS origin without credentials, path, query or fragment. TLS verification
|
|
37
|
+
is mandatory. The runner does not provision resources, read credentials, follow
|
|
38
|
+
redirects or fetch redirect destinations. Every request has an absolute deadline
|
|
39
|
+
(default 3 seconds, configurable 50–10,000 ms); the suite has a 60-second deadline.
|
|
40
|
+
Responses stop at 64 KiB and headers at 16 KiB. It sends at most 12 requests,
|
|
41
|
+
sequentially, using identity encoding. Timeout, oversized body and TLS failures
|
|
42
|
+
produce failed findings. Reports include no arbitrary response body/header data.
|
|
43
|
+
|
|
44
|
+
`runProviderConformance(target, transport, options)` supports local adapter
|
|
45
|
+
replays through an explicit callback. The transport receives an AbortSignal;
|
|
46
|
+
callbacks must honor it to release their own resources. The runner can bound
|
|
47
|
+
waiting for a custom callback, but cannot terminate arbitrary caller code.
|
|
48
|
+
Its report always says `evidence: "local-adapter"` and
|
|
49
|
+
`providerVerification: "unverified"`.
|
|
50
|
+
|
|
51
|
+
## Evidence interpretation
|
|
52
|
+
|
|
53
|
+
Reports have `schemaVersion: 1`, `fixtureVersion: 1`, target, origin, timestamp,
|
|
54
|
+
a fixture-case SHA-256, caller-supplied release/Git labels, request count, overall
|
|
55
|
+
`pass` and a finding for every case. Findings report expected/observed status
|
|
56
|
+
and which assertion failed. The case digest identifies probe expectations;
|
|
57
|
+
it is not an artifact or deployed configuration digest.
|
|
58
|
+
|
|
59
|
+
Live HTTP reports say `evidence: "deployment-http"` and
|
|
60
|
+
`providerVerification: "observed"`, including failed attempts. **Observed does
|
|
61
|
+
not mean passing**: inspect `pass` and every finding. Target/provider identity,
|
|
62
|
+
ownership and release labels are caller assertions, not independent attestation.
|
|
63
|
+
Save reports alongside a Git revision and deployment identity in the operator's
|
|
64
|
+
release records; review origins and labels before publishing them.
|
|
65
|
+
|
|
66
|
+
No real AWS, Vercel or Cloudflare deployment evidence is checked in. Provider
|
|
67
|
+
provisioning and deployment verification remain pending operator-owned accounts
|
|
68
|
+
and explicit deployment URLs. CI passing must never be reported as an actual
|
|
69
|
+
provider deployment result, independent security review, or soak/recovery proof.
|
|
70
|
+
|
|
71
|
+
## Remaining transport differences
|
|
72
|
+
|
|
73
|
+
AWS payload v2 coalesces repeated header values; its adapter conservatively
|
|
74
|
+
interprets comma-separated non-cookie headers as repeats. Vercel's local Node
|
|
75
|
+
adapter can observe raw header counts; real ingress may normalize first.
|
|
76
|
+
Cloudflare's Fetch interface can expose already-coalesced values and normalized
|
|
77
|
+
URLs. Encoded slashes, malformed URL syntax, repeated scalar headers and multiple
|
|
78
|
+
Set-Cookie delivery require target-specific edge checks beyond this common suite.
|
|
79
|
+
The local adapter regression suites retain those implementation-level checks.
|
|
80
|
+
|
|
81
|
+
This fixture intentionally has no policy settings. A passing common-subset
|
|
82
|
+
report does not establish compression, distributed throttling, caching, agent
|
|
83
|
+
policy updates or project-specific guarantees. Use project-specific deployment
|
|
84
|
+
verification and production operational checks in addition to this small suite.
|
package/docs/READINESS.md
CHANGED
|
@@ -22,6 +22,13 @@ and active/disabled/expired state. It includes routes from YAML includes. A
|
|
|
22
22
|
parameter pattern is one route; its possible URLs are not a finite route count.
|
|
23
23
|
A static mount is one route, even when it contains many files.
|
|
24
24
|
|
|
25
|
+
`routes --compare previous.json` diffs the current inventory against a saved
|
|
26
|
+
`routes` report: added, removed and changed routes (handler, methods, state,
|
|
27
|
+
middleware count, policies, generated marker and the policy description). It
|
|
28
|
+
prints JSON, or Markdown tables with `--format markdown`, and always exits 0;
|
|
29
|
+
it reports, it does not judge. The [GitHub action](CI.md) posts this diff on
|
|
30
|
+
pull requests.
|
|
31
|
+
|
|
25
32
|
`audit --expect-routes N` compares N with the total configured count. Its summary
|
|
26
33
|
separately counts active, disabled and expired routes and groups by handler.
|
|
27
34
|
A mismatch exits nonzero. Keep N reviewed in your application CI so accidentally
|
|
@@ -117,7 +124,20 @@ The runtime suite covers many generic protocol/security/reload cases. Apps must
|
|
|
117
124
|
supply their own business and boundary fixtures. Automated remote destination
|
|
118
125
|
health, redirect-chain/loop analysis, DNS/TLS checks, sustained soak/load profiles,
|
|
119
126
|
coverage by function branch and historical performance comparison remain planned.
|
|
120
|
-
Run the local audit in CI now
|
|
127
|
+
Run the local audit in CI now (the [project action](CI.md) wires validate, test,
|
|
128
|
+
audit and the route diff into GitHub pull requests); do not label a passing local
|
|
129
|
+
audit “production certified.”
|
|
130
|
+
Once a candidate is deployed, `urlcode verify-deployment --target` compares its
|
|
131
|
+
responses with this project; see [deployment checks](DEPLOYMENT-CHECKS.md).
|
|
132
|
+
|
|
133
|
+
Before writing fixtures, `urlcode explain /route` shows what the compiled
|
|
134
|
+
configuration will do for a path: effective methods, the handler, the middleware
|
|
135
|
+
chain, validated inputs, the policies in effect and the cache outcome, so a
|
|
136
|
+
fixture asserts declared behavior rather than a guess. `urlcode manifest --json`
|
|
137
|
+
(also written by `build` as `manifest.json`) lists every route, the capabilities
|
|
138
|
+
in use, external requirements and per-target support with the revision digest,
|
|
139
|
+
which is the document to attach to a release review. Both read the
|
|
140
|
+
configuration only; they are not evidence that a deployment serves it.
|
|
121
141
|
|
|
122
142
|
Routes with middleware need explicit request fixtures with meaningful response
|
|
123
143
|
assertions for every active method. Audit cannot infer their behavior from the
|