@jimhoyd/urlcode 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CONTRIBUTING.md +80 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +20 -0
  4. package/README.md +342 -0
  5. package/ROADMAP.md +248 -0
  6. package/SECURITY.md +40 -0
  7. package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
  8. package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
  9. package/data/agents/ai-crawlers.json +1240 -0
  10. package/data/agents/crawlers.json +10515 -0
  11. package/data/agents/index.d.ts +3 -0
  12. package/data/agents/index.js +2453 -0
  13. package/data/agents/monitoring.json +1520 -0
  14. package/data/agents/seo.json +3865 -0
  15. package/dist/BUILD-MANIFEST.json +60 -0
  16. package/dist/adapters.js +59 -0
  17. package/dist/agent-lists.js +59 -0
  18. package/dist/assets.js +129 -0
  19. package/dist/authoring.js +68 -0
  20. package/dist/aws.js +110 -0
  21. package/dist/build-cloudflare.js +164 -0
  22. package/dist/cli.js +185 -0
  23. package/dist/client-address.js +85 -0
  24. package/dist/cloudflare.js +165 -0
  25. package/dist/compliance-rules/baseline.js +98 -0
  26. package/dist/compliance-rules/privacy.js +38 -0
  27. package/dist/compliance-rules/shared.js +62 -0
  28. package/dist/compliance-rules/strict.js +61 -0
  29. package/dist/compliance.js +219 -0
  30. package/dist/config-worker.js +9 -0
  31. package/dist/config.js +172 -0
  32. package/dist/errors.js +8 -0
  33. package/dist/function-sources.js +54 -0
  34. package/dist/function-worker.js +105 -0
  35. package/dist/functions.js +174 -0
  36. package/dist/guest-api.js +114 -0
  37. package/dist/header-validation.js +18 -0
  38. package/dist/http-policy.js +74 -0
  39. package/dist/http-response.js +84 -0
  40. package/dist/index.js +11 -0
  41. package/dist/link-api.js +136 -0
  42. package/dist/link-cli.js +141 -0
  43. package/dist/link-events.js +76 -0
  44. package/dist/link-records.js +31 -0
  45. package/dist/link-store-worker.js +150 -0
  46. package/dist/link-store.js +250 -0
  47. package/dist/logging.js +22 -0
  48. package/dist/management-policy.js +41 -0
  49. package/dist/match.js +124 -0
  50. package/dist/observability.js +242 -0
  51. package/dist/plugins.js +74 -0
  52. package/dist/policies/agents.js +248 -0
  53. package/dist/policies/cache.js +297 -0
  54. package/dist/policies/compression.js +187 -0
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies/throttle.js +131 -0
  57. package/dist/policies.js +142 -0
  58. package/dist/policy.js +55 -0
  59. package/dist/prerender.js +165 -0
  60. package/dist/project-tests.js +23 -0
  61. package/dist/readiness.js +216 -0
  62. package/dist/router.js +166 -0
  63. package/dist/runtime.js +244 -0
  64. package/dist/scaffold.js +0 -0
  65. package/dist/scripts/operational-drills.js +75 -0
  66. package/dist/server.js +270 -0
  67. package/dist/site.js +0 -0
  68. package/dist/sqlite-version.js +6 -0
  69. package/dist/types/adapters.d.ts +12 -0
  70. package/dist/types/agent-lists.d.ts +5 -0
  71. package/dist/types/assets.d.ts +13 -0
  72. package/dist/types/authoring.d.ts +2 -0
  73. package/dist/types/aws.d.ts +35 -0
  74. package/dist/types/build-cloudflare.d.ts +14 -0
  75. package/dist/types/cli.d.ts +2 -0
  76. package/dist/types/client-address.d.ts +9 -0
  77. package/dist/types/cloudflare.d.ts +75 -0
  78. package/dist/types/compliance-rules/baseline.d.ts +12 -0
  79. package/dist/types/compliance-rules/privacy.d.ts +6 -0
  80. package/dist/types/compliance-rules/shared.d.ts +24 -0
  81. package/dist/types/compliance-rules/strict.d.ts +9 -0
  82. package/dist/types/compliance.d.ts +119 -0
  83. package/dist/types/config-worker.d.ts +1 -0
  84. package/dist/types/config.d.ts +21 -0
  85. package/dist/types/errors.d.ts +7 -0
  86. package/dist/types/function-sources.d.ts +21 -0
  87. package/dist/types/function-worker.d.ts +1 -0
  88. package/dist/types/functions.d.ts +92 -0
  89. package/dist/types/guest-api.d.ts +16 -0
  90. package/dist/types/header-validation.d.ts +2 -0
  91. package/dist/types/http-policy.d.ts +39 -0
  92. package/dist/types/http-response.d.ts +43 -0
  93. package/dist/types/index.d.ts +11 -0
  94. package/dist/types/link-api.d.ts +30 -0
  95. package/dist/types/link-cli.d.ts +36 -0
  96. package/dist/types/link-events.d.ts +27 -0
  97. package/dist/types/link-records.d.ts +11 -0
  98. package/dist/types/link-store-worker.d.ts +1 -0
  99. package/dist/types/link-store.d.ts +130 -0
  100. package/dist/types/logging.d.ts +9 -0
  101. package/dist/types/management-policy.d.ts +9 -0
  102. package/dist/types/match.d.ts +81 -0
  103. package/dist/types/observability.d.ts +104 -0
  104. package/dist/types/plugins.d.ts +27 -0
  105. package/dist/types/policies/agents.d.ts +86 -0
  106. package/dist/types/policies/cache.d.ts +82 -0
  107. package/dist/types/policies/compression.d.ts +41 -0
  108. package/dist/types/policies/security.d.ts +37 -0
  109. package/dist/types/policies/throttle.d.ts +55 -0
  110. package/dist/types/policies.d.ts +36 -0
  111. package/dist/types/policy.d.ts +21 -0
  112. package/dist/types/prerender.d.ts +31 -0
  113. package/dist/types/project-tests.d.ts +13 -0
  114. package/dist/types/readiness.d.ts +121 -0
  115. package/dist/types/router.d.ts +11 -0
  116. package/dist/types/runtime.d.ts +91 -0
  117. package/dist/types/scaffold.d.ts +17 -0
  118. package/dist/types/server.d.ts +37 -0
  119. package/dist/types/site.d.ts +12 -0
  120. package/dist/types/sqlite-version.d.ts +1 -0
  121. package/dist/types/types.d.ts +332 -0
  122. package/dist/types/vercel.d.ts +12 -0
  123. package/dist/types.js +177 -0
  124. package/dist/vercel.js +65 -0
  125. package/docs/AI-AUTHORING.md +113 -0
  126. package/docs/ASSETS.md +106 -0
  127. package/docs/AWS.md +77 -0
  128. package/docs/BEST-PRACTICES.md +267 -0
  129. package/docs/CAPACITY.md +208 -0
  130. package/docs/CLOUDFLARE.md +110 -0
  131. package/docs/COMPLIANCE.md +242 -0
  132. package/docs/DYNAMIC-LINKS.md +561 -0
  133. package/docs/FUNCTION-SECURITY.md +113 -0
  134. package/docs/HTTP.md +129 -0
  135. package/docs/INSTALL.md +90 -0
  136. package/docs/LOAD-TESTING.md +91 -0
  137. package/docs/LOCAL-DEVELOPMENT.md +99 -0
  138. package/docs/MANAGEMENT-SECURITY.md +82 -0
  139. package/docs/MIDDLEWARE.md +83 -0
  140. package/docs/MONITORING.md +132 -0
  141. package/docs/OBSERVABILITY.md +229 -0
  142. package/docs/OPERATIONAL-PROOF.md +42 -0
  143. package/docs/OPERATIONS.md +207 -0
  144. package/docs/ORGANIZATION.md +135 -0
  145. package/docs/PERFORMANCE.md +72 -0
  146. package/docs/PLUGINS.md +234 -0
  147. package/docs/POLICIES.md +489 -0
  148. package/docs/PRERENDER.md +193 -0
  149. package/docs/PROJECT-DIRECTION.md +51 -0
  150. package/docs/READINESS.md +125 -0
  151. package/docs/README.md +58 -0
  152. package/docs/RELEASE-READINESS.md +95 -0
  153. package/docs/RELEASE-SECURITY.md +72 -0
  154. package/docs/RESILIENCE.md +160 -0
  155. package/docs/ROUTING.md +95 -0
  156. package/docs/SANDBOX-REVIEW.md +59 -0
  157. package/docs/SCAFFOLDING.md +72 -0
  158. package/docs/SECURITY-AUDIT.md +124 -0
  159. package/docs/SITE.md +150 -0
  160. package/docs/SPECIFICATION.md +232 -0
  161. package/docs/SPIKE-EXTENSIONS.md +475 -0
  162. package/docs/STANDARDS.md +303 -0
  163. package/docs/STARTERS.md +51 -0
  164. package/docs/TUNNELS.md +75 -0
  165. package/docs/TYPESCRIPT.md +102 -0
  166. package/docs/VERCEL.md +107 -0
  167. package/docs/YAML-GUIDE.md +521 -0
  168. package/docs/YAML-REFERENCE.md +328 -0
  169. package/docs/policies/agents.md +182 -0
  170. package/docs/policies/cache.md +139 -0
  171. package/docs/policies/compression.md +168 -0
  172. package/docs/policies/security.md +160 -0
  173. package/docs/policies/throttle.md +103 -0
  174. package/examples/assets/.env.example +1 -0
  175. package/examples/assets/.gitattributes +2 -0
  176. package/examples/assets/Makefile +30 -0
  177. package/examples/assets/README.md +18 -0
  178. package/examples/assets/functions/hello.mjs +3 -0
  179. package/examples/assets/gitignore.template +7 -0
  180. package/examples/assets/public/about.html +2 -0
  181. package/examples/assets/public/assets/example.txt +1 -0
  182. package/examples/assets/public/guide.txt +1 -0
  183. package/examples/assets/tests/requests.json +44 -0
  184. package/examples/assets/urlcode.yaml +27 -0
  185. package/examples/aws/README.md +13 -0
  186. package/examples/aws/handler.mjs +5 -0
  187. package/examples/aws/public/index.html +3 -0
  188. package/examples/aws/public/notes.txt +1 -0
  189. package/examples/aws/template.yaml +33 -0
  190. package/examples/aws/tests/requests.json +9 -0
  191. package/examples/aws/urlcode.yaml +19 -0
  192. package/examples/cloudflare/README.md +18 -0
  193. package/examples/cloudflare/package.json +14 -0
  194. package/examples/cloudflare/tests/requests.json +9 -0
  195. package/examples/cloudflare/urlcode.yaml +24 -0
  196. package/examples/cloudflare/wrangler.toml +5 -0
  197. package/examples/compliance/README.md +20 -0
  198. package/examples/compliance/rules.mjs +65 -0
  199. package/examples/cookbook/README.md +21 -0
  200. package/examples/cookbook/functions/choice.mjs +4 -0
  201. package/examples/cookbook/functions/echo.mjs +3 -0
  202. package/examples/cookbook/functions/hello.mjs +3 -0
  203. package/examples/cookbook/functions/text.mjs +3 -0
  204. package/examples/cookbook/middleware/headers.mjs +6 -0
  205. package/examples/cookbook/public/about.html +1 -0
  206. package/examples/cookbook/public/assets/index.html +1 -0
  207. package/examples/cookbook/public/assets/site.css +1 -0
  208. package/examples/cookbook/public/favicon.svg +1 -0
  209. package/examples/cookbook/public/guide.txt +1 -0
  210. package/examples/cookbook/public/llms.txt +6 -0
  211. package/examples/cookbook/routes/code.yaml +55 -0
  212. package/examples/cookbook/routes/files.yaml +17 -0
  213. package/examples/cookbook/routes/policies.yaml +28 -0
  214. package/examples/cookbook/routes/redirects.yaml +38 -0
  215. package/examples/cookbook/routes/responses.yaml +26 -0
  216. package/examples/cookbook/tests/requests.json +254 -0
  217. package/examples/cookbook/urlcode.yaml +22 -0
  218. package/examples/live-links/README.md +11 -0
  219. package/examples/live-links/tests/requests.json +6 -0
  220. package/examples/live-links/urlcode.yaml +16 -0
  221. package/examples/monitoring/blackbox-jobs.yaml +37 -0
  222. package/examples/monitoring/prometheus-rules.yaml +88 -0
  223. package/examples/monitoring/prometheus-scrape.yaml +20 -0
  224. package/examples/monitoring/vector.toml +65 -0
  225. package/examples/prerender/README.md +48 -0
  226. package/examples/prerender/functions/page.mjs +6 -0
  227. package/examples/prerender/middleware/template.mjs +29 -0
  228. package/examples/prerender/prerender.d.mts +4 -0
  229. package/examples/prerender/prerender.mjs +63 -0
  230. package/examples/prerender/tests/requests.json +39 -0
  231. package/examples/prerender/urlcode.yaml +50 -0
  232. package/examples/tunnel/dev-with-ngrok.sh +61 -0
  233. package/examples/vercel/README.md +13 -0
  234. package/examples/vercel/api/index.js +5 -0
  235. package/examples/vercel/package.json +8 -0
  236. package/examples/vercel/public/index.html +3 -0
  237. package/examples/vercel/public/notes.txt +1 -0
  238. package/examples/vercel/tests/requests.json +48 -0
  239. package/examples/vercel/urlcode.yaml +19 -0
  240. package/examples/vercel/vercel.json +10 -0
  241. package/llms.txt +46 -0
  242. package/package.json +123 -0
  243. package/schemas/urlcode.schema.json +1230 -0
  244. package/starters/default/.gitattributes +1 -0
  245. package/starters/default/Makefile +30 -0
  246. package/starters/default/README.md +34 -0
  247. package/starters/default/functions/hello.mjs +3 -0
  248. package/starters/default/gitignore.template +8 -0
  249. package/starters/default/middleware/headers.mjs +6 -0
  250. package/starters/default/routes/functions.yaml +20 -0
  251. package/starters/default/routes/marketing/links.yaml +7 -0
  252. package/starters/default/starter.json +5 -0
  253. package/starters/default/tests/requests.json +56 -0
  254. package/starters/default/urlcode.yaml +8 -0
