@jimhoyd/urlcode 0.3.0

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 (254) hide show
  1. package/CONTRIBUTING.md +80 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +20 -0
  4. package/README.md +342 -0
  5. package/ROADMAP.md +248 -0
  6. package/SECURITY.md +40 -0
  7. package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
  8. package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
  9. package/data/agents/ai-crawlers.json +1240 -0
  10. package/data/agents/crawlers.json +10515 -0
  11. package/data/agents/index.d.ts +3 -0
  12. package/data/agents/index.js +2453 -0
  13. package/data/agents/monitoring.json +1520 -0
  14. package/data/agents/seo.json +3865 -0
  15. package/dist/BUILD-MANIFEST.json +60 -0
  16. package/dist/adapters.js +59 -0
  17. package/dist/agent-lists.js +59 -0
  18. package/dist/assets.js +129 -0
  19. package/dist/authoring.js +68 -0
  20. package/dist/aws.js +110 -0
  21. package/dist/build-cloudflare.js +164 -0
  22. package/dist/cli.js +185 -0
  23. package/dist/client-address.js +85 -0
  24. package/dist/cloudflare.js +165 -0
  25. package/dist/compliance-rules/baseline.js +98 -0
  26. package/dist/compliance-rules/privacy.js +38 -0
  27. package/dist/compliance-rules/shared.js +62 -0
  28. package/dist/compliance-rules/strict.js +61 -0
  29. package/dist/compliance.js +219 -0
  30. package/dist/config-worker.js +9 -0
  31. package/dist/config.js +172 -0
  32. package/dist/errors.js +8 -0
  33. package/dist/function-sources.js +54 -0
  34. package/dist/function-worker.js +105 -0
  35. package/dist/functions.js +174 -0
  36. package/dist/guest-api.js +114 -0
  37. package/dist/header-validation.js +18 -0
  38. package/dist/http-policy.js +74 -0
  39. package/dist/http-response.js +84 -0
  40. package/dist/index.js +11 -0
  41. package/dist/link-api.js +136 -0
  42. package/dist/link-cli.js +141 -0
  43. package/dist/link-events.js +76 -0
  44. package/dist/link-records.js +31 -0
  45. package/dist/link-store-worker.js +150 -0
  46. package/dist/link-store.js +250 -0
  47. package/dist/logging.js +22 -0
  48. package/dist/management-policy.js +41 -0
  49. package/dist/match.js +124 -0
  50. package/dist/observability.js +242 -0
  51. package/dist/plugins.js +74 -0
  52. package/dist/policies/agents.js +248 -0
  53. package/dist/policies/cache.js +297 -0
  54. package/dist/policies/compression.js +187 -0
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies/throttle.js +131 -0
  57. package/dist/policies.js +142 -0
  58. package/dist/policy.js +55 -0
  59. package/dist/prerender.js +165 -0
  60. package/dist/project-tests.js +23 -0
  61. package/dist/readiness.js +216 -0
  62. package/dist/router.js +166 -0
  63. package/dist/runtime.js +244 -0
  64. package/dist/scaffold.js +0 -0
  65. package/dist/scripts/operational-drills.js +75 -0
  66. package/dist/server.js +270 -0
  67. package/dist/site.js +0 -0
  68. package/dist/sqlite-version.js +6 -0
  69. package/dist/types/adapters.d.ts +12 -0
  70. package/dist/types/agent-lists.d.ts +5 -0
  71. package/dist/types/assets.d.ts +13 -0
  72. package/dist/types/authoring.d.ts +2 -0
  73. package/dist/types/aws.d.ts +35 -0
  74. package/dist/types/build-cloudflare.d.ts +14 -0
  75. package/dist/types/cli.d.ts +2 -0
  76. package/dist/types/client-address.d.ts +9 -0
  77. package/dist/types/cloudflare.d.ts +75 -0
  78. package/dist/types/compliance-rules/baseline.d.ts +12 -0
  79. package/dist/types/compliance-rules/privacy.d.ts +6 -0
  80. package/dist/types/compliance-rules/shared.d.ts +24 -0
  81. package/dist/types/compliance-rules/strict.d.ts +9 -0
  82. package/dist/types/compliance.d.ts +119 -0
  83. package/dist/types/config-worker.d.ts +1 -0
  84. package/dist/types/config.d.ts +21 -0
  85. package/dist/types/errors.d.ts +7 -0
  86. package/dist/types/function-sources.d.ts +21 -0
  87. package/dist/types/function-worker.d.ts +1 -0
  88. package/dist/types/functions.d.ts +92 -0
  89. package/dist/types/guest-api.d.ts +16 -0
  90. package/dist/types/header-validation.d.ts +2 -0
  91. package/dist/types/http-policy.d.ts +39 -0
  92. package/dist/types/http-response.d.ts +43 -0
  93. package/dist/types/index.d.ts +11 -0
  94. package/dist/types/link-api.d.ts +30 -0
  95. package/dist/types/link-cli.d.ts +36 -0
  96. package/dist/types/link-events.d.ts +27 -0
  97. package/dist/types/link-records.d.ts +11 -0
  98. package/dist/types/link-store-worker.d.ts +1 -0
  99. package/dist/types/link-store.d.ts +130 -0
  100. package/dist/types/logging.d.ts +9 -0
  101. package/dist/types/management-policy.d.ts +9 -0
  102. package/dist/types/match.d.ts +81 -0
  103. package/dist/types/observability.d.ts +104 -0
  104. package/dist/types/plugins.d.ts +27 -0
  105. package/dist/types/policies/agents.d.ts +86 -0
  106. package/dist/types/policies/cache.d.ts +82 -0
  107. package/dist/types/policies/compression.d.ts +41 -0
  108. package/dist/types/policies/security.d.ts +37 -0
  109. package/dist/types/policies/throttle.d.ts +55 -0
  110. package/dist/types/policies.d.ts +36 -0
  111. package/dist/types/policy.d.ts +21 -0
  112. package/dist/types/prerender.d.ts +31 -0
  113. package/dist/types/project-tests.d.ts +13 -0
  114. package/dist/types/readiness.d.ts +121 -0
  115. package/dist/types/router.d.ts +11 -0
  116. package/dist/types/runtime.d.ts +91 -0
  117. package/dist/types/scaffold.d.ts +17 -0
  118. package/dist/types/server.d.ts +37 -0
  119. package/dist/types/site.d.ts +12 -0
  120. package/dist/types/sqlite-version.d.ts +1 -0
  121. package/dist/types/types.d.ts +332 -0
  122. package/dist/types/vercel.d.ts +12 -0
  123. package/dist/types.js +177 -0
  124. package/dist/vercel.js +65 -0
  125. package/docs/AI-AUTHORING.md +113 -0
  126. package/docs/ASSETS.md +106 -0
  127. package/docs/AWS.md +77 -0
  128. package/docs/BEST-PRACTICES.md +267 -0
  129. package/docs/CAPACITY.md +208 -0
  130. package/docs/CLOUDFLARE.md +110 -0
  131. package/docs/COMPLIANCE.md +242 -0
  132. package/docs/DYNAMIC-LINKS.md +561 -0
  133. package/docs/FUNCTION-SECURITY.md +113 -0
  134. package/docs/HTTP.md +129 -0
  135. package/docs/INSTALL.md +90 -0
  136. package/docs/LOAD-TESTING.md +91 -0
  137. package/docs/LOCAL-DEVELOPMENT.md +99 -0
  138. package/docs/MANAGEMENT-SECURITY.md +82 -0
  139. package/docs/MIDDLEWARE.md +83 -0
  140. package/docs/MONITORING.md +132 -0
  141. package/docs/OBSERVABILITY.md +229 -0
  142. package/docs/OPERATIONAL-PROOF.md +42 -0
  143. package/docs/OPERATIONS.md +207 -0
  144. package/docs/ORGANIZATION.md +135 -0
  145. package/docs/PERFORMANCE.md +72 -0
  146. package/docs/PLUGINS.md +234 -0
  147. package/docs/POLICIES.md +489 -0
  148. package/docs/PRERENDER.md +193 -0
  149. package/docs/PROJECT-DIRECTION.md +51 -0
  150. package/docs/READINESS.md +125 -0
  151. package/docs/README.md +58 -0
  152. package/docs/RELEASE-READINESS.md +95 -0
  153. package/docs/RELEASE-SECURITY.md +72 -0
  154. package/docs/RESILIENCE.md +160 -0
  155. package/docs/ROUTING.md +95 -0
  156. package/docs/SANDBOX-REVIEW.md +59 -0
  157. package/docs/SCAFFOLDING.md +72 -0
  158. package/docs/SECURITY-AUDIT.md +124 -0
  159. package/docs/SITE.md +150 -0
  160. package/docs/SPECIFICATION.md +232 -0
  161. package/docs/SPIKE-EXTENSIONS.md +475 -0
  162. package/docs/STANDARDS.md +303 -0
  163. package/docs/STARTERS.md +51 -0
  164. package/docs/TUNNELS.md +75 -0
  165. package/docs/TYPESCRIPT.md +102 -0
  166. package/docs/VERCEL.md +107 -0
  167. package/docs/YAML-GUIDE.md +521 -0
  168. package/docs/YAML-REFERENCE.md +328 -0
  169. package/docs/policies/agents.md +182 -0
  170. package/docs/policies/cache.md +139 -0
  171. package/docs/policies/compression.md +168 -0
  172. package/docs/policies/security.md +160 -0
  173. package/docs/policies/throttle.md +103 -0
  174. package/examples/assets/.env.example +1 -0
  175. package/examples/assets/.gitattributes +2 -0
  176. package/examples/assets/Makefile +30 -0
  177. package/examples/assets/README.md +18 -0
  178. package/examples/assets/functions/hello.mjs +3 -0
  179. package/examples/assets/gitignore.template +7 -0
  180. package/examples/assets/public/about.html +2 -0
  181. package/examples/assets/public/assets/example.txt +1 -0
  182. package/examples/assets/public/guide.txt +1 -0
  183. package/examples/assets/tests/requests.json +44 -0
  184. package/examples/assets/urlcode.yaml +27 -0
  185. package/examples/aws/README.md +13 -0
  186. package/examples/aws/handler.mjs +5 -0
  187. package/examples/aws/public/index.html +3 -0
  188. package/examples/aws/public/notes.txt +1 -0
  189. package/examples/aws/template.yaml +33 -0
  190. package/examples/aws/tests/requests.json +9 -0
  191. package/examples/aws/urlcode.yaml +19 -0
  192. package/examples/cloudflare/README.md +18 -0
  193. package/examples/cloudflare/package.json +14 -0
  194. package/examples/cloudflare/tests/requests.json +9 -0
  195. package/examples/cloudflare/urlcode.yaml +24 -0
  196. package/examples/cloudflare/wrangler.toml +5 -0
  197. package/examples/compliance/README.md +20 -0
  198. package/examples/compliance/rules.mjs +65 -0
  199. package/examples/cookbook/README.md +21 -0
  200. package/examples/cookbook/functions/choice.mjs +4 -0
  201. package/examples/cookbook/functions/echo.mjs +3 -0
  202. package/examples/cookbook/functions/hello.mjs +3 -0
  203. package/examples/cookbook/functions/text.mjs +3 -0
  204. package/examples/cookbook/middleware/headers.mjs +6 -0
  205. package/examples/cookbook/public/about.html +1 -0
  206. package/examples/cookbook/public/assets/index.html +1 -0
  207. package/examples/cookbook/public/assets/site.css +1 -0
  208. package/examples/cookbook/public/favicon.svg +1 -0
  209. package/examples/cookbook/public/guide.txt +1 -0
  210. package/examples/cookbook/public/llms.txt +6 -0
  211. package/examples/cookbook/routes/code.yaml +55 -0
  212. package/examples/cookbook/routes/files.yaml +17 -0
  213. package/examples/cookbook/routes/policies.yaml +28 -0
  214. package/examples/cookbook/routes/redirects.yaml +38 -0
  215. package/examples/cookbook/routes/responses.yaml +26 -0
  216. package/examples/cookbook/tests/requests.json +254 -0
  217. package/examples/cookbook/urlcode.yaml +22 -0
  218. package/examples/live-links/README.md +11 -0
  219. package/examples/live-links/tests/requests.json +6 -0
  220. package/examples/live-links/urlcode.yaml +16 -0
  221. package/examples/monitoring/blackbox-jobs.yaml +37 -0
  222. package/examples/monitoring/prometheus-rules.yaml +88 -0
  223. package/examples/monitoring/prometheus-scrape.yaml +20 -0
  224. package/examples/monitoring/vector.toml +65 -0
  225. package/examples/prerender/README.md +48 -0
  226. package/examples/prerender/functions/page.mjs +6 -0
  227. package/examples/prerender/middleware/template.mjs +29 -0
  228. package/examples/prerender/prerender.d.mts +4 -0
  229. package/examples/prerender/prerender.mjs +63 -0
  230. package/examples/prerender/tests/requests.json +39 -0
  231. package/examples/prerender/urlcode.yaml +50 -0
  232. package/examples/tunnel/dev-with-ngrok.sh +61 -0
  233. package/examples/vercel/README.md +13 -0
  234. package/examples/vercel/api/index.js +5 -0
  235. package/examples/vercel/package.json +8 -0
  236. package/examples/vercel/public/index.html +3 -0
  237. package/examples/vercel/public/notes.txt +1 -0
  238. package/examples/vercel/tests/requests.json +48 -0
  239. package/examples/vercel/urlcode.yaml +19 -0
  240. package/examples/vercel/vercel.json +10 -0
  241. package/llms.txt +46 -0
  242. package/package.json +123 -0
  243. package/schemas/urlcode.schema.json +1230 -0
  244. package/starters/default/.gitattributes +1 -0
  245. package/starters/default/Makefile +30 -0
  246. package/starters/default/README.md +34 -0
  247. package/starters/default/functions/hello.mjs +3 -0
  248. package/starters/default/gitignore.template +8 -0
  249. package/starters/default/middleware/headers.mjs +6 -0
  250. package/starters/default/routes/functions.yaml +20 -0
  251. package/starters/default/routes/marketing/links.yaml +7 -0
  252. package/starters/default/starter.json +5 -0
  253. package/starters/default/tests/requests.json +56 -0
  254. package/starters/default/urlcode.yaml +8 -0
