@jimhoyd/urlcode 0.4.2 → 0.4.6

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.
Files changed (214) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +19 -15
  4. package/SECURITY.md +5 -3
  5. package/dist/BUILD-MANIFEST.json +30 -27
  6. package/dist/agent-context.js +82 -0
  7. package/dist/agents-guide.js +42 -42
  8. package/dist/authoring.js +12 -2
  9. package/dist/body-schema.js +159 -0
  10. package/dist/build-cloudflare.js +2 -0
  11. package/dist/capabilities.js +1 -1
  12. package/dist/cli.js +28 -12
  13. package/dist/config.js +74 -6
  14. package/dist/context.js +4 -6
  15. package/dist/errors.js +3 -1
  16. package/dist/examples.js +1 -1
  17. package/dist/extensions.js +60 -2
  18. package/dist/http-policy.js +19 -4
  19. package/dist/http-response.js +2 -2
  20. package/dist/init-with.js +71 -9
  21. package/dist/mcp.js +15 -2
  22. package/dist/pattern-guard.js +32 -0
  23. package/dist/policies/security.js +0 -0
  24. package/dist/project-tests.js +35 -11
  25. package/dist/readiness.js +184 -32
  26. package/dist/recipes.js +1 -1
  27. package/dist/router.js +17 -0
  28. package/dist/runtime.js +26 -3
  29. package/dist/scaffold.js +0 -0
  30. package/dist/server.js +26 -3
  31. package/dist/site.js +0 -0
  32. package/dist/tooling.js +2 -1
  33. package/dist/types/agent-context.d.ts +44 -0
  34. package/dist/types/authoring.d.ts +3 -1
  35. package/dist/types/body-schema.d.ts +58 -0
  36. package/dist/types/config.d.ts +10 -2
  37. package/dist/types/context.d.ts +1 -1
  38. package/dist/types/errors.d.ts +9 -1
  39. package/dist/types/examples.d.ts +1 -1
  40. package/dist/types/extensions.d.ts +47 -2
  41. package/dist/types/http-policy.d.ts +3 -0
  42. package/dist/types/init-with.d.ts +10 -1
  43. package/dist/types/pattern-guard.d.ts +10 -0
  44. package/dist/types/project-tests.d.ts +9 -0
  45. package/dist/types/readiness.d.ts +68 -0
  46. package/dist/types/recipes.d.ts +1 -1
  47. package/dist/types/runtime.d.ts +4 -0
  48. package/dist/types/server.d.ts +9 -1
  49. package/dist/types/tooling.d.ts +1 -0
  50. package/dist/types/types.d.ts +13 -0
  51. package/dist/types.js +10 -2
  52. package/dist/typescript-authoring.js +5 -3
  53. package/dist/verify-deployment.js +16 -10
  54. package/examples/body-validation/README.md +16 -0
  55. package/examples/body-validation/example.yaml +17 -0
  56. package/examples/body-validation/tests/requests.json +15 -0
  57. package/examples/body-validation/urlcode.yaml +34 -0
  58. package/examples/coverage-waiver/README.md +8 -0
  59. package/examples/coverage-waiver/example.yaml +16 -0
  60. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  61. package/examples/coverage-waiver/tests/requests.json +3 -0
  62. package/examples/coverage-waiver/urlcode.yaml +10 -0
  63. package/examples/data-dir/README.md +39 -0
  64. package/examples/data-dir/data/welcome.txt +1 -0
  65. package/examples/data-dir/example.yaml +22 -0
  66. package/examples/data-dir/functions/note.mjs +18 -0
  67. package/examples/data-dir/tests/requests.json +6 -0
  68. package/examples/data-dir/urlcode.yaml +6 -0
  69. package/examples/lifecycle/README.md +15 -0
  70. package/examples/lifecycle/example.yaml +19 -0
  71. package/examples/lifecycle/functions/notes.mjs +29 -0
  72. package/examples/lifecycle/tests/requests.json +12 -0
  73. package/examples/lifecycle/urlcode.yaml +29 -0
  74. package/examples/not-found/README.md +10 -0
  75. package/examples/not-found/example.yaml +17 -0
  76. package/examples/not-found/public/404.html +3 -0
  77. package/examples/not-found/public/index.html +3 -0
  78. package/examples/not-found/tests/requests.json +7 -0
  79. package/examples/not-found/urlcode.yaml +7 -0
  80. package/examples/shared-blocks/README.md +11 -0
  81. package/examples/shared-blocks/example.yaml +17 -0
  82. package/examples/shared-blocks/tests/requests.json +7 -0
  83. package/examples/shared-blocks/urlcode.yaml +37 -0
  84. package/llms-full.txt +364 -49
  85. package/llms.txt +43 -5
  86. package/package.json +15 -22
  87. package/recipes/static-page/README.md +9 -0
  88. package/recipes/static-page/public/index.html +11 -0
  89. package/recipes/static-page/recipe.yaml +21 -0
  90. package/recipes/static-page/tests/requests.json +22 -0
  91. package/recipes/static-page/urlcode.yaml +7 -0
  92. package/recipes/static-plus-api/README.md +6 -0
  93. package/recipes/static-plus-api/urlcode.yaml +4 -0
  94. package/recipes/store-crud/README.md +53 -0
  95. package/recipes/store-crud/recipe.yaml +31 -0
  96. package/recipes/store-crud/tests/requests.json +18 -0
  97. package/recipes/store-crud/urlcode.yaml +18 -0
  98. package/schemas/urlcode.schema.json +125 -60
  99. package/skills/urlcode/SKILL.md +47 -27
  100. package/starters/default/AGENTS.md +43 -43
  101. package/starters/page/README.md +14 -0
  102. package/starters/page/public/index.html +12 -0
  103. package/starters/page/tests/requests.json +17 -0
  104. package/starters/page/urlcode.yaml +6 -0
  105. package/.claude-plugin/marketplace.json +0 -18
  106. package/CONTRIBUTING.md +0 -148
  107. package/ROADMAP.md +0 -70
  108. package/docs/AI-AUTHORING.md +0 -339
  109. package/docs/ASSETS.md +0 -107
  110. package/docs/AUTH-BACKUP.md +0 -32
  111. package/docs/AWS.md +0 -86
  112. package/docs/BEST-PRACTICES.md +0 -276
  113. package/docs/BULK.md +0 -79
  114. package/docs/CAPABILITIES.md +0 -192
  115. package/docs/CAPACITY.md +0 -305
  116. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  117. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  118. package/docs/CI.md +0 -147
  119. package/docs/CLOUDFLARE.md +0 -109
  120. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  121. package/docs/COMPLIANCE.md +0 -239
  122. package/docs/COMPOSING-A-SITE.md +0 -287
  123. package/docs/CONDITIONS.md +0 -74
  124. package/docs/CONTAINER-PROMOTION.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -304
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -403
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -251
  131. package/docs/HTTP.md +0 -129
  132. package/docs/INSTALL.md +0 -128
  133. package/docs/INTERCHANGE.md +0 -134
  134. package/docs/LOAD-TESTING.md +0 -91
  135. package/docs/LOCAL-DEVELOPMENT.md +0 -102
  136. package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
  137. package/docs/MIDDLEWARE.md +0 -102
  138. package/docs/MONITORING.md +0 -115
  139. package/docs/OBSERVABILITY.md +0 -222
  140. package/docs/OPEN-DECISIONS.md +0 -149
  141. package/docs/OPERATIONAL-PROOF.md +0 -41
  142. package/docs/OPERATIONS.md +0 -201
  143. package/docs/ORGANIZATION.md +0 -135
  144. package/docs/PERFORMANCE.md +0 -72
  145. package/docs/PLUGINS.md +0 -271
  146. package/docs/POLICIES.md +0 -211
  147. package/docs/PRERENDER.md +0 -245
  148. package/docs/PROJECT-DIRECTION.md +0 -118
  149. package/docs/PROVIDER-VERIFICATION.md +0 -84
  150. package/docs/READINESS.md +0 -153
  151. package/docs/README.md +0 -93
  152. package/docs/RECIPES.md +0 -99
  153. package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
  154. package/docs/RELEASE-0.4.1.md +0 -73
  155. package/docs/RELEASE-0.4.2.md +0 -30
  156. package/docs/RELEASE-READINESS.md +0 -146
  157. package/docs/RELEASE-SECURITY.md +0 -100
  158. package/docs/RESILIENCE.md +0 -161
  159. package/docs/ROUTING.md +0 -92
  160. package/docs/SANDBOX-REVIEW.md +0 -72
  161. package/docs/SCAFFOLDING.md +0 -70
  162. package/docs/SECURITY-AUDIT.md +0 -164
  163. package/docs/SITE.md +0 -150
  164. package/docs/SPECIFICATION.md +0 -359
  165. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  166. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  167. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  168. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
  169. package/docs/STANDARDS.md +0 -311
  170. package/docs/STARTERS.md +0 -83
  171. package/docs/STATIC.md +0 -105
  172. package/docs/TOOLING.md +0 -298
  173. package/docs/TUNNELS.md +0 -72
  174. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  175. package/docs/TYPESCRIPT.md +0 -123
  176. package/docs/VERCEL.md +0 -114
  177. package/docs/VERSION-ALIGNMENT.md +0 -88
  178. package/docs/YAML-GUIDE.md +0 -57
  179. package/docs/YAML-REFERENCE.md +0 -449
  180. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  181. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  182. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  183. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  184. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  185. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  186. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  188. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  189. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  190. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  191. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  192. package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
  193. package/docs/archive/README.md +0 -29
  194. package/docs/policies/agents.md +0 -182
  195. package/docs/policies/cache.md +0 -152
  196. package/docs/policies/compression.md +0 -169
  197. package/docs/policies/contract.md +0 -52
  198. package/docs/policies/hardened.md +0 -56
  199. package/docs/policies/interoperability.md +0 -169
  200. package/docs/policies/operations.md +0 -45
  201. package/docs/policies/security.md +0 -161
  202. package/docs/policies/throttle.md +0 -103
  203. package/docs/yaml/assets.md +0 -36
  204. package/docs/yaml/conditions.md +0 -20
  205. package/docs/yaml/functions.md +0 -168
  206. package/docs/yaml/middleware.md +0 -31
  207. package/docs/yaml/organization.md +0 -74
  208. package/docs/yaml/policies.md +0 -37
  209. package/docs/yaml/redirects.md +0 -64
  210. package/docs/yaml/responses.md +0 -57
  211. package/docs/yaml/site.md +0 -24
  212. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  213. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
  214. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
