@jimhoyd/urlcode 0.4.0-alpha.1 → 0.4.0-alpha.3

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 (232) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +36 -22
  2. package/.claude/skills/urlcode-operations/SKILL.md +16 -22
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +83 -80
  6. package/ROADMAP.md +61 -331
  7. package/SECURITY.md +23 -8
  8. package/dist/BUILD-MANIFEST.json +42 -47
  9. package/dist/agents-guide.js +18 -18
  10. package/dist/build-static.js +136 -0
  11. package/dist/capabilities.js +85 -31
  12. package/dist/capability-query.js +0 -1
  13. package/dist/cli.js +24 -32
  14. package/dist/compliance-rules/baseline.js +2 -10
  15. package/dist/compliance-rules/privacy.js +5 -16
  16. package/dist/compliance-rules/shared.js +0 -2
  17. package/dist/compliance.js +6 -8
  18. package/dist/config.js +15 -8
  19. package/dist/context.js +9 -10
  20. package/dist/examples.js +2 -2
  21. package/dist/explain-cli.js +7 -5
  22. package/dist/explain.js +10 -5
  23. package/dist/extensions.js +61 -2
  24. package/dist/function-sources.js +34 -2
  25. package/dist/function-worker.js +3 -1
  26. package/dist/functions.js +84 -13
  27. package/dist/guest-api.js +29 -3
  28. package/dist/index.js +3 -6
  29. package/dist/manifest.js +11 -7
  30. package/dist/mcp-authoring.js +2 -2
  31. package/dist/mcp.js +1 -1
  32. package/dist/observability.js +1 -21
  33. package/dist/policies/cache.js +8 -3
  34. package/dist/policies.js +3 -1
  35. package/dist/policy.js +28 -9
  36. package/dist/prerender.js +4 -0
  37. package/dist/project-tests.js +3 -3
  38. package/dist/readiness.js +35 -9
  39. package/dist/route-diff.js +12 -5
  40. package/dist/router.js +5 -7
  41. package/dist/runtime.js +77 -58
  42. package/dist/sandbox.js +48 -0
  43. package/dist/scaffold.js +0 -0
  44. package/dist/scripts/operational-drills.js +12 -54
  45. package/dist/server.js +3 -29
  46. package/dist/tooling.js +1 -1
  47. package/dist/trusted-functions.js +210 -0
  48. package/dist/types/build-static.d.ts +43 -0
  49. package/dist/types/capabilities.d.ts +14 -5
  50. package/dist/types/compliance-rules/shared.d.ts +0 -2
  51. package/dist/types/compliance.d.ts +0 -3
  52. package/dist/types/config.d.ts +2 -1
  53. package/dist/types/context.d.ts +2 -1
  54. package/dist/types/examples.d.ts +1 -1
  55. package/dist/types/explain.d.ts +6 -0
  56. package/dist/types/extensions.d.ts +55 -0
  57. package/dist/types/function-sources.d.ts +4 -0
  58. package/dist/types/functions.d.ts +48 -5
  59. package/dist/types/guest-api.d.ts +1 -0
  60. package/dist/types/index.d.ts +3 -6
  61. package/dist/types/manifest.d.ts +5 -3
  62. package/dist/types/observability.d.ts +1 -14
  63. package/dist/types/project-tests.d.ts +1 -2
  64. package/dist/types/readiness.d.ts +12 -3
  65. package/dist/types/router.d.ts +2 -1
  66. package/dist/types/runtime.d.ts +0 -27
  67. package/dist/types/sandbox.d.ts +12 -0
  68. package/dist/types/scaffold.d.ts +0 -2
  69. package/dist/types/server.d.ts +1 -4
  70. package/dist/types/tooling.d.ts +3 -3
  71. package/dist/types/trusted-functions.d.ts +29 -0
  72. package/dist/types/types.d.ts +22 -8
  73. package/dist/types/verify-deployment.d.ts +2 -2
  74. package/dist/types.js +25 -5
  75. package/dist/typescript-authoring.js +67 -18
  76. package/dist/verify-deployment.js +3 -3
  77. package/docs/AI-AUTHORING.md +109 -10
  78. package/docs/ASSETS.md +2 -1
  79. package/docs/AUTH-BACKUP.md +32 -0
  80. package/docs/AWS.md +4 -4
  81. package/docs/BEST-PRACTICES.md +17 -8
  82. package/docs/CAPABILITIES.md +30 -17
  83. package/docs/CAPACITY.md +128 -31
  84. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  85. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  86. package/docs/CI.md +8 -3
  87. package/docs/CLOUDFLARE.md +1 -2
  88. package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
  89. package/docs/COMPLIANCE.md +6 -9
  90. package/docs/DEPLOYMENT-CHECKS.md +1 -1
  91. package/docs/DEVELOPMENT-PIPELINE.md +181 -0
  92. package/docs/EXTENSIONS.md +193 -11
  93. package/docs/FRAMEWORK.md +53 -33
  94. package/docs/FUNCTION-SECURITY.md +173 -32
  95. package/docs/INSTALL.md +0 -5
  96. package/docs/LOAD-TESTING.md +4 -4
  97. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  98. package/docs/MIDDLEWARE.md +27 -16
  99. package/docs/MONITORING.md +2 -19
  100. package/docs/OBSERVABILITY.md +7 -16
  101. package/docs/OPEN-DECISIONS.md +184 -0
  102. package/docs/OPERATIONAL-PROOF.md +26 -30
  103. package/docs/OPERATIONS.md +23 -32
  104. package/docs/POLICIES.md +27 -8
  105. package/docs/PRERENDER.md +25 -13
  106. package/docs/PROJECT-DIRECTION.md +35 -10
  107. package/docs/READINESS.md +6 -2
  108. package/docs/README.md +20 -41
  109. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  110. package/docs/RELEASE-READINESS.md +50 -30
  111. package/docs/RELEASE-SECURITY.md +72 -86
  112. package/docs/RESILIENCE.md +16 -15
  113. package/docs/ROUTING.md +7 -10
  114. package/docs/SANDBOX-REVIEW.md +19 -6
  115. package/docs/SCAFFOLDING.md +0 -2
  116. package/docs/SECURITY-AUDIT.md +42 -2
  117. package/docs/SPECIFICATION.md +63 -27
  118. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +288 -0
  119. package/docs/SPIKE-BUSINESS-SUITE.md +16 -0
  120. package/docs/SPIKE-CORE-LAYERING.md +368 -0
  121. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +211 -0
  122. package/docs/SPIKE-LAMBDA-COMPILE.md +12 -2
  123. package/docs/STARTERS.md +1 -1
  124. package/docs/STATIC.md +105 -0
  125. package/docs/TOOLING.md +17 -12
  126. package/docs/TUNNELS.md +0 -3
  127. package/docs/TYPESCRIPT-AUTHORING.md +32 -12
  128. package/docs/TYPESCRIPT.md +25 -4
  129. package/docs/VERCEL.md +4 -5
  130. package/docs/VERSION-ALIGNMENT.md +46 -0
  131. package/docs/YAML-GUIDE.md +0 -3
  132. package/docs/YAML-REFERENCE.md +16 -6
  133. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  134. package/docs/{MANAGEMENT-SECURITY.md → archive/2026-09-19/MANAGEMENT-SECURITY.md} +20 -0
  135. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +26 -8
  136. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +274 -43
  137. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  138. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  139. package/docs/archive/2026-09-19/ROADMAP.md +386 -0
  140. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +12 -2
  141. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +28 -12
  142. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
  143. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +18 -2
  144. package/docs/archive/README.md +27 -0
  145. package/docs/policies/agents.md +1 -1
  146. package/docs/policies/cache.md +13 -0
  147. package/docs/policies/compression.md +3 -2
  148. package/docs/policies/security.md +3 -2
  149. package/examples/assets/example.yaml +2 -2
  150. package/examples/aws/example.yaml +1 -1
  151. package/examples/cloudflare/example.yaml +1 -1
  152. package/examples/conditions/example.yaml +1 -1
  153. package/examples/cookbook/example.yaml +1 -1
  154. package/examples/cookbook/middleware/bucket.mjs +12 -2
  155. package/examples/cookbook/middleware/locale.mjs +7 -3
  156. package/examples/cookbook/route-index.json +1 -1
  157. package/examples/cookbook/routes/middleware.yaml +1 -1
  158. package/examples/egress/example.yaml +1 -1
  159. package/examples/extensions/example.yaml +1 -1
  160. package/examples/prerender/README.md +14 -6
  161. package/examples/prerender/example.yaml +1 -1
  162. package/examples/prerender/functions/page.mjs +4 -2
  163. package/examples/prerender/middleware/template.mjs +1 -1
  164. package/examples/prerender/prerender.mjs +1 -1
  165. package/examples/prerender/urlcode.yaml +8 -4
  166. package/examples/provider-conformance/example.yaml +1 -1
  167. package/examples/vercel/example.yaml +1 -1
  168. package/llms-full.txt +686 -239
  169. package/llms.txt +27 -15
  170. package/package.json +32 -5
  171. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  172. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +36 -22
  173. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +16 -22
  174. package/recipes/authenticated-json-api/README.md +4 -3
  175. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  176. package/recipes/authenticated-json-api/recipe.yaml +2 -2
  177. package/recipes/contact-form/functions/contact.mjs +2 -1
  178. package/recipes/contact-form/recipe.yaml +3 -3
  179. package/recipes/cors-api/README.md +2 -2
  180. package/recipes/cors-api/recipe.yaml +2 -2
  181. package/recipes/health-page/README.md +1 -1
  182. package/recipes/health-page/recipe.yaml +1 -1
  183. package/recipes/json-api/README.md +1 -1
  184. package/recipes/json-api/recipe.yaml +4 -4
  185. package/recipes/middleware/README.md +8 -4
  186. package/recipes/middleware/middleware/bucket.mjs +12 -2
  187. package/recipes/middleware/middleware/locale.mjs +7 -3
  188. package/recipes/middleware/recipe.yaml +1 -1
  189. package/recipes/protected-download/README.md +1 -1
  190. package/recipes/protected-download/recipe.yaml +2 -2
  191. package/recipes/redirect/recipe.yaml +1 -1
  192. package/recipes/static-plus-api/README.md +2 -2
  193. package/recipes/static-plus-api/public/index.html +1 -1
  194. package/recipes/static-plus-api/recipe.yaml +2 -2
  195. package/recipes/static-plus-api/urlcode.yaml +1 -1
  196. package/recipes/typescript/README.md +2 -1
  197. package/recipes/typescript/recipe.yaml +5 -5
  198. package/recipes/webhook-receiver/README.md +5 -1
  199. package/recipes/webhook-receiver/recipe.yaml +2 -1
  200. package/recipes/webhook-receiver/urlcode.yaml +7 -0
  201. package/schemas/recipe.schema.json +4 -3
  202. package/schemas/urlcode.schema.json +14 -41
  203. package/skills/urlcode/SKILL.md +32 -11
  204. package/starters/default/AGENTS.md +18 -18
  205. package/starters/default/urlcode.yaml +0 -1
  206. package/dist/link-api.js +0 -136
  207. package/dist/link-cli.js +0 -141
  208. package/dist/link-events.js +0 -76
  209. package/dist/link-records.js +0 -31
  210. package/dist/link-store-worker.js +0 -150
  211. package/dist/link-store.js +0 -250
  212. package/dist/management-policy.js +0 -40
  213. package/dist/sqlite-version.js +0 -6
  214. package/dist/types/link-api.d.ts +0 -30
  215. package/dist/types/link-cli.d.ts +0 -37
  216. package/dist/types/link-events.d.ts +0 -27
  217. package/dist/types/link-records.d.ts +0 -11
  218. package/dist/types/link-store-worker.d.ts +0 -1
  219. package/dist/types/link-store.d.ts +0 -130
  220. package/dist/types/management-policy.d.ts +0 -8
  221. package/dist/types/sqlite-version.d.ts +0 -1
  222. package/docs/DYNAMIC-LINKS.md +0 -61
  223. package/docs/links/cli.md +0 -110
  224. package/docs/links/limits.md +0 -175
  225. package/docs/links/management-api.md +0 -80
  226. package/docs/links/pools.md +0 -75
  227. package/docs/links/setup.md +0 -135
  228. package/docs/yaml/links.md +0 -30
  229. package/examples/live-links/README.md +0 -11
  230. package/examples/live-links/example.yaml +0 -21
  231. package/examples/live-links/tests/requests.json +0 -6
  232. package/examples/live-links/urlcode.yaml +0 -16