@@ -0,0 +1,113 @@
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
9
+
10
+ - Function sources are parsed/snapshotted without importing them into Node.
11
+ - Code runs in QuickJS inside WebAssembly, with no host JS functions/objects
12
+ exposed to the guest. Request/response/context use a JSON/string boundary.
13
+ - No `process`, `require`, Node built-ins, filesystem, shell, sockets, fetch,
14
+ WebSocket, workers, native extensions or ambient environment is available.
15
+ - Module resolution is restricted to the route's declared middleware and function relative JavaScript
16
+ dependency graphs inside the project. Symlink escapes, remote/bare imports and
17
+ dynamic imports in source fail. Runtime-created imports cannot broaden access.
18
+ - A fresh guest heap/module state per invocation prevents state crossing requests.
19
+ - 32 MiB guest heap, 512 KiB stack, source/input/output/header limits, bounded
20
+ concurrency, guest interruption and an independent worker termination deadline.
21
+ - External bindings are denied by default. Project YAML cannot self-authorize.
22
+ Operator grants are exact-name, route-scoped and pinned to configuration/source.
23
+
24
+ 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.
28
+
29
+ ## Granting selected bindings
30
+
31
+ An application may request a named binding in YAML, but only an operator can
32
+ approve it. Inspect what the app requests without executing any module:
33
+
34
+ ```sh
35
+ urlcode permissions --project /srv/my-links
36
+ ```
37
+
38
+ This prints a proposed JSON shape with `version: 1`, `projectSha256` and `routes`.
39
+ It grants nothing. Review the code/configuration and keep only necessary bindings.
40
+ Save the policy **outside the application checkout**, in an operator-controlled
41
+ file; never let application authors or deployment artifacts overwrite it.
42
+
43
+ ```json
44
+ {
45
+ "version": 1,
46
+ "projectSha256": "REPLACE_WITH_THE_REVIEWED_PROJECT_DIGEST",
47
+ "routes": {
48
+ "/customer/{id}": {
49
+ "env": ["API_MODE"],
50
+ "secrets": ["customer_api_key"]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ The placeholder deliberately does not validate. Use the actual digest produced
57
+ by inspection. Then, with values securely injected into the process:
58
+
59
+ ```sh
60
+ urlcode validate --project /srv/my-links --policy /etc/urlcode/my-links-policy.json
61
+ urlcode serve --project /srv/my-links --policy /etc/urlcode/my-links-policy.json
62
+ ```
63
+
64
+ `dev`, `test` and `validate --local` use the same policy rules even for `.env.local`.
65
+ 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.
69
+
70
+ 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).
86
+
87
+ ## Next capability work
88
+
89
+ Outbound requests need a host-owned broker with explicit destination/method
90
+ allowlists, private/metadata/loopback-address restrictions, DNS/rebinding defenses,
91
+ redirect revalidation, deadlines and byte/concurrency limits. Application YAML
92
+ 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.
96
+
97
+ ## Verification and remaining risk
98
+
99
+ Tests attempt constructor/eval escapes, Node/filesystem/shell/network imports,
100
+ runtime-created imports, cross-request prototype/state pollution, oversized
101
+ allocations, loops, unauthorized secret requests and stale/repo-local policies.
102
+ These are regression tests, not a proof of complete security.
103
+
104
+ The URLCode host, parser, QuickJS/WASM engine, native runtime and dependencies
105
+ remain trusted computing components that need patching and review. Guest heap
106
+ limits do not cap all host/WASM RSS; use OS/container memory/CPU/PID limits as an
107
+ additional layer. Native engine bugs or resource exhaustion remain residual risks.
108
+ For a public arbitrary-code/multi-tenant service, require independent security
109
+ review plus process/VM-level isolation and operational controls before launch.
110
+ Do not advertise this release as an audited hostile multi-tenant hosting platform.
111
+
112
+ Implementation references: [QuickJS/WASM project](https://github.com/justjake/quickjs-emscripten)
113
+ and its [runtime isolation/limits API](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/QuickJSRuntime.md).
package/docs/HTTP.md ADDED
@@ -0,0 +1,129 @@
1
+ # HTTP request and response configuration
2
+
3
+ Implemented in alpha.4. This is a documented HTTP subset, not a promise that every
4
+ HTTP feature is configurable. It builds on [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html).
5
+
6
+ ```yaml
7
+ version: "1"
8
+ routes:
9
+ /echo:
10
+ methods: [POST]
11
+ request:
12
+ body:
13
+ required: true
14
+ maxBytes: 16384
15
+ contentTypes: [application/json]
16
+ format: json
17
+ function:
18
+ source: functions/echo.mjs
19
+ response:
20
+ headers:
21
+ Cache-Control: no-store
22
+ X-App: my-links
23
+ /go:
24
+ redirect:
25
+ url: https://example.com
26
+ status: 302
27
+ response:
28
+ headers:
29
+ Cache-Control: public, max-age=60
30
+ /status:
31
+ respond:
32
+ status: 200
33
+ json: {ok: true}
34
+ ```
35
+
36
+ `functions/echo.mjs`:
37
+
38
+ ```js
39
+ export default async function echo(request) {
40
+ return Response.json(await request.json());
41
+ }
42
+ ```
43
+
44
+ ## Requests
45
+
46
+ Keep ordinary routes short: omit `methods` to accept GET and HEAD. Set
47
+ `methods: [POST]` for a POST-only handler, or `methods: [GET, HEAD, POST]` for all
48
+ three. Use uppercase method names. Explicit lists replace the defaults; GET does
49
+ not implicitly add HEAD when a list is supplied. The schema advertises the same
50
+ default as the runtime. No declaration is needed for the default 302 redirect
51
+ status or default `Cache-Control: no-store` on functions/redirects.
52
+
53
+
54
+ | Field | Behavior |
55
+ |---|---|
56
+ | `methods` | Allowed methods, default GET/HEAD; exact lists, 405 plus Allow on mismatch |
57
+ | `parameters` | Required/defaulted/typed path, query and header inputs; see the specification |
58
+ | `request.body.required` | Reject an empty body with 400; default false |
59
+ | `request.body.maxBytes` | 0–1048576; tighter per-route budget, enforced while reading fixed/chunked bodies; 413 on overflow |
60
+ | `request.body.contentTypes` | Exact lowercase MIME essences for nonempty bodies; parameters ignored; mismatch/missing type returns 415 |
61
+ | `request.body.format` | `text`: validate UTF-8; `json`: validate UTF-8, JSON media type and JSON syntax; malformed input returns 400 |
62
+
63
+ The operator request limit remains an upper bound; YAML cannot raise it. A route
64
+ without body policy keeps the existing server limit. A configured body policy
65
+ rejects nonidentity Content-Encoding for nonempty bodies; no automatic decompression.
66
+ Empty optional bodies skip media/format checks. Inputs are validated before the
67
+ handler; the original body remains available through function `request.text()` or
68
+ `request.json()`. No YAML body interpolation or automatic argument binding.
69
+ Request header inputs use `parameters` with `in: header`; this is validation,
70
+ not arbitrary modification or forwarding of the incoming request.
71
+
72
+ ## Responses
73
+
74
+ `response.headers` maps HTTP names to literal strings. Names are case insensitive;
75
+ duplicate spellings and invalid names/control characters fail activation. YAML
76
+ values replace the same handler headers, including all prior Set-Cookie values.
77
+ Only `Set-Cookie` accepts a list, producing separate header lines:
78
+
79
+ ```yaml
80
+ response:
81
+ headers:
82
+ Cache-Control: no-store
83
+ Set-Cookie:
84
+ - "theme=light; Path=/; SameSite=Lax; Secure"
85
+ - "notice=seen; Path=/; HttpOnly; SameSite=Lax; Secure"
86
+ ```
87
+
88
+ Use functions for dynamic cookies; never commit session credentials or secret
89
+ values into header literals. Header configuration applies to handler responses,
90
+ including declared error statuses, but not runtime validation/errors (400, 404,
91
+ 405, 413, 415, 500, etc.). Defaults remain `no-store`, `nosniff` and a request ID.
92
+ Header policy is bounded to 64 keys/16 KiB; merged function headers remain bounded.
93
+
94
+ Framing, hop-by-hop headers, Location, Allow, range/cache validators,
95
+ Content-Encoding, X-Request-ID and X-Content-Type-Options are reserved to the
96
+ runtime/handler. Configure redirect URLs/status on `redirect`; asset content type,
97
+ cache and disposition on its own handler. Asset metadata cannot be overridden by
98
+ `response.headers`. On functions/declared responses, Content-Type may be configured;
99
+ JSON declarations require a JSON type. No response header secret interpolation.
100
+
101
+ `respond` is an additional native handler (exactly one handler per route):
102
+
103
+ - `status`: 200–599, default 200; 206 and 304 are reserved for native asset semantics.
104
+ - `text`: literal UTF-8 body, default content type text/plain.
105
+ - `json`: any JSON-compatible YAML value, serialized with application/json.
106
+ - Omit both for an empty body; declaring both fails. Body limit is 1 MiB.
107
+ - Status 204/205 cannot declare a nonempty body. HEAD always suppresses the body.
108
+
109
+ Functions still return their own Response/status/body. YAML header policy does
110
+ not replace function status/body. Asset handlers retain conditional/HEAD/range
111
+ behavior described in [assets](ASSETS.md). Use OPTIONS explicitly if you need a
112
+ declared response; merely adding a header does not implement CORS preflight.
113
+
114
+ ## Still outside this contract
115
+
116
+ Automatic CORS/preflight policy, cookie parsing/signing, authentication, body JSON
117
+ Schema validation, multipart/file uploads, streaming, content negotiation,
118
+ WebSocket upgrades and proxies are not implemented. Do not advertise these as
119
+ supported just because raw headers can be declared. Compression negotiation,
120
+ security-header profiles, per-client throttling, User-Agent policy and HTTP
121
+ caching strategies exist only as optional, off-by-default
122
+ [policies](POLICIES.md); a project that declares none keeps the identity-only
123
+ behavior described here, and YAML `response.headers` beat any header a policy
124
+ would add. Future features need their own portable semantics and tests; unknown
125
+ YAML fields fail.
126
+
127
+ Middleware runs after route/method/input/body validation and before YAML response
128
+ header overrides. See [middleware](MIDDLEWARE.md) for ordering and native body
129
+ preservation rules.
@@ -0,0 +1,90 @@
1
+ # Installing URLCode
2
+
3
+ URLCode is a Node CLI. Every channel below installs the same published tarball;
4
+ pick whichever fits how you already manage tools. All of them require
5
+ **Node.js 22.13 or newer**. The tarball ships plain JavaScript built from the
6
+ TypeScript source (`dist/`, with declarations; see [TypeScript](TYPESCRIPT.md)),
7
+ so the installed `urlcode` command runs `dist/cli.js` and needs no build tool.
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
+ ## npm
15
+
16
+ ```sh
17
+ npm install --global urlcode
18
+ urlcode --help
19
+ ```
20
+
21
+ Project-local, which is what an application repository should normally pin:
22
+
23
+ ```sh
24
+ npm install --save-dev urlcode
25
+ npx urlcode validate
26
+ ```
27
+
28
+ ## Homebrew
29
+
30
+ ```sh
31
+ brew tap jimhoyd-com/urlcode
32
+ brew install urlcode
33
+ ```
34
+
35
+ The tap's formula is generated from the published tarball for each release and
36
+ attached to the GitHub release as `urlcode.rb`.
37
+
38
+ ## Install script
39
+
40
+ ```sh
41
+ curl -fsSL https://raw.githubusercontent.com/jimhoyd-com/urlcode/main/install.sh | sh
42
+ ```
43
+
44
+ It downloads the release tarball, verifies its SHA-256 against the release's
45
+ `SHA256SUMS`, and installs with npm. Options:
46
+
47
+ ```sh
48
+ curl -fsSL .../install.sh | sh -s -- --version 0.2.0 --prefix "$HOME/.local"
49
+ ```
50
+
51
+ `--prefix` avoids needing privileges for a global npm directory; add
52
+ `$PREFIX/bin` to `PATH`. Piping a script into a shell means trusting the source
53
+ for that moment: to inspect first, download it, read it, then run it.
54
+
55
+ ## Container
56
+
57
+ ```sh
58
+ docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" ghcr.io/jimhoyd-com/urlcode:0.2.0 \
59
+ serve --project /project --host 0.0.0.0
60
+ ```
61
+
62
+ The image runs the same built runtime, `node /opt/urlcode/dist/cli.js`, as its
63
+ entry point. Pin the digest rather than a tag for a deployment, and give the
64
+ container its own resource limits. See [operations](OPERATIONS.md).
65
+
66
+ ## From source
67
+
68
+ ```sh
69
+ git clone https://github.com/jimhoyd-com/urlcode.git
70
+ cd urlcode
71
+ make dev
72
+ ```
73
+
74
+ A clone runs the TypeScript source directly (`node src/cli.ts`, Node 22.18+),
75
+ with no build step; see [local development](LOCAL-DEVELOPMENT.md).
76
+
77
+ ## Verify what you installed
78
+
79
+ Releases carry Sigstore provenance signed by the release workflow. Before
80
+ trusting a downloaded artifact:
81
+
82
+ ```sh
83
+ gh attestation verify urlcode-0.2.0.tgz --repo jimhoyd-com/urlcode \
84
+ --signer-workflow jimhoyd-com/urlcode/.github/workflows/release.yml
85
+ ```
86
+
87
+ A signature establishes where an artifact came from. It is not a statement that
88
+ the release is safe for your workload, and it is not a production-readiness
89
+ claim; see [release readiness](RELEASE-READINESS.md) and
90
+ [release security](RELEASE-SECURITY.md).
@@ -0,0 +1,91 @@
1
+ # Load testing a deployment
2
+
3
+ `urlcode benchmark` sends a project's own workload at a running runtime and
4
+ reports throughput, tail latency and how many requests were refused. It answers
5
+ one question — *does this deployment meet its budget under this load* — and
6
+ nothing else. It is not a capacity model, not a soak harness and not a
7
+ substitute for the drills in [release readiness](RELEASE-READINESS.md).
8
+
9
+ ## Measure the deployment, not a fresh local snapshot
10
+
11
+ By default the command starts a private runtime and loads that. Useful for a
12
+ regression budget in CI; useless for judging a deployment, because it bypasses
13
+ your TLS termination, proxy, network and host.
14
+
15
+ `--target` sends the same workload at something already running:
16
+
17
+ ```sh
18
+ urlcode benchmark --project . --target https://links.example \
19
+ --requests 5000 --concurrency 8 --warmup 100 --max-p95-ms 50
20
+ ```
21
+
22
+ The project is still required: it supplies the routes and fixtures that make up
23
+ the workload. Load goes to the target; the local runtime is only consulted for
24
+ the plan. `rssMiB` is `null` in this mode, because the memory of the machine
25
+ running the benchmark says nothing about the machine under test.
26
+
27
+ Point `--target` only at systems you operate. A load generator aimed at someone
28
+ else's host is an attack, whatever the intent.
29
+
30
+ ## Warm up before measuring
31
+
32
+ `--warmup N` sends and discards N requests first. A cold asset snapshot, an
33
+ empty connection pool and a just-started function worker are not what a latency
34
+ budget is about. Warm-up traffic reaches the deployment and is excluded from
35
+ every statistic; `warmupRequests` records how many.
36
+
37
+ ## Reading the report
38
+
39
+ | Field | Meaning |
40
+ |---|---|
41
+ | `pass` | Every request completed, none failed, and `p95Ms` met `--max-p95-ms` if given. |
42
+ | `completed` / `complete` | How many requests ran. Short of `requested` means the `--seconds` budget ended the run first — the numbers describe a shorter run than you asked for. |
43
+ | `failed` | Responses that did not match the expectation for that case. |
44
+ | `transportErrors` | Connections that never produced a response: refused, reset or timed out. Distinct from a deployment deliberately refusing work. |
45
+ | `shedResponses` | 503 and 504 responses — admission, function-pool capacity or a deadline. Not errors so much as the runtime protecting itself. |
46
+ | `p50Ms` / `p95Ms` / `p99Ms` | Latency percentiles over completed requests. |
47
+ | `statuses` | Full status histogram, so a "pass" that is secretly all redirects is visible. |
48
+ | `rssMiB` | Local mode only. |
49
+
50
+ ## A worked example
51
+
52
+ Against the default starter, which has one function route:
53
+
54
+ ```
55
+ $ urlcode serve --project starters/default --port 3456
56
+ $ urlcode benchmark --project starters/default --target http://127.0.0.1:3456 \
57
+ --requests 60 --warmup 10 --concurrency 4
58
+ pass: false failed: 6 shedResponses: 6 p95Ms: 6.7
59
+ ```
60
+
61
+ Six of sixty requests were shed. Not a bug: `serve` runs **2 function workers**
62
+ by default, so a concurrency of 4 exceeds the pool and the runtime returns 503
63
+ rather than queueing without bound. Raising the pool:
64
+
65
+ ```
66
+ $ urlcode serve --project starters/default --port 3457 --workers 8
67
+ $ urlcode benchmark ... --concurrency 4
68
+ pass: true failed: 0 shedResponses: 0 statuses: {"200":24,"302":36}
69
+ ```
70
+
71
+ That is the loop this tool exists for: measure, read `shedResponses`, tune the
72
+ [capacity controls](CAPACITY.md), measure again. More workers cost memory and
73
+ CPU; the right number is the one your workload and host justify, not the
74
+ largest one that makes a number go green.
75
+
76
+ ## What a passing run does not prove
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.
80
+ - **Redirects are not followed**, so a redirect's destination is never loaded.
81
+ - **One client, one host, no slow peers.** Tail latency under adversarial
82
+ clients, connection churn or packet loss is not measured.
83
+ - **Not a soak.** `--seconds` caps at 300. Memory drift, file-descriptor leaks
84
+ and log-volume growth need a long run watched through
85
+ [monitoring](MONITORING.md).
86
+ - **A number from one environment is not a claim about another.** Record the
87
+ runtime and application revisions, host, and command with any figure you keep.
88
+
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.
@@ -0,0 +1,99 @@
1
+ # Local development
2
+
3
+ Use Node.js 22.18+ and npm (CI targets Node 22, 24 and 26). The runtime is
4
+ written in TypeScript and runs from source with no build step: `npm run dev`
5
+ is `node src/cli.ts`, which Node runs through its own type stripping (that is
6
+ why a contributor needs 22.18, while an installed package still runs on 22.13).
7
+ `npm run typecheck` is the type gate and part of `npm run verify`.
8
+ `npm run build` emits the JavaScript in `dist/` that the package and container
9
+ ship, together with the declarations; `dist` is never committed. Make is an optional
10
+ shortcut layer; npm and the CLI work on Windows, macOS and Linux. No global
11
+ package install, hosting account, database or Docker is needed for the local loop.
12
+
13
+ ## Try the runtime
14
+
15
+ From the runtime checkout, `make dev` installs locked dependencies if needed and
16
+ starts the function/redirect starter at http://127.0.0.1:3000. Without Make, run `npm ci`
17
+ once, then `npm run dev`. Dependency installation requires npm registry access;
18
+ the examples themselves work locally.
19
+
20
+ Try `/hello/Ada` (sandboxed function) and `/go` (redirect).
21
+ For pages/files/downloads, run `make dev PROJECT=examples/assets` instead. Edit the files in
22
+ `starters/default/` to experiment. `dev` watches configuration, source and assets;
23
+ invalid edits leave the last valid snapshot running. Ctrl+C drains and stops it.
24
+ Runtime source changes under `src/` require restarting the dev command; project
25
+ reload is not a runtime-code watcher.
26
+
27
+ ## Own an application
28
+
29
+ Run `make init DEST=../my-links`, then
30
+ `make dev PROJECT=../my-links`. The CLI equivalents from the runtime checkout:
31
+
32
+ ```sh
33
+ npm run init -- ../my-links
34
+ npm run dev -- --project ../my-links
35
+ npm run validate -- --project ../my-links
36
+ npm run test:project -- --project ../my-links
37
+ ```
38
+
39
+ There is one starter, containing both examples. Initialization never overwrites
40
+ an existing directory. Once created, edits belong to your app repository; upgrading
41
+ the runtime does not regenerate them. Each starter has a Makefile for its own
42
+ `dev`, `serve`, `validate`, `test` and `doctor` commands. It uses an installed
43
+ `urlcode`, or an explicit runtime command:
44
+
45
+ ```sh
46
+ cd ../my-links
47
+ make dev URLCODE='node /path/to/urlcode/src/cli.ts'
48
+ # Without Make or a global install:
49
+ node /path/to/urlcode/src/cli.ts dev
50
+ ```
51
+
52
+ ## Command reference (runtime checkout)
53
+
54
+ | Make | npm | Purpose |
55
+ |---|---|---|
56
+ | `make setup` | `npm ci` | Install exact dependencies; replaces node_modules |
57
+ | `make dev` | `npm run dev` | Watched function/redirect starter, local dotenv |
58
+ | `make validate` | `npm run validate` | Validate the app and local bindings |
59
+ | `make test-project` | `npm run test:project` | App HTTP assertions, redirects not followed |
60
+ | `make test` | `npm test` | Runtime unit, HTTP and sandbox tests |
61
+ | — | `npm run typecheck` | Strict TypeScript check of runtime, scripts, tests and benchmarks |
62
+ | `make verify` | `npm run verify` | Lint, type check, syntax/JSON checks and runtime tests |
63
+ | — | `npm run build` | Emit `dist/` (stripped JavaScript and declarations); never committed |
64
+ | `make test-package` | `npm run test:package` | Actual archive install and starter tests; registry access |
65
+ | `make serve` | `npm run serve` | Fixed snapshot, no watcher or dotenv |
66
+ | `make doctor` | `npm run doctor` | Runtime/platform details |
67
+
68
+ Run `make help` for shortcuts. `PROJECT` defaults to `starters/default`; `HOST`
69
+ to `127.0.0.1`; `PORT` to `3000`. Quote paths containing spaces:
70
+
71
+ ```sh
72
+ make dev PROJECT="../my-links demo" PORT=3001
73
+ npm run dev -- --project "../my-links demo" --port 3001
74
+ ```
75
+
76
+ Make automatically runs `npm ci` when its dependency marker is missing or older
77
+ than package metadata. Use `make setup` after manually changing node_modules.
78
+ Do not run setup concurrently with a running dev server or tests.
79
+
80
+ ## Environment and troubleshooting
81
+
82
+ `.env.local` belongs in the selected app directory and is ignored by Git; process
83
+ environment values take precedence. No starter requires secret values. Do not
84
+ copy placeholder credentials into a working secret store. External env/secret
85
+ bindings still need an operator policy outside the app, pinned to its config/code.
86
+ Inspect and set it up using the [security guide](FUNCTION-SECURITY.md); pass it
87
+ through the CLI, for example `npm run dev -- --project ../my-links --policy /path/to/policy.json`.
88
+ Local convenience never bypasses the function sandbox or grants permissions.
89
+
90
+ - Port busy: change `PORT=3001` or pass `--port 3001` through npm.
91
+ - Missing Make: use the npm commands; Make is not a runtime dependency.
92
+ - Invalid edits: run validation for diagnostics; fix the project and the watcher retries.
93
+ - Changed runtime source: stop and restart dev, then `npm run typecheck`. Changed app source: reload is automatic.
94
+ - Missing dependency or wrong Node: check `node --version`, then `npm ci`.
95
+ - Need access from another device: explicitly use `HOST=0.0.0.0` or `--host 0.0.0.0`;
96
+ this exposes the development listener to your network. Loopback remains the default.
97
+
98
+ Production deployment uses `serve` behind the HTTPS setup described in
99
+ [operations](OPERATIONS.md). These shortcuts do not provision providers or select a license.
@@ -0,0 +1,82 @@
1
+ # Private management and durable mutation audit
2
+
3
+ Management binds only `127.0.0.1` or `::1`. Use an authenticated private tunnel
4
+ (e.g. SSH/VPN with loopback forwarding); never publish its port through a public
5
+ proxy or container port mapping. This is operator management, not an end-user
6
+ account service. Browser Origin requests remain rejected.
7
+
8
+ ## Individual credentials
9
+
10
+ Use `urlcode links api --project ./my-links --store /operator/links.sqlite
11
+ --auth-file /operator/management.json --host 127.0.0.1 --port 3001` (one line).
12
+ The policy is operator-owned, outside the application, at most 64 KiB and mode
13
+ 600 on POSIX. Protect its parent directory and apply equivalent Windows ACLs.
14
+ It is not YAML and must never be placed in the application or Git.
15
+
16
+ Generate each token with `node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"`
17
+ on a trusted operator machine. Deliver it through your secret manager. Hash the
18
+ raw token using SHA-256 without a newline; the server expects `Authorization:
19
+ Bearer <token>`. Tokens must contain 43–256 base64url characters. Never use a
20
+ human password as a token. This synthetic policy illustrates the shape; replace
21
+ the hash with the real token hash and choose a short operational expiry:
22
+
23
+ ```json
24
+ {
25
+ "version": 1,
26
+ "credentials": [{
27
+ "id": "operator-alice",
28
+ "sha256": "0000000000000000000000000000000000000000000000000000000000000000",
29
+ "expires": "2026-10-01T00:00:00Z",
30
+ "collections": ["links"],
31
+ "actions": ["get", "list", "create", "update", "delete"]
32
+ }]
33
+ }
34
+ ```
35
+
36
+ At most 128 credentials; unique IDs and hashes; explicit collection and action
37
+ allowlists with no wildcards. Readers should receive only `get` and `list`.
38
+ An expired, removed or `"revoked": true` credential gets 401; a valid credential
39
+ outside its scope gets 403. The entire policy is validated on every authenticated
40
+ request. Malformed/unreadable policy fails closed with 503, without retaining an
41
+ old permissive copy. Write a replacement file with mode 600 and atomically rename
42
+ it over the policy. No restart is needed. Requests already authorized may finish;
43
+ revocation does not cancel an in-flight transaction. Credential administration is
44
+ an operator filesystem action, never exposed through this HTTP API.
45
+
46
+ Legacy `--token-file` remains available for local compatibility, with full access
47
+ to that listener's collection and actor `legacy-shared`. Prefer `--auth-file` for
48
+ attribution, expiry and revocation. They are mutually exclusive. These bearer
49
+ credentials do not establish verified human identity, MFA, SSO or session login.
50
+ Host administrators and custom embedding code remain trusted operators.
51
+
52
+ ## Audit durability and recovery
53
+
54
+ The built-in SQLite writer creates an additive `urlcode_link_audit` table when
55
+ opening a store. Every successful create/update/delete (including CLI and embedded
56
+ store calls) records revision, UTC timestamp, actor, request ID, collection,
57
+ action and SHA-256 of the short code in the **same transaction** as the mutation.
58
+ The default local CLI actor is `local-operator`. Scoped HTTP management requires
59
+ an adapter explicitly declaring atomic audit support; the built-in store provides it.
60
+
61
+ An audit insert failure rolls back both mutation and revision. Conditional-write
62
+ conflicts and rejected requests produce no successful-mutation row. HTTP request
63
+ logs still describe failed/aborted requests and remain best effort. A client timeout
64
+ may follow a committed transaction: reconcile the revision and request ID before
65
+ retrying. A lost HTTP reply is not evidence that the transaction was rolled back.
66
+
67
+ Inspect the journal with an operator-only SQLite client, for example:
68
+
69
+ ```sql
70
+ SELECT revision, timestamp, actor, request_id, collection, action, code_sha256
71
+ FROM urlcode_link_audit WHERE revision > 0 ORDER BY revision LIMIT 100;
72
+ ```
73
+
74
+ No token, URL, request body or raw short code is stored in the journal. Code hashes
75
+ are correlatable and guessable for short codes; restrict access accordingly.
76
+ This is a durable local transaction journal, **not tamper-evident external storage**.
77
+ An administrator with database access can change it. Export ordered revisions to
78
+ your protected collector/backup system and monitor lag, gaps and disk/WAL growth.
79
+ There is no automatic pruning: define retention and verify archival before any
80
+ operator purge. Keep audit records in backups. User_version remains 1 because
81
+ this table is additive; older readers work, but older writers do not emit audit
82
+ rows. Never roll a management writer back to an unaudited revision.