@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,160 @@
1
+ # DDoS, overload and recovery playbook
2
+
3
+ This is an operator plan for the current self-hosted release, not a claim of built-in DDoS
4
+ protection, managed failover or tested high availability. URLCode's sandbox and
5
+ resource limits reduce some failure modes; they cannot protect a saturated
6
+ network link. Keep the public origin behind your existing protected ingress.
7
+
8
+ ## Defense responsibilities
9
+
10
+ | Layer | Operator action | What URLCode provides today |
11
+ |---|---|---|
12
+ | Upstream network | Hosting/CDN DDoS mitigation and an escalation contact; protect bandwidth before it reaches the origin | No network-level mitigation service |
13
+ | Edge/proxy | TLS, per-client and aggregate request/connection budgets, header/body/time limits; restrict direct origin access | Private bind default; no automatic TLS/WAF. Optional per-instance [`throttle` and `agents` policies](POLICIES.md) as a second layer behind the edge, with `--trusted-proxies` naming the hops allowed to set `X-Forwarded-For` |
14
+ | Application | Validate inputs, bound expensive work, authenticate sensitive operations | Strict route/body validation; sandbox deadlines and no execution queue |
15
+ | Process/container | CPU/RAM/PID limits, restart backoff, least privilege, read-only reviewed app | Worker isolation, bounded worker replacement, health and request logs |
16
+ | Release/recovery | Known-good artifacts, candidate verification, traffic switching, rollback drills | Local validation/tests/audit; explicit snapshot reload; no orchestration |
17
+
18
+ NGINX provides request-rate controls and connection controls keyed by values such
19
+ as client IP. Their counters do not cover every stage of connection handling;
20
+ connection limiting starts after request headers are read. Treat them as layers,
21
+ not a complete volumetric defense. Use aggregate budgets too, and test legitimate
22
+ users behind shared NATs. Sources: [request limits](https://nginx.org/en/docs/http/ngx_http_limit_req_module.html),
23
+ [connection limits](https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html).
24
+
25
+ If a CDN/proxy sits ahead of your ingress, configure trusted client-IP restoration
26
+ for only that provider's verified proxy addresses. Never use arbitrary incoming
27
+ X-Forwarded-For as the rate-limit identity. Keep the origin firewall/private
28
+ network restricted to the intended ingress. URLCode deliberately does not trust
29
+ forwarded headers to construct its public URL; set `--origin` explicitly.
30
+
31
+ The optional [`throttle` policy](policies/throttle.md) adds a per-client budget
32
+ inside the runtime, and the [`agents` policy](policies/agents.md) refuses listed
33
+ User-Agents before a body is read or the sandbox starts. Both are a second
34
+ layer behind the edge, not a replacement for it: counters are per instance,
35
+ the socket and admission limits still apply first, and a flood still costs
36
+ connections. The client identity is the socket peer unless
37
+ `urlcode serve --trusted-proxies <cidr,...>` names the proxies allowed to set
38
+ `X-Forwarded-For`; a forwarded header from any other peer is ignored, and an
39
+ unresolved client shares one bucket rather than being exempt. See
40
+ [policies](POLICIES.md).
41
+
42
+ Cache only responses whose semantics permit public caching. Never cache private,
43
+ credential-bearing or personalized responses as a blanket mitigation. Default
44
+ function/redirect responses are no-store; assets default to revalidation. Use
45
+ versioned immutable asset URLs when appropriate. An NGINX/native-route exporter
46
+ is not implemented; adding a reverse proxy does not bypass runtime execution.
47
+
48
+ ## Threats and behavior to expect
49
+
50
+ | Failure or traffic pattern | Current consequence | Mitigation / recovery |
51
+ |---|---|---|
52
+ | High-rate ordinary requests | Event-loop, logging, bandwidth or socket exhaustion | Filter/rate-limit upstream; scale only within measured resource/cost budgets |
53
+ | Flood of functions or middleware | Two default worker slots fill; further calls receive 503 | Bound programmable traffic before the origin; isolate heavy workloads |
54
+ | Infinite/slow application code | Shared invocation deadline returns 504; worker terminated/replaced | Identify bad release/route with protected diagnostics; roll back or block route at ingress |
55
+ | Guest invalid response or failure | Generic 502 | Compare with last deployment; run fixture on a private candidate |
56
+ | Repeated worker exits | Replacement backs off (250 ms doubling to 30 s) and keeps retrying; readiness stays 503 until every slot serves | Contain the cause; load is shed meanwhile. Replacement never stops, because a guest deadline is reachable from ordinary request input and must not disable functions until an operator restarts |
57
+ | Large/slow requests | 64 admitted application requests, body/header limits and receipt timeouts; copies still consume memory | Smaller proxy/body budgets and connection admission limits |
58
+ | Slow response readers | Retain admission/output buffers until finish/disconnect; 15-second socket inactivity timeout | Proxy downstream timeouts and connection controls |
59
+ | Corrupt YAML/code/asset update | Dev/explicit reload rejects candidate and retains old snapshot | Restore reviewed files; verify actual active version; do not assume edit activated |
60
+ | Host OOM or process crash | In-process recovery cannot preserve service | Supervisor with backoff; route traffic to healthy replica; restore tested image |
61
+ | Disk/log sink trouble | Logs may drop; startup/config reads can fail | Disk/RSS alerts, bounded retention and log-drop monitoring |
62
+ | Bandwidth flood at origin link | App may be unreachable regardless of worker limits | Hosting/network mitigation provider escalation |
63
+
64
+ A 503/504 can also be an intentional application response. Runtime logs do not
65
+ include a per-route reason code, so status alone cannot prove an attack or a
66
+ capacity failure. Correlate ingress metrics, release changes and controlled
67
+ probes. Do not add secrets, full query strings or request bodies to diagnostics.
68
+
69
+ ## Prepare before exposure
70
+
71
+ - Name an incident owner, backup and hosting/ingress escalation contacts. Decide
72
+ who can change filters, switch traffic, approve rollback and rotate credentials.
73
+ - Retain known-good runtime/app commits, locks, images and compatible policy files.
74
+ Keep protected backups outside the host. Secret values belong in your secret
75
+ system, never the artifact archive or Git.
76
+ - Keep at least one verified rollback path; use redundant replicas if your
77
+ availability objective needs them. A single instance cannot promise continuity
78
+ through a host failure or full restart.
79
+ - Define acceptable successful RPS, p95/p99, error rates and resource utilization
80
+ using [capacity tests](CAPACITY.md). Set alerts for deviations, readiness failures,
81
+ restarts/OOM, memory pressure, bandwidth, log drops and infrastructure spend.
82
+ - Restrict `/_urlcode/*` to operator access at ingress. Keep liveness and readiness
83
+ separate: busy workers do not make readiness fail; missing workers do.
84
+ - Use bounded retries with backoff/jitter where retries are safe. Do not retry
85
+ non-idempotent requests blindly or create a synchronized retry storm.
86
+
87
+ ## Incident procedure
88
+
89
+ 1. **Confirm and record.** Note start time, affected service, runtime/app/image
90
+ versions, error/latency trends and recent changes. Check upstream reachability,
91
+ process health and readiness through an operator-only path. Distinguish an
92
+ ingress incident from a bad deployment or legitimate traffic increase.
93
+ 2. **Protect capacity.** Apply reviewed ingress filters/admission limits and
94
+ contact the mitigation provider for network saturation. Restrict an abusive
95
+ route at ingress if needed. Preserve working simple routes where possible.
96
+ Do not raise worker counts/timeouts blindly or scale without a spend ceiling.
97
+ 3. **Stabilize.** Remove failed replicas from traffic. If a release is implicated,
98
+ switch to the retained verified revision; avoid repeatedly rebuilding snapshots
99
+ under attack. For a bad function, an ingress block is faster than a YAML edit
100
+ that still needs deployment and may invalidate binding policy.
101
+ 4. **Restore deliberately.** Start a candidate with the intended runtime/app,
102
+ assets, bindings and matching external policy. Check private readiness and
103
+ representative response assertions. Switch a small traffic share first when
104
+ supported, observe, then increase. Drain the old instance before stopping it.
105
+ 5. **Verify recovery.** Confirm successful traffic, latency, RSS and error rates
106
+ return to the agreed envelope. Check native and programmable routes, HEAD,
107
+ redirect destinations and critical asset behavior. Readiness alone is not enough.
108
+ 6. **Close and improve.** Record cause, actions, outage duration, lost telemetry,
109
+ customer impact and costs. Keep useful mitigations; remove temporary broad
110
+ restrictions carefully. Add a regression fixture or drill for the failure.
111
+
112
+ Restarting can replace failed workers, but it does not fix a malicious request
113
+ pattern, broken release or saturated link. Rotate credentials if exposure is
114
+ suspected, not automatically for every traffic spike. Redeploy to load new values;
115
+ serve does not refresh them. No credential values should enter incident notes.
116
+
117
+ ## Recovery objectives and data
118
+
119
+ Choose RTO (acceptable recovery time) and RPO (acceptable data loss) per deployment;
120
+ URLCode does not promise values. A stateless YAML deployment can be recreated
121
+ from retained immutable artifacts, subject to recovery of DNS/ingress and secret
122
+ access. Log loss is possible under pressure and has a separate retention target.
123
+ Stored live-link records and future app-owned state need their own backups and restore
124
+ verification; Git route configuration does not back up runtime data.
125
+
126
+ A rollback needs the previous app/runtime and its matching policy and compatible
127
+ secret bindings. The health version combines configuration and asset digests;
128
+ it is not a complete code/release identity. Track Git/image identities externally.
129
+ Browser/CDN-cached permanent redirects may continue after rollback; choose cache
130
+ lifetimes accordingly and plan edge purges. Rollback cannot undo external side
131
+ effects, and current URLCode has no state migration/restore control plane.
132
+
133
+ ## Safe recovery drills and pass criteria
134
+
135
+ Run only against owned, isolated test deployments with fixed request/time budgets
136
+ and stop conditions. Coordinate with the hosting provider for any network stress
137
+ exercise; do not generate uncontrolled floods or target third-party destinations.
138
+
139
+ | Drill | Evidence to retain |
140
+ |---|---|
141
+ | Saturate the function pool with bounded test handlers | Excess work receives bounded failures; native probe remains observable; normal traffic recovers afterward |
142
+ | Handler timeout/worker replacement | 504 observed, replacement or bounded unavailable state understood, no leaked request data |
143
+ | Invalid dev reload | Old response stays active; rejection logged; corrected candidate activates |
144
+ | Kill a disposable replica | Supervisor backoff and health-based routing work; successful capacity of survivors measured |
145
+ | Roll back a bad release | Prior exact response restored with matching policy; actual recovery time recorded |
146
+ | Restore on a clean host | Artifacts, policy and secure bindings suffice; assets and all critical fixtures pass |
147
+ | Simulate a slow/full log sink | Dropped-log reporting is observed; service and disk usage remain bounded |
148
+ | Remove mitigation after a bounded overload | Latency/errors/resources return to baseline without a restart loop |
149
+
150
+ Existing unit/HTTP tests cover several component failures; these deployment drills
151
+ are a plan, not evidence they have all been run. Remaining gaps include distributed
152
+ admission/fairness, production metrics/exporters, dedicated slow-reader protection,
153
+ provider-level mitigation validation and sustained failure/soak testing. These are
154
+ free-runtime/operator requirements.
155
+
156
+ For optional live links, protect the separate management listener and token, bound
157
+ its traffic, and back up the SQLite store with the documented closed-store or
158
+ SQLite-aware procedure. Store worker failure returns 503; stop the cause before
159
+ reloading/restarting. An uncertain mutation may have committed. See
160
+ [dynamic-link recovery](DYNAMIC-LINKS.md).
@@ -0,0 +1,95 @@
1
+ # Route matching and new links
2
+
3
+ ## Supported path patterns
4
+
5
+ URLCode matches paths, not regular expressions. Matching is case-sensitive and
6
+ covers the whole path; trailing slashes are significant. Query strings are inputs,
7
+ not part of the route key.
8
+
9
+ | Route key | Matches | Does not match |
10
+ |---|---|---|
11
+ | `/go` | `/go`, `/go?campaign=spring` | `/Go`, `/go/`, `/go/extra` |
12
+ | `/r/{code}` | `/r/abc`, `/r/123` | `/r/`, `/r/abc/extra` |
13
+ | `/r/{code}/details` | `/r/abc/details` | `/r/abc/other/details` |
14
+ | `/assets/*` with a `static` handler | Files under `/assets/`, including `/assets/css/site.css` | `/assets`, `/assets-other/site.css` |
15
+
16
+ A `{parameter}` captures exactly one nonempty path segment. It is **not greedy**:
17
+ it cannot consume slashes or the rest of a URL. Declare each path parameter as a
18
+ required string in `parameters`. Encoded slashes (`%2F`) and backslashes are
19
+ rejected, so encoding a slash cannot bypass this rule.
20
+
21
+ Only static directory handlers support `/*`, at the end of an otherwise literal
22
+ path. It covers the remaining nested file path; it is not a named capture or a
23
+ regex operator. Matching a mount does not guarantee a response file exists:
24
+ missing files return 404. It is not a catch-all for functions or redirects.
25
+
26
+ No regex routes, greedy parameters, optional segments, partial-segment parameters,
27
+ `**` globs, or regex constraints inside `{code}` are implemented. Characters such
28
+ as `.` and `+` have no regex meaning in a literal path. Do not paste a regex into
29
+ a route key: some regex-looking text is legal literal text, while unsupported
30
+ syntax may fail validation. Parameter-schema `pattern` is also unsupported.
31
+
32
+ ## Precedence and ambiguity
33
+
34
+ 1. An exact literal route wins.
35
+ 2. A parameterized route wins next; more literal segments means higher priority.
36
+ 3. Static mounts follow; the longest matching mount prefix wins.
37
+
38
+ YAML order and include-file order do not decide priority. For example, `/r/help`
39
+ wins over `/r/{code}`. Among parameter routes, `/r/fixed/{item}` wins over
40
+ `/r/{group}/{item}` for `/r/fixed/book`. The latter still handles `/r/team/book`.
41
+
42
+ Equally specific overlapping parameter routes fail configuration validation.
43
+ For example, `/r/{code}` and `/r/{name}` conflict, as do `/a/{x}` and `/{y}/b`.
44
+ Disjoint patterns with equal specificity are allowed. Duplicate exact route keys
45
+ also fail, including duplicates across included files.
46
+
47
+ Selection happens before method, enabled/expiry and input validation. A selected
48
+ route returning 405, 404, 410 or 400 does not fall through to another route.
49
+ Likewise, a missing file in the longest selected static mount does not fall back
50
+ to a shorter mount. See [HTTP](HTTP.md) and [the contract](SPECIFICATION.md).
51
+
52
+ ## Adding a configured link today
53
+
54
+ ```sh
55
+ urlcode add https://example.com/new-page --alias new-link --project ./my-links
56
+ ```
57
+
58
+ This validates and writes `/new-link` to the project's `urlcode.yaml`. It is a
59
+ local authoring command, not a live route-registration API. It does not commit
60
+ to Git or contact a running server. Commit/review the resulting definition as
61
+ part of your normal deployment workflow.
62
+
63
+ | Running mode | How new YAML routes become active |
64
+ |---|---|
65
+ | `urlcode dev` | Watches ordinary project files about every 500 ms, builds and validates a complete replacement snapshot, then swaps it in without restarting the HTTP server |
66
+ | `urlcode serve` | Fixed snapshot; restart/redeploy to activate configuration changes |
67
+ | Embedded server API | The returned server exposes `await app.reload()` for an explicit full snapshot replacement; this is not an HTTP admin endpoint |
68
+
69
+ Reload is a full configuration/source/asset snapshot rebuild, not an incremental
70
+ single-route insertion. Existing requests finish on the old snapshot; new
71
+ requests use the replacement once it is ready. An invalid candidate leaves the
72
+ previous snapshot serving. Watcher exclusions and binding-policy rules still
73
+ apply: configuration changes invalidate revision-pinned external binding grants.
74
+ See [reload details](SPECIFICATION.md#reload-and-status) and
75
+ [operator policy](FUNCTION-SECURITY.md).
76
+
77
+ ## A TinyURL-style service: application data versus route definitions
78
+
79
+ For a service where visitors constantly create short links, the intended design
80
+ is one stable route such as `/r/{code}` plus a lookup of `code -> destination` in
81
+ application-owned persistent data. A new short code then changes data, not YAML,
82
+ and needs no route rebuild or service restart. The same distinction applies to
83
+ any per-visitor session record. Git owns route behavior and code; user-created records have
84
+ their own persistence, backup and export lifecycle.
85
+
86
+ This is now implemented for short-link redirects through the optional `link`
87
+ handler, local SQLite storage, CLI and a separate authenticated management API.
88
+ See [dynamic links](DYNAMIC-LINKS.md) for complete YAML, setup, consistency and
89
+ backup details. No route reload is needed for committed record changes.
90
+
91
+ Functions still cannot access databases, the filesystem or network directly.
92
+ The native link handler performs the bounded lookup, and middleware can wrap a
93
+ successful redirect. General application state and realtime sessions remain
94
+ future work. SQLite is optional and supports local same-host processes; a
95
+ multi-host deployment needs a different adapter. A cache is not the durable store.
@@ -0,0 +1,59 @@
1
+ # Independent sandbox review gate
2
+
3
+ Status: **external assessment not performed**. Internal source review, CI, CodeQL
4
+ and adversarial regressions are useful evidence, not an independent sign-off.
5
+ Do not host anonymous hostile multi-tenant code before this gate is closed.
6
+ No unrestricted Node execution fallback is permitted.
7
+
8
+ ## Review package
9
+
10
+ Freeze an exact runtime commit, lockfile, container digest, app/policy examples
11
+ with synthetic credentials, Node/SQLite/QuickJS/WASM versions and deployment
12
+ resource settings. Give a reviewer independent of the implementation access to:
13
+
14
+ - `src/functions.ts`, worker/guest implementation and `src/policy.ts`: VM creation,
15
+ module graph, import denial, export validation, binding grants and message bridge.
16
+ - `src/config.ts`, `src/config-worker.ts`, router and assets: parser/schema limits,
17
+ file containment, activation, memory amplification and host-side compilation.
18
+ - HTTP server, management API/policy and link-store worker: request smuggling,
19
+ admission, body/response framing, authorization, revocation and atomic audit.
20
+ - `test/sandbox.test.ts`, middleware/config/links/logging/reload tests, Dockerfile,
21
+ protected workflows and `docs/FUNCTION-SECURITY.md`.
22
+
23
+ Run `npm ci --ignore-scripts`, `npm run verify`, `npm run test:package`, and
24
+ `node scripts/operational-drills.ts`. Record the exact commands and result files.
25
+ CI adds constrained-container and real disposable-volume exhaustion tests.
26
+ Use only disposable local/staging systems with synthetic data.
27
+
28
+ ## Threat model and required probes
29
+
30
+ An attacker controls project YAML, included files, function/module source, public
31
+ requests, request bodies, exported values and static content. The operator controls
32
+ the host, deployment, external policy, credentials, database and project activation.
33
+ Guests receive only explicitly granted values; granted secrets can be returned by
34
+ that guest. QuickJS/WASM is the code boundary; worker threads alone are not.
35
+
36
+ Probe module cycles and loader escapes, malformed bridge messages, huge strings,
37
+ arrays and ArrayBuffers, deep prototypes, exceptions/getters, asynchronous jobs,
38
+ infinite loops, repeated worker replacement, capability confusion, cross-route
39
+ and cross-invocation leakage, parser/schema amplification, filesystem races,
40
+ slow peers, reload overlap and process/container exhaustion. Test unauthorized
41
+ management reads/writes, stale credentials, expiry boundaries, malformed policy,
42
+ CAS races, audit failures, poisoned databases and full disks. Verify native routes
43
+ and health/recovery remain useful after each bounded guest failure.
44
+
45
+ Trusted computing base includes Node/V8, QuickJS/WASM, bindings, dependencies,
46
+ OS/kernel, container runtime, secrets/policy administration and build infrastructure.
47
+ Current containment does not provide tenant CPU fairness, per-tenant process RSS,
48
+ networked microVM isolation, or a proof against engine vulnerabilities. Worker heap
49
+ limits exclude external buffers and do not replace a process/container memory cap.
50
+
51
+ ## Closure criteria and deliverable
52
+
53
+ The maintainer records reviewer identity/independence, scope, dates, tested commit
54
+ and environment, methodology, findings with reproductions, severity, remediations
55
+ and retest evidence. All critical/high boundary findings must be fixed and retested;
56
+ residual risks require explicit owner acceptance. Publish a sanitized assessment
57
+ summary and retain exploit details privately through GitHub security advisories.
58
+ Reopen review for new capabilities, engine/bridge changes or major isolation changes.
59
+ An external review is necessary here, but still does not certify production capacity.
@@ -0,0 +1,72 @@
1
+ # Create placeholders from YAML
2
+
3
+ Write `urlcode.yaml` first, then generate its missing local references:
4
+
5
+ ```sh
6
+ urlcode scaffold --project ./my-links --dry-run
7
+ urlcode scaffold --project ./my-links
8
+ urlcode validate --project ./my-links --local
9
+ ```
10
+
11
+ The project and entry YAML must already exist. `--dry-run` reports the same plan
12
+ without writing. Existing files are preserved byte-for-byte; the command never
13
+ adds exports to existing modules or overwrites implementations. Run it again after
14
+ adding declarations. No application code is executed and no credentials are read.
15
+
16
+ For example:
17
+
18
+ ```yaml
19
+ version: "1"
20
+ routes:
21
+ /hello:
22
+ function:
23
+ source: functions/hello.mjs
24
+ export: handle
25
+ middleware:
26
+ - source: middleware/auth.mjs
27
+ /about:
28
+ page:
29
+ file: public/about.html
30
+ /assets/*:
31
+ static:
32
+ directory: public/assets
33
+ index: index.html
34
+ ```
35
+
36
+ This creates the two modules, an HTML page and the static directory/index.
37
+ Both modules return **501 Not implemented** until replaced. In particular, an
38
+ auth middleware placeholder does not silently allow requests through. Named
39
+ exports sharing a new module are combined; using the same export as both a
40
+ function and middleware is rejected as ambiguous. Configured arguments remain
41
+ in YAML for your implementation to use.
42
+
43
+ | Reference | Missing-file behavior |
44
+ |---|---|
45
+ | Included YAML | Creates `version: "1"` and empty routes; reports that definitions are still required |
46
+ | Function/middleware `.js` or `.mjs` | Creates requested exports returning 501 |
47
+ | Page/download `.html` or `.htm` | Creates a clearly labeled placeholder page |
48
+ | `.txt`, `.md`, `.csv`, `.css`, `.js`, `.mjs`, `.json` assets | Creates minimal placeholder text; JSON is `{}` |
49
+ | Static directory and configured index | Creates directories and index placeholder; cannot infer the rest of a site |
50
+ | PDF, image, archive or other asset | Reports unresolved; does not fabricate a corrupt binary file |
51
+ | Environment/secret references | Lists required external names; creates no values, policy grants or dotenv files |
52
+ | Link collection | Reports the required operator binding; creates no database or records |
53
+
54
+ All schema-valid route declarations, including disabled ones, are considered.
55
+ Existing included files are read; nested includes and duplicate routes fail.
56
+ Scaffolding checks schema and file safety, not complete routing semantics or
57
+ business correctness. Files use safe project-relative paths; traversal, hidden
58
+ paths, sensitive names, symlinks, hardlinked existing files and conflicting uses
59
+ are rejected. Keep the project operator-owned while writing. Planning errors
60
+ make no changes; an I/O failure while writing can leave some new placeholders,
61
+ which a subsequent run preserves. No transaction spans the whole filesystem.
62
+
63
+ After editing placeholders, run `validate`, add explicit response fixtures and
64
+ run `test` and `audit`. Validation catches missing modules/imports/exports/assets
65
+ and bindings. It does not prove that placeholder logic is complete. The report
66
+ always says `needsImplementation: true`; it is not a production-readiness gate.
67
+ Scaffolding does not recursively invent dependencies imported by existing code,
68
+ crawl HTML/CSS links, generate binary content, or implement your business logic.
69
+ See [readiness checks](READINESS.md) and [asset behavior](ASSETS.md).
70
+
71
+ Scaffolding enforces the entry-level `dynamicLinks` opt-in for `link` handlers
72
+ and reports the effective boolean. It never enables this capability for you.
@@ -0,0 +1,124 @@
1
+ # Security review — 2026-09-17
2
+
3
+ Scope: follow-up source review of worker/connection replacement, probe admission,
4
+ request correlation, operational logging, dependency/release supply chain and the
5
+ live-link Node requirement. Regression tests accompany the fixes. Internal review,
6
+ not an independent penetration test.
7
+
8
+ ## Findings fixed in this revision
9
+
10
+ | Finding | Impact and evidence | Fix / regression |
11
+ |---|---|---|
12
+ | Function workers latched off permanently after bounded churn | Availability: replacement stopped after three exits in a minute and was never retried, so eight deadline-exceeded requests — reachable from ordinary request input to any function whose runtime depends on its input — disabled every function route for the life of the process. Reproduced against `serve`, which never reloads: `/fast` and readiness stayed 503 indefinitely | Replacement now backs off (250 ms doubling to a 30-second ceiling) and keeps retrying; a completed invocation clears the backoff. Load is shed while a slot is down, and `function_worker` events record each attempt. Regression drives eight deadlines, then asserts the pool serves and readiness returns to 200 |
13
+ | Link-store connections were never replaced | Availability: one operation reaching the five-second deadline, or any abrupt worker exit, terminated the connection with no replacement path, permanently failing that pool while the records themselves were intact on disk | Connections share the same backed-off replacement and emit `link_store_worker` events. Activation still fails closed and is never retried behind the caller. Covered by the existing 22-test live-link suite; the post-activation failure branch has no automated test because the worker could not be crashed deterministically from a test |
14
+ | Health probes bypassed admission control | Resource exposure: probes were answered outside the in-flight budget, an unmetered path on a public listener, and report the configuration digest and route count without authentication | Probes keep a separate bounded budget (16 by default) so they stay available under application saturation without being unmetered. Operations documents keeping them internal |
15
+ | Request correlation broke at the ingress, and logs could not attribute anything | Operability: `x-request-id` was always regenerated, so traces did not survive a proxy hop, and request records carried no method or route, leaving per-route error rates and abuse (including the exhaustion above) undetectable | Opt-in `--trust-request-id` accepts a single safe upstream value; `--request-log detailed` adds the method and the matched route pattern. Pattern and method come from reviewed configuration, never from request text. Regression asserts a spoofed ID is ignored by default and that path, parameter and query text never reach the log |
16
+ | Deployment capacity controls were unreachable from the supported deployment | Availability: workers, deadlines and byte limits existed only in the embedding JS API while the container entrypoint is the CLI, so the supported target was fixed at two function workers with no way to tune | `serve`/`dev` accept `--workers`, `--function-timeout-ms`, `--max-response-bytes`, `--max-body-bytes`, `--max-in-flight` and `--max-in-flight-health`, validated before the listener starts |
17
+ | Live-link tests failed rather than skipped on an unpatched Node build | Signal loss: a current Node 22 release bundling SQLite 3.51.2 turned 21 tests red for an environmental reason, hiding real regressions behind expected noise | The suite skips with the detected version named, one CI job asserts the suite actually runs somewhere, `doctor` reports `liveLinks`, and activation names the detected version |
18
+ | Release artifacts and dependencies were not gated | Supply chain: advisories were checked by hand on a date, and Actions and the container base image floated on mutable tags | `npm audit --omit=dev` fails CI on runtime advisories, Actions and the base image are pinned by SHA/digest, and Dependabot proposes npm, Actions and image updates weekly |
19
+
20
+ Replacement backs off but does not stop. A cause that keeps recurring keeps the
21
+ instance shedding load with readiness at 503 rather than recovering silently;
22
+ that is an operator signal, not self-healing. Alert on sustained
23
+ `function_worker` and `link_store_worker` restart events.
24
+
25
+ # Security review — 2026-09-16
26
+
27
+ Scope: source review of HTTP serving/management, log handling, filesystem and
28
+ configuration loading, sandbox/module/binding boundaries, stored-link pooling,
29
+ scaffolding, package/container configuration and current dependency advisories.
30
+ Local adversarial regression tests accompany fixes. This is an internal review,
31
+ not an independent penetration test or certification. No production systems,
32
+ third-party targets or external accounts were attacked.
33
+
34
+ ## Findings fixed in this revision
35
+
36
+ | Finding | Impact and evidence | Fix / regression |
37
+ |---|---|---|
38
+ | Asynchronous log sink errors escaped request error handling | Availability: a local Writable reporting an async error terminated the process with an unhandled error event | Shared sink error handling suppresses further writes to a failed sink; synchronous failures also contained. Regression covers both failure modes and listener reuse |
39
+ | Management lacked bounded in-flight HTTP admission and socket inactivity enforcement | Resource exposure: connection limits alone did not bound active pipelined work or stalled output; public listener protections were absent here | Default 32 admitted requests through finish/disconnect, 10-second socket inactivity timeout, overload 503 and idempotent shutdown. Regression stalls an authenticated upload, checks overload, waits for disconnect, then verifies recovery |
40
+ | Development watcher read entire unrelated JSON/JS/YAML files | Local availability: a repository file outside the validated dependency graph could cause excessive allocations or fail watching solely because its contents were unreadable | Watch fingerprints use file metadata; actual configuration/source loading retains validation. Regression starts dev with an unreadable unrelated JSON file |
41
+ | Management had no request-level audit events | Operational visibility: mutations, authentication failures and aborted requests lacked structured records | Redacted events include timestamp, request ID, collection, action, authentication result, status and finish/abort outcome. Tests prove tokens, short codes and destinations are absent |
42
+ | Management accepted normalized dot-segment path aliases | Defense-in-depth: URL normalization admitted alternative endpoint spellings that could disagree with upstream path policies; no authorization bypass was demonstrated | Reject raw path normalization before endpoint dispatch; authentication still precedes path handling |
43
+
44
+ Management event status 0 means no response headers were sent before disconnect.
45
+ An aborted request may have committed a mutation: reconcile record/version before
46
+ retrying. These logs are best effort, may be dropped under pressure, identify a configured credential ID (or a legacy shared token), and are not a
47
+ tamper-evident journal. Successful store mutations now have separate transactional
48
+ audit records; see [management security](MANAGEMENT-SECURITY.md). A failed sink needs collector/operator recovery;
49
+ URLCode cannot report failures reliably through the same broken output stream.
50
+
51
+ ## Boundaries checked
52
+
53
+ - Guest code stays inside QuickJS/WASM, with fresh invocation state, denied host
54
+ APIs, bounded modules/memory/deadlines and no unrestricted fallback. Import
55
+ allowlists and external revision-pinned binding grants remain in force.
56
+ - Public serving cannot mutate the native link store. Management is a separate
57
+ token-protected listener with conditional writes, scoped collection and bounded
58
+ bodies. YAML requires explicit live-link opt-in; it cannot grant guest storage.
59
+ - SQL uses bound parameters; read/write pools have independent bounded admission,
60
+ patched-SQLite checks, read-only serving and commit/version tests.
61
+ - Asset/scaffold paths reject traversal and unsafe filesystem references. Static
62
+ publication is explicit; sensitive-name filtering is not a secret detector.
63
+ - Log output excludes request URLs, bodies, headers, tokens and user exceptions.
64
+ Management adds safe operational context without logging stored destinations.
65
+ - `npm audit` reported zero known advisories on this date. This does not cover
66
+ every Node, SQLite, WASM engine, operating-system or container vulnerability.
67
+
68
+ Existing and new automated tests cover these contracts; they do not constitute a
69
+ proof that the sandbox engine or complete application is vulnerability-free.
70
+
71
+ ## Remaining gaps, prioritized
72
+
73
+ **Before exposing hostile multi-tenant workloads:** obtain independent review of
74
+ WASM/host boundaries, module loading and resource exhaustion. The
75
+ [independent-review package](SANDBOX-REVIEW.md) defines scope and closure evidence.
76
+ YAML parsing now has an aggregate source cap and a bounded worker with a hard
77
+ wall deadline. Route compilation has a cooperative deadline; process-wide RSS,
78
+ individual host operations and overlapping snapshots still require deployment
79
+ limits. No anonymous code/config upload service is approved by these changes.
80
+
81
+ **Management remains private:** literal loopback binding, external per-credential
82
+ collection/action scopes, expiry and hot revocation, plus atomic SQLite mutation
83
+ audits are implemented. Legacy shared tokens remain for compatibility. There is
84
+ no public user-account system, MFA/SSO, built-in rate limiter, credential issuance
85
+ service or external tamper-evident archive. See [management security](MANAGEMENT-SECURITY.md).
86
+
87
+ **Before claiming operational readiness:** execute sustained mixed-workload soak,
88
+ backup restoration, disk-full, process-kill, proxy timeout and rollback drills on
89
+ the real deployment. Monitor disk/WAL growth, pool failures/rejections, RSS,
90
+ readiness, missing logs and restarts. SQLite is single-host; there is no automatic
91
+ multi-host failover or server-database adapter. Logging retention/rotation is owned
92
+ by the external collector and still needs deployment recipes and verification.
93
+
94
+ **Post-0.1 release hardening:** establish a private vulnerability reporting/support
95
+ policy and patch response ownership before managed or hostile multi-tenant use.
96
+ Release artifacts need immutable image and
97
+ dependency identities, upstream vulnerability monitoring and a reviewed update
98
+ process. At the audit cutoff, CI actions/base images used mutable version tags. The
99
+ repository-governance follow-up pins them and enables dependency maintenance,
100
+ secret protection, CodeQL and private reporting; see [governance](../GOVERNANCE.md).
101
+ CI now also gates runtime advisories with `npm audit --omit=dev`. A manual main-only
102
+ signed candidate/SBOM workflow is now defined; see
103
+ [release security](RELEASE-SECURITY.md). It does not publish to registries. The 0.1.0 release is licensed under
104
+ Apache-2.0; the license does not close the independent-review gates.
105
+
106
+ **Application responsibility:** HTML/JS assets are active browser content; choose
107
+ appropriate CSP, cookie flags, authorization and cache policy. Granted secrets
108
+ can intentionally be returned by code receiving them. Filename filters cannot
109
+ identify secrets stored under innocent names. Operator directories/volumes must
110
+ remain protected from other host processes; filesystem checks do not protect
111
+ against a privileged host attacker racing mutations.
112
+
113
+ See [release gates](RELEASE-READINESS.md), [function security](FUNCTION-SECURITY.md),
114
+ [operations](OPERATIONS.md) and [resilience](RESILIENCE.md). Free-product and
115
+ portability boundaries remain unchanged; this audit is not deployment readiness proof.
116
+
117
+ Repeatable local/CI drills now cover mixed HTTP load, quiesced backup restoration,
118
+ configuration rollback and disposable volume exhaustion/recovery. Real deployment
119
+ acceptance remains open; see [operational proof](OPERATIONAL-PROOF.md).
120
+
121
+ The hardening CI pass also exposed a failed-store initialization cleanup race on
122
+ Windows: rejection could precede worker termination and leave the DB file briefly
123
+ locked. Initialization now closes the DB and awaits worker termination before
124
+ returning failure. The missing-metadata regression exercises this cleanup path.