@@ -0,0 +1,193 @@
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, so the
6
+ sandbox, its deadline and its memory budget are not on the request path at all.
7
+
8
+ The shared orchestration ships as a build helper, `@jimhoyd/urlcode/prerender`, and the
9
+ runnable recipe is [`examples/prerender`](../examples/prerender/README.md), which
10
+ consumes it. Both are covered by `test/prerender.test.ts`. Everything here uses
11
+ the existing runtime: prerendering adds no field to route YAML, no CLI command
12
+ and no capability.
13
+
14
+ ## Why render ahead of time
15
+
16
+ A function response body is readable by middleware through `text()`. A native
17
+ `page`, `static`, `download`, `redirect`, `respond` or `link` body is not — that
18
+ opacity is a deliberate contract, not a gap, and it is what keeps native file
19
+ serving free of guest code. See [middleware](MIDDLEWARE.md).
20
+
21
+ So a shared template cannot be wrapped around a native file at request time. The
22
+ answer is to apply the template while the content is still a function response,
23
+ and publish the result:
24
+
25
+ ```
26
+ function + template middleware ──render once──▶ HTML file ──▶ page route
27
+ (sandbox, build time) (no sandbox)
28
+ ```
29
+
30
+ The alternative — reading Markdown through `next().text()` on a native route —
31
+ is not supported and should not be attempted. Prepare content at build time.
32
+
33
+ ## The build helper
34
+
35
+ ```js
36
+ import {prerenderPages, assertNativeProject, pageFileName} from '@jimhoyd/urlcode/prerender';
37
+
38
+ const rendered = await prerenderPages('./render-source', './out/pages', {
39
+ origin: 'https://docs.example', // what a page sees as its own origin
40
+ });
41
+ // rendered.pages → [{path: '/guide', file: 'guide.html', bytes: 531}, …]
42
+ // rendered.fixtures → byte-for-byte GET and empty HEAD cases, ready to extend
43
+ // rendered.count, rendered.bytes, rendered.directory
44
+ ```
45
+
46
+ `prerenderPages` owns everything that is easy to get wrong and nothing that is
47
+ site-specific. It activates the source project, selects the active literal GET
48
+ function routes, renders each one through its middleware, checks the status and
49
+ content type, enforces the budgets, derives and validates a safe output filename,
50
+ writes the files and closes the runtime — then hands back metadata. It does not
51
+ write a project, choose response headers, copy assets or compile content: the
52
+ caller assembles a project, or a generated include, from `pages` and `fixtures`.
53
+
54
+ `assertNativeProject(project, {allow})` activates a project and proves it cannot
55
+ execute guest code to answer a request: every route is one of the allowed native
56
+ handlers and none carries middleware. `allow` defaults to `['page', 'static',
57
+ 'download']`, which is what a real site serves; narrow it to `['page']` for a
58
+ page-only artifact. Run it on the **final assembled site**, not only on the
59
+ rendered pages, so what you deploy is what was checked.
60
+
61
+ | Option | Default | Meaning |
62
+ |---|---|---|
63
+ | `origin` | `http://localhost` | HTTP(S) origin a render sees; no path or credentials |
64
+ | `fileName` | `pageFileName` | Route path to filename; the path and the result are both validated either way |
65
+ | `ignoreUnrenderable` | `false` | Allow source routes this build will not render |
66
+ | `maxPages` | 500 | Pages in one render |
67
+ | `maxPageBytes` | 512 KiB | Bytes per rendered page |
68
+ | `maxTotalBytes` | 32 MiB | Bytes across the whole render |
69
+ | `log` | none | `{event: 'prerendered', path, file, bytes}` per page |
70
+
71
+ ## What the helper guarantees
72
+
73
+ The runtime enforces its own protections when a generated project activates:
74
+ asset declarations reject absolute paths, traversal, dot segments, symlinks,
75
+ hardlinks and nonregular files, and static trees refuse `urlcode.yaml`,
76
+ `package.json`, `package-lock.json`, hidden entries, `node_modules` and
77
+ `.pem/.key/.p12/.pfx/.env` files. See [assets](ASSETS.md).
78
+
79
+ Those checks fail a deployment. The helper fails the build earlier and more
80
+ specifically:
81
+
82
+ - **Filenames are validated, never trusted.** `pageFileName` maps one route path
83
+ to one flat name: segments joined with `~`, which cannot occur in a segment, so
84
+ `/a/b` (`a~b.html`) and `/a-b` (`a-b.html`) are distinct rather than a silent
85
+ collision, and the mapping stays injective for every accepted path. Dots,
86
+ underscores and mixed case are fine, so a docs URL like `/docs/ASSETS.md`
87
+ works. Parameters, wildcards, traversal and dot segments are rejected. A custom
88
+ `fileName` hook is allowed — hashing the route is a reasonable choice — but its
89
+ result goes through the same check: a flat name, no leading dot, not a
90
+ protected name, `.html`, and unique **case-insensitively**, because on macOS
91
+ and Windows two names differing only in case are one file.
92
+ - **Directories may not overlap.** In either direction: a build must not write
93
+ into the reviewed source, nor read a source nested inside its output.
94
+ - **Nothing is written until everything renders.** Pages are held in memory and
95
+ written only after the last one passes, and the pages directory must not
96
+ already exist — it is created, along with any missing parents, only once every
97
+ render has succeeded, so a failed build creates nothing at all. An existing
98
+ pages directory is refused with an error carrying `code: 'EEXIST'`. If your
99
+ artifact has a root above that directory, claiming it is yours: check it before
100
+ calling, and let the helper create it as a parent after the render.
101
+ - **Every render is checked.** A non-200 status, a content type that is not
102
+ `text/html`, an empty body, an oversized body or an exceeded aggregate budget
103
+ fails the build instead of publishing a file.
104
+ - **Bytes are preserved.** The response body is a byte array, not a string. It is
105
+ kept as a `Buffer` through the file and its fixture alike, so a multi-byte
106
+ character is never re-encoded or truncated.
107
+ - **Skipping is explicit.** By default a source route the build would not render
108
+ fails it, because silently rendering a subset publishes an incomplete site that
109
+ looks whole. Pass `ignoreUnrenderable` when a mixed project is intended.
110
+ - **The runtime is always closed.** In a `finally`, so a failing build exits
111
+ instead of hanging on its worker threads.
112
+
113
+ The helper is operator build tooling. It runs in Node with normal filesystem
114
+ access because it is not guest code; nothing here gives the sandbox a filesystem,
115
+ and no host-code fallback is introduced. It is a separate package export from the
116
+ runtime for that reason. Review it as you review any deployment tooling.
117
+
118
+ ## Assembling a site
119
+
120
+ What the helper returns is deliberately not a project, because that is the part
121
+ every site does differently. A small site writes one `page` route per file, as
122
+ [`examples/prerender`](../examples/prerender/README.md) does in about twenty
123
+ lines. A larger one copies the rendered pages next to its own assets, adds
124
+ `static` and `download` routes and response security headers, keeps a committed
125
+ entry point and writes only a generated include, then extends `fixtures` with its
126
+ own cases before asserting the whole thing is native:
127
+
128
+ ```js
129
+ const rendered = await prerenderPages(renderSource, 'project/public/pages');
130
+ for (const page of rendered.pages)
131
+ config.routes[page.path] = {page: {file: `public/pages/${page.file}`}, response: {headers: security}};
132
+ await writeFile('project/generated/routes.yaml', stringify(config));
133
+ await writeFile('project/tests/requests.json', JSON.stringify([...rendered.fixtures, ...ownCases]));
134
+ await assertNativeProject('project');
135
+ ```
136
+
137
+ Applying your own `response.headers` is expected; the helper never chooses them
138
+ for you and never discards them.
139
+
140
+ A site that renders straight into the tree it serves, rather than into a staging
141
+ project, needs no copy step at all — point `prerenderPages` at the pages
142
+ directory inside the serving project, keeping the render source outside it.
143
+
144
+ ## Limits worth knowing before you design a site
145
+
146
+ | Limit | Value | Where |
147
+ |---|---|---|
148
+ | Function/middleware response body | 1 MiB default (`--max-response-bytes`) | render step |
149
+ | Rendered page bytes | 512 KiB (`maxPageBytes`) | helper |
150
+ | Rendered pages, total bytes | 500, 32 MiB (`maxPages`, `maxTotalBytes`) | helper |
151
+ | Middleware entries per route | 16 | source project |
152
+ | Asset file size | 16 MiB | generated project |
153
+ | Total unique asset bytes | 64 MiB | generated project |
154
+ | Static entries traversed | 10,000 | generated project |
155
+ | Directory depth | 20 | generated project |
156
+
157
+ Startup snapshots asset bytes in memory, and a reload can briefly hold two
158
+ snapshots. A large site is bounded by the generated project's memory, not by the
159
+ render step. For collections beyond these budgets, publish to an external asset
160
+ service and redirect; provider asset adapters are not implemented.
161
+
162
+ ## Larger sites: generating the source project
163
+
164
+ The example keeps page content as reviewed literal `args` in YAML, which stays
165
+ readable and lets `dev` serve the site live. A site with hundreds of pages
166
+ instead generates its source project from host-prepared content: the build reads
167
+ its Markdown or data, compiles and sanitizes it in Node, writes a temporary
168
+ project whose routes carry that HTML as literal arguments, renders it with the
169
+ helper, and discards the temporary project.
170
+
171
+ That keeps every property intact — content is still reviewed input, guest code
172
+ still reads nothing from disk, and the published artifact is still inert. Two
173
+ things to hold onto:
174
+
175
+ - **Generated YAML is operator input.** It is written by your build, from your
176
+ content, and reviewed like any other deployment artifact. Route YAML never
177
+ gains the ability to name host code or a callback.
178
+ - **Sanitize before rendering, not after.** The template escapes the values it
179
+ interpolates, but content injected as raw HTML is published as written. Whatever
180
+ produces that HTML owns its safety.
181
+
182
+ The [urlcode-docs showcase](https://github.com/jimhoyd-com/urlcode-docs) builds
183
+ its site this way. That is a working integration, not a deployment or performance
184
+ claim.
185
+
186
+ ## What this is not
187
+
188
+ Not a static-site generator: no Markdown, no sanitizer, no asset pipeline, no
189
+ incremental or watch build, no link checking, no sitemap. Not a way to make
190
+ native bodies readable. Not a template engine — the template is ordinary
191
+ middleware you write. Not a CLI command: prerendering is a step inside a build
192
+ that already runs JavaScript, so the helper is a library. Content compilation,
193
+ sanitization, search, asset assembly and deployment stay in the application.
@@ -0,0 +1,51 @@
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
+ ## What URLCode is
10
+
11
+ A portable runtime for programmable URL behavior. A project declares its public
12
+ URL surface in YAML, adds isolated JavaScript only where declarative handlers are
13
+ not enough, and runs the same definition locally, in a container, or on operator
14
+ infrastructure. The project format is deliberately bounded so a runtime can
15
+ validate it, inspect it, test it and eventually carry it across hosting providers.
16
+
17
+ Git owns route definitions and code. Operators own credentials, storage and
18
+ capability grants. Application data stays in the operator's systems.
19
+
20
+ ## What URLCode is not
21
+
22
+ - **Not a URL shortener.** Short links are one handler beside redirects,
23
+ validated responses, request functions, middleware, pages, static assets and
24
+ downloads. The [live-link store](DYNAMIC-LINKS.md) is optional and single-host.
25
+ - **Not a general Node web framework.** Guest code runs inside WASM isolation
26
+ with no ambient filesystem, network or Node APIs. Behavior that cannot be
27
+ expressed in the bounded contract is rejected rather than emulated.
28
+ - **Not a hosting account system.** There is no end-user identity, billing or
29
+ public account surface. Management is a private operator API.
30
+ - **Not a provider configuration format.** Provider infrastructure settings do
31
+ not belong in route behavior YAML. A project must remain runnable on a laptop
32
+ or in a single container wherever else it is deployed.
33
+
34
+ ## Application projects
35
+
36
+ Applications built on URLCode are ordinary consumers of the public runtime.
37
+ They exist to prove the contract is sufficient in practice, and they get no
38
+ private fork or privileged capability. If an application needs something the
39
+ runtime cannot express, that is a gap in the public contract to close in the
40
+ open, not a reason for a special path. See the [roadmap](../ROADMAP.md).
41
+
42
+ ## License
43
+
44
+ The runtime is free and open-source software under the
45
+ [Apache License 2.0](../LICENSE). Contribution terms follow from the same
46
+ license; see [contributing](../CONTRIBUTING.md).
47
+
48
+ The license covers the runtime source in this repository. It grants no rights in
49
+ any application's own routes, content or data, and it is not a warranty or a
50
+ production-readiness claim — those are set by the
51
+ [readiness register](RELEASE-READINESS.md) and [security policy](../SECURITY.md).
@@ -0,0 +1,125 @@
1
+ # Test every route, then measure it
2
+
3
+ Alpha.5 includes a local coverage gate and an assertion-aware project benchmark.
4
+ These validate a local snapshot, not the reachability of external redirect
5
+ services or the correctness of an entire production deployment.
6
+
7
+ ```sh
8
+ urlcode routes --project ../my-links
9
+ urlcode audit --project ../my-links --expect-routes 2
10
+ urlcode benchmark --project ../my-links --requests 1000 --concurrency 2 --max-p95-ms 50
11
+ ```
12
+
13
+ All three activate/validate the project with the same isolated runtime and use
14
+ local environment loading like `test`. Pass an external `--policy` for explicitly
15
+ authorized bindings. No destination redirects are followed, credentials are not
16
+ printed, and no remote load-test target is accepted.
17
+
18
+ ## Inventory and count reconciliation
19
+
20
+ `routes` reports each configured route's pattern, handler, exact allowed methods
21
+ and active/disabled/expired state. It includes routes from YAML includes. A
22
+ parameter pattern is one route; its possible URLs are not a finite route count.
23
+ A static mount is one route, even when it contains many files.
24
+
25
+ `audit --expect-routes N` compares N with the total configured count. Its summary
26
+ separately counts active, disabled and expired routes and groups by handler.
27
+ A mismatch exits nonzero. Keep N reviewed in your application CI so accidentally
28
+ removing a route cannot silently reduce the test workload. Change it intentionally
29
+ when adding/removing routes; do not calculate the expected value from the same YAML.
30
+
31
+ ## Generated checks plus explicit examples
32
+
33
+ The audit generates GET/HEAD checks for concrete native redirects, declared
34
+ responses, pages/downloads and every snapshotted static file. It checks status,
35
+ redirect Location, file metadata/length and declared bodies where applicable.
36
+ It also checks literal disabled/expired routes for 404/410. Generated checks are
37
+ contract consistency checks; they cannot decide whether your intended destination
38
+ or content is correct. Keep independent expected outcomes in fixtures too.
39
+
40
+ Functions, parameterized routes, required inputs/bodies and non-GET/HEAD methods
41
+ need fixtures in `tests/requests.json`:
42
+
43
+ ```json
44
+ [
45
+ {"path":"/hello/Ada","status":200,"expectBody":"{\"message\":\"Hello, Ada!\"}"},
46
+ {"path":"/hello/Ada","method":"HEAD","status":200,"expectBody":""},
47
+ {"path":"/go","status":302,"expectHeaders":{"location":"https://example.com/"}},
48
+ {"path":"/go","method":"POST","status":405,"expectHeaders":{"allow":"GET, HEAD"}},
49
+ {"path":"/missing","status":404}
50
+ ]
51
+ ```
52
+
53
+ Each case may supply `method`, string-valued `headers`, a text `body`, expected
54
+ `status`, string-valued `expectHeaders`, and exact UTF-8 `expectBody`. Status is
55
+ required. A passing case needs at least one body/header assertion to count toward
56
+ coverage; status-only successes appear in `unassertedCases`. Choose assertions
57
+ that verify your intended business result, not just a generic header. Fixtures
58
+ are limited to 10,000 cases/16 MiB; checked response bodies to 16 MiB. Requests have
59
+ 10-second transport timeouts. Failures do not stop subsequent checks. Status 0
60
+ means a transport/response-limit failure. Output reports case numbers/statuses,
61
+ not response bodies, header values or fixture URLs that may contain private data.
62
+
63
+ Coverage uses the route that actually matched. A literal route shadowing a
64
+ parameter example cannot count toward parameter coverage. Each active route and
65
+ allowed method needs a passing normal response (below 400); an intentionally
66
+ error-valued native `respond` check can cover its declared outcome. A negative
67
+ fixture alone cannot prove a function works normally. Disabled/expired routes
68
+ are counted separately and excluded from active coverage requirements. Inactive
69
+ parameter patterns still need explicit negative fixtures to exercise them.
70
+
71
+ `ready: true` requires a nonempty active project, matching expected count (when
72
+ supplied), zero failed checks and no uncovered active route/method combinations.
73
+ It means this local gate passed, not that all branches, parameter values or assets
74
+ have independent business assertions. Function routes intentionally serving only
75
+ errors cannot satisfy normal-response coverage in this release. Time-dependent
76
+ expiry is evaluated at audit start; avoid running a gate exactly at expiry.
77
+
78
+ `urlcode test` runs only explicit fixtures. `audit` adds generated native checks,
79
+ counts and coverage. Both execute locally and never follow redirect destinations.
80
+ Audits run sequentially to avoid mistaking worker saturation for a routing failure.
81
+
82
+ ## Benchmark your actual project
83
+
84
+ The benchmark cycles generated checks and explicit successful GET/HEAD fixtures.
85
+ POST/PUT/PATCH/DELETE/OPTIONS and expected error cases are excluded. Function
86
+ GET/HEAD handlers still execute: use synthetic test data and reviewed bindings.
87
+ The workload is case-weighted, not a simulation of real user traffic. A short run
88
+ may not reach every case; compare `exercisedWorkloadCases` with `workloadCases`.
89
+
90
+ Output includes requested/completed count, assertion failures, status histogram,
91
+ startup time, throughput, p50/p95/p99 response time, process RSS, Node and OS.
92
+ Any wrong status/header/body, incomplete run or exceeded `--max-p95-ms` budget
93
+ exits nonzero. Warmup is zero and is reported explicitly. Client/server share one
94
+ process; RSS and latency are local measurements, not server-only production SLAs.
95
+
96
+ Defaults: 1,000 requests, concurrency 2, 30-second scheduling budget. Bounds:
97
+ 1–100,000 requests, 1–32 concurrent requests, `--seconds` 1–300. In-flight requests
98
+ may finish after the scheduling budget, bounded by their timeout. Higher function
99
+ concurrency can legitimately cause 503 because the default pool has two workers.
100
+ Choose a latency budget from repeatable measurements on your intended host.
101
+ The existing synthetic `npm run benchmark -- 10000` is still a separate scale test.
102
+
103
+ ## What a release should prove
104
+
105
+ | Check | Evidence to require |
106
+ |---|---|
107
+ | Counts and coverage | Reviewed expected count; every active route/method covered; disabled/expired routes accounted for |
108
+ | Correct happy paths | Exact redirect destinations/status/query handling; function bodies/headers; representative parameter and asset examples |
109
+ | Invalid inputs | Missing/duplicate/wrong-type inputs; malformed paths/encoding; wrong methods; bad JSON/media type; oversized bodies |
110
+ | Response contracts | HEAD empty bodies, Allow headers, cookies, cache policy, download names/MIME; ETag/304 and range/206/416 fixtures |
111
+ | Configuration changes | Invalid candidate keeps last-good routes; valid reload updates behavior; removed routes are intentional |
112
+ | Code containment | Runtime security suite passes; no ambient filesystem/network access; grants narrow and revision-pinned |
113
+ | Capacity and failure | Representative mix and concurrency; low errors and repeatable latency; timeouts, overload recovery and memory over sustained runs |
114
+ | Deployment | Fresh install; real HTTPS/domain/health smoke; rollback; shutdown; logs/alerts; explicitly authorized destination reachability checks |
115
+
116
+ The runtime suite covers many generic protocol/security/reload cases. Apps must
117
+ supply their own business and boundary fixtures. Automated remote destination
118
+ health, redirect-chain/loop analysis, DNS/TLS checks, sustained soak/load profiles,
119
+ coverage by function branch and historical performance comparison remain planned.
120
+ Run the local audit in CI now; do not label a passing local audit “production certified.”
121
+
122
+ Routes with middleware need explicit request fixtures with meaningful response
123
+ assertions for every active method. Audit cannot infer their behavior from the
124
+ underlying redirect or asset handler, so it does not generate native checks for
125
+ those routes. The route inventory includes a middleware count.
package/docs/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # URLCode documentation
2
+
3
+ Start with [project direction](PROJECT-DIRECTION.md) for what URLCode is, what it
4
+ is not, how application projects fit, and the license.
5
+
6
+ For runtime 0.2.0. Use documentation pinned to your runtime revision.
7
+ `version: "1"` is the stable project-format contract for this release line.
8
+
9
+ | Goal | Start here |
10
+ |---|---|
11
+ | Install the CLI | [Installation](INSTALL.md) |
12
+ | Generate placeholders from YAML | [Scaffolding](SCAFFOLDING.md) |
13
+ | Write YAML with examples | [YAML guide and recipes](YAML-GUIDE.md) |
14
+ | Look up every accepted field | [Generated field reference](YAML-REFERENCE.md), [JSON Schema](../schemas/urlcode.schema.json) |
15
+ | Let an AI build routes | [AI authoring guide](AI-AUTHORING.md), [llms.txt](../llms.txt) |
16
+ | Run examples | [25-route cookbook](../examples/cookbook/README.md), [prerender recipe](../examples/prerender/README.md), [small starter](STARTERS.md) |
17
+ | Explore a standalone application | [URLCode Shortener demo](https://github.com/jimhoyd-com/urlcode-shortener), [build retrospective](https://github.com/jimhoyd-com/urlcode-shortener/blob/main/docs/BUILD-RETROSPECTIVE.md) |
18
+ | Explore a static docs-site integration | [URLCode Docs project](https://github.com/jimhoyd-com/urlcode-docs), [build retrospective](https://github.com/jimhoyd-com/urlcode-docs/blob/main/docs/BUILD-RETROSPECTIVE.md) — synced from this repository, which remains the source of truth |
19
+ | Create/update short links live | [Dynamic links, storage and management API](DYNAMIC-LINKS.md) |
20
+ | Understand exact behavior | [Specification](SPECIFICATION.md), [routing](ROUTING.md), [HTTP](HTTP.md) |
21
+ | Reuse code around routes | [Middleware](MIDDLEWARE.md) |
22
+ | Throttle, block agents, set security headers, compress or cache from YAML | [Policies](POLICIES.md): [throttle](policies/throttle.md), [agents](policies/agents.md), [security](policies/security.md), [compression](policies/compression.md), [cache](policies/cache.md) |
23
+ | Add host behavior in operator code | [Plugins](PLUGINS.md) |
24
+ | Use the API from TypeScript | [TypeScript: shipped declarations, exports, build and fidelity](TYPESCRIPT.md) |
25
+ | Check declared configuration against standards-referenced rules | [Compliance](COMPLIANCE.md) |
26
+ | See how the runtime conforms to the standards it uses | [Standards audit](STANDARDS.md) |
27
+ | Serve pages/files/downloads | [Assets](ASSETS.md) |
28
+ | Generate robots.txt, sitemap.xml, favicon, security.txt and llms.txt | [Site conventions](SITE.md) |
29
+ | Publish a site with no request-time guest code | [Prerendering helper and recipe](PRERENDER.md) |
30
+ | Keep code and YAML readable | [Organization and readability practices](BEST-PRACTICES.md) |
31
+ | Organize YAML across folders | [Organization](ORGANIZATION.md) |
32
+ | Work locally | [Local development](LOCAL-DEVELOPMENT.md) |
33
+ | Share a local project publicly | [Tunnels](TUNNELS.md) |
34
+ | Watch a deployment | [Monitoring](MONITORING.md) |
35
+ | Wire your own monitoring or scrape metrics | [Observability](OBSERVABILITY.md) |
36
+ | Load test a deployment | [Load testing](LOAD-TESTING.md) |
37
+ | Deploy to Vercel | [Vercel adapter](VERCEL.md) |
38
+ | Deploy to AWS Lambda | [AWS adapter](AWS.md) |
39
+ | Deploy to Cloudflare Workers | [Cloudflare target](CLOUDFLARE.md) |
40
+ | Handle secrets/untrusted code | [Function security](FUNCTION-SECURITY.md) |
41
+ | Prove responses and counts | [Readiness](READINESS.md) |
42
+ | Estimate concurrency/memory | [Capacity and limits](CAPACITY.md), [measurements](PERFORMANCE.md) |
43
+ | Prepare for overload/DDoS/recovery | [Resilience playbook](RESILIENCE.md) |
44
+ | Deploy and roll back | [Operations](OPERATIONS.md) |
45
+ | Review security findings and gaps | [Internal security audit](SECURITY-AUDIT.md) |
46
+ | Assess release readiness | [Evidence and open gates](RELEASE-READINESS.md) |
47
+ | See unfinished work | [Roadmap](../ROADMAP.md) |
48
+ | Read the design behind policies, plugins and templates | [Extensions spike](SPIKE-EXTENSIONS.md) |
49
+
50
+ Examples are educational unless backed by the runnable cookbook/fixtures.
51
+ Infrastructure limits are deployment settings, not fields to invent in route YAML.
52
+
53
+ ## Security and acceptance
54
+
55
+ - [Independent sandbox review package](SANDBOX-REVIEW.md)
56
+ - [Private management credentials and atomic audit](MANAGEMENT-SECURITY.md)
57
+ - [Operational drills and deployment acceptance](OPERATIONAL-PROOF.md)
58
+ - [Candidate signing and release security](RELEASE-SECURITY.md)
@@ -0,0 +1,95 @@
1
+ # Release readiness
2
+
3
+ Status: stable 0.1 self-hosted release. Production approval remains specific to
4
+ the workload and deployment environment.
5
+ This register describes the current public runtime, not future promises.
6
+ Use the contract and docs from the same pinned commit as your installed runtime.
7
+
8
+ ## What is aligned
9
+
10
+ - One portable YAML project, explicit includes, seven mutually exclusive handlers,
11
+ per-route middleware and consistent request/response validation.
12
+ - One starter with a function route first and an ordinary redirect second.
13
+ Clone urlcode-template or use `urlcode init`; neither requires a database.
14
+ - Native handlers avoid user-code execution unless middleware is attached.
15
+ - Live links are mutable records behind a declared route. Git owns definitions;
16
+ an optional external SQLite store owns records. This is single-host storage.
17
+ - Untrusted functions run in isolated QuickJS/WASM with no ambient filesystem,
18
+ network or Node APIs. Host bindings require external revision-pinned approval.
19
+ - The runtime is released under Apache-2.0.
20
+
21
+ ## Regression evidence
22
+
23
+ `npm run verify` is the lint, syntax/schema-reference and unit/HTTP regression gate.
24
+ `npm run test:package` installs the packed artifact and exercises initialized apps,
25
+ route audits, bounded benchmarks, assets and live links. CI runs Node 22/24/26 on
26
+ Linux/macOS/Windows and tests the container under resource restrictions.
27
+
28
+ | Area | Covered behavior | Practical limit |
29
+ |---|---|---|
30
+ | Routing and HTTP | Exact/parameter/static precedence, methods, inputs, assets, middleware and response assertions | Stable 0.1 contract; unsupported semantics reject rather than emulate |
31
+ | Isolation | Sandbox capability/permission boundaries, deadlines, memory and invalid outputs | Not an independent security assessment or multi-tenant service certification |
32
+ | Overload | Function/store queue caps; HTTP admission saturation, separate bounded probe budget, health availability and recovery after upload completion/disconnect | 64 application requests default; no fairness, upstream DDoS protection or end-to-end deadline |
33
+ | Worker replacement | Repeated guest deadlines shed load and the pool returns to service after backoff, rather than latching off for the life of the process | Store-connection replacement shares this logic but its failure branch has no automated test; a crash there is covered by reasoning and review only |
34
+ | Persistence | Committed writes visible to independent readers; concurrent CAS, restart and abrupt writer exit | SQLite on one host; no distributed availability |
35
+ | Shutdown | Full accepted store queue drains; new work rejects; repeated close shares completion | Existing deadlines can still fail; uncertain writes must be reconciled |
36
+ | Management | Token boundaries, body limits, origin rejection, conditional mutations, endpoint-specific Allow headers | Private operator API, not public end-user account management |
37
+ | Activation/recovery | Invalid reload retains last-good snapshot; corrupt revision metadata rejects activation | No deployment orchestration or automatic database repair |
38
+ | Packaging | Packed installation and starter examples tested; sensitive files excluded | No published npm/Homebrew release or provider adapter guarantee |
39
+
40
+ `npm audit --omit=dev` now runs in CI and fails the build on any runtime advisory;
41
+ development-only advisories are reported without blocking. Dependabot proposes npm,
42
+ GitHub Actions and base-image updates weekly. Actions and the container base image
43
+ are pinned by immutable SHA/digest, so a rebuild cannot silently change the runtime.
44
+ A passing audit is a dated check against known advisories, not proof of safety.
45
+
46
+ ## Gates before production approval
47
+
48
+ These remain open. Record workload, runtime/app/image revisions, environment,
49
+ commands, results and owner for each exercise; do not convert a passing local
50
+ benchmark into a universal throughput claim.
51
+
52
+ 1. Independent review of host/sandbox boundaries and authenticated management.
53
+ 2. Sustained load and soak on intended hardware, through the actual TLS/proxy
54
+ path: successful throughput, tail latency, 503/504 rate, RSS and recovery.
55
+ `urlcode benchmark --target` measures a running deployment through its real
56
+ path and separates shed responses from transport errors; see
57
+ [load testing](LOAD-TESTING.md). The tool is GET/HEAD only and caps at 300
58
+ seconds, so it does not by itself close this gate.
59
+ Include mixed native/function/live-link workloads and slow peers.
60
+ 3. A real backup/restore drill on a separate host/volume, with record/version
61
+ reconciliation and measured recovery time/data loss. Keep SQLite WAL files
62
+ consistent; copying a live main database file alone is not a backup.
63
+ 4. Kill/restart, resource exhaustion, disk-full and rolling deployment/rollback
64
+ exercises with the chosen supervisor, ingress and persistent storage.
65
+ 5. Alerting and ownership for sustained errors, latency, readiness, dropped logs,
66
+ disk space, restarts and backups. Pick service objectives for the actual app.
67
+ 6. Stable-release support commitments before packaging/public reuse claims.
68
+ Private security reporting and the current support baseline are documented in
69
+ SECURITY.md.
70
+
71
+ License selection is resolved: URLCode is released under the Apache License 2.0,
72
+ `package.json` declares it, and the repository carries the full license text.
73
+ The remaining gates above are engineering and operational, not legal.
74
+
75
+ The full free-product roadmap additionally includes bulk interchange tooling,
76
+ installers/Homebrew, provider adapters and reusable templates/signals. Those
77
+ features are not implemented merely because
78
+ YAML has a portable design. Node process/container hosting is the supported
79
+ execution target today; AWS/Vercel/Cloudflare adapters and guest network/realtime
80
+ capabilities remain future work. See [roadmap](../ROADMAP.md).
81
+
82
+ The [internal security audit](SECURITY-AUDIT.md) records reproduced findings, fixes
83
+ and prioritized gaps. Its regressions supplement these gates; they do not replace
84
+ independent assessment or real deployment exercises.
85
+
86
+ ## Hardening follow-up
87
+
88
+ Implemented: bounded YAML workers and aggregate source budgets, cooperative route
89
+ compilation deadline, loopback-only management, scoped/expiring/revocable operator
90
+ credentials, atomic SQLite mutation audits, executable local/CI operational drills,
91
+ and a main-only candidate signing/SBOM workflow.
92
+
93
+ Still required: [independent review](SANDBOX-REVIEW.md), [actual deployment proof](OPERATIONAL-PROOF.md),
94
+ and publication/support arrangements. The Apache-2.0 license and the 0.2.0 self-hosted
95
+ release do not close the security and deployment gates. See [release process](RELEASE-SECURITY.md).
@@ -0,0 +1,72 @@
1
+ # Candidate and release security process
2
+
3
+ URLCode 0.2.0 is the Apache-2.0 self-hosted baseline; licensing is defined
4
+ in [LICENSE](../LICENSE). Two workflows share one audited build path and differ
5
+ only in what they do with its output:
6
+
7
+ - `candidate.yml` is dispatched manually on main and retains a **signed build
8
+ candidate** for 30 days without publishing it. Use it to review a commit
9
+ before tagging it.
10
+ - `release.yml` runs on a `v*` tag whose commit is already on main, and publishes
11
+ a GitHub release. Publication to npm and GHCR is opt-in per repository variable
12
+ (`PUBLISH_NPM`, `PUBLISH_CONTAINER`), so a release can be artifacts-only.
13
+
14
+ Neither workflow is a statement that a release is production-ready; see
15
+ [release readiness](RELEASE-READINESS.md).
16
+
17
+ 1. Report vulnerabilities privately through [GitHub advisories](https://github.com/jimhoyd-com/urlcode/security/advisories/new).
18
+ The maintainer triages impact, confirms affected exact revisions, coordinates a
19
+ fix/retest privately, and publishes an advisory with upgrade guidance when safe.
20
+ Current reviewed main is the security support baseline; no guaranteed SLA/backports.
21
+ 2. Change dependencies through protected PRs. Review upstream provenance/advisories,
22
+ lockfile integrity and tests. Direct dependencies are exact, npm installs use
23
+ `npm ci`, CI actions use full commit SHAs, and container bases use SHA-256 digests.
24
+ Dependabot proposes updates; it does not authorize merging. Never silently
25
+ refresh dependencies during a candidate build.
26
+ 3. After protected main checks pass, a maintainer manually dispatches
27
+ `.github/workflows/candidate.yml` **on main**, or pushes a `v<version>` tag to
28
+ run `.github/workflows/release.yml`. The release workflow refuses a tag whose
29
+ commit is not an ancestor of main, refuses a tag that disagrees with
30
+ `package.json`, and refuses to build a private or unlicensed package. It reads and validates the digest-pinned Node image directly from Dockerfile,
31
+ installs without lifecycle scripts, verifies, package-tests and runs local drills.
32
+ It packs the runtime and creates a CycloneDX dependency SBOM plus a manifest
33
+ recording source commit, lockfile hash, engine versions and artifact hashes.
34
+ The package contains `dist/`, produced inside that build from the tagged
35
+ TypeScript sources by Node's type stripping (`scripts/build.ts`); `dist` is
36
+ never committed. The manifest records the Node version that stripped it, the
37
+ locked TypeScript version and a SHA-256 per emitted file
38
+ (`dist/BUILD-MANIFEST.json`), so a download can be verified by running
39
+ `npm run build` on the tagged commit with that Node version and comparing its
40
+ `dist/` file by file. The build strips types and rewrites specifier
41
+ extensions; it never bundles, minifies or transforms syntax, so every line of
42
+ `dist/x.js` is the corresponding line of `src/x.ts`. CI's `build-fidelity`
43
+ job builds twice and diffs the trees, so the transform is known to be
44
+ deterministic before a tag is cut.
45
+ 4. The pinned official `actions/attest` action signs provenance for the package,
46
+ SBOM and manifest using short-lived GitHub OIDC/Sigstore credentials. No long-lived
47
+ signing key is stored. Signing permissions exist only in this manual job; build
48
+ commands run in a container without passing GitHub tokens. Candidate files are
49
+ retained as GitHub Actions artifacts for 30 days. A release additionally attaches
50
+ them to the GitHub release, and publishes to npm with `--provenance` and to GHCR
51
+ when those repository variables are enabled.
52
+ 5. Download the candidate for the intended commit and verify **each file**, e.g.
53
+ `gh attestation verify urlcode-0.2.0.tgz --repo jimhoyd-com/urlcode --signer-workflow jimhoyd-com/urlcode/.github/workflows/candidate.yml --source-ref refs/heads/main --source-digest APPROVED_COMMIT_SHA`
54
+ (use `release.yml` as the signer workflow for a tagged release).
55
+ Check the verified provenance's source commit against the approved commit, and
56
+ compare package/SBOM hashes with the signed manifest. A signature establishes
57
+ provenance, not safety, reproducible bytes or production approval. See
58
+ [GitHub verification](https://cli.github.com/manual/gh_attestation_verify) and
59
+ [the official attestation action](https://github.com/actions/attest).
60
+ 6. Before registry publication, assign patch/release ownership and retention, and
61
+ document version/support policy. Before claiming hostile multi-tenant or
62
+ deployment-specific readiness, close independent-review and deployment gates. Keep last-good verified artifacts and compatible policies;
63
+ rehearse rollback. Never overwrite a published artifact/version or downgrade a
64
+ management writer past its audit/security baseline.
65
+
66
+ The SBOM describes npm dependencies; it is not a complete OS/container SBOM. Hosted
67
+ runners and action runtimes remain platform-controlled. Digest pins improve supply
68
+ chain integrity but do not prove byte-for-byte reproducibility or engine safety.
69
+ Signing verification must be demonstrated on a successful main workflow run before
70
+ claiming a candidate has been signed. Workflow definition alone is not that evidence:
71
+ `release.yml` has never been executed, so no release has yet been produced or signed
72
+ by it, and the npm and GHCR publication paths are unproven until a real tag runs.