@@ -1,11 +1,75 @@
1
- # Untrusted function execution
2
-
3
- Application code is untrusted even when it came from your own Git repository.
4
- A compromised dependency, template or contribution must not inherit the URLCode
5
- server's authority. Alpha.2 replaces alpha.1's Node execution entirely. There
6
- is no `unsafe`, `trusted` or automatic host-execution fallback.
7
-
8
- ## Boundaries enforced now
1
+ # Function execution: trusted by default, sandboxed opt-in
2
+
3
+ `function` and `middleware` routes run **trusted and unsandboxed by default**:
4
+ in the host process, with full Node, filesystem and network access, exactly
5
+ like any other project code (docs/SPIKE-DEFAULT-TRUST-MODEL.md). This is a
6
+ deliberate, maintainer-decided reversal of alpha.2's blanket sandbox — see
7
+ that spike document for the full rationale. It is a call the project makes,
8
+ not a property the runtime can verify: URLCode cannot know whether your code
9
+ is safe to trust, only whether you asked for isolation.
10
+
11
+ Declare `sandbox: true` on a route when its code specifically warrants
12
+ isolation: it processes input from a source the project doesn't fully trust
13
+ (a third-party webhook payload, for example), it is a contribution nobody on
14
+ the team has reviewed, or it handles a secret sensitive enough that a bug in
15
+ that one route should not be able to reach the rest of the process or the
16
+ filesystem. A sandboxed route runs in QuickJS inside WebAssembly, in a
17
+ separate worker thread, with none of the host access described below — its
18
+ guarantees are unchanged from every earlier release and are described in
19
+ full in the rest of this document. Absence of `sandbox` (or `sandbox: false`)
20
+ means trusted; there is no separate `unsafe`/`trusted` field to opt back into
21
+ the old sandboxed-by-default behavior — set `sandbox: true` per route instead.
22
+
23
+ **Either way, binding grants are unaffected.** Trusting a route's code by
24
+ default does not grant it any `env`/`secrets` it was not explicitly declared
25
+ in YAML and approved by an operator policy pinned to the project revision
26
+ (see "Granting selected bindings" below). A trusted function only *can* do
27
+ more with Node once it runs — it does not receive anything more than a
28
+ sandboxed one would.
29
+
30
+ This is a claim about `context`/`context.secrets` injection, not an
31
+ access-control guarantee on trusted code. The binding grant governs only what
32
+ URLCode hands a route through `context`; it does not restrict what trusted
33
+ (non-`sandbox`) code can independently do, because that code has full Node
34
+ access by design. A trusted function can read `process.env`, open files or
35
+ make network calls on its own regardless of what its route was or was not
36
+ granted — withholding a binding grant limits what URLCode gives the code
37
+ through `context`, not what the code itself, running with full Node access,
38
+ can go and get. A sandboxed route has no such independent access: the guest
39
+ API is all it has, so its binding grant *is* effectively its whole reach into
40
+ the environment. Trusted code's reach is not bounded that way; treat the
41
+ grant as scoping `context`, not as scoping the process.
42
+
43
+ ## Migrating to the trusted default
44
+
45
+ If you are upgrading a project from a release before this change shipped:
46
+ **every existing `function` and `middleware` route silently changes execution
47
+ mode**, from sandboxed to trusted, unless it already has (or you add)
48
+ `sandbox: true`. This is a real behavior change on upgrade, not a
49
+ documentation update — a route that used to run with no filesystem or network
50
+ access will, after the upgrade, run with full Node access unless you opt it
51
+ back into the sandbox.
52
+
53
+ Before upgrading:
54
+
55
+ - List every `function` and `middleware` route in the project.
56
+ - For each one, decide whether you fully trust that code to run in-process
57
+ with full Node/filesystem/network access — the same trust you would extend
58
+ to any other code you deploy to that server.
59
+ - Add `sandbox: true` explicitly to any route whose code you do not fully
60
+ trust, that processes input from a source you don't control, or that handles
61
+ a secret binding you want isolated — before you upgrade, not after.
62
+ - Routes you do want running trusted need no change; that is now the default.
63
+
64
+ The change moves the sandbox from an unconditional guarantee to an explicit,
65
+ per-route choice, mainly for performance: the previous blanket sandbox capped
66
+ concurrency at two workers with no queue shared across every function route on
67
+ the server, which does not scale to real concurrent traffic. It also brings
68
+ first-party code in line with how the rest of the Node ecosystem treats
69
+ deployed application code. The sandbox itself is unchanged for routes that opt
70
+ into it; only the default for routes that declare neither option has changed.
71
+
72
+ ## What "sandboxed" (`sandbox: true`) still guarantees
9
73
 
