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

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 (191) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +24 -8
  2. package/.claude/skills/urlcode-operations/SKILL.md +10 -16
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/README.md +75 -62
  5. package/ROADMAP.md +61 -16
  6. package/SECURITY.md +23 -8
  7. package/dist/BUILD-MANIFEST.json +38 -43
  8. package/dist/agents-guide.js +18 -18
  9. package/dist/build-static.js +134 -0
  10. package/dist/capabilities.js +85 -31
  11. package/dist/capability-query.js +0 -1
  12. package/dist/cli.js +23 -31
  13. package/dist/compliance-rules/baseline.js +2 -10
  14. package/dist/compliance-rules/privacy.js +5 -16
  15. package/dist/compliance-rules/shared.js +0 -2
  16. package/dist/compliance.js +6 -8
  17. package/dist/config.js +15 -8
  18. package/dist/context.js +9 -10
  19. package/dist/examples.js +2 -2
  20. package/dist/explain-cli.js +3 -3
  21. package/dist/explain.js +5 -6
  22. package/dist/extensions.js +61 -2
  23. package/dist/function-sources.js +34 -2
  24. package/dist/function-worker.js +3 -1
  25. package/dist/functions.js +84 -13
  26. package/dist/guest-api.js +29 -3
  27. package/dist/index.js +3 -6
  28. package/dist/manifest.js +5 -7
  29. package/dist/mcp.js +1 -1
  30. package/dist/observability.js +1 -21
  31. package/dist/policies/cache.js +8 -3
  32. package/dist/policy.js +28 -9
  33. package/dist/project-tests.js +3 -3
  34. package/dist/readiness.js +32 -9
  35. package/dist/router.js +5 -7
  36. package/dist/runtime.js +77 -58
  37. package/dist/sandbox.js +48 -0
  38. package/dist/scaffold.js +0 -0
  39. package/dist/scripts/operational-drills.js +12 -54
  40. package/dist/server.js +3 -29
  41. package/dist/tooling.js +1 -1
  42. package/dist/trusted-functions.js +210 -0
  43. package/dist/types/build-static.d.ts +43 -0
  44. package/dist/types/capabilities.d.ts +14 -5
  45. package/dist/types/compliance-rules/shared.d.ts +0 -2
  46. package/dist/types/compliance.d.ts +0 -3
  47. package/dist/types/config.d.ts +2 -1
  48. package/dist/types/context.d.ts +2 -1
  49. package/dist/types/examples.d.ts +1 -1
  50. package/dist/types/extensions.d.ts +55 -0
  51. package/dist/types/function-sources.d.ts +4 -0
  52. package/dist/types/functions.d.ts +48 -5
  53. package/dist/types/guest-api.d.ts +1 -0
  54. package/dist/types/index.d.ts +3 -6
  55. package/dist/types/manifest.d.ts +1 -3
  56. package/dist/types/observability.d.ts +1 -14
  57. package/dist/types/project-tests.d.ts +1 -2
  58. package/dist/types/readiness.d.ts +10 -3
  59. package/dist/types/router.d.ts +2 -1
  60. package/dist/types/runtime.d.ts +0 -27
  61. package/dist/types/sandbox.d.ts +12 -0
  62. package/dist/types/scaffold.d.ts +0 -2
  63. package/dist/types/server.d.ts +1 -4
  64. package/dist/types/tooling.d.ts +3 -3
  65. package/dist/types/trusted-functions.d.ts +29 -0
  66. package/dist/types/types.d.ts +14 -7
  67. package/dist/types/verify-deployment.d.ts +2 -2
  68. package/dist/types.js +17 -4
  69. package/dist/typescript-authoring.js +55 -17
  70. package/dist/verify-deployment.js +3 -3
  71. package/docs/AI-AUTHORING.md +104 -9
  72. package/docs/AWS.md +4 -4
  73. package/docs/BEST-PRACTICES.md +3 -2
  74. package/docs/CAPABILITIES.md +29 -16
  75. package/docs/CAPACITY.md +128 -31
  76. package/docs/CLOUDFLARE.md +1 -2
  77. package/docs/COMPLIANCE.md +6 -9
  78. package/docs/DEPLOYMENT-CHECKS.md +1 -1
  79. package/docs/EXTENSIONS.md +183 -11
  80. package/docs/FRAMEWORK.md +35 -19
  81. package/docs/FUNCTION-SECURITY.md +129 -32
  82. package/docs/INSTALL.md +0 -5
  83. package/docs/LOAD-TESTING.md +4 -4
  84. package/docs/MIDDLEWARE.md +27 -16
  85. package/docs/MONITORING.md +2 -19
  86. package/docs/NEXT-PHASE-PLAN.md +12 -4
  87. package/docs/NEXT-STEPS.md +259 -40
  88. package/docs/OBSERVABILITY.md +7 -16
  89. package/docs/OPEN-DECISIONS.md +212 -0
  90. package/docs/OPERATIONAL-PROOF.md +26 -30
  91. package/docs/OPERATIONS.md +23 -32
  92. package/docs/POLICIES.md +15 -4
  93. package/docs/PRERENDER.md +2 -2
  94. package/docs/PROJECT-DIRECTION.md +34 -9
  95. package/docs/README.md +13 -11
  96. package/docs/RELEASE-READINESS.md +49 -29
  97. package/docs/RELEASE-SECURITY.md +89 -3
  98. package/docs/RESILIENCE.md +16 -15
  99. package/docs/ROUTING.md +8 -10
  100. package/docs/SANDBOX-REVIEW.md +19 -6
  101. package/docs/SCAFFOLDING.md +0 -2
  102. package/docs/SECURITY-AUDIT.md +40 -0
  103. package/docs/SPECIFICATION.md +60 -26
  104. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
  105. package/docs/SPIKE-BUSINESS-SUITE.md +8 -0
  106. package/docs/SPIKE-CORE-LAYERING.md +337 -0
  107. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
  108. package/docs/SPIKE-EXTENSION-MODEL.md +2 -2
  109. package/docs/SPIKE-EXTENSIONS.md +5 -0
  110. package/docs/SPIKE-LAMBDA-COMPILE.md +4 -2
  111. package/docs/SPIKE-MONOREPO.md +322 -0
  112. package/docs/STARTERS.md +1 -1
  113. package/docs/STATIC.md +94 -0
  114. package/docs/TOOLING.md +13 -9
  115. package/docs/TUNNELS.md +0 -3
  116. package/docs/TYPESCRIPT-AUTHORING.md +27 -12
  117. package/docs/TYPESCRIPT.md +25 -4
  118. package/docs/USABILITY-REVIEW.md +7 -1
  119. package/docs/VERCEL.md +4 -5
  120. package/docs/VERSION-ALIGNMENT.md +205 -0
  121. package/docs/YAML-GUIDE.md +0 -3
  122. package/docs/YAML-REFERENCE.md +16 -6
  123. package/docs/policies/cache.md +13 -0
  124. package/examples/assets/example.yaml +1 -1
  125. package/examples/aws/example.yaml +1 -1
  126. package/examples/cloudflare/example.yaml +1 -1
  127. package/examples/conditions/example.yaml +1 -1
  128. package/examples/cookbook/example.yaml +1 -1
  129. package/examples/cookbook/middleware/bucket.mjs +12 -2
  130. package/examples/cookbook/middleware/locale.mjs +7 -3
  131. package/examples/egress/example.yaml +1 -1
  132. package/examples/extensions/example.yaml +1 -1
  133. package/examples/prerender/README.md +2 -2
  134. package/examples/prerender/example.yaml +1 -1
  135. package/examples/provider-conformance/example.yaml +1 -1
  136. package/examples/vercel/example.yaml +1 -1
  137. package/llms-full.txt +594 -219
  138. package/llms.txt +23 -12
  139. package/package.json +10 -3
  140. package/packaging/claude-plugin/.claude-plugin/plugin.json +1 -1
  141. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +24 -8
  142. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +10 -16
  143. package/recipes/authenticated-json-api/recipe.yaml +1 -1
  144. package/recipes/contact-form/recipe.yaml +1 -1
  145. package/recipes/cors-api/recipe.yaml +1 -1
  146. package/recipes/health-page/recipe.yaml +1 -1
  147. package/recipes/json-api/recipe.yaml +1 -1
  148. package/recipes/middleware/middleware/bucket.mjs +12 -2
  149. package/recipes/middleware/middleware/locale.mjs +7 -3
  150. package/recipes/middleware/recipe.yaml +1 -1
  151. package/recipes/protected-download/recipe.yaml +1 -1
  152. package/recipes/redirect/recipe.yaml +1 -1
  153. package/recipes/static-plus-api/recipe.yaml +1 -1
  154. package/recipes/typescript/README.md +2 -1
  155. package/recipes/typescript/recipe.yaml +1 -1
  156. package/recipes/webhook-receiver/README.md +5 -1
  157. package/recipes/webhook-receiver/recipe.yaml +2 -1
  158. package/recipes/webhook-receiver/urlcode.yaml +7 -0
  159. package/schemas/recipe.schema.json +4 -3
  160. package/schemas/urlcode.schema.json +14 -41
  161. package/skills/urlcode/SKILL.md +29 -8
  162. package/starters/default/AGENTS.md +18 -18
  163. package/starters/default/urlcode.yaml +0 -1
  164. package/dist/link-api.js +0 -136
  165. package/dist/link-cli.js +0 -141
  166. package/dist/link-events.js +0 -76
  167. package/dist/link-records.js +0 -31
  168. package/dist/link-store-worker.js +0 -150
  169. package/dist/link-store.js +0 -250
  170. package/dist/management-policy.js +0 -40
  171. package/dist/sqlite-version.js +0 -6
  172. package/dist/types/link-api.d.ts +0 -30
  173. package/dist/types/link-cli.d.ts +0 -37
  174. package/dist/types/link-events.d.ts +0 -27
  175. package/dist/types/link-records.d.ts +0 -11
  176. package/dist/types/link-store-worker.d.ts +0 -1
  177. package/dist/types/link-store.d.ts +0 -130
  178. package/dist/types/management-policy.d.ts +0 -8
  179. package/dist/types/sqlite-version.d.ts +0 -1
  180. package/docs/DYNAMIC-LINKS.md +0 -61
  181. package/docs/MANAGEMENT-SECURITY.md +0 -82
  182. package/docs/links/cli.md +0 -110
  183. package/docs/links/limits.md +0 -175
  184. package/docs/links/management-api.md +0 -80
  185. package/docs/links/pools.md +0 -75
  186. package/docs/links/setup.md +0 -135
  187. package/docs/yaml/links.md +0 -30
  188. package/examples/live-links/README.md +0 -11
  189. package/examples/live-links/example.yaml +0 -21
  190. package/examples/live-links/tests/requests.json +0 -6
  191. package/examples/live-links/urlcode.yaml +0 -16
