@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
@@ -1,139 +0,0 @@
1
- # Historical record
2
-
3
- Archived 2026-09-19. This records an earlier implementation or proposal, not
4
- current instructions. See the [current roadmap](../../../ROADMAP.md),
5
- [current contract](../../SPECIFICATION.md) and [open decisions](../../OPEN-DECISIONS.md).
6
- Remaining acceptance work is not declared complete by archiving this record.
7
-
8
- <!-- trust-model-prose: historical-file -->
9
- <!-- guidance-claims: ignore-file -->
10
-
11
- # Usability review: URLCode against the tools people reach for instead
12
-
13
- Status: an honest assessment written 2026-09-18 against the source in the four
14
- repositories at that date. It is opinion grounded in the code and docs, not a
15
- benchmark. Its purpose is to name the friction a first-time user or an AI
16
- agent meets, compare it with what competing tools do, and rank the changes
17
- that would matter most. It does not change any contract by itself.
18
-
19
- > **Update:** written the day before `0.4.0-alpha.2` reversed the default
20
- > execution mode. Where the table below credits functions and middleware with
21
- > isolation, that now describes a route declaring `sandbox: true`; the default
22
- > is trusted, in-process execution with full Node, npm and filesystem access.
23
- > See [SPIKE-DEFAULT-TRUST-MODEL.md](../../SPIKE-DEFAULT-TRUST-MODEL.md).
24
-
25
- ## 1. Who the competitors are, by rung
26
-
27
- URLCode is not one product; it competes with a different tool on each rung
28
- of [the ladder](../../FRAMEWORK.md#the-ladder).
29
-
30
- | Rung | What people use today | Where URLCode is better | Where it is worse today |
31
- |---|---|---|---|
32
- | Redirects, responses | Netlify `_redirects`, Cloudflare `_redirects`/Rules, Vercel `vercel.json`, nginx | One portable file that validates before deploy, tests with fixtures, counts routes, converts from those formats and refuses lossy conversions | Those files are two columns; `urlcode.yaml` needs `version`, a route key and a handler object. Nobody needs a validator for ten redirects |
33
- | Pages, files | Any static host, Astro, Hugo | Same file, no build step, native ranges/ETags, `site` conventions | No content pipeline, no templating for pages outside the extension kit |
34
- | Functions, middleware | Cloudflare Workers, Vercel Functions, Hono, Express | Real isolation with a fresh heap per call; typed args from YAML; secrets only by revision-pinned grant; runs the same on a laptop | No `fetch`, no timers, no streaming, no npm packages in guests. Every framework above lets you call an API from a function; here you declare a `proxy` or stop |
35
- | Live links | Bitly, Dub, Short.io, a Postgres table | Records without reloads, versioned writes, private management API, no account system to run | Single host SQLite that needs a patched Node build; no dashboard until you install admin |
36
- | Accounts | Clerk, Auth0, Auth.js, Better Auth, Lucia, Supabase Auth, Devise | Everything is operator-owned and reviewable: no vendor, no callback URLs on someone else's dashboard, no per-MAU bill; passkeys, OIDC, TOTP, recovery and an account page in one package | Install is a reviewed-tarball ritual, a host file, a SHA-256 pin and a JSON-on-stdin bootstrap. Clerk is `npm i` and one env var. Better Auth is a config object and a CLI migration |
37
- | Administration | Django admin, Rails Administrate, Retool, Forest Admin, Supabase Studio | Built on the same service with two-person cases and audited reveal, which none of the generic admins give you | Only manages auth's entities; there is no way to expose the project's own data. Django admin is one line per model |
38
- | Full app in one place | Rails, Django, Laravel, Next.js + Clerk + Prisma, Supabase | The whole thing is declarative and portable; an agent can generate and validate it without a build; the trust boundary between generated code and the operator is explicit | Everything that is not a route, a function or an account is out of scope: no data model, no ORM, no forms for your own tables, no client bundle, no email templates for your app |
39
-
40
- The honest summary: URLCode wins on portability, validation, isolation and
41
- operator ownership, and loses on the first fifteen minutes and on anything
42
- that needs application data.
43
-
44
- ## 2. The first fifteen minutes
45
-
46
- What a new person does today, compared with the fastest competitor at each
47
- step. Times are what the docs imply, not measurements.
48
-
49
- | Step | URLCode today | Fastest competitor | Gap |
50
- |---|---|---|---|
51
- | Install | `npm i -g @jimhoyd/urlcode` or brew or a checksum script; three channels documented at equal weight | `npx create-next-app` | Too many equal choices on the first screen. Pick one, put the rest in the install guide |
52
- | First project | `urlcode init` writes a function route, a redirect and fixtures | `wrangler init` | Equal |
53
- | First redirect | 5 lines of YAML for one redirect | 1 line in `_redirects` | Acceptable once, heavy for 200. `bulk-import` exists but a person starting small never learns it |
54
- | First function | Must know: `parameters` with `schema`, `args` with `from: path`, the Request/Response subset, that `fetch` is absent | Hono: `app.get('/x', c => c.json(...))` | The typed-args design is right but needs a two-line minimal form. Today the smallest function route is 10 lines |
55
- | Add accounts | Clone two private repos, build tarballs with `pack-sources.mjs --core --auth --ui --admin --core-revision SHA`, install four tarballs, `urlcode-auth init`, compute `inspectExtensionRevision`, paste the SHA into an env var, `bootstrap` with JSON on stdin, `serve --host-file --origin` | Clerk: `npm i @clerk/nextjs`, one env var, wrap the app | This is the largest gap in the framework. Most of it is a consequence of the packages being unpublished and of the revision pin, and it is the step the README leads with |
56
- | Add admin | `urlcode-admin init` writes everything | Django `admin.site.register(Model)` | Comparable once installed |
57
- | Deploy | Container, Node process, or an adapter that refuses functions, links and extensions | `vercel deploy` | The refusals are correct but the person learns them at deploy time. `urlcode capabilities` exists and is not in the quick start |
58
-
59
- ## 3. What an AI agent meets
60
-
61
- The framework's claim is that an agent should build from a handful of
62
- redirects to a full application without rebuilding the core. Measured
63
- against that claim:
64
-
65
- - **Strong:** one JSON Schema, a generated field reference, fixtures the agent
66
- must write, `validate`, `test` and `audit` that give exact failures with the
67
- route named, an MCP server for read-only inspection, and a capability
68
- matrix that lists what does not exist. Few frameworks tell an agent what it
69
- cannot do. This is the right foundation.
70
- - **Weak:** the documentation was organized by feature history rather than by
71
- task. Before this review, `llms.txt` listed 40 documents at equal weight,
72
- three of them status logs, and the README opened with release history and
73
- "unreleased source" caveats. An agent reading it spent its context on
74
- provenance rather than on the shape of a project. The framework page and the
75
- reorganized index in this change address that; the remaining cost is the
76
- size of the reference documents themselves (the YAML guide, policies and
77
- dynamic links are each over 400 lines).
78
- - **Missing:** an agent cannot yet discover the extension packages' YAML from
79
- the core schema. `extensions.auth.config` is validated by auth's schema at
80
- activation, but there is no way to ask the installed runtime "what config
81
- does `auth` accept" without the host file. A `urlcode extensions --schema`
82
- command that reads the host file and prints each extension's configuration
83
- and policy schemas would close this, and would let `urlcode mcp` serve them.
84
- - **Missing:** no single command creates the whole layered project. Today it
85
- is `urlcode init`, then `urlcode-auth init`, then `urlcode-admin init`, each
86
- with its own directory conventions. One `urlcode init --with auth,admin`
87
- that delegates to the installed extension packages' scaffolds would make
88
- the ladder real for an agent.
89
-
90
- ## 4. Ranked recommendations
91
-
92
- Ordered by how much each would change the experience per unit of work, and
93
- whether it touches a contract.
94
-
95
- 1. **Publish the three extension packages** (even as `0.1.0-alpha` with the
96
- caveats their status files carry). Every install step in section 2's
97
- "add accounts" row except the revision pin existed because they were
98
- unpublished. This was a decision, not code; the repositories already had
99
- the release checks. No contract change. Done 2026-09-18: all three are on
100
- npm as `0.1.0-alpha.x` (review still pending, issue 58).
101
- 2. **`urlcode init --with auth,admin,ui`.** Delegate to each installed
102
- package's existing scaffold; write one host file and one README. No contract
103
- change; a CLI addition in core that calls into optional peers.
104
- 3. **Print extension schemas.** `urlcode extensions --host-file … --json`
105
- listing each registered extension's name, version, configuration schema and
106
- policy schema, and expose it through `urlcode mcp`. No contract change.
107
- 4. **A short form for the common function route.** Allow `function:
108
- functions/hello.mjs` as a string with path parameters inferred as required
109
- strings of bounded length, expanding to today's long form. This is a schema
110
- addition (`version: "1"` stays valid) and the single largest cut in YAML
111
- for first-time users and agents. Needs the usual generated-reference and
112
- cookbook updates.
113
- 5. **Lead the README with the ladder, not the release history.** Done in this
114
- change; keep it that way. Move status caveats to the readiness register.
115
- 6. **One install channel on the first screen.** npm first; brew, script and
116
- container in the install guide.
117
- 7. **Fold the `presentation`/`ui` split.** Auth and admin still render through
118
- the primitives while the kit is the documented way to restyle. Finishing
119
- kit adoption (already listed in the ui status file) removes the one place
120
- where the framework's story and its code differ.
121
- 8. **Later, and a real contract question:** application data. The thing every
122
- competitor on the last rung has and URLCode does not is a place for the
123
- project's own records with an admin view. The runtime already has one
124
- bounded store (links) and one admin surface (auth's entities). Whether a
125
- declared `collection` handler with operator-owned SQLite and an admin
126
- registration belongs in the framework is the next spike worth writing.
127
- Without it, "full-fledged application" means "site with accounts".
128
-
129
- ## 5. What is fine and should stay
130
-
131
- - The operator host file. It is the reason the project can be untrusted and
132
- portable at the same time; Clerk's convenience is bought with a vendor in
133
- the loop. Keep the boundary, make the file generated.
134
- - The revision pin. It is unusual and it is what makes "an agent changed the
135
- YAML" a reviewable event rather than a silent grant. Keep it, print it
136
- loudly, and make `init` write it.
137
- - Refusing instead of degrading on every target. The failure names the route.
138
- - The capability matrix in the AI guide. Extend it to the extension packages
139
- rather than softening it.
@@ -1,116 +0,0 @@
1
- # Historical record
2
-
3
- Archived 2026-09-20 from [open decisions](../../OPEN-DECISIONS.md), which was
4
- reconciled against its issues that day (#242). These sections record completed
5
- work and dated observations exactly as they stood on 2026-09-19 (core `db375bf`
6
- and the package sources of that date); they are not current instructions and
7
- their version numbers, dist-tags, commits and pins are not current. Versions
8
- and channels: [version alignment](../../VERSION-ALIGNMENT.md) and
9
- `npm run release:status`. Archiving does not declare any security, deployment,
10
- recovery or provider check performed. The live middleware decision remains in
11
- open decisions.
12
-
13
- <!-- trust-model-prose: historical-file -->
14
- <!-- guidance-claims: ignore-file -->
15
- <!-- local-links: historical-file -->
16
-
17
- ## Closed questions removed from the active list
18
-
19
- - Publishing convention is recorded in [version alignment](../../VERSION-ALIGNMENT.md):
20
- publishable manifests on main, releases through reviewed tags/workflows.
21
- - Core `0.4.0-alpha.2` and current extension releases exist; publishing that
22
- already-shipped version is not a next step.
23
- - Auth/admin kit adoption and shared form helpers are implemented in their code.
24
- - The UI primitive fallback is retired, which settles the question this table
25
- carried. Auth and admin now render every screen through the kit and refuse
26
- activation without it (`packages/auth/src/auth.ts`: "there is no
27
- shared-primitive fallback"), so "keep both" no longer describes the code.
28
- - The template pins `0.4.0-alpha.2`. Its skill differences were read against that
29
- pin: omitted handlers and advice about the removed management API are stale,
30
- not intentional older-version behavior.
31
- - The guidance checks run through `npm run check` inside `verify`; a regex check
32
- is not a schema validator for every example. Extending its coverage is tracked
33
- separately, not a reason to weaken review or bypass required checks.
34
-
35
- ## Done: the monorepo migration is complete
36
-
37
- **Closed 2026-09-19.** `urlcode-ui`, `urlcode-auth` and `urlcode-admin` are
38
- workspace packages under `packages/`, and all three have been released from
39
- this repository — `@jimhoyd/urlcode-ui@0.1.0-alpha.6`,
40
- `@jimhoyd/urlcode-auth@0.1.0-alpha.6`, `@jimhoyd/urlcode-admin@0.1.0-alpha.4`,
41
- each on `alpha` with `latest` deliberately held behind. Core's dist-tags are
42
- unchanged. The three source repositories are gone; their history survives only
43
- as verified `git bundle`s, because the repository allows squash merges only and
44
- the imported commits did not survive onto `main`.
45
-
46
- The operational runbook is [DEVELOPMENT-PIPELINE.md](../../DEVELOPMENT-PIPELINE.md)
47
- and [RELEASE-SECURITY.md](../../RELEASE-SECURITY.md). The plan itself is archived at
48
- [archive/2026-09-19/SPIKE-MONOREPO.md](../2026-09-19/SPIKE-MONOREPO.md),
49
- whose closing note records what the plan got wrong — chiefly that its
50
- strongest argument, the reach of the enforcing checks, only became true after
51
- both checkers were changed to discover workspace packages.
52
-
53
- The section below is kept for the middleware decision it records, which is
54
- still the reason there is no `packages/middleware`.
55
-
56
- ## Accepted: monorepo first — middleware withdrawn rather than consolidated
57
-
58
- The maintainer confirmed that monorepo work is starting now. The earlier
59
- recommendation to keep repositories separate for now is superseded.
60
-
61
- **Reversed 2026-09-19: the middleware half of this decision no longer applies.**
62
- This section used to say "move middleware into the monorepo as its own package
63
- first, preserving its existing API and behavior," and explicitly: "do not
64
- unpublish or retire the middleware package as part of the initial move."
65
- That instruction was overtaken. `@jimhoyd/urlcode-middleware` has been
66
- **unpublished** from npm at `0.1.0-alpha.2` and `jimhoyd-com/urlcode-middleware`
67
- **deleted** — the package was withdrawn outright instead of migrated, so there
68
- is no `packages/middleware` to create and no subsequent fold-into-core step.
69
- The reversal is recorded here rather than deleted because the instruction it
70
- replaces was explicit, and a reader who remembers it should be able to see that
71
- it was changed deliberately and not simply forgotten.
72
-
73
- Nothing was lost in capability terms: per-route middleware is **native to
74
- core** via the `middleware:` array ([MIDDLEWARE.md](../../MIDDLEWARE.md)), and the
75
- deleted package only ever offered the same behavior through the extension
76
- seam. Trust remains the default and explicit sandbox choices keep their
77
- meaning. The generic extension wrapping hook (`ExtensionInstance.middleware`,
78
- `RuntimeExtension.cacheSensitive`) stays in core's contract for other
79
- extensions — it is no longer exercised by any shipped package, which is worth
80
- knowing before it is assumed to be covered. Static targets continue rejecting
81
- request-time middleware because there is no server to run it.
82
-
83
- Migration scope is therefore **core, auth, admin and UI**. Template and the
84
- distribution tap stay outside that package move. The observed stale peer pins
85
- and checkout-limited guidance checks strengthen the case for shared
86
- verification: carry those checks across the new package paths, since merely
87
- moving files does not prove every generated skill is covered. The cleanup PRs
88
- that superseded the earlier zero-open-PR survey have since merged, and a fresh
89
- survey again reports zero open pull requests across all four in-scope
90
- repositories — re-run it per repository immediately before that repository
91
- moves rather than trusting this line.
92
-
93
- The [archived monorepo plan](../2026-09-19/SPIKE-MONOREPO.md) records
94
- migration context.
95
- [Issue 172](https://github.com/jimhoyd-com/urlcode/issues/172), which tracked
96
- "consolidate middleware into core after moving it into the monorepo," was
97
- **closed on 2026-09-19** as moot — there was nothing left to consolidate.
98
- Migration starting is not a claim that it has landed.
99
-
100
- ## Source review baseline
101
-
102
- | Repository | Reviewed commit | Code checked |
103
- |---|---|---|
104
- | core | `db375bf` | Runtime dispatch, schema normalization, capabilities, static compiler, prerender, MCP and resource generators |
105
- | auth | `71957dd` | Lifecycle hooks, UI rendering and shared helper imports |
106
- | admin | `f3b4882` | UI rendering, auth-service integration and shared helper imports |
107
- | UI | `0e96f7f` | Shared forms, kit/host exports and copied core contract |
108
- | ~~middleware~~ | `f201f4b` | Extension wrapping, per-entry sandbox dispatch and scaffolding — **repository deleted 2026-09-19; this baseline is unreachable except through the local `urlcode-middleware.bundle`** |
109
- | template | `4e09e50` | Exact core pin, generated guide and both vendored skills |
110
-
111
- The Homebrew tap (`73eaaef`) still selects stable core `0.3.0`; its old trust
112
- behavior belongs to that pin and must not be rewritten as alpha.2 behavior.
113
- The other organization tap and Scoop bucket contain Gitroll, not URLCode.
114
- The deleted documentation repository is historical context, not a second source
115
- of current contracts; its former GitHub links no longer resolve. This review is targeted source inspection, not an audit of
116
- every execution path or an independent security assessment.
@@ -1,29 +0,0 @@
1
- # Historical records
2
-
3
- These documents retain earlier reasoning and evidence. They are not authoring
4
- instructions or an active backlog. Read [the current roadmap](../../ROADMAP.md),
5
- [open decisions](../OPEN-DECISIONS.md) and [implemented contract](../SPECIFICATION.md)
6
- first. An old example can describe a removed feature or an earlier trust model.
7
-
8
- Archived on 2026-09-19 after checking the current code:
9
-
10
- | Record | Why archived | Current owner of remaining work |
11
- |---|---|---|
12
- | [Monorepo spike](2026-09-19/SPIKE-MONOREPO.md) | The migration it proposed is complete: ui, auth and admin are workspace packages and all three have been released from this repository | [Development pipeline](../DEVELOPMENT-PIPELINE.md) and [release security](../RELEASE-SECURITY.md) |
13
- | [Previous roadmap](2026-09-19/ROADMAP.md) | Release chronology mixed with current planning | Current roadmap and release readiness |
14
- | [Detailed phases](2026-09-19/NEXT-STEPS.md) | Most authoring/tooling/UI work delivered; repeated stale backlog | Current roadmap, issues and open decisions |
15
- | [Previous decisions](2026-09-19/OPEN-DECISIONS.md) | Mixed settled releases, obsolete repository counts and live questions | Current open decisions |
16
- | [Next-phase implementation](2026-09-19/NEXT-PHASE-PLAN.md) | Delivered source snapshot for alpha.1 | Specification and provider evidence |
17
- | [Extension delivery sequence](2026-09-19/EXTENSION-IMPLEMENTATION.md) | Registration, auth/admin and UI now implemented | Package status files and core issue 58 |
18
- | [Policy/extension exploration](2026-09-19/SPIKE-EXTENSIONS.md) | Main design delivered; obsolete link APIs | Policies, plugins and extensions guides |
19
- | [Extension model review](2026-09-19/SPIKE-EXTENSION-MODEL.md) | Historical design rationale | Current extension contract |
20
- | [Usability review](2026-09-19/USABILITY-REVIEW.md) | Predates trusted execution and retired packages | New benchmark observations |
21
- | [Management security](2026-09-19/MANAGEMENT-SECURITY.md) | Describes the removed core link-management API | No current core replacement |
22
- | [Lambda compile spike](2026-09-19/SPIKE-LAMBDA-COMPILE.md) | Proposal declined 2026-09-19 in favour of one Node deployment per project | Open decisions; AWS and Vercel guides |
23
- | [Completed decisions (2026-09-20)](2026-09-20/OPEN-DECISIONS-COMPLETED.md) | Closed questions, the completed monorepo migration and the 2026-09-19 source baseline, moved out of open decisions (#242) | Current open decisions and release readiness |
24
-
25
- Open proposals stay outside the archive with their status stated at the top.
26
- The accepted trust-by-default decision remains directly linked from the current
27
- security contract; its `SPIKE-` filename is retained for existing references.
28
- Security audits and operational evidence remain available: “implemented” never
29
- means their unperformed review, deployment or recovery work is complete.
@@ -1,182 +0,0 @@
1
- # The `agents` policy
2
-
3
- Denies or reports requests by their `User-Agent`, before anything else runs.
4
- It is the cheapest refusal in the request chain (`agents`, then `throttle`,
5
- then the cache lookup), so a denied crawler never counts against a quota,
6
- never reaches the sandbox and never fills the origin cache.
7
-
8
- ```yaml
9
- version: "1"
10
- policies: # project defaults, or per route under routes.<pattern>.policies
11
- agents:
12
- deny: [ai-crawlers] # bundled list names, or project-relative .json files
13
- allow: [monitoring] # allow always wins over deny
14
- denyPatterns: ["^curl/"] # linear-time regex subset, matched case-insensitively
15
- allowPatterns: ["^Mozilla/5\\.0 \\(compatible; Googlebot"]
16
- denyEmpty: false # deny a missing or blank User-Agent
17
- status: 403 # 400-599
18
- mode: enforce # enforce | report
19
- ```
20
-
21
- The `hardened` profile sets `deny: [ai-crawlers]` with status 403. A route can
22
- override any key, or set `agents: false` to switch the policy off for itself.
23
-
24
- ## Semantics
25
-
26
- - Matching is against the `User-Agent` request header only and is
27
- case-insensitive. Nothing else about the request (address, path, other
28
- headers) takes part.
29
- - Evaluation order: if any `allow` list or `allowPatterns` entry matches, the
30
- request passes and nothing is logged. Otherwise the first `deny` list (in
31
- the order written) or `denyPatterns` entry that matches denies it.
32
- - `urlcode audit`, `test` and `benchmark` send generated probes as
33
- `Mozilla/5.0 (compatible; RouteProbe/0.1)`;
34
- a fixture may set its own `user-agent` header. Upstream lists include
35
- short unanchored names (the `ai-crawlers` list carries `Code`, which
36
- matches any agent containing that word), so a custom allow pattern for
37
- your own tooling is worth declaring before denying a whole list.
38
- - `denyEmpty: true` denies a request with no `User-Agent`, or one that is only
39
- whitespace. Allow rules cannot match an empty header, so this always wins
40
- for empty headers.
41
- - A denial answers `status` (default 403) with `content-type:
42
- text/plain; charset=utf-8`, `cache-control: no-store` and the body
43
- `Forbidden\n`. Response policies that also run at request time (cache,
44
- compression) are skipped on the early response; `security` headers are
45
- still applied.
46
- - Every denial is logged as `{ event: 'agents', route, list, outcome }`, where
47
- `list` is the list name, the file path as written in YAML, `pattern` for
48
- `denyPatterns`, or `empty` for `denyEmpty`. The raw header value is never
49
- logged: a `User-Agent` is attacker-controlled text.
50
- - `mode: report` logs the same event with `outcome: 'reported'` and never
51
- denies. Run a new deny list in report mode for a release, read the log, then
52
- switch to `enforce`.
53
-
54
- ## Allow before deny
55
-
56
- Broad deny lists without an explicit allow are the most common self-inflicted
57
- outage in this space. `crawlers` contains every search engine; `ai-crawlers`
58
- contains agents some operators want (for example `ChatGPT-User` or
59
- `Applebot-Extended` when a site chooses to appear in AI search). Keep an
60
- explicit `allow` or `allowPatterns` entry for the agents you depend on, and
61
- anchor it: `^Mozilla/5\.0 \(compatible; Googlebot` cannot be satisfied by a
62
- `Googlebot` token dropped in the middle of an unrelated string.
63
-
64
- Matching is string matching. A client can claim any `User-Agent`, and the
65
- genuine search crawlers publish the way to check a claim (reverse DNS for
66
- Googlebot, bingbot and Applebot; the IETF `web-bot-auth` HTTP Message
67
- Signature drafts for newer agents). That verification needs network calls and
68
- vendor data, so it belongs in a [host plugin](../archive/2026-09-19/SPIKE-EXTENSIONS.md) that
69
- runs after this policy, not in the runtime.
70
-
71
- ## Bundled lists
72
-
73
- | Name | Contents | Upstream | Licence | Refresh |
74
- | --- | --- | --- | --- | --- |
75
- | `ai-crawlers` | every agent in `robots.json` (AI training, AI search and assistant crawlers) | [ai-robots-txt/ai.robots.txt](https://github.com/ai-robots-txt/ai.robots.txt) | MIT | pinned tag in `scripts/sync-agent-lists.ts` |
76
- | `crawlers` | every crawler, bot and automated client in `crawler-user-agents.json` | [monperrus/crawler-user-agents](https://github.com/monperrus/crawler-user-agents) | MIT (revisions after 2016-11-07 only) | same |
77
- | `seo` | `crawlers` entries tagged `seo` (backlink and rank trackers such as AhrefsBot, SemrushBot, MJ12bot) | derived from crawler-user-agents | MIT | same |
78
- | `monitoring` | `crawlers` entries tagged `monitoring` (UptimeRobot, Pingdom, StatusCake, ...) | derived from crawler-user-agents | MIT | same |
79
-
80
- Each list lives in `data/agents/<name>.json` as
81
- `{ name, description, source: { repository, url, license, tag, commit, file, fetchedAt }, entries }`
82
- with one entry per pattern: `{ name, pattern, source, sourceRevision, addedAt }`.
83
- The generated `data/agents/index.js` mirrors the `[name, pattern]` pairs so the
84
- policy module (which also runs inside the Cloudflare Worker) needs no
85
- filesystem. The upstream licences are reproduced verbatim under
86
- `data/agents/LICENSES/` and named in `NOTICE`, as Apache-2.0 section 4(d)
87
- requires. `urlcode audit` and the runtime's policy inventory report the list
88
- names, pattern counts and the pinned revision each list was built from, so a
89
- deploy carries a known list version and a rollback rolls the list back too.
90
-
91
- ### Refreshing
92
-
93
- ```sh
94
- node scripts/sync-agent-lists.ts # fetch pinned upstreams, validate, write data/agents/ (also: npm run sync:agents)
95
- node scripts/sync-agent-lists.ts --check # exit 1 when the committed files are stale
96
- ```
97
-
98
- The script fetches each upstream at the tag and commit pinned in its `sources`
99
- table, normalises entries to the schema above (an ai.robots.txt agent name
100
- becomes an escaped literal pattern; a crawler-user-agents pattern is kept and
101
- named by its literal prefix), validates every pattern against the subset below
102
- (rewriting `{n,}` to `{n,64}`, and dropping and printing anything else),
103
- preserves `addedAt` from the previous file or the upstream `addition_date`, and
104
- writes the JSON lists, the index and the licence copies. To move to a newer
105
- upstream release, change the `tag` and `commit` pins and rerun; the revision
106
- appears in every entry and in the audit output. Refresh through an ordinary
107
- pull request so the list diff is reviewed like code. Behind an HTTPS proxy,
108
- set `NODE_USE_ENV_PROXY=1` so `fetch` honours `HTTPS_PROXY` and
109
- `NODE_EXTRA_CA_CERTS`.
110
-
111
- ## Project lists
112
-
113
- `deny` and `allow` also accept a project-relative path ending in `.json`:
114
-
115
- ```yaml
116
- policies:
117
- agents:
118
- deny: [agents/deny.json]
119
- ```
120
-
121
- The file is either an array of entries or `{ "entries": [...] }`, each entry
122
- `{ "name": "curl", "pattern": "^curl/" }` (`name` optional; the other fields
123
- of the bundled schema are ignored). It must stay inside the project, hold at
124
- most 4096 entries and pass the same pattern validation as YAML patterns. The
125
- log names the path as written. The file is read once at activation; the
126
- Cloudflare build embeds its entries in the artifact so the Worker never reads a
127
- file.
128
-
129
- ## The pattern subset
130
-
131
- Patterns in `denyPatterns`, `allowPatterns` and every list file are validated
132
- at activation. A pattern outside the subset fails activation with a
133
- `ConfigError` naming the route, the key and the reason, so a project cannot
134
- turn the matcher into a denial-of-service vector by editing YAML. Allowed:
135
-
136
- - anchors `^` and `$`; literals; `.`
137
- - escapes: `\d \w \s \D \W \S \b \B \t \n \r \f \v \0`, `\xHH`, `\uHHHH`, and
138
- a backslash before any punctuation (`\.`, `\/`, `\(`, `\-`, ...)
139
- - character classes `[...]` and `[^...]` with ranges and the escapes above
140
- - groups `(...)` and `(?:...)`, and alternation `|`
141
- - quantifiers `*`, `+`, `?`, `{n}` and `{n,m}` with `m <= 64`, on a single
142
- atom (a literal, escape, class or `.`); `?` may also follow a group that
143
- contains no quantifier, for optional words such as `(?:bot)?`
144
- - at most 256 bytes
145
-
146
- Rejected: backreferences (`\1`, `\k<name>`), lookahead and lookbehind, named
147
- groups, unicode property escapes, `\c` control escapes, nested character
148
- classes, `{n,}` and bounds above 64, lazy or stacked quantifiers (`+?`, `**`),
149
- quantifiers on anchors, and `*`, `+` or `{n,m}` on a group (so `(a+)+` and
150
- `(ab)*` fail). Each list is compiled into one alternated `RegExp` with the `i`
151
- flag, so a request costs one pass per list rather than one per pattern.
152
-
153
- ## robots.txt
154
-
155
- Denying an agent is not the same as asking it to stay away. Well-behaved
156
- crawlers read `/robots.txt` (RFC 9309) before fetching anything, and the AI
157
- crawlers in `ai-crawlers` are the ones that upstream tracks as respecting or
158
- ignoring it. Generate one from the same bundled list with
159
- [`site.robots`](../SITE.md) so the file and the policy cannot drift apart:
160
-
161
- ```yaml
162
- site:
163
- robots:
164
- disallow: [ai-crawlers]
165
- policies:
166
- agents:
167
- deny: [ai-crawlers]
168
- ```
169
-
170
- This serves `/robots.txt` with one `User-agent:` line per list entry followed
171
- by `Disallow: /`, then `User-agent: *` / `Allow: /`. A hand-written
172
- `/robots.txt` `respond` route still works and takes precedence over the
173
- generated one. The `agents` policy then enforces the same decision for clients
174
- that ignore the file.
175
-
176
- ## Targets
177
-
178
- | Target | Support |
179
- | --- | --- |
180
- | self-hosted (`urlcode serve`) | native |
181
- | Vercel, AWS | native |
182
- | Cloudflare | compiled: the build validates the policy, embeds the effective configuration (and any project list entries) in the artifact, and the Worker compiles it at startup without filesystem access |
@@ -1,152 +0,0 @@
1
- # `policies.cache`
2
-
3
- Named HTTP caching strategies, enforced on the host. A route picks one
4
- strategy from a fixed catalogue; every row is a pattern with a name outside
5
- this project and a defined header output, so a browser, a CDN and the
6
- runtime's own origin memory cache all read the same thing. Explicit fields
7
- (`maxAge`, `staleWhileRevalidate`, `staleIfError`, `cdnMaxAge`, `originTtl`)
8
- override what a strategy implies.
9
-
10
- ```yaml
11
- policies:
12
- cache: { strategy: revalidate } # project default
13
- routes:
14
- /feed:
15
- function: { source: feed.mjs }
16
- policies:
17
- cache: { strategy: swr, maxAge: 30, staleWhileRevalidate: 300, vary: [Accept-Language] }
18
- ```
19
-
20
- A route may also write `cache` directly, next to `function` or `middleware`,
21
- instead of nesting it under `policies`. The compiler expands it to
22
- `policies.cache` before anything else reads the project ([policies](../POLICIES.md)),
23
- so it is pure sugar over the form above and is refused alongside a direct
24
- `policies.cache` on the same route:
25
-
26
- ```yaml
27
- routes:
28
- /feed:
29
- function: { source: feed.mjs }
30
- cache: { strategy: swr, maxAge: 30 } # same as policies: { cache: {...} }
31
- ```
32
-
33
- ## Strategies
34
-
35
- | `strategy` | Emitted headers | Origin memory cache | Typical use |
36
- |---|---|---|---|
37
- | `no-store` | `Cache-Control: no-store` | off | personalized, secret-bearing |
38
- | `revalidate` | `Cache-Control: no-cache`, `ETag` (a strong SHA-256 tag is computed for results without one); answers `304` to `If-None-Match`, and to `If-Modified-Since` when the result carries `Last-Modified` | off | HTML, anything that must be fresh but is cheap to validate |
39
- | `public` | `Cache-Control: public, max-age=N` (`maxAge` required) | on only when `originTtl` > 0 | stable API answers, feeds |
40
- | `immutable` | `Cache-Control: public, max-age=31536000, immutable` (RFC 8246; `maxAge` overrides the year) | off | content-hashed URLs only; refused elsewhere unless `force: true` |
41
- | `swr` | `Cache-Control: public, max-age=N, stale-while-revalidate=M` (RFC 5861; both required) | on: fresh for `originTtl` (default `maxAge`), then stale served once | hot functions, link previews |
42
- | `sie` | as `swr` plus `stale-if-error=K` (`staleIfError` required; `staleWhileRevalidate` optional) | on, as `swr` | keep answering during an upstream failure (headers only at the origin; see below) |
43
- | `micro` | `Cache-Control: no-store` to clients | on: `originTtl` default 1 s, at most 5 s unless `force: true` | the NGINX micro-cache: absorb a thundering herd without changing what a browser sees |
44
- | `cdn-only` | `Cache-Control: no-store` plus `CDN-Cache-Control: max-age=N` (RFC 9213; `cdnMaxAge` required) | off | let the CDN cache while browsers do not |
45
- | `private` | `Cache-Control: private, max-age=N` (`maxAge` required) | off | per-user data a browser may keep |
46
-
47
- `immutable` accepts a route whose pattern has a segment with eight or more
48
- hex characters (`/app.3f2a9c1d.js`) or a parameter named like a digest
49
- (`{hash}`, `{digest}`, `{sha}`, `{version}`, `{build}`, `{rev}`,
50
- `{fingerprint}`). Anything else fails configuration with the route named
51
- unless `force: true`.
52
-
53
- ## Who owns `Cache-Control`
54
-
55
- Explicit beats strategy, in this order:
56
-
57
- 1. A `response.headers.cache-control` declared in YAML on the route is kept
58
- as written; the strategy does not touch it.
59
- 2. An asset handler's `cacheControl` (`page`, `download`, `static`) is kept
60
- when the cache policy is only inherited from the project or a profile.
61
- When the route itself declares `policies.cache`, the strategy overrides it.
62
- 3. A handler result that already says `no-store` or `private` is kept and is
63
- never stored: a personalized answer under a `public` route stays private.
64
- 4. Otherwise the strategy's header replaces whatever the handler emitted.
65
-
66
- `Vary` is merged with the declared `vary` names (no duplicates, `*` left
67
- alone), for every strategy, so the origin key and the wire header agree.
68
- Compression adds `Accept-Encoding` to `Vary` after this policy.
69
-
70
- ## Origin memory cache
71
-
72
- On for `swr`, `sie`, `micro`, and `public` with `originTtl` set. It is an
73
- origin-side micro-cache, so a client's own `Cache-Control: no-cache` or
74
- `max-age=0` request directive does not bypass it (RFC 9111 §5.2.1 lets an
75
- origin decide that); conditional and range requests do, as below. Rules:
76
-
77
- - Only `GET` and `HEAD` are looked up; only `GET` results are stored, and a
78
- `HEAD` hit serves the `GET` entry's headers with an empty body and the
79
- entry's `Content-Length`. A route whose `methods` exclude `GET` has the
80
- origin cache off.
81
- - Only statuses in `statuses` (default `200, 301, 302, 404, 410`) are stored.
82
- - A request carrying `If-None-Match`, `If-Modified-Since`, `If-Match`,
83
- `If-Unmodified-Since` or `Range` is never answered from the origin cache:
84
- a stored entry is a full `200` representation and the handler owns
85
- validators and ranges, so such requests always reach it (an asset route
86
- keeps its own `304`, `412` and `206` answers).
87
- - Never stored: results carrying `Set-Cookie`; routes declaring `secrets`;
88
- results whose handler `Cache-Control` says `private` or `no-store`; bodies
89
- larger than `maxBytes` (default 1 MiB).
90
- - Key: route pattern, request path, query string and the values of the
91
- declared `vary` request headers. The method is not part of the key so
92
- `HEAD` shares the `GET` entry.
93
- - Hits carry `Age` (RFC 9111). No non-standard headers are added.
94
- - Concurrent misses for one key coalesce: the first request reaches the
95
- handler, up to 64 others wait for its result, and any beyond that proceed
96
- to the handler themselves. If the fill fails, waiters fall through to the
97
- handler rather than receiving the error.
98
- - Bounds: `maxEntries` per route configuration (default 10000) and 64 MiB of
99
- bodies across the whole runtime; the least recently used entry is evicted
100
- first. The store belongs to one runtime instance and is dropped on close and
101
- reload, so a deploy never serves the previous code's output.
102
- - Stored entries hold the handler's headers as they were after YAML
103
- `response.headers` and this policy ran, before security headers and
104
- compression. Those run again on every hit, so a hit is compressed and
105
- hardened the same way as a miss. Bodies are stored uncompressed.
106
-
107
- ### `swr` at the origin: stale served once, next request refreshes
108
-
109
- A policy has no handle to the route's handler, so it cannot revalidate in the
110
- background. The origin-side approximation is: a request that finds an entry
111
- past `originTtl` but within `staleWhileRevalidate` is answered from the stale
112
- entry immediately, and the entry is flagged so the next request for that key
113
- goes to the handler and replaces it. If that refresh fails the flag is
114
- cleared and the stale entry may be served once more, as long as it is within
115
- the window. Beyond `max-age + stale-while-revalidate` nothing is served from
116
- memory. Clients and CDNs that honour RFC 5861 do their own background
117
- revalidation from the emitted header.
118
-
119
- ### `sie` limitation
120
-
121
- `stale-if-error` is header-only at the origin. Policies observe errors but
122
- cannot replace a thrown error with a result (the runtime rethrows), so a
123
- handler failure is not answered from a stale entry by this runtime. Downstream
124
- caches honouring RFC 5861 still serve stale on a 5xx. Origin-side
125
- stale-if-error needs the plugin API or a runtime change that lets a policy's
126
- error hook return a result.
127
-
128
- ## Targets
129
-
130
- | Target | Support | Note |
131
- |---|---|---|
132
- | node | native | headers and origin cache per runtime instance |
133
- | vercel | native | headers; the origin cache is per function instance, so hit rates depend on instance reuse |
134
- | aws | native | headers; the origin cache is per Lambda instance |
135
- | cloudflare | refused | the Worker build has no policy runtime; activation fails with the route named |
136
-
137
- ## Diagnostics
138
-
139
- `describe()` (in `urlcode test`, the readiness plan and plugin activation)
140
- reports `strategy`, the emitted `cacheControl` string (or `explicit response
141
- header` / `asset handler` when something else owns it), `cdnCacheControl`,
142
- `originTtl`, `staleWhileRevalidate`, `staleIfError`, `vary` and whether the
143
- origin cache is `on`.
144
-
145
- Log events, through the runtime's `log`:
146
-
147
- ```json
148
- { "event": "cache", "route": "/feed", "outcome": "hit" }
149
- ```
150
-
151
- `outcome` is `hit`, `stale`, `miss` or `store`. The route is the configured
152
- pattern, never the request path or key.