10
74
  - Function sources are parsed/snapshotted without importing them into Node.
11
75
  - Code runs in QuickJS inside WebAssembly, with no host JS functions/objects
@@ -22,9 +86,79 @@ is no `unsafe`, `trusted` or automatic host-execution fallback.
22
86
  Operator grants are exact-name, route-scoped and pinned to configuration/source.
23
87
 
24
88
  The guest API is intentionally narrower than Node or full Fetch; see the
25
- [implemented contract](SPECIFICATION.md). Existing functions using Node/network
26
- or binary/stream APIs must be rewritten for the supported profile or wait for a
27
- reviewed capability implementation. Redirects need none of this machinery.
89
+ [implemented contract](SPECIFICATION.md). A function moving from trusted to
90
+ `sandbox: true` that uses Node/network or binary/stream APIs must be rewritten
91
+ for the supported guest profile, or stay trusted. Redirects need none of this
92
+ machinery either way.
93
+
94
+ This engine — worker spawning, the module-allowlist walk, the two-layer
95
+ deadline, `maxBytes` and response-shape validation — is one implementation
96
+ shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
97
+ the public primitive an extension package uses to run a project-supplied hook
98
+ through this exact isolation when the project's own config declares
99
+ `sandbox: true` on it (see [EXTENSIONS.md](EXTENSIONS.md#project-level-lifecycle-hooks)
100
+ and [TYPESCRIPT.md](TYPESCRIPT.md)). Every guarantee above applies identically
101
+ through that entry point; there is no separate, weaker sandbox for extensions
102
+ to reach for, and no "trusted" mode exported there — trusted execution needs
103
+ no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
104
+
105
+ ## What the trusted default can and can't do
106
+
107
+ A trusted route (no `sandbox`, or `sandbox: false`) has none of the guest
108
+ restrictions above:
109
+
110
+ - Full Node built-ins, `process`, the filesystem, `fetch`, sockets, workers
111
+ and npm packages are available, exactly as in any other Node module.
112
+ - Module resolution is ordinary Node ESM resolution: bare specifiers, dynamic
113
+ `import()` and node_modules all work. There is no dependency-graph allowlist
114
+ and no per-module/total source-size budget (function-sources.ts's
115
+ `MODULE_LIMIT`/`MODULE_BYTE_LIMIT`/`TOTAL_BYTE_LIMIT` apply only to what a
116
+ sandboxed snapshot bundles).
117
+ - Node's own module cache is shared across invocations and across the whole
118
+ process; there is no fresh heap per call. Module-level state persists
119
+ between requests exactly like an ordinary long-running Node server, so a
120
+ trusted function that mutates shared/global state affects later requests
121
+ the way hand-written server code would.
122
+ - There is no worker-thread deadline that force-terminates a stuck call. A
123
+ trusted invocation races a configurable timeout, but that race can only
124
+ reject the *call*; it cannot preempt code that blocks the event loop
125
+ synchronously. See [capacity](CAPACITY.md) for what this means for one slow
126
+ or hung trusted route's effect on the rest of the process.
127
+ - A snapshot reload re-imports a trusted route's own entry file fresh (each
128
+ reload gets its own cache-busted module registration), so editing the
129
+ `source` file a route declares and reloading picks up the change, the same
130
+ as the sandboxed pool rebuilding from scratch. A file that entry file
131
+ merely *imports* is not similarly busted: Node's own module cache is
132
+ keyed by the resolved URL of that import statement, which this runtime
133
+ does not rewrite, so an edited dependency two files deep from the route
134
+ keeps serving its old content until the process restarts. Restructure a
135
+ route so the code you expect to hot-reload is the declared entry file
136
+ itself, or restart rather than reload after editing a trusted route's
137
+ dependencies. A `sandbox: true` route has no such gap: reload always
138
+ rebuilds its whole snapshot, dependencies included.
139
+
140
+ What does **not** change with trust: `args` are still exactly the validated
141
+ values the route declares (never raw request input), and `env`/`secrets` are
142
+ still exactly what the route's YAML requests and an operator policy grants,
143
+ pinned to the project revision — trust changes where code runs, not what
144
+ it is handed *through `context`*. It does not change what the code can go get
145
+ on its own once it is running; see "binding grants are unaffected" above for
146
+ that distinction.
147
+
148
+ ## Trusted code, not trusted requests
149
+
150
+ "Trusted" describes the code's authorship — first-party project code you
151
+ reviewed and deployed — not the requests it handles. Every request, in either
152
+ mode, still carries client-controlled path, query, header and body data that is
153
+ exactly as adversarial as it always was. Running trusted means that code
154
+ executes with full Node access if it mishandles that input; it does not mean
155
+ the input itself became safe to trust. Declare `parameters` and `request.body`
156
+ validation in YAML, check `args` and any other request data again inside
157
+ function/middleware code, and implement your own authentication and
158
+ authorization — no route, sandboxed or trusted, adds automatic auth.
159
+ `sandbox: true` narrows what a bug or an unreviewed dependency in the *code*
160
+ can do with that same request data; it is not a substitute for validating or
161
+ authenticating the request itself.
28
162
 