@@ -21,8 +21,7 @@ route pattern and the reason named:
21
21
 
22
22
  | Handler | Why it is refused |
23
23
  | --- | --- |
24
- | `function`, `middleware` | need worker threads and the QuickJS WASM engine |
25
- | `link` | needs a durable writable store the platform does not provide here |
24
+ | `function`, `middleware` | need the self-hosted Node lifecycle, and worker threads and the QuickJS WASM engine when a route declares `sandbox: true` |
26
25
  | `page`, `static`, `download` | need a platform static-asset binding, not an inline copy |
27
26
  | `env`, `secrets` | would have to be baked into the artifact |
28
27
 
@@ -49,11 +49,11 @@ A **project** rule runs once with:
49
49
  |---|---|
50
50
  | `document` | The parsed and validated `urlcode.yaml` (includes are merged into `routes`) |
51
51
  | `routes` | Route configuration by pattern, as written in YAML |
52
- | `plan` | `testPlan()`: `inventory[]` (`path`, `handler`, `methods`, `middleware`, `policies`, `state`), `policies` (the per-route describe map), `dynamicLinks` |
52
+ | `plan` | `testPlan()`: `inventory[]` (`path`, `handler`, `methods`, `middleware`, `policies`, `state`), `policies` (the per-route describe map) |
53
53
  | `policies` | `effectivePolicies(document, route)` by pattern: the merged configuration of every policy on each route |