package/docs/POLICIES.md DELETED
@@ -1,211 +0,0 @@
1
- # Policies
2
-
3
- Policies are host-enforced behavior declared in YAML around a route: a
4
- per-client request budget, a User-Agent deny list, a security-header profile,
5
- content-coding negotiation and an HTTP caching strategy. They run in the host
6
- process, outside function/middleware execution — trusted or sandboxed alike —
7
- so they can see the client address, keep counters across requests and touch
8
- the transport, which route middleware by design cannot
9
- ([middleware](MIDDLEWARE.md), [function security](FUNCTION-SECURITY.md)).
10
-
11
- Everything here is optional and off by default. A project with no `policies`
12
- key and no `profiles` key behaves exactly as before: no policy module is
13
- compiled and the request path is unchanged. Each policy has its own page:
14
-
15
- | Policy | Page | What it does |
16
- |---|---|---|
17
- | `throttle` | [throttle](policies/throttle.md) | Sliding-window request budget with `RateLimit`/`RateLimit-Policy` headers, 429 and `Retry-After` |
18
- | `agents` | [agents](policies/agents.md) | Deny or allow by `User-Agent`, from bundled lists, project files and bounded patterns |
19
- | `security` | [security](policies/security.md) | OWASP Secure Headers Project response headers, with per-header `set`/`unset` |
20
- | `compression` | [compression](policies/compression.md) | `Accept-Encoding` negotiation; assets precompressed at snapshot time |
21
- | `cache` | [cache](policies/cache.md) | Named strategies from RFC 9111/5861/8246/9213 plus an origin memory cache |
22
-
23
- The design and the reasoning behind each choice are in the
24
- [extensions spike](archive/2026-09-19/SPIKE-EXTENSIONS.md). Operators who need behavior the
25
- declarative block cannot express pass host [plugins](PLUGINS.md) in code; plugins
26
- are never named in YAML.
27
-
28
- ## Declaring policies
29
-
30
- ```yaml
31
- version: "1"
32
- policies: # project defaults; every key optional
33
- profile: hardened # built-in, or a name under `profiles`
34
- throttle: { quota: 60, window: 60 }
35
- routes:
36
- /api/lookup/{id}:
37
- function: { source: functions/lookup.mjs }
38
- policies:
39
- throttle: { quota: 10, window: 60 }
40
- cache: false
41
- ```
42
-
43
- `policies` at the top level sets project defaults; `routes.<path>.policies`
44
- adjusts them for one route. Two route-level short forms exist. `auth`
45
- expands to `policies.extensions.auth` when the project declares an auth
46
- [extension](EXTENSIONS.md), carrying the same keys minus `required`;
47
- `required: false` documents the intent and emits no policy. It accepts
48
- `required`, `role`, `permission`, `verified`, `freshWithinSeconds` and
49
- `onDeny` and nothing else — `role` is singular, and there is no `roles`. Like
50
- `cache` below, it is refused rather than silently ignored in three cases: when
51
- the project declares no `extensions.auth`, when the route also sets
52
- `policies.extensions.auth` (use one form), and when the route sets
53
- `policies.extensions: false`. `cache: {strategy, maxAge, ...}` expands to
54
- `policies.cache` the same way — the compiler merges it into that route's
55
- `policies` before anything else reads the project, so `routes`, `audit` and
56
- `explain` see only the expanded long form, and it is refused alongside a
57
- direct `policies.cache` on the same route (use one form):
58
-
59
- ```yaml
60
- routes:
61
- /feed:
62
- function: { source: functions/feed.mjs }
63
- cache: { strategy: swr, maxAge: 30 } # expands to policies: { cache: {...} }
64
- ```
65
-
66
- Both accept the same keys: `profile` plus one entry
67
- per policy, each either an object or `false`. Unknown keys fail validation, as
68
- everywhere in the project format. The
69
- [field reference](YAML-REFERENCE.md) lists every accepted field with its
70
- bounds; the [JSON Schema](../schemas/urlcode.schema.json) is the source.
71
-
72
- Values are behavior, never infrastructure: a quota is a number a proxy or CDN
73
- can restate, while trusted proxy ranges, storage URLs and vendor rule
74
- identifiers stay in operator configuration. That is what keeps a project
75
- portable between a laptop, a container and an adapter.
76
-
77
- ## Where policies run
78
-
79
- Policies run once the route is known and before its contract is checked. A
80
- denied agent or an exhausted budget is answered without reading a body or
81
- starting the sandbox. The self-hosted pipeline:
82
-
83
- ```
84
- socket limits → admission → body read
85
- → runtime.handle(): match → enabled/expires
86
- → plugins onRequest (outermost, first plugin first)
87
- → agents → throttle → cache lookup (request phase; may short-circuit)
88
- → methods (405) → request.body checks
89
- → native handler | guest chain
90
- → YAML response.headers
91
- → cache store → throttle headers → security → compression (response phase)
92
- → plugins onResponse (reverse order, first plugin last)
93
- → writeResponse
94
- ```
95
-
96
- Request order is fixed: agents first because a denial is the cheapest
97
- outcome, then throttle, then the cache lookup, so a denied or refused request
98
- neither counts against a budget nor touches the cache. Response order is the
99
- cache store, then the throttle's `RateLimit` headers (after the store, so a
100
- cached copy is never stamped with one client's remaining budget), then
101
- security headers, then compression last so every header it depends on is
102
- already final. YAML `response.headers` are applied by the runtime before the
103
- response phase, so an explicit header always beats a profile default.
104
-
105
- A result produced by a request-phase policy (an agent denial, a throttle
106
- refusal, a cache hit) skips that policy's own response hook and passes through
107
- the others: a hit is not stored twice but still carries the client's
108
- rate-limit headers, and a denial is not stored because its status is not
109
- cacheable. A throttle refusal carries its own `RateLimit` and `Retry-After`
110
- headers. A plugin short-circuit ran before any policy, so it skips the
111
- response hook of every policy that has a request phase; security headers and
112
- compression still apply to it.
113
-
114
- Two responses bypass the response phase entirely: the `405` the runtime returns
115
- for an undeclared method, and any error the runtime throws (404 for no match,
116
- 410 for an expired route, 413/415 from body checks, sandbox 502/504). Those
117
- receive the runtime's standard headers only. Policies with an `onError` hook
118
- and plugin `onError` hooks observe thrown errors; they cannot change them.
119
-
120
- Adapters call `handle()` directly and delegate socket limits and admission to
121
- the provider; the policy order inside `handle()` is the same. The Cloudflare
122
- Worker runs the same request and response order with the two policies it can
123
- carry.
124
-
125
- ## Portability and the per-target table
126
-
127
- A project is portable when a second person can run it elsewhere and get the
128
- same declared behavior or an explicit refusal. A target that cannot honor a
129
- policy refuses activation naming the route and the policy, exactly as adapters
130
- refuse functions and middleware; nothing degrades silently. One exception is
131
- stated rather than hidden: a policy the platform itself already provides is
132
- **delegated**, meaning accepted and dropped, and reported as such in the
133
- inventory, so one YAML can serve a Node host and a serverless host without
134
- edits. The self-hosted
135
- message reads `/path declares policies.throttle, which the vercel target
136
- cannot enforce`; the Cloudflare build reports
137
- `/path: policies.throttle cannot be compiled for this target`.
138
-
139
- | Policy | Self-hosted (`node`) | Vercel / AWS | Cloudflare build |
140
- |---|---|---|---|
141
- | `agents` | native | native | compiled into the artifact |
142
- | `security` | native | native | compiled into the artifact |
143
- | `throttle` | native, in-process counters | native only with `partition: route`; `client` and `client-route` refused because a client fans across instances and the budget would silently be quota × instances | refused |
144
- | `compression` | native | delegated: the provider compresses | delegated |
145
- | `cache` | native: headers plus origin memory cache | native | refused |
146
-
147
- "Compiled" means the effective configuration for every route is validated at
148
- build time and carried in the Worker artifact; the Worker has no filesystem and
149
- no Node imports, so only modules free of both qualify. The Worker's client
150
- identity is the platform's `cf-connecting-ip`; the serverless adapters use the
151
- platform-set source address (`sourceIp` on Lambda, the leftmost
152
- `X-Forwarded-For` entry the platform writes on Vercel). None of these read a
153
- forwarded header a client could have set.
154
-
155
- The cross-request state a policy keeps (throttle counters, the origin cache) is
156
- per runtime instance on every target, never shared between replicas or
157
- serverless instances. Given identical request bytes and project, every target
158
- answers with the same status and headers; only that state may differ.
159
-
160
- ## Merge semantics
161
-
162
- The effective configuration for a route is built from four layers, each
163
- merged over the one below:
164
-
165
- 1. the project `profile` (built-in or custom),
166
- 2. the project `policies` keys,
167
- 3. the route `profile`, if the route names one,
168
- 4. the route `policies` keys.
169
-
170
- Within a layer, `false` removes a policy declared below it; an object merges
171
- shallowly over what is there, so a route can tighten one number without
172
- restating the rest. Only top-level keys of each policy merge: a route that
173
- writes `agents: { deny: [crawlers] }` replaces the whole `deny` array, not one
174
- entry of it.
175
-
176
- ```yaml
177
- policies:
178
- profile: hardened
179
- throttle: { quota: 5 } # hardened's window and partition remain
180
- profiles:
181
- mine:
182
- security: { headers: oshp-no-csp }
183
- routes:
184
- /feed:
185
- policies:
186
- profile: mine # merges over the project layer, not instead of it
187
- throttle: false # removed for this route only
188
- cache: { strategy: swr, maxAge: 3 }
189
- ```
190
-
191
- Here `/feed` ends up with `security: { headers: oshp-no-csp }`, the
192
- `hardened` agents and compression entries, no throttle, and the route's cache
193
- entry. Any other route gets `hardened` with `throttle.quota` at 5.
194
-
195
- `profiles` is a top-level map of reusable policy sets, each a `policies`
196
- object without a `profile` key. A custom profile whose name matches a built-in
197
- shadows it, so a project can redefine `hardened` and every reference to it
198
- resolves to the project's version. Naming an unknown profile fails validation.
199
-
200
- When any policy is declared anywhere in the project, every route is compiled
201
- against the effective table; a route with nothing effective simply has empty
202
- chains. When none is declared, no policy code runs.
203
-
204
- ## Further pages
205
-
206
- | Page | Sections |
207
- |---|---|
208
- | [The built-in `hardened` profile and hardening guidance](policies/hardened.md) | The built-in `hardened` profile; Hardened configuration guidance |
209
- | [The policy contract and your own patterns](policies/contract.md) | The policy contract in TypeScript; Supplying your own patterns |
210
- | [Client identity, inventory and logging](policies/operations.md) | Client identity and `--trusted-proxies`; What `routes` and `audit` report; Logging |
211
- | [Interoperability between policies](policies/interoperability.md) | Interoperability |
package/docs/PRERENDER.md DELETED
@@ -1,245 +0,0 @@
1
- # Prerendering pages into a native static project
2
-
3
- Render a project's function and middleware routes once, at build time, into a
4
- project whose routes are all native `page` handlers. The published site answers
5
- from prevalidated byte buffers: no guest code runs to serve a request at all,
6
- so whatever execution mode the source routes used — trusted by default, or
7
- isolated QuickJS/WASM where a route declares `sandbox: true` — along with its
8
- deadline and memory budget where sandboxed, is not on the request path.
9
-
10
- The shared orchestration ships as a build helper, `@jimhoyd/urlcode/prerender`, and the
11
- runnable recipe is [`examples/prerender`](../examples/prerender/README.md), which
12
- consumes it. Both are covered by `test/prerender.test.ts`. Everything here uses
13
- the existing runtime: prerendering adds no field to route YAML, no CLI command
14
- and no capability.
15
-
16
- ## Why render ahead of time
17
-
18
- A function response body is readable by middleware through `text()`. A native
19
- `page`, `static`, `download`, `redirect` or `respond` body is not — that
20
- opacity is a deliberate contract, not a gap, and it is what keeps native file
21
- serving free of guest code. See [middleware](MIDDLEWARE.md).
22
-
23
- So a shared template cannot be wrapped around a native file at request time. The
24
- answer is to apply the template while the content is still a function response,
25
- and publish the result:
26
-
27
- ```
28
- function + template middleware ──render once──▶ HTML file ──▶ page route
29
- (trusted or sandboxed, build time) (no guest code)
30
- ```
31
-
32
- The alternative — reading Markdown through `next().text()` on a native route —
33
- is not supported and should not be attempted. Prepare content at build time.
34
-
35
- ## The build helper
36
-
37
- ```js
38
- import {prerenderPages, assertNativeProject, pageFileName} from '@jimhoyd/urlcode/prerender';
39
-
40
- const rendered = await prerenderPages('./render-source', './out/pages', {
41
- origin: 'https://docs.example', // what a page sees as its own origin
42
- });
43
- // rendered.pages → [{path: '/guide', file: 'guide.html', bytes: 531}, …]
44
- // rendered.fixtures → byte-for-byte GET and empty HEAD cases, ready to extend
45
- // rendered.count, rendered.bytes, rendered.directory
46
- ```
47
-
48
- `prerenderPages` owns everything that is easy to get wrong and nothing that is
49
- site-specific. It activates the source project, selects the active literal GET
50
- function routes, renders each one through its middleware, checks the status and
51
- content type, enforces the budgets, derives and validates a safe output filename,
52
- writes the files and closes the runtime — then hands back metadata. It does not
53
- write a project, choose response headers, copy assets or compile content: the
54
- caller assembles a project, or a generated include, from `pages` and `fixtures`.
55
-
56
- `assertNativeProject(project, {allow})` activates a project and proves it cannot
57
- execute guest code to answer a request: every route is one of the allowed native
58
- handlers and none carries middleware. `allow` defaults to `['page', 'static',
59
- 'download']`, which is what a real site serves; narrow it to `['page']` for a
60
- page-only artifact. Run it on the **final assembled site**, not only on the
61
- rendered pages, so what you deploy is what was checked.
62
-
63
- | Option | Default | Meaning |
64
- |---|---|---|
65
- | `origin` | `http://localhost` | HTTP(S) origin a render sees; no path or credentials |
66
- | `fileName` | `pageFileName` | Route path to filename; the path and the result are both validated either way |
67
- | `ignoreUnrenderable` | `false` | Allow source routes this build will not render |
68
- | `maxPages` | 500 | Pages in one render |
69
- | `maxPageBytes` | 512 KiB | Bytes per rendered page |
70
- | `maxTotalBytes` | 32 MiB | Bytes across the whole render |
71
- | `log` | none | `{event: 'prerendered', path, file, bytes}` per page |
72
-
73
- ## What the helper guarantees
74
-
75
- The runtime enforces its own protections when a generated project activates:
76
- asset declarations reject absolute paths, traversal, dot segments, symlinks,
77
- hardlinks and nonregular files, and static trees refuse `urlcode.yaml`,
78
- `package.json`, `package-lock.json`, hidden entries, `node_modules` and
79
- `.pem/.key/.p12/.pfx/.env` files. See [assets](ASSETS.md).
80
-
81
- Those checks fail a deployment. The helper fails the build earlier and more
82
- specifically:
83
-
84
- - **Filenames are validated, never trusted.** `pageFileName` maps one route path
85
- to one flat name: segments joined with `~`, which cannot occur in a segment, so
86
- `/a/b` (`a~b.html`) and `/a-b` (`a-b.html`) are distinct rather than a silent
87
- collision, and the mapping stays injective for every accepted path. Dots,
88
- underscores and mixed case are fine, so a docs URL like `/docs/ASSETS.md`
89
- works. Parameters, wildcards, traversal and dot segments are rejected. A custom
90
- `fileName` hook is allowed — hashing the route is a reasonable choice — but its
91
- result goes through the same check: a flat name, no leading dot, not a
92
- protected name, `.html`, and unique **case-insensitively**, because on macOS
93
- and Windows two names differing only in case are one file.
94
- - **Directories may not overlap.** In either direction: a build must not write
95
- into the reviewed source, nor read a source nested inside its output.
96
- - **Nothing is written until everything renders.** Pages are held in memory and
97
- written only after the last one passes, and the pages directory must not
98
- already exist — it is created, along with any missing parents, only once every
99
- render has succeeded, so a failed build creates nothing at all. An existing
100
- pages directory is refused with an error carrying `code: 'EEXIST'`. If your
101
- artifact has a root above that directory, claiming it is yours: check it before
102
- calling, and let the helper create it as a parent after the render.
103
- - **Every render is checked.** A non-200 status, a content type that is not
104
- `text/html`, an empty body, an oversized body or an exceeded aggregate budget
105
- fails the build instead of publishing a file.
106
- - **Bytes are preserved.** The response body is a byte array, not a string. It is
107
- kept as a `Buffer` through the file and its fixture alike, so a multi-byte
108
- character is never re-encoded or truncated.
109
- - **Skipping is explicit.** By default a source route the build would not render
110
- fails it, because silently rendering a subset publishes an incomplete site that
111
- looks whole. Pass `ignoreUnrenderable` when a mixed project is intended.
112
- - **The runtime is always closed.** In a `finally`, so a failing build exits
113
- instead of hanging on its worker threads.
114
-
115
- The helper is operator build tooling. It runs in Node with normal filesystem
116
- access because it is not guest code; it does not itself widen a source route's
117
- declared execution mode — a route with `sandbox: true` still renders isolated,
118
- with no filesystem, and no host-code fallback is introduced for it. It is a
119
- separate package export from the runtime for that reason. Review it as you
120
- review any deployment tooling.
121
-
122
- ## Assembling a site
123
-
124
- What the helper returns is deliberately not a project, because that is the part
125
- every site does differently. A small site writes one `page` route per file, as
126
- [`examples/prerender`](../examples/prerender/README.md) does in about twenty
127
- lines. A larger one copies the rendered pages next to its own assets, adds
128
- `static` and `download` routes and response security headers, keeps a committed
129
- entry point and writes only a generated include, then extends `fixtures` with its
130
- own cases before asserting the whole thing is native:
131
-
132
- ```js
133
- const rendered = await prerenderPages(renderSource, 'project/public/pages');
134
- for (const page of rendered.pages)
135
- config.routes[page.path] = {page: {file: `public/pages/${page.file}`}, response: {headers: security}};
136
- await writeFile('project/generated/routes.yaml', stringify(config));
137
- await writeFile('project/tests/requests.json', JSON.stringify([...rendered.fixtures, ...ownCases]));
138
- await assertNativeProject('project');
139
- ```
140
-
141
- Applying your own `response.headers` is expected; the helper never chooses them
142
- for you and never discards them.
143
-
144
- A site that renders straight into the tree it serves, rather than into a staging
145
- project, needs no copy step at all — point `prerenderPages` at the pages
146
- directory inside the serving project, keeping the render source outside it.
147
-
148
- ## Limits worth knowing before you design a site
149
-
150
- | Limit | Value | Where |
151
- |---|---|---|
152
- | Function modules per snapshot | 127 | source project; the render splits into passes |
153
- | Function module source bytes | 1 MiB each, 4 MiB total per snapshot | source project; the render splits into passes |
154
- | Function/middleware response body | 1 MiB default (`--max-response-bytes`) | render step |
155
- | Rendered page bytes | 512 KiB (`maxPageBytes`) | helper |
156
- | Rendered pages, total bytes | 500, 32 MiB (`maxPages`, `maxTotalBytes`) | helper |
157
- | Middleware entries per route | 16 | source project |
158
- | Asset file size | 16 MiB | generated project |
159
- | Total unique asset bytes | 64 MiB | generated project |
160
- | Static entries traversed | 10,000 | generated project |
161
- | Directory depth | 20 | generated project |
162
-
163
- Startup snapshots asset bytes in memory, and a reload can briefly hold two
164
- snapshots. A large site is bounded by the generated project's memory, not by the
165
- render step. For collections beyond these budgets, publish to an external asset
166
- service and redirect; provider asset adapters are not implemented.
167
-
168
- ## Function budgets
169
-
170
- The first two rows above are the sandbox's snapshot budgets: at most 127 guest
171
- modules and 4 MiB of module source in one snapshot. They are deliberate — part
172
- of what [function security](FUNCTION-SECURITY.md) promises about sandboxed guest
173
- code — and the render step does not relax them for trusted generated content.
174
- Serving a project that crosses either still fails at startup, naming the module
175
- that crossed it:
176
-
177
- ```
178
- ConfigError: Function source limit exceeded: /pages/reference.mjs (12841 bytes)
179
- brings the snapshot to 4196103 bytes, over the total limit of 4194304 bytes
180
- ```
181
-
182
- `prerenderPages` does not inherit that as a page ceiling. Before rendering it
183
- measures each route's module closure, reading sources only, and packs the routes
184
- into **passes** that each stay inside the budgets. It then builds one runtime per
185
- pass, holding only that pass's snapshot, and renders that pass's pages. A render
186
- that needs more than one pass logs `{event: 'prerender-passes', passes}`.
187
-
188
- Nothing about the contract changes: all passes render before anything is
189
- written, into one output directory that must not already exist, so a failure in
190
- the last pass leaves no partial artifact — the same atomicity a single pass has.
191
- Output filenames are checked for collision across passes, and `maxPages`,
192
- `maxTotalBytes` and the returned fixtures count the whole render, not a pass.
193
-
194
- Two consequences worth knowing:
195
-
196
- - **A module shared by every page is paid for in every pass.** A template
197
- middleware is counted once per pass, not once per render, so it costs bytes
198
- against each pass's budget.
199
- - **One route must still fit one snapshot.** A single route whose own modules
200
- and their imports exceed the budgets cannot be split, and fails with the
201
- collector's message. That is a route to make smaller, not a pass to add.
202
-
203
- The `urlcode-docs` site rendered 62 documentation pages this way. That
204
- repository has since been deleted, so no link is given; the runnable
205
- version of the same pattern is [`examples/prerender`](../examples/prerender/README.md).
206
-
207
- ## Larger sites: generating the source project
208
-
209
- The example keeps page content as reviewed literal `args` in YAML, which stays
210
- readable and lets `dev` serve the site live. A site with hundreds of pages
211
- instead generates its source project from host-prepared content: the build reads
212
- its Markdown or data, compiles and sanitizes it in Node, writes a temporary
213
- project whose routes carry that HTML as literal arguments, renders it with the
214
- helper, and discards the temporary project.
215
-
216
- That keeps every property intact — content is still reviewed input, guest code
217
- still reads nothing from disk, and the published artifact is still inert. Two
218
- things to hold onto:
219
-
220
- - **Generated YAML is operator input.** It is written by your build, from your
221
- content, and reviewed like any other deployment artifact. Route YAML never
222
- gains the ability to name host code or a callback.
223
- - **Sanitize before rendering, not after.** The template escapes the values it
224
- interpolates, but content injected as raw HTML is published as written. Whatever
225
- produces that HTML owns its safety.
226
-
227
- The `urlcode-docs` site built itself this way before that repository was
228
- retired. It was a working integration, not a deployment or performance claim.
229
-
230
- ## What this is not
231
-
232
- Not a static-site generator: no Markdown, no sanitizer, no asset pipeline, no
233
- incremental or watch build, no link checking, no sitemap. Not a way to make
234
- native bodies readable. Not a template engine — the template is ordinary
235
- middleware you write. Not a CLI command: prerendering is a step inside a build
236
- that already runs JavaScript, so the helper is a library. Content compilation,
237
- sanitization, search, asset assembly and deployment stay in the application.
238
-
239
-
240
- Trusted build-time functions and middleware use ordinary Node imports, including
241
- npm packages and dynamic imports. The pass planner applies source-graph budgets
242
- only to `sandbox: true` routes; it does not parse trusted modules as sandbox
243
- code. Page-count and output-byte budgets still apply to both modes. After
244
- rendering, emit native page/file routes without middleware, then use the static
245
- build target. Static hosting cannot execute middleware on incoming requests.
@@ -1,118 +0,0 @@
1
- # Project direction
2
-
3
- This page states the product boundary: what URLCode is, what it is not, how
4
- application projects relate to the runtime, and the license it is released
5
- under. It describes intent and
6
- boundaries, not a delivery schedule; the [roadmap](../ROADMAP.md) owns sequence
7
- and the [readiness register](RELEASE-READINESS.md) owns what is proven.
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
-
24
- ## What URLCode is
25
-
26
- A portable runtime for programmable URL behavior. A project declares its public
27
- URL surface in YAML, adds JavaScript only where declarative handlers are
28
- not enough, and runs the same definition locally, in a container, or on operator
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
34
- validate it, inspect it, test it and eventually carry it across hosting providers.
35
-
36
- Git owns route definitions and code. Operators own credentials, storage and
37
- capability grants. Application data stays in the operator's systems.
38
-
39
- ## What URLCode is not
40
-
41
- - **Not a URL shortener.** Core is redirects, validated responses, request
42
- functions, middleware, pages, static assets and downloads. Stored short
43
- links are not a core handler; the `urlcode-dynamic-link` extension package
44
- that owned them has been retired and unpublished.
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.
53
- - **Not a hosting account system.** There is no end-user identity, billing or
54
- public account surface. Management is a private operator API.
55
- - **Not a provider configuration format.** Provider infrastructure settings do
56
- not belong in route behavior YAML. A project must remain runnable on a laptop
57
- or in a single container wherever else it is deployed.
58
-
59
- ## Application projects
60
-
61
- Applications built on URLCode are ordinary consumers of the public runtime.
62
- They exist to prove the contract is sufficient in practice, and they get no
63
- private fork or privileged capability. If an application needs something the
64
- runtime cannot express, that is a gap in the public contract to close in the
65
- open, not a reason for a special path. See the [roadmap](../ROADMAP.md).
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 [the roadmap](../ROADMAP.md) puts the benchmark before the features.
108
-
109
- ## License
110
-
111
- The runtime is free and open-source software under the
112
- [Apache License 2.0](../LICENSE). Contribution terms follow from the same
113
- license; see [contributing](../CONTRIBUTING.md).
114
-
115
- The license covers the runtime source in this repository. It grants no rights in
116
- any application's own routes, content or data, and it is not a warranty or a
117
- production-readiness claim — those are set by the
118
- [readiness register](RELEASE-READINESS.md) and [security policy](../SECURITY.md).
@@ -1,84 +0,0 @@
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.