29
163
  ## Granting selected bindings
30
164
 
@@ -63,26 +197,28 @@ urlcode serve --project /srv/my-links --policy /etc/urlcode/my-links-policy.json
63
197
 
64
198
  `dev`, `test` and `validate --local` use the same policy rules even for `.env.local`.
65
199
  The JavaScript API accepts an equivalent operator-supplied `permissions` object.
66
- Every config/module change invalidates the grant; inspect/review the new revision
67
- before updating the operator file. Policies are read at startup, not hot-reloaded.
68
- A failed development candidate leaves the previous approved snapshot running.
200
+ Every config change invalidates the grant, and so does a module change within
201
+ what the approval digest actually hashes: for a sandboxed route, its
202
+ middleware/function sources and their full dependency graph; for a trusted
203
+ route, only its own entry-file source (see the next paragraph — a trusted
204
+ route's transitive dependencies are explicitly **not** part of that digest).
205
+ Inspect/review the new revision before updating the operator file. Policies
206
+ are read at startup, not hot-reloaded. A failed development candidate leaves
207
+ the previous approved snapshot running.
69
208
 
70
209
  Granting a secret deliberately makes it available to every middleware and function
71
- in that route. Middleware sources and their dependencies are included in the
72
- approval digest; changes invalidate grants. The whole chain shares one fresh
73
- guest heap and one execution deadline. Code can
74
- include any granted data in its HTTP response. A sandbox cannot promise secrecy
75
- from code authorized to read a value. Minimize grants, use scoped/short-lived
76
- credentials and revoke/restart when needed. Other routes get none of that context.
77
-
78
- ## Native live-link storage
79
-
80
- The optional `link` handler can read an explicitly operator-bound collection.
81
- Its database is outside the project and public serving opens it read-only. This
82
- is a native handler, not a guest capability: functions/middleware receive no SQL,
83
- filesystem handle, database path or management token. Native link data changes do
84
- not authorize new code or bindings. Management requires a separate operator CLI
85
- or token-protected listener. See [dynamic links](DYNAMIC-LINKS.md).
210
+ in that route, trusted or sandboxed alike. A sandboxed route's middleware
211
+ sources and their full dependency graph are included in the approval digest,
212
+ as before; a trusted route's own entry-file source is included too, so
213
+ changing that file's content invalidates the grant, but a change to a helper
214
+ module it merely imports does not by itself (see function-sources.ts's
215
+ `collectTrustedSources`) a known, documented gap versus the sandboxed path's
216
+ full dependency-graph hashing: a trusted route's grant scope is entry-file-only,
217
+ not transitive. Either way, code can include any granted data
218
+ in its HTTP response: neither the sandbox nor the trusted default promises
219
+ secrecy from code that was explicitly authorized to read a value. Minimize
220
+ grants, use scoped/short-lived credentials and revoke/restart when needed.
221
+ Other routes get none of that context.
86
222
 
87
223
  ## Next capability work
88
224
 
@@ -90,9 +226,14 @@ Outbound requests need a host-owned broker with explicit destination/method
90
226
  allowlists, private/metadata/loopback-address restrictions, DNS/rebinding defenses,
91
227
  redirect revalidation, deadlines and byte/concurrency limits. Application YAML
92
228
  must not grant those permissions. Persistent state needs similarly scoped access.
93
- Until such brokers are implemented and tested, these capabilities are unavailable.
94
- Provider adapters must preserve this boundary or reject deployment; they cannot
95
- silently replace sandbox execution with unrestricted Node functions.
229
+ Until such brokers are implemented and tested, these capabilities are unavailable
230
+ to a *sandboxed* route. Provider adapters must preserve a `sandbox: true`
231
+ route's isolation or reject deployment; they cannot silently downgrade a
232
+ route that explicitly asked for the sandbox into unrestricted Node execution.
233
+ (A trusted route, by contrast, already has unrestricted Node execution by
234
+ design on the self-hosted target — see "What the trusted default can and
235
+ can't do" above; non-Node targets refuse `function`/`middleware` entirely,
236
+ trusted or sandboxed, since neither execution mode exists there.)
96
237
 
97
238
  ## Verification and remaining risk
98
239
 
package/docs/INSTALL.md CHANGED
@@ -6,11 +6,6 @@ pick whichever fits how you already manage tools. All of them require
6
6
  TypeScript source (`dist/`, with declarations; see [TypeScript](TYPESCRIPT.md)),
7
7
  so the installed `urlcode` command runs `dist/cli.js` and needs no build tool.
8
8
 
9
- Live short-link storage additionally needs a Node build carrying the patched
10
- SQLite WAL fix. Run `urlcode doctor` after installing and check `liveLinks`;
11
- everything except live links works on any supported build. See
12
- [dynamic links](DYNAMIC-LINKS.md#node-build-requirement).
13
-
14
9
  ## npm
15
10
 
16
11
  ```sh
@@ -75,8 +75,8 @@ largest one that makes a number go green.
75
75
 
76
76
  ## What a passing run does not prove
77
77
 
78
- - **GET and HEAD only.** Function routes with request bodies, live-link writes
79
- and the management API are not exercised. `workload` states this in every report.
78
+ - **GET and HEAD only.** Function routes with request bodies are not exercised.
79
+ `workload` states this in every report.
80
80
  - **Redirects are not followed**, so a redirect's destination is never loaded.
81
81
  - **One client, one host, no slow peers.** Tail latency under adversarial
82
82
  clients, connection churn or packet loss is not measured.
@@ -87,5 +87,5 @@ largest one that makes a number go green.
87
87
  runtime and application revisions, host, and command with any figure you keep.
88
88
 
89
89
  `scripts/operational-drills.ts` covers the adjacent ground — mixed
90
- native/function/live-link load, an invalid reload, rollback and a quiesced
91
- backup/restore — as a local proof, never a statement about production.
90
+ native/function load, an invalid reload and rollback as a local proof,
91
+ never a statement about production.
@@ -10,6 +10,9 @@ ship, together with the declarations; `dist` is never committed. Make is an opti
10
10
  shortcut layer; npm and the CLI work on Windows, macOS and Linux. No global
11
11
  package install, hosting account, database or Docker is needed for the local loop.
12
12
 
13
+ For the repository's prose-only checks, CI selection and release helper commands,
14
+ see [the development pipeline](DEVELOPMENT-PIPELINE.md).
15
+
13
16
  ## Try the runtime
14
17
 
15
18
  From the runtime checkout, `make dev` installs locked dependencies if needed and
@@ -2,7 +2,10 @@
2
2
 
3
3
  Middleware is reusable JavaScript around any route handler. It is optional and
4
4
  route-local; plain redirects and assets retain their native fast path when no
5
- middleware is attached. Adding middleware requires sandbox execution.
5
+ middleware is attached. Middleware runs trusted and unsandboxed by default,
6
+ in-process with full Node access, the same as a `function` route; add
7
+ `sandbox: true` on the route to run the whole chain isolated instead (see
8
+ [trust model and sandbox opt-in](FUNCTION-SECURITY.md)).
6
9
 
7
10
  ```yaml
8
11
  version: "1"
@@ -49,37 +52,45 @@ reading one consumes it for downstream code. There is no `clone()` or streaming
49
52
  API; pass parsed data through `context.state` when needed.
50
53
 
51
54
  Function responses support the existing text/JSON guest API. To transform their
52
- body, read it and return a new `Response`. Native redirect/respond/link/page/static/
55
+ body, read it and return a new `Response`. Native redirect/respond/page/static/
53
56
  download bodies are opaque and cannot be read through `text()` or `json()`.
54
57
  Returning the same native response preserves original bytes, including binary
55
58
  files, ranges and HEAD lengths. You may add headers, but cannot change its
56
59
  original status or existing native headers while preserving that body. To replace
57
60
  status, destination or content, return a new `Response` instead. Replacement
58
- responses follow the normal sandbox text/JSON and size limits. To wrap a shared
61
+ responses follow the normal response size limits, and (on a `sandbox: true`
62
+ route) the guest's text/JSON constraints. To wrap a shared
59
63
  template around file content, render it through a function at build time and
60
64
  publish the result: see [prerendering](PRERENDER.md).
61
65
 
62
66
  Route selection, enabled/expiry checks, methods and input/body validation run
63
67
  before middleware. Their errors do not pass through the chain. A missing file
64
- inside a selected static mount is a downstream 404 response. Stored-link lookup
65
- errors (missing, disabled, expired or unavailable store) precede middleware. YAML
68
+ inside a selected static mount is a downstream 404 response. YAML
66
69
  `response.headers` apply last and override matching middleware headers. Runtime
67
70
  framing and asset metadata protections still apply.
68
71
 
69
- ## Isolation and testing
72
+ ## Trust, isolation and testing
70
73
 
71
- The whole chain and handler run in one fresh QuickJS/WASM guest with one memory
72
- budget and one deadline. No Node, filesystem, shell, fetch or ambient environment
73
- is exposed. Modules can only access this route's declared dependency graphs.
74
- All middleware receive that route's approved bindings, so review the whole chain;
75
- source changes invalidate grants. See [security](FUNCTION-SECURITY.md).
74
+ The whole chain and handler run as one unit, in one execution mode, chosen by
75
+ the route's `sandbox` field not a per-middleware-entry choice. By default
76
+ (`sandbox` false/absent) that means trusted, in-process execution with full
77
+ Node access and no fixed worker-pool ceiling. With `sandbox: true` it means
78
+ one fresh QuickJS/WASM guest with one memory budget and one deadline: no
79
+ Node, filesystem, shell, fetch or ambient environment, and modules limited to
80
+ this route's declared dependency graph. Either way, all middleware on a route
81
+ receive that route's approved bindings, so review the whole chain; source
82
+ changes invalidate grants. See [trust model and sandbox opt-in](FUNCTION-SECURITY.md).
76
83
 
77
- Invalid responses and repeated `next()` calls fail with 502, exhausted capacity
78
- returns 503, and the shared deadline returns 504. Middleware cannot extend the
79
- deadline or catch the outer worker termination. Forgotten downstream work is
80
- still drained within that deadline.
84
+ Invalid responses and repeated `next()` calls fail with 502, and the deadline
85
+ returns 504 either way. A `sandbox: true` chain also sheds load with 503 when
86
+ the shared worker pool is exhausted, and cannot extend the deadline or catch
87
+ the outer worker termination; forgotten downstream work is still drained
88
+ within it. A trusted chain has no worker pool to exhaust (see
89
+ [capacity](CAPACITY.md)), but its deadline is a race against the call's own
90
+ promise rather than a forced kill — it cannot preempt code that blocks the
91
+ event loop synchronously.
81
92
 
82
93
  Include explicit request fixtures for middleware-wrapped routes: test success,
83
94
  early responses, validation failures and every configured method. Audit will
84
95
  report missing coverage instead of assuming native handler behavior. Benchmark
85
- with middleware enabled to measure its actual sandbox overhead.
96
+ with middleware enabled to measure its actual overhead, sandboxed or trusted.
@@ -19,7 +19,7 @@ analytics from these logs, and adding it would mean logging user data. See
19
19
  | Endpoint | Meaning | Alert when |
20
20
  |---|---|---|
21
21
  | `GET /_urlcode/health` | The process is alive and not shutting down. | It fails at all (`UrlcodeDown`). |
22
- | `GET /_urlcode/ready` | The active snapshot, every function worker and every configured link-store reader are available. | It fails for longer than replacement takes (`UrlcodeNotReady`). |
22
+ | `GET /_urlcode/ready` | The active snapshot and every function worker are available. | It fails for longer than replacement takes (`UrlcodeNotReady`). |
23
23
 
24
24
  Both return `{status, version, routes}`. A third endpoint,
25
25
  `GET /_urlcode/metrics`, exists only with `startServer({ metrics: true })` and
@@ -38,27 +38,10 @@ configuration digest and route count, so keep them internal.
38
38
  | `reload` | `status` (`ok`/`rejected`); `version` and `routes` on `ok` | A `rejected` reload means the last-good snapshot is still serving and a deploy did not take effect. |
39
39
  | `watch` | `status` | Development watcher failure; not used by `serve`. |
40
40
  | `function_worker` | `status` (`started`/`restarting`), `slot`; `attempt` and `delayMs` on `restarting` | Sustained `restarting` means a function is failing on real traffic. |
41
- | `link_store_worker` | `status`, `readOnly`, `attempt`, `delayMs` | The same signal for link-store connections. `status: "restarting"` reports an automatic replacement with its backoff; sustained restarts mean the underlying fault is not recoverable. |
42
- | `link_observer` | `status` (`failed`/`dropped`/`closed`); `reason` on `failed`; `dropped` on `dropped`; the delivery totals on `closed` | Only when an operator enables `linkEvents`. The link event channel below could not keep up or its collector failed. `dropped` means click records were discarded; like `logs_dropped`, anything built on that channel is incomplete while it fires. |
43
41
  | `logs_dropped` | `count` | The logger shed records because the collector fell behind. Every other signal is unreliable while this fires. |
44
42
  | `observer` | `status` (`failed`), `name` | An in-process observer threw; the request was unaffected. Written to the log only, never to observers. Sustained failures mean the observer's own sink is broken. |
45
43
  | `throttle`, `agents`, `cache` | `route`, `outcome`; `remaining` or `list` | Policy decisions; see [policies](POLICIES.md). `throttle` logs `allowed` only in report mode. |
46
44
  | `site` | `key`, `path`, `status` (`generated`/`shadowed`); or `severity` (`info`/`warning`) and `message` | Activation records for [site conventions](SITE.md). `shadowed` means a declared route took the path; an `info`/`warning` line reports an omitted `Sitemap:` line (no `--origin`), skipped list names or a far-future `security.txt` expiry. |
47
- | `management_request` | `timestamp`, `requestId`, `collection`, `action`, `authenticated`, `principal`, `status`, `outcome`, `durationMs` | Operator activity on the link-management API. `status` 0 means no response headers were sent before the peer disconnected; such a request may still have committed a mutation. |
48
-
49
- ### The link event channel
50
-
51
- `link_request` is **not** a stdout record. It is delivered to an `observe()`
52
- function the embedding operator process supplies, after the response is over, so
53
- it can never change, delay or fail a redirect. It carries `requestId`,
54
- `collection`, `route`, `method`, `status`, `outcome`
55
- (`completed`/`aborted`/`missing`/`disabled`/`expired`/`invalid_code`/`invalid_record`/`unavailable`)
56
- and `durationMs`. The short code is redacted unless `includeCode` is set, because
57
- a code identifies the link somebody followed.
58
-
59
- The queue is bounded: under overload it drops events and reports the count
60
- through `link_observer` rather than growing memory. Alert on those drops if you
61
- count clicks — a quiet channel and a dropping channel look identical downstream.
62
45
 
63
46
  Startup prints `listening` with the effective `origin`, which is what functions
64
47
  and absolute URLs see. Behind a proxy or tunnel this must be your public origin;
@@ -69,7 +52,7 @@ forwarded headers are deliberately not trusted. See [tunnels](TUNNELS.md).
69
52
  `startServer({ metrics: true })` serves `GET /_urlcode/metrics` in Prometheus
70
53
  text format: requests by status class and by configured route, in-flight
71
54
  gauges, shed 503s, reloads, worker restarts and healthy slots, policy
72
- outcomes, link outcomes, dropped logs and observer errors, all prefixed
55
+ outcomes, dropped logs and observer errors, all prefixed
73
56
  `urlcode_`. The same numbers are available in process as `app.metrics()`. The
74
57
  endpoint shares the probes' admission budget and bind host and is off by
75
58
  default; it discloses route patterns and traffic shape, so keep it internal
@@ -33,16 +33,12 @@ named.
33
33
  | `reload` | `status` `ok`/`rejected`; `version` string and `routes` integer on `ok` | `app.reload()` or the development watcher swapped, or refused to swap, the snapshot. |
34
34
  | `watch` | `status` `failed` | The development watcher could not fingerprint the project. |
35
35
  | `function_worker` | `status` `started`/`restarting`, `slot` integer; `attempt` and `delayMs` integers on `restarting` | A function worker became ready or is scheduled for replacement. |
36
- | `link_store_worker` | `status` `started`/`restarting`, `readOnly` boolean; `attempt` and `delayMs` on `restarting` | The same for a link-store connection. |
37
36
  | `signal` | `outcome` (`accepted`, `delivered`, `failed`, `dropped`), positive `count` | Best-effort webhook totals; no destination, request data or secrets. |
38
- | `link_observer` | `status` `failed`/`dropped`/`closed`; `reason` `timeout`/`error` on `failed`; `dropped` integer on `dropped`; `queued`, `delivered`, `dropped`, `failed`, `timedOut`, `closed` on `closed` | Only with `linkEvents`: the link collector failed, the queue shed events, or the channel drained at shutdown. |
39
- | `link_request` | `requestId`, `collection`, `route`, `code` (only with `includeCode`), `method`, `status`, `outcome`, `durationMs` | Delivered to the `linkEvents.observe` function only, after the response is over. It never reaches the log or observers; its `outcome` is counted in the metrics. |
40
37
  | `logs_dropped` | `count` integer | The JSON logger shed records because stdout was not writable. Written by the logger itself, so observers do not see it. |
41
38
  | `observer` | `status` `failed`, `name` string | An observer hook threw or rejected. Written to the default log only, never to observers. |
42
39
  | `throttle` | `route`, `outcome` `allowed`/`exceeded`, `remaining` integer | A throttle decision. `allowed` is logged only in `mode: report`; enforce mode logs refusals. |
43
40
  | `agents` | `route`, `list` string, `outcome` `denied`/`reported` | A User-Agent matched a list. The list name is logged, never the header. |
44
41
  | `cache` | `route`, `outcome` `hit`/`stale`/`miss`/`store` | A cache lookup or store. |
45
- | `management_request` | `timestamp`, `requestId`, `collection`, `action`, `authenticated`, `principal`, `status`, `outcome`, `durationMs` | The link-management API (`startLinkApi`), which has its own logger; `principal` is the operator-configured principal id. |
46
42
  | `listening` | `address`, `port`, `mode`, `origin` | Printed once by the CLI at startup, not emitted by the server. |
47
43
 
48
44
  Every event carries `event` (its name). Numbers are JSON numbers, never
@@ -51,11 +47,10 @@ strings.
51
47
  ### Privacy guarantees
52
48
 
53
49
  No event, snapshot or exposition carries a request URL, path, query string,
54
- header, body, client address, User-Agent string, binding, secret, stored link
55
- destination or user exception text. `route` is always a configured pattern
50
+ header, body, client address, User-Agent string, binding, secret or user
51
+ exception text. `route` is always a configured pattern
56
52
  from reviewed YAML. `requestId` is server-generated unless
57
- `--trust-request-id` accepts one from a trusted proxy. `code` in
58
- `link_request` is redacted unless the operator sets `includeCode`. An observer
53
+ `--trust-request-id` accepts one from a trusted proxy. An observer
59
54
  that logs should keep the same rule; nothing in an event lets it break it.
60
55
 
61
56
  ## Observers
@@ -98,12 +93,12 @@ call. A hook that throws or returns a rejecting promise is isolated: the
98
93
  request is unaffected, the next observer still runs, `observers.errors` in
99
94
  the snapshot increments and one `observer` record goes to the default log.
100
95
  Nothing is retried; an observer that needs delivery guarantees owns its own
101
- queue, like the [link event channel](MONITORING.md#the-link-event-channel).
96
+ queue.
102
97
 
103
98
  `onMetrics` receives a fresh snapshot every `metricsIntervalMs`
104
99
  (`startServer` option, `0` off by default, 1 s to 1 h) and once at `close()`.
105
- `onClose` runs in reverse order after the runtime and the link channel have
106
- drained. `app.observers` lists the `{ name, version }` pairs.
100
+ `onClose` runs in reverse order after the runtime has closed.
101
+ `app.observers` lists the `{ name, version }` pairs.
107
102
 
108
103
  `createRuntime(project, { observers })` takes the same array for embedding
109
104
  without the server: the runtime's own sink and counters are then yours, and
@@ -124,7 +119,7 @@ and capped at 10 000 keys.
124
119
 
125
120
  | Field | Type | Meaning |
126
121
  |---|---|---|
127
- | `version` | gauge | Snapshot shape version, currently `1`. |
122
+ | `version` | gauge | Snapshot shape version, currently `2`. |
128
123
  | `uptimeSeconds`, `rssBytes` | gauge | Process facts. |
129
124
  | `requests.total`, `requests.byStatusClass.{2xx,3xx,4xx,5xx}` | counter | Application responses, including shed 503s. |
130
125
  | `requests.inFlight` | gauge | Requests holding application admission now. |
@@ -135,13 +130,10 @@ and capped at 10 000 keys.
135
130
  | `watch.failed` | counter | Development watcher failures. |
136
131
  | `functionWorkers.started`, `functionWorkers.restarts` | counter | Worker starts and scheduled replacements. |
137
132
  | `functionWorkers.healthySlots`, `functionWorkers.slots` | gauge | Ready slots and configured slots of the serving runtime. |
138
- | `linkStoreWorkers.started`, `linkStoreWorkers.restarts` | counter | The same for link-store connections. |
139
133
  | `policies.throttle.{allowed,exceeded}` | counter | Throttle decisions (see the catalogue for what enforce mode logs). |
140
134
  | `policies.agents.{denied,reported}` | counter | Agents decisions. |
141
135
  | `policies.cache.{hit,stale,miss,store}` | counter | Cache outcomes. |
142
- | `linkRequests.{completed,aborted,missing,disabled,expired,invalid_code,invalid_record,unavailable}` | counter | Dynamic link outcomes, counted whether or not `linkEvents` is set. |
143
136
  | `signals.{accepted,delivered,failed,dropped}` | counter | Best-effort webhook outcomes; exposed as `signals_total` with outcome labels. |
144
- | `linkObserver.failed`, `linkObserver.dropped` | counter | Link collector failures and dropped events. |
145
137
  | `logsDropped` | counter | Records the JSON logger shed. |
146
138
  | `observers.errors` | counter | Observer hooks that threw or rejected. |
147
139
 
@@ -171,7 +163,6 @@ urlcode_function_worker_restarts_total 0
171
163
  urlcode_function_worker_healthy_slots 2
172
164
  urlcode_throttle_total{outcome="exceeded"} 14
173
165
  urlcode_cache_total{outcome="hit"} 511
174
- urlcode_link_requests_total{outcome="completed"} 0
175
166
  urlcode_logs_dropped_total 0
176
167
  urlcode_observer_errors_total 0
177
168
  urlcode_uptime_seconds 86400