54
54
  | `origin` | The declared public origin, or `null` |
55
55
  | `target` | `node` unless the caller states another |
56
- | `host` | `{ requestLog, linkEvents, includeCode }` as declared for the deployment; `null` where undeclared |
56
+ | `host` | `{ requestLog }` as declared for the deployment; `null` where undeclared |
57
57
 
58
58
  A **route** rule runs once per inventory entry and additionally receives
59
59
  `route` (the inventory entry), `config` (that route's YAML), `policy` (the
@@ -86,7 +86,6 @@ cache and compression secrets handling in `src/policies/cache.ts` and
86
86
  | `rfc6585/throttle-functions` | [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585) §4 | medium | Every active function or middleware route has an effective `policies.throttle` | Declare a throttle on the route or the project |
87
87
  | `rfc9309/robots` | [RFC 9309](https://www.rfc-editor.org/rfc/rfc9309) | low | Some active route declares `policies.agents`, or an active `/robots.txt` `respond` route exists | Add a `/robots.txt` route or an agents deny list |
88
88
  | `rfc9110/expired-routes` | [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110) §15.5.11 | info | Lists routes past `expires` that still answer 410 | Remove them once the 410 window has served its purpose |
89
- | `ops/management-private` | [Management security](MANAGEMENT-SECURITY.md) | info | Reminder when `dynamicLinks: true`: the management API and `/_urlcode` probes belong on a private bind | Run `links api` privately with an auth file; keep probes internal |
90
89
 
91
90
  ### `strict` (baseline plus)
92
91
 
@@ -102,15 +101,13 @@ cache and compression secrets handling in `src/policies/cache.ts` and
102
101
 
103
102
  These rules check deployment settings, so `audit` takes `--request-log` to
104
103
  declare the level the deployment uses (the audit process itself always logs
105
- nothing). The CLI cannot enable link events, so it declares `linkEvents:
106
- false`; embedders pass `host` themselves. The references are the runtime's
104
+ nothing); embedders pass `host` themselves. The references are the runtime's
107
105
  own [logging guarantees](MONITORING.md): records carry no URL, query, header,
108
106
  body or binding, and `detailed` adds only the method and route pattern.
109
107
 
110
108
  | Rule | Standard | Severity | Checks | Remediation |
111
109
  |---|---|---|---|---|
112
110
  | `privacy/request-log-minimal` | [Monitoring](MONITORING.md), Log records | medium (`info` when undeclared) | `host.requestLog` is `minimal` | Use the default log unless per-route rates are required |
113
- | `privacy/link-events-off` | [Monitoring](MONITORING.md), The link event channel | medium; `high` with `includeCode` (`info` when undeclared and `dynamicLinks` is on) | The link event channel is off, or on without `includeCode` | Enable it only for a declared purpose; never disclose codes |
114
111
  | `privacy/detailed-log-parameters` | [Monitoring](MONITORING.md), Log records | low | With `detailed` logging, no active route takes parameters (records name the pattern and method, never values) | Keep `minimal` on parameterised deployments |
115
112
 
116
113
  ## Writing custom rules
@@ -159,9 +156,9 @@ Without any compliance flag the audit report is unchanged apart from
159
156
  "findings": [{ "rule": "rfc6585/throttle-functions", "severity": "medium", "route": "/hello/{name}",
160
157
  "message": "…", "remediation": "…", "standard": { "name": "RFC 6585 …", "reference": "…", "section": "…" } }],
161
158
  "counts": { "high": 0, "medium": 4, "low": 11, "info": 1 }, "pass": true,
162
- "evidence": { "routes": 21, "active": 19, "dynamicLinks": false, "policies": ["agents", "cache", "security", "throttle"],
159
+ "evidence": { "routes": 21, "active": 19, "policies": ["agents", "cache", "security", "throttle"],
163
160
  "files": ["urlcode.yaml", "routes/code.yaml"], "origin": null, "target": "node",
164
- "host": { "requestLog": "minimal", "linkEvents": false, "includeCode": null },
161
+ "host": { "requestLog": "minimal" },
165
162
  "scope": "declared configuration and runtime facts; not a deployment or certification" } }
166
163
  ```
167
164
 
@@ -190,7 +187,7 @@ const report = await runCompliance(runtime, {
190
187
  rules: [], override: {}, disable: [], // as a rules module would export them
191
188
  ignore: ['rfc9110/expired-routes'],
192
189
  origin: 'https://links.example',
193
- host: { requestLog: 'minimal', linkEvents: false }, // what the deployment is configured with
190
+ host: { requestLog: 'minimal' }, // what the deployment is configured with
194
191
  });
195
192
  await runtime.close();
196
193
  ```
@@ -19,7 +19,7 @@ URLs for; defaults to the target), `--expect-routes N`, `--expect-metrics`
19
19
  (per request, 100-120000, default 10000), `--fail-on high|medium|low|info|none`
20
20
  (default `high`), the `audit` compliance flags (`--compliance`,
21
21
  `--compliance-rules`, `--compliance-ignore`, `--compliance-warn`), and the
22
- usual `--policy` and `--link-store` bindings the local snapshot needs to start.
22
+ usual `--policy` binding the local snapshot needs to start.
23
23
 
24
24
  ## What it verifies
25
25
 
@@ -1,10 +1,16 @@
1
1
  # Operator-installed extensions
2
2
 
3
- Extensions are trusted operator modules, separate from application WASM. Auth
3
+ Extensions are trusted operator modules, separate from a project's own
4
+ `function`/`middleware` code. Auth
4
5
  and admin implementations live in `urlcode-auth` and `urlcode-admin`; the runtime
5
6
  supplies only the generic integration contract. No project file can import a host
6
7
  extension or choose its npm package.
7
8
 
9
+ Stored short links are moving out of core this way too: a future
10
+ `urlcode-dynamic-link` package (mount-based, like `auth`/`admin`, not yet
11
+ published) will own the durable link store, its CLI and management API. Core
12
+ no longer has a native `link` handler or a `dynamicLinks` project flag.
13
+
8
14
  A project declares versioned configuration and exclusive route mounts:
9
15
 
10
16
  ```yaml
@@ -52,9 +58,9 @@ fails, naming the route, when `auth` appears without an `extensions.auth`
52
58
  declaration, next to `policies.extensions.auth`, or next to
53
59
  `policies.extensions: false`.
54
60
 
55
- The same shape is reserved for the cache policy: a future `cache: {strategy,
56
- maxAge}` route key may expand to `policies.cache` in the same pass. It is not
57
- implemented; declare `policies.cache` today (see [policies](POLICIES.md)).
61
+ The same shape is used for the cache policy: a route-level `cache: {strategy,
62
+ maxAge, ...}` expands to `policies.cache` in the same pass (see
63
+ [policies](POLICIES.md)).
58
64
 
59
65
  The configuration and requirement objects above are validated by the installed
60
66
  extension's schemas. They are examples of extension-owned fields, not built-in
@@ -71,17 +77,86 @@ project and place the exact returned SHA-256 in each registration's
71
77
  in the revision. Changing them requires an explicit operator reapproval.
72
78
 
73
79
  Registrations provide a name, contract version, target list, JSON configuration
74
- schema, optional policy schema and activation factory. Activation receives the
80
+ schema, optional policy schema, an optional declared `cacheSensitive` (below)
81
+ and activation factory. Activation receives the
75
82
  canonical operator origin, target, revision and mount bases. Its instance handles
76
- bounded requests and, when used in policies, authorizes requests. Missing
83
+ bounded requests and, when named in a route's policies, gates the request via
84
+ `authorize`, wraps the rest of the pipeline via `middleware`, or both (see
85
+ [Wrapping a route](#wrapping-a-route-extension-middleware) above). Missing
77
86
  registrations, stale grants, invalid configuration and unsupported targets fail
78
87
  activation. Multiple mounts cannot overlap other declared routes.
79
88
 
80
89
  For extension-protected routes, agents/throttle run before authorization and
81
- cache access happens only after authorization. Extension routes and protected
82
- routes reject cache strategies other than no-store; every resulting response is
83
- forced to no-store after host response hooks. Compression is disabled on these
84
- responses.
90
+ cache access happens only after authorization. This part is unconditional:
91
+ naming any extension in `policies.extensions` always runs its `authorize()`
92
+ (when it implements one) before the route's own handler, whatever this
93
+ section says next.
94
+
95
+ An `extension:` mount is always confidential: its route rejects cache
96
+ strategies other than no-store, and every response is forced to no-store
97
+ after host response hooks, with compression disabled. A `policies.extensions`
98
+ route (no mount, `authorize`/`middleware` only) gets the same treatment
99
+ **unless every extension it names explicitly declares
100
+ `cacheSensitive: false`** on its `RuntimeExtension` registration. That field
101
+ defaults to sensitive (unset or `true`): the safe default is unchanged, and
102
+ relaxing it is an explicit, reviewed operator opt-in an extension author
103
+ makes once, in host code, never inferred from a route or from a response the
104
+ extension happens to return. It exists for a generic, cache-transparent
105
+ extension whose `middleware()` is pure request/response wrapping with no
106
+ access-control semantics of its own (a logging or header-rewriting
107
+ extension, for example) — declared this way, its wrapped route keeps
108
+ whatever `Cache-Control` its own handler sets, exactly like the native
109
+ `middleware:` array already does, and compression is not disabled either. A
110
+ route naming more than one extension stays confidential if any one of them
111
+ is sensitive (or leaves the field unset); one `cacheSensitive: false`
112
+ extension cannot relax a route that also names a sensitive one. This can
113
+ only relax the no-store floor a generic extension would otherwise inherit —
114
+ it has no effect on `authorize()`, which runs the same way regardless, and
115
+ `auth`/`admin`-style extensions gating real access must leave it at the
116
+ default.
117
+
118
+ ## Wrapping a route: extension middleware
119
+
120
+ `authorize` is a gate: it runs once, before the route's handler, and can only
121
+ either let the request through unchanged or answer instead of it. It cannot
122
+ see or change what the handler itself returns.
123
+
124
+ `middleware` is a wrap. An extension instance may implement it alongside or
125
+ instead of `authorize`, attached the same way, via
126
+ `policies.extensions.<name>` on a route (no `extension:` mount required); its
127
+ `config` is exactly the same per-route value `authorize`'s `requirement`
128
+ receives, validated once against the extension's `policySchema`:
129
+
130
+ ```ts
131
+ middleware?(config: Readonly<Record<string, unknown>>, request: ExtensionRequest,
132
+ next: () => Promise<HandlerResult>): HandlerResult | Promise<HandlerResult>;
133
+ ```
134
+
135
+ `next()` invokes the rest of the pipeline for that route: any other extension
136
+ `middleware()` also declared on the route (see below), then the route's own
137
+ native `middleware:` chain and handler, dispatched through the sandboxed or
138
+ trusted engine exactly as it is today. Calling it lets the hook run code
139
+ before and after the rest of the pipeline, inspecting or mutating the
140
+ `HandlerResult` it resolves to — the same "add a header to whatever the
141
+ handler returns" shape as the native `middleware/headers.mjs` cookbook
142
+ recipe, but declared by an operator-installed extension instead of project
143
+ code. Skipping it short-circuits everything after that point, the same
144
+ capability `authorize` already has, just usable from either side of the
145
+ handler now. `next()` may be called at most once; calling it again throws.
146
+
147
+ A route naming more than one extension in `policies.extensions` chains every
148
+ one that implements `middleware`, in the order the keys are declared, each
149
+ one's `next()` reaching the next one and the innermost `next()` reaching the
150
+ native pipeline — the first declared name is outermost. This is purely
151
+ additive at the `policies.extensions` layer and never touches the native
152
+ `middleware:` array, its schema, or its dispatch, all of which are unchanged.
153
+
154
+ `authorize` and `middleware` compose on the same route, from the same or
155
+ different extensions, without special-casing: `authorize` always runs first
156
+ (unchanged), and any declared `middleware()` wraps everything after that
157
+ point, including the rest of the authorize-gated pipeline. A route naming an
158
+ extension via `policies.extensions` only requires that extension to
159
+ implement `authorize`, `middleware`, or both — never both unconditionally.
85
160
 
86
161
  One exception exists for content-hashed assets. A registration may declare
87
162
  `immutableAssets: {prefix: '/static'}`, a normalized literal path under each
@@ -105,6 +180,103 @@ HTML/JS on an authentication origin must be trusted by that site's operator.
105
180
  Cloudflare refuses extensions until its artifact format supports their execution.
106
181
  Node adapter conformance is not a live-provider deployment claim.
107
182
 
183
+ ## Project-level lifecycle hooks
184
+
185
+ `authorize` and `middleware` let core call *into* an extension. They do not
186
+ let a project hand its own code *to* an extension to run at a defined point.
187
+ That gap matters once an extension has meaningful lifecycle events —
188
+ registration, deletion, an administrative action, a link resolution, and so
189
+ on. Presentation already has a standard layering mechanism for this: a
190
+ project customizes an extension's *look* through `urlcode-ui`'s
191
+ `copy`/`extra.css`/`templates` without forking it. Behavior needs the same
192
+ standard, or every extension author (and every project depending on one)
193
+ either reinvents it or forks the extension. `urlcode-auth` and
194
+ `urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
195
+ [SPIKE-AUTH.md](https://github.com/jimhoyd-com/urlcode-auth/blob/main/docs/SPIKE-AUTH.md)
196
+ already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
197
+
198
+ **The pattern.** An extension with lifecycle events an author judges worth
199
+ exposing should let the project name its own function in the extension's own
200
+ `config`, using the same source shape `function`/`middleware` routes already
201
+ use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
202
+ and add its own `sandbox` boolean next to it (below). The extension's own
203
+ `activate()` reads that config, and its own runtime dispatch — not a new core
204
+ primitive, an ordinary call the extension package makes with the request
205
+ context it already has — invokes the named function at the lifecycle point
206
+ it defines, with a typed input and a typed verdict the extension's own
207
+ schema documents. For example, an auth-style extension might declare:
208
+
209
+ ```yaml
210
+ extensions:
211
+ auth:
212
+ version: "1"
213
+ config:
214
+ hooks:
215
+ beforeRegister:
216
+ source: ./hooks/registration-rule.mjs
217
+ export: default
218
+ onSignUp:
219
+ source: ./hooks/on-signup.mjs
220
+ ```
221
+
222
+ with `beforeRegister` called before an account is created, given a typed
223
+ `{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
224
+ or `{allow: false, reason}`), and `onSignUp` called after, for side effects
225
+ such as provisioning a workspace. The exact hook names, input/verdict shapes
226
+ and invocation points are the extension's own design — `hooks` is not a core
227
+ schema key — but the source/export/sandbox shape, and the fact that this is
228
+ project code the extension calls rather than an operator callback in
229
+ `host.mjs`, should be consistent across extensions so an author who has
230
+ learned one has learned the pattern.
231
+
232
+ **Trust and execution mode.** Project-level lifecycle hooks are first-party
233
+ project code, the same category as any `function`/`middleware` route, and
234
+ follow the same trusted-by-default rule with no special case
235
+ (docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)):
236
+ trusted, in-process execution by default; a project sets `sandbox: true` on
237
+ a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
238
+ the mechanism `function`/`middleware` routes already use and for the same
239
+ reason — the hook's own code, not the trustworthiness of whatever triggered
240
+ it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
241
+ route needs `sandbox: true`"). This was raised as an open question — whether
242
+ a lifecycle hook should always run sandboxed because it makes a
243
+ security-relevant decision — and settled the other way: uniformity with the
244
+ rest of the trust model was chosen over hardwiring isolation for lifecycle
245
+ hooks specifically, the same "no special case" call already made between
246
+ `function` and `middleware` trust. A `beforeRegister` hook enforcing "only
247
+ `@acme.com` may register" is the project's own governance rule over its own
248
+ signup flow; it is not more dangerous than any other route the project
249
+ wrote, and does not get a different default.
250
+
251
+ Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
252
+ `src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
253
+ each half of a hook's `sandbox: true` opt-in has its own answer:
254
+
255
+ - **Trusted (the default, no `sandbox: true`).** No core primitive is needed
256
+ or provided: this is ordinary first-party project code, and the
257
+ extension's own `activate()` already has `ExtensionActivation.root` to
258
+ resolve the hook's `source` against and can `import()` it directly, the
259
+ same way any trusted `function`/`middleware` route does.
260
+ - **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
261
+ `SandboxPool`, the same QuickJS/worker-thread engine that backs a
262
+ sandboxed `function`/`middleware` route — the identical module-allowlist
263
+ walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
264
+ response-shape validation, with no separate or weaker engine for
265
+ extensions. It takes an explicit list of `{source, export}` entries
266
+ (resolve a hook's `source` string with the re-exported `functionFile()`,
267
+ the same resolution/validation a native route's `source` gets) instead of
268
+ anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
269
+ in place of a `FunctionRoute`. There is no "trusted" mode exported
270
+ alongside it — `SandboxPool` is only ever the isolated path; see
271
+ [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md) and
272
+ [TYPESCRIPT.md](TYPESCRIPT.md) for the full contract.
273
+
274
+ An extension honoring a hook's `sandbox: true` is expected to actually
275
+ isolate that invocation through `SandboxPool` now that the primitive exists
276
+ (or document plainly that it does not yet, rather than accepting the field
277
+ and silently running it trusted) — say which, in the extension's own docs,
278
+ so an author reading them is not misled about what opt-in exists.
279
+
108
280
  ## Discovering schemas
109
281
 
110
282
  Each registration carries the JSON Schemas that validate its `config` block and
@@ -161,7 +333,7 @@ and each extension's own operator files. Core never bundles or imports the
161
333
  extension packages at build time; at run time it resolves
162
334
  `@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
163
335
  the invoking directory (so `npm install @jimhoyd/urlcode-auth` in that
164
- directory, from npm where the packages are published as `0.1.0-alpha.1`
336
+ directory, from npm where the packages are published as `0.1.0-alpha.x`
165
337
  prereleases, is the normal path and what makes `--with auth` work), imports
166
338
  the package and calls its
167
339
  `scaffold` export with this request:
package/docs/FRAMEWORK.md CHANGED
@@ -9,14 +9,14 @@ claim here is implemented in the linked repository; nothing is roadmap.
9
9
 
10
10
  | Package | Repository | What it adds | How a project declares it |
11
11
  |---|---|---|---|
12
- | `@jimhoyd/urlcode` | [urlcode](https://github.com/jimhoyd-com/urlcode) | The runtime: YAML routes, isolated functions and middleware, pages and assets, live short links, policies, site conventions, CLI, provider adapters, the extension contract | `urlcode.yaml` with `version: "1"` |
12
+ | `@jimhoyd/urlcode` | [urlcode](https://github.com/jimhoyd-com/urlcode) | The runtime: YAML routes, functions and middleware (trusted by default, `sandbox: true` opt-in), pages and assets, policies, site conventions, CLI, provider adapters, the extension contract | `urlcode.yaml` with `version: "1"` |
13
13
  | `@jimhoyd/urlcode-ui` | [urlcode-ui](https://github.com/jimhoyd-com/urlcode-ui) | Shared presentation: escaped templates, shadcn/ui partials, one stylesheet with light and dark, themes, translations, the `ui` extension that serves the kit's assets | `extensions.ui` plus an asset mount route |
14
14
  | `@jimhoyd/urlcode-auth` | [urlcode-auth](https://github.com/jimhoyd-com/urlcode-auth) | Accounts: password, passkeys, OpenID Connect, email codes, TOTP, recovery, sessions, roles, registration modes, account page, operator CLI | `extensions.auth` plus an `/account/*` mount and `policies.extensions.auth` on protected routes |
15
15
  | `@jimhoyd/urlcode-admin` | [urlcode-admin](https://github.com/jimhoyd-com/urlcode-admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
16
16
 
17
17
  The core is Apache-2.0 and released. The three extension packages are
18
- Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.1`,
19
- `@jimhoyd/urlcode-auth@0.1.0-alpha.1`, `@jimhoyd/urlcode-admin@0.1.0-alpha.1`,
18
+ Apache-2.0 and published to npm as alphas (`@jimhoyd/urlcode-ui@0.1.0-alpha.4`,
19
+ `@jimhoyd/urlcode-auth@0.1.0-alpha.2`, `@jimhoyd/urlcode-admin@0.1.0-alpha.2`,
20
20
  on core `0.4.0-alpha.1`). An alpha on npm is a distribution channel, not an
21
21
  endorsement: the source is complete, but independent review, deployment
22
22
  evidence and an accessibility assessment are still pending
@@ -24,6 +24,9 @@ evidence and an accessibility assessment are still pending
24
24
  files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode-auth/blob/main/IMPLEMENTATION-STATUS.md),
25
25
  [admin](https://github.com/jimhoyd-com/urlcode-admin/blob/main/IMPLEMENTATION-STATUS.md),
26
26
  [ui](https://github.com/jimhoyd-com/urlcode-ui/blob/main/IMPLEMENTATION-STATUS.md).
27
+ Which core version each package supports, how it declares that, and the order
28
+ in which a core change reaches the downstream repositories are recorded in
29
+ [core version alignment](VERSION-ALIGNMENT.md).
27
30
 
28
31
  ## The ladder
29
32
 
@@ -38,22 +41,27 @@ Each rung's YAML is valid on every rung above it.
38
41
  llms.txt) and `policies` (throttle, agents, security headers, compression,
39
42
  cache). Still no code.
40
43
  3. **Functions and middleware.** `function` routes and ordered `middleware`
41
- in isolated JavaScript (QuickJS inside WebAssembly, fresh heap per call, no
42
- Node, filesystem or network). Secrets reach a function only through an
43
- operator grant pinned to the project revision.
44
- 4. **Live short links.** A `link` route backed by an optional SQLite store,
45
- with CLI and a private management API. Records change without reloads.
46
- 5. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
44
+ in JavaScript, trusted and in-process by default; a route declaring
45
+ `sandbox: true` runs isolated instead (QuickJS inside WebAssembly, fresh
46
+ heap per call, no Node, filesystem or network). Secrets reach a function
47
+ only through an operator grant pinned to the project revision.
48
+ 4. **Accounts.** The `auth` extension: sign-in, registration, MFA, account
47
49
  page and protected routes. The operator installs it in a host file outside
48
50
  the project; YAML only declares the mount and configuration.
49
- 6. **Administration.** The `admin` extension on the same service: manage the
51
+ 5. **Administration.** The `admin` extension on the same service: manage the
50
52
  people who signed up, their sessions and roles, review the audit trail.
51
- 7. **Your own look.** A shared `presentation` (catalogue and theme variables)
53
+
54
+ 6. **Your own look.** A shared `presentation` (catalogue and theme variables)
52
55
  restyles auth and admin together; the `ui` extension adds the template kit,
53
56
  project copy, template and stylesheet overrides for kit-rendered pages.
54
57
 
55
- Rungs 1 to 4 need only the core package. Rungs 5 to 7 need the extension
56
- packages, installed from npm as `0.1.0-alpha.1` prereleases, and a Node host
58
+ Stored short links previously sat here as a native `link` route; that handler
59
+ was removed from core. A future `urlcode-dynamic-link` package (not yet
60
+ published, so this one line is the exception to "nothing is roadmap" above)
61
+ is expected to own them the same way `auth`/`admin` own their mounts.
62
+
63
+ Rungs 1 to 3 need only the core package. Rungs 4 to 6 need the extension
64
+ packages, installed from npm as `0.1.0-alpha.x` prereleases, and a Node host
57
65
  with a patched SQLite build; see each repository's README for the exact
58
66
  requirement.
59
67
 
@@ -151,12 +159,17 @@ These are the facts that keep generated projects valid. The full matrix is in
151
159
  logical names; the host file chooses the implementation. There is no
152
160
  `--extension` flag, no `import` in YAML, no interpolation.
153
161
  - **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
154
- `function`, `link`, `proxy`, `conditional` or `extension`, plus optional
162
+ `function`, `proxy`, `conditional` or `extension`, plus optional
155
163
  `middleware`. Paths are exact or single-segment `{param}`; `/*` only on
156
164
  `static` and `extension` mounts. No regex.
157
- - **Guest code is sandboxed.** Functions see a text/JSON `Request`/`Response`
158
- subset, validated `args` and granted `env`. No `fetch`, Node, filesystem or
159
- timers. Do not write code that needs them; declare a `proxy` or a binding.
165
+ - **`function`/`middleware` code is trusted by default, sandboxed opt-in.**
166
+ It runs in-process with full Node access unless the route declares
167
+ `sandbox: true`, which isolates it to a text/JSON `Request`/`Response`
168
+ subset, validated `args` and granted `env`, with no `fetch`, Node,
169
+ filesystem or timers. Either way, `args`/`env`/`secrets` are exactly what
170
+ the route declares and an operator grants — trust changes where code runs,
171
+ not what it is handed. See docs/SPIKE-DEFAULT-TRUST-MODEL.md and
172
+ docs/FUNCTION-SECURITY.md.
160
173
  - **Authentication is host processing.** Do not build login forms, session
161
174
  cookies or password checks in functions. Declare `policies.extensions.auth`
162
175
  on the route; the runtime withholds `Cookie` and `Authorization` from guests.
@@ -165,8 +178,11 @@ These are the facts that keep generated projects valid. The full matrix is in
165
178
  with the route named; nothing degrades silently.
166
179
  - **Provider targets refuse what they cannot enforce.** Cloudflare runs
167
180
  redirects and declared responses only. Serverless adapters refuse functions,
168
- links, proxy, signals and extensions. Check
169
- `urlcode capabilities --target NAME` before promising a deployment.
181
+ proxy, signals and extensions. The `static` target (S3 + CloudFront,
182
+ no server) refuses everything that needs request-time logic, keeping only
183
+ `redirect`/`respond`/`page`/`static`/`download` — see [static
184
+ hosting](STATIC.md). Check `urlcode capabilities --target NAME` before
185
+ promising a deployment.
170
186
  - **Report evidence, not hope.** The commands above are the evidence. Local
171
187
  tests are not deployment, soak or independent security review.
172
188
 
@@ -1,11 +1,46 @@
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
+ ## What "sandboxed" (`sandbox: true`) still guarantees
9
44
 
10
45
  - Function sources are parsed/snapshotted without importing them into Node.
11
46
  - Code runs in QuickJS inside WebAssembly, with no host JS functions/objects
@@ -22,9 +57,64 @@ is no `unsafe`, `trusted` or automatic host-execution fallback.
22
57
  Operator grants are exact-name, route-scoped and pinned to configuration/source.
23
58
 
24
59
  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.
60
+ [implemented contract](SPECIFICATION.md). A function moving from trusted to
61
+ `sandbox: true` that uses Node/network or binary/stream APIs must be rewritten
62
+ for the supported guest profile, or stay trusted. Redirects need none of this
63
+ machinery either way.
64
+
65
+ This engine — worker spawning, the module-allowlist walk, the two-layer
66
+ deadline, `maxBytes` and response-shape validation — is one implementation
67
+ shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
68
+ the public primitive an extension package uses to run a project-supplied hook
69
+ through this exact isolation when the project's own config declares
70
+ `sandbox: true` on it (see [EXTENSIONS.md](EXTENSIONS.md#project-level-lifecycle-hooks)
71
+ and [TYPESCRIPT.md](TYPESCRIPT.md)). Every guarantee above applies identically
72
+ through that entry point; there is no separate, weaker sandbox for extensions
73
+ to reach for, and no "trusted" mode exported there — trusted execution needs
74
+ no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
75
+
76
+ ## What the trusted default can and can't do
77
+
78
+ A trusted route (no `sandbox`, or `sandbox: false`) has none of the guest
79
+ restrictions above:
80
+
81
+ - Full Node built-ins, `process`, the filesystem, `fetch`, sockets, workers
82
+ and npm packages are available, exactly as in any other Node module.
83
+ - Module resolution is ordinary Node ESM resolution: bare specifiers, dynamic
84
+ `import()` and node_modules all work. There is no dependency-graph allowlist
85
+ and no per-module/total source-size budget (function-sources.ts's
86
+ `MODULE_LIMIT`/`MODULE_BYTE_LIMIT`/`TOTAL_BYTE_LIMIT` apply only to what a
87
+ sandboxed snapshot bundles).
88
+ - Node's own module cache is shared across invocations and across the whole
89
+ process; there is no fresh heap per call. Module-level state persists
90
+ between requests exactly like an ordinary long-running Node server, so a
91
+ trusted function that mutates shared/global state affects later requests
92
+ the way hand-written server code would.
93
+ - There is no worker-thread deadline that force-terminates a stuck call. A
94
+ trusted invocation races a configurable timeout, but that race can only
95
+ reject the *call*; it cannot preempt code that blocks the event loop
96
+ synchronously. See [capacity](CAPACITY.md) for what this means for one slow
97
+ or hung trusted route's effect on the rest of the process.
98
+ - A snapshot reload re-imports a trusted route's own entry file fresh (each
99
+ reload gets its own cache-busted module registration), so editing the
100
+ `source` file a route declares and reloading picks up the change, the same
101
+ as the sandboxed pool rebuilding from scratch. A file that entry file
102
+ merely *imports* is not similarly busted: Node's own module cache is
103
+ keyed by the resolved URL of that import statement, which this runtime
104
+ does not rewrite, so an edited dependency two files deep from the route
105
+ keeps serving its old content until the process restarts. Restructure a
106
+ route so the code you expect to hot-reload is the declared entry file
107
+ itself, or restart rather than reload after editing a trusted route's
108
+ dependencies. A `sandbox: true` route has no such gap: reload always
109
+ rebuilds its whole snapshot, dependencies included.
110
+
111
+ What does **not** change with trust: `args` are still exactly the validated
112
+ values the route declares (never raw request input), and `env`/`secrets` are
113
+ still exactly what the route's YAML requests and an operator policy grants,
114
+ pinned to the project revision — trust changes where code runs, not what
115
+ it is handed *through `context`*. It does not change what the code can go get
116
+ on its own once it is running; see "binding grants are unaffected" above for
117
+ that distinction.
28
118
 
29
119
  ## Granting selected bindings
30
120
 
@@ -63,26 +153,28 @@ urlcode serve --project /srv/my-links --policy /etc/urlcode/my-links-policy.json
63
153
 
64
154
  `dev`, `test` and `validate --local` use the same policy rules even for `.env.local`.
65
155
  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.
156
+ Every config change invalidates the grant, and so does a module change within
157
+ what the approval digest actually hashes: for a sandboxed route, its
158
+ middleware/function sources and their full dependency graph; for a trusted
159
+ route, only its own entry-file source (see the next paragraph — a trusted
160
+ route's transitive dependencies are explicitly **not** part of that digest).
161
+ Inspect/review the new revision before updating the operator file. Policies
162
+ are read at startup, not hot-reloaded. A failed development candidate leaves
163
+ the previous approved snapshot running.
69
164
 
70
165
  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).
166
+ in that route, trusted or sandboxed alike. A sandboxed route's middleware
167
+ sources and their full dependency graph are included in the approval digest,
168
+ as before; a trusted route's own entry-file source is included too, so
169
+ changing that file's content invalidates the grant, but a change to a helper
170
+ module it merely imports does not by itself (see function-sources.ts's
171
+ `collectTrustedSources`) a known, documented gap versus the sandboxed path's
172
+ full dependency-graph hashing: a trusted route's grant scope is entry-file-only,
173
+ not transitive. Either way, code can include any granted data
174
+ in its HTTP response: neither the sandbox nor the trusted default promises
175
+ secrecy from code that was explicitly authorized to read a value. Minimize
176
+ grants, use scoped/short-lived credentials and revoke/restart when needed.
177
+ Other routes get none of that context.
86
178
 
87
179
  ## Next capability work
88
180
 
@@ -90,9 +182,14 @@ Outbound requests need a host-owned broker with explicit destination/method
90
182
  allowlists, private/metadata/loopback-address restrictions, DNS/rebinding defenses,
91
183
  redirect revalidation, deadlines and byte/concurrency limits. Application YAML
92
184
  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.
185
+ Until such brokers are implemented and tested, these capabilities are unavailable
186
+ to a *sandboxed* route. Provider adapters must preserve a `sandbox: true`
187
+ route's isolation or reject deployment; they cannot silently downgrade a
188
+ route that explicitly asked for the sandbox into unrestricted Node execution.
189
+ (A trusted route, by contrast, already has unrestricted Node execution by
190
+ design on the self-hosted target — see "What the trusted default can and
191
+ can't do" above; non-Node targets refuse `function`/`middleware` entirely,
192
+ trusted or sandboxed, since neither execution mode exists there.)
96
193
 
97
194
  ## Verification and remaining risk
98
195
 
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