@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.1

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 (326) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +106 -0
  2. package/.claude/skills/urlcode-operations/SKILL.md +114 -0
  3. package/.claude-plugin/marketplace.json +18 -0
  4. package/CONTRIBUTING.md +30 -2
  5. package/README.md +157 -230
  6. package/ROADMAP.md +85 -2
  7. package/SECURITY.md +8 -1
  8. package/dist/BUILD-MANIFEST.json +62 -32
  9. package/dist/adapters.js +4 -23
  10. package/dist/agent-lists.js +1 -1
  11. package/dist/agents-guide.js +113 -0
  12. package/dist/authoring-files.js +60 -0
  13. package/dist/authoring.js +11 -1
  14. package/dist/aws.js +4 -3
  15. package/dist/build-cloudflare.js +11 -24
  16. package/dist/bulk.js +37 -0
  17. package/dist/capabilities.js +208 -0
  18. package/dist/capability-query.js +72 -0
  19. package/dist/catalog.js +105 -0
  20. package/dist/cli.js +154 -15
  21. package/dist/client-address.js +1 -1
  22. package/dist/compliance-rules/baseline.js +8 -8
  23. package/dist/compliance-rules/privacy.js +3 -3
  24. package/dist/compliance-rules/strict.js +5 -5
  25. package/dist/conditions.js +88 -0
  26. package/dist/config.js +60 -4
  27. package/dist/context.js +156 -0
  28. package/dist/ecosystem-cli.js +88 -0
  29. package/dist/egress.js +98 -0
  30. package/dist/examples.js +92 -0
  31. package/dist/explain-cli.js +64 -0
  32. package/dist/explain.js +132 -0
  33. package/dist/extensions.js +172 -0
  34. package/dist/function-sources.js +15 -3
  35. package/dist/index.js +37 -0
  36. package/dist/init-with.js +165 -0
  37. package/dist/interchange-cli.js +42 -0
  38. package/dist/interchange.js +189 -0
  39. package/dist/link-cli.js +1 -1
  40. package/dist/management-policy.js +0 -1
  41. package/dist/manifest.js +111 -0
  42. package/dist/match.js +2 -2
  43. package/dist/mcp-authoring.js +147 -0
  44. package/dist/mcp.js +97 -0
  45. package/dist/observability.js +6 -0
  46. package/dist/operator-host.js +29 -0
  47. package/dist/plugins.js +12 -0
  48. package/dist/policies/agents.js +2 -2
  49. package/dist/policies/compression.js +2 -1
  50. package/dist/policies/security.js +0 -0
  51. package/dist/policies.js +1 -1
  52. package/dist/policy.js +29 -7
  53. package/dist/prerender.js +100 -41
  54. package/dist/project-tests.js +3 -3
  55. package/dist/provider-verification.js +92 -0
  56. package/dist/proxy.js +44 -0
  57. package/dist/readiness.js +5 -5
  58. package/dist/recipes.js +41 -0
  59. package/dist/route-diff.js +106 -0
  60. package/dist/router.js +42 -2
  61. package/dist/runtime.js +95 -14
  62. package/dist/schema-query.js +62 -0
  63. package/dist/signals.js +24 -0
  64. package/dist/site.js +0 -0
  65. package/dist/tooling.js +96 -0
  66. package/dist/types/adapters.d.ts +7 -4
  67. package/dist/types/agent-lists.d.ts +0 -1
  68. package/dist/types/agents-guide.d.ts +17 -0
  69. package/dist/types/authoring-files.d.ts +10 -0
  70. package/dist/types/aws.d.ts +3 -1
  71. package/dist/types/build-cloudflare.d.ts +1 -0
  72. package/dist/types/bulk.d.ts +27 -0
  73. package/dist/types/capabilities.d.ts +55 -0
  74. package/dist/types/capability-query.d.ts +24 -0
  75. package/dist/types/catalog.d.ts +65 -0
  76. package/dist/types/client-address.d.ts +0 -1
  77. package/dist/types/compliance-rules/baseline.d.ts +1 -9
  78. package/dist/types/compliance-rules/privacy.d.ts +1 -4
  79. package/dist/types/compliance-rules/strict.d.ts +0 -5
  80. package/dist/types/conditions.d.ts +19 -0
  81. package/dist/types/config.d.ts +20 -2
  82. package/dist/types/context.d.ts +65 -0
  83. package/dist/types/ecosystem-cli.d.ts +17 -0
  84. package/dist/types/egress.d.ts +46 -0
  85. package/dist/types/examples.d.ts +50 -0
  86. package/dist/types/explain-cli.d.ts +11 -0
  87. package/dist/types/explain.d.ts +95 -0
  88. package/dist/types/extensions.d.ts +122 -0
  89. package/dist/types/function-sources.d.ts +5 -0
  90. package/dist/types/index.d.ts +33 -0
  91. package/dist/types/init-with.d.ts +30 -0
  92. package/dist/types/interchange-cli.d.ts +16 -0
  93. package/dist/types/interchange.d.ts +42 -0
  94. package/dist/types/link-cli.d.ts +2 -1
  95. package/dist/types/management-policy.d.ts +0 -1
  96. package/dist/types/manifest.d.ts +81 -0
  97. package/dist/types/match.d.ts +1 -0
  98. package/dist/types/mcp-authoring.d.ts +92 -0
  99. package/dist/types/mcp.d.ts +12 -0
  100. package/dist/types/observability.d.ts +2 -0
  101. package/dist/types/operator-host.d.ts +8 -0
  102. package/dist/types/plugins.d.ts +2 -0
  103. package/dist/types/policies/agents.d.ts +0 -2
  104. package/dist/types/policies/compression.d.ts +2 -0
  105. package/dist/types/policies/security.d.ts +0 -1
  106. package/dist/types/policy.d.ts +15 -4
  107. package/dist/types/project-tests.d.ts +3 -1
  108. package/dist/types/provider-verification.d.ts +53 -0
  109. package/dist/types/proxy.d.ts +21 -0
  110. package/dist/types/readiness.d.ts +1 -1
  111. package/dist/types/recipes.d.ts +30 -0
  112. package/dist/types/route-diff.d.ts +27 -0
  113. package/dist/types/runtime.d.ts +11 -0
  114. package/dist/types/schema-query.d.ts +12 -0
  115. package/dist/types/signals.d.ts +25 -0
  116. package/dist/types/site.d.ts +0 -1
  117. package/dist/types/tooling.d.ts +115 -0
  118. package/dist/types/types.d.ts +57 -0
  119. package/dist/types/typescript-authoring.d.ts +12 -0
  120. package/dist/types/vercel.d.ts +3 -1
  121. package/dist/types/verify-deployment.d.ts +47 -0
  122. package/dist/types.js +21 -2
  123. package/dist/typescript-authoring.js +104 -0
  124. package/dist/vercel.js +4 -3
  125. package/dist/verify-deployment.js +270 -0
  126. package/docs/AI-AUTHORING.md +130 -8
  127. package/docs/BULK.md +79 -0
  128. package/docs/CAPABILITIES.md +179 -0
  129. package/docs/CAPACITY.md +1 -1
  130. package/docs/CI.md +142 -0
  131. package/docs/CONDITIONS.md +74 -0
  132. package/docs/DEPLOYMENT-CHECKS.md +108 -0
  133. package/docs/DYNAMIC-LINKS.md +18 -518
  134. package/docs/EGRESS.md +125 -0
  135. package/docs/EXTENSIONS.md +226 -0
  136. package/docs/FRAMEWORK.md +182 -0
  137. package/docs/INSTALL.md +45 -7
  138. package/docs/INTERCHANGE.md +134 -0
  139. package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
  140. package/docs/MIDDLEWARE.md +2 -0
  141. package/docs/NEXT-PHASE-PLAN.md +90 -0
  142. package/docs/NEXT-STEPS.md +415 -0
  143. package/docs/OBSERVABILITY.md +4 -2
  144. package/docs/OPERATIONAL-PROOF.md +4 -1
  145. package/docs/OPERATIONS.md +6 -3
  146. package/docs/PLUGINS.md +37 -0
  147. package/docs/POLICIES.md +12 -309
  148. package/docs/PRERENDER.md +40 -0
  149. package/docs/PROJECT-DIRECTION.md +42 -0
  150. package/docs/PROVIDER-VERIFICATION.md +84 -0
  151. package/docs/READINESS.md +21 -1
  152. package/docs/README.md +82 -31
  153. package/docs/RECIPES.md +99 -0
  154. package/docs/RELEASE-READINESS.md +11 -9
  155. package/docs/RELEASE-SECURITY.md +27 -4
  156. package/docs/SECURITY-AUDIT.md +1 -1
  157. package/docs/SPECIFICATION.md +95 -8
  158. package/docs/SPIKE-BUSINESS-SUITE.md +1013 -0
  159. package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
  160. package/docs/SPIKE-EXTENSIONS.md +1 -0
  161. package/docs/SPIKE-LAMBDA-COMPILE.md +199 -0
  162. package/docs/STANDARDS.md +150 -142
  163. package/docs/STARTERS.md +21 -1
  164. package/docs/TOOLING.md +291 -0
  165. package/docs/TYPESCRIPT-AUTHORING.md +67 -0
  166. package/docs/TYPESCRIPT.md +1 -1
  167. package/docs/USABILITY-REVIEW.md +123 -0
  168. package/docs/YAML-GUIDE.md +18 -479
  169. package/docs/YAML-REFERENCE.md +127 -16
  170. package/docs/links/cli.md +110 -0
  171. package/docs/links/limits.md +175 -0
  172. package/docs/links/management-api.md +80 -0
  173. package/docs/links/pools.md +75 -0
  174. package/docs/links/setup.md +135 -0
  175. package/docs/policies/agents.md +1 -1
  176. package/docs/policies/contract.md +52 -0
  177. package/docs/policies/hardened.md +56 -0
  178. package/docs/policies/interoperability.md +169 -0
  179. package/docs/policies/operations.md +45 -0
  180. package/docs/yaml/assets.md +36 -0
  181. package/docs/yaml/conditions.md +20 -0
  182. package/docs/yaml/functions.md +160 -0
  183. package/docs/yaml/links.md +30 -0
  184. package/docs/yaml/middleware.md +29 -0
  185. package/docs/yaml/organization.md +74 -0
  186. package/docs/yaml/policies.md +37 -0
  187. package/docs/yaml/redirects.md +64 -0
  188. package/docs/yaml/responses.md +57 -0
  189. package/docs/yaml/site.md +24 -0
  190. package/examples/assets/example.yaml +17 -0
  191. package/examples/aws/example.yaml +20 -0
  192. package/examples/cloudflare/example.yaml +19 -0
  193. package/examples/compliance/example.yaml +11 -0
  194. package/examples/conditions/README.md +12 -0
  195. package/examples/conditions/example.yaml +19 -0
  196. package/examples/conditions/tests/requests.json +13 -0
  197. package/examples/conditions/urlcode.yaml +24 -0
  198. package/examples/cookbook/README.md +8 -4
  199. package/examples/cookbook/example.yaml +17 -0
  200. package/examples/cookbook/functions/catalog.mjs +3 -0
  201. package/examples/cookbook/functions/fail.mjs +4 -0
  202. package/examples/cookbook/functions/items.mjs +3 -0
  203. package/examples/cookbook/functions/profile.mjs +3 -0
  204. package/examples/cookbook/functions/resource.mjs +3 -0
  205. package/examples/cookbook/functions/status.mjs +3 -0
  206. package/examples/cookbook/middleware/auth.mjs +48 -0
  207. package/examples/cookbook/middleware/body.mjs +15 -0
  208. package/examples/cookbook/middleware/bucket.mjs +19 -0
  209. package/examples/cookbook/middleware/cors.mjs +21 -0
  210. package/examples/cookbook/middleware/debug.mjs +13 -0
  211. package/examples/cookbook/middleware/envelope.mjs +11 -0
  212. package/examples/cookbook/middleware/errors.mjs +11 -0
  213. package/examples/cookbook/middleware/etag.mjs +18 -0
  214. package/examples/cookbook/middleware/locale.mjs +16 -0
  215. package/examples/cookbook/middleware/maintenance.mjs +10 -0
  216. package/examples/cookbook/middleware/methods.mjs +15 -0
  217. package/examples/cookbook/middleware/negotiate.mjs +20 -0
  218. package/examples/cookbook/middleware/referer.mjs +12 -0
  219. package/examples/cookbook/middleware/request-id.mjs +16 -0
  220. package/examples/cookbook/route-index.json +676 -0
  221. package/examples/cookbook/routes/middleware.yaml +126 -0
  222. package/examples/cookbook/tests/requests.json +526 -0
  223. package/examples/cookbook/urlcode.yaml +1 -0
  224. package/examples/egress/README.md +22 -0
  225. package/examples/egress/example.yaml +19 -0
  226. package/examples/egress/urlcode.yaml +19 -0
  227. package/examples/extensions/README.md +7 -0
  228. package/examples/extensions/example.yaml +21 -0
  229. package/examples/extensions/urlcode.yaml +25 -0
  230. package/examples/live-links/example.yaml +21 -0
  231. package/examples/monitoring/example.yaml +8 -0
  232. package/examples/prerender/example.yaml +16 -0
  233. package/examples/provider-conformance/README.md +12 -0
  234. package/examples/provider-conformance/example.yaml +14 -0
  235. package/examples/provider-conformance/urlcode.yaml +34 -0
  236. package/examples/tunnel/example.yaml +8 -0
  237. package/examples/vercel/example.yaml +19 -0
  238. package/llms-full.txt +2709 -0
  239. package/llms.txt +48 -19
  240. package/package.json +29 -7
  241. package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
  242. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +106 -0
  243. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +114 -0
  244. package/recipes/authenticated-json-api/README.md +51 -0
  245. package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
  246. package/recipes/authenticated-json-api/recipe.yaml +34 -0
  247. package/recipes/authenticated-json-api/tests/requests.json +39 -0
  248. package/recipes/authenticated-json-api/urlcode.yaml +12 -0
  249. package/recipes/contact-form/README.md +25 -0
  250. package/recipes/contact-form/functions/contact.mjs +17 -0
  251. package/recipes/contact-form/recipe.yaml +33 -0
  252. package/recipes/contact-form/tests/requests.json +47 -0
  253. package/recipes/contact-form/urlcode.yaml +18 -0
  254. package/recipes/cors-api/README.md +16 -0
  255. package/recipes/cors-api/functions/items.mjs +3 -0
  256. package/recipes/cors-api/middleware/cors.mjs +21 -0
  257. package/recipes/cors-api/recipe.yaml +26 -0
  258. package/recipes/cors-api/tests/requests.json +65 -0
  259. package/recipes/cors-api/urlcode.yaml +12 -0
  260. package/recipes/health-page/README.md +13 -0
  261. package/recipes/health-page/recipe.yaml +23 -0
  262. package/recipes/health-page/tests/requests.json +36 -0
  263. package/recipes/health-page/urlcode.yaml +19 -0
  264. package/recipes/json-api/README.md +6 -0
  265. package/recipes/json-api/functions/echo.mjs +3 -0
  266. package/recipes/json-api/recipe.yaml +25 -0
  267. package/recipes/json-api/tests/requests.json +34 -0
  268. package/recipes/json-api/urlcode.yaml +12 -0
  269. package/recipes/middleware/README.md +34 -0
  270. package/recipes/middleware/functions/catalog.mjs +3 -0
  271. package/recipes/middleware/functions/fail.mjs +4 -0
  272. package/recipes/middleware/functions/items.mjs +3 -0
  273. package/recipes/middleware/functions/profile.mjs +3 -0
  274. package/recipes/middleware/functions/resource.mjs +3 -0
  275. package/recipes/middleware/functions/status.mjs +3 -0
  276. package/recipes/middleware/middleware/auth.mjs +48 -0
  277. package/recipes/middleware/middleware/body.mjs +15 -0
  278. package/recipes/middleware/middleware/bucket.mjs +19 -0
  279. package/recipes/middleware/middleware/cors.mjs +21 -0
  280. package/recipes/middleware/middleware/debug.mjs +13 -0
  281. package/recipes/middleware/middleware/envelope.mjs +11 -0
  282. package/recipes/middleware/middleware/errors.mjs +11 -0
  283. package/recipes/middleware/middleware/etag.mjs +18 -0
  284. package/recipes/middleware/middleware/locale.mjs +16 -0
  285. package/recipes/middleware/middleware/maintenance.mjs +10 -0
  286. package/recipes/middleware/middleware/methods.mjs +15 -0
  287. package/recipes/middleware/middleware/negotiate.mjs +20 -0
  288. package/recipes/middleware/middleware/referer.mjs +12 -0
  289. package/recipes/middleware/middleware/request-id.mjs +16 -0
  290. package/recipes/middleware/public/guide.txt +1 -0
  291. package/recipes/middleware/recipe.yaml +50 -0
  292. package/recipes/middleware/tests/requests.json +528 -0
  293. package/recipes/middleware/urlcode.yaml +127 -0
  294. package/recipes/protected-download/README.md +22 -0
  295. package/recipes/protected-download/files/report.txt +1 -0
  296. package/recipes/protected-download/recipe.yaml +31 -0
  297. package/recipes/protected-download/tests/requests.json +32 -0
  298. package/recipes/protected-download/urlcode.yaml +15 -0
  299. package/recipes/redirect/README.md +7 -0
  300. package/recipes/redirect/recipe.yaml +25 -0
  301. package/recipes/redirect/tests/requests.json +19 -0
  302. package/recipes/redirect/urlcode.yaml +9 -0
  303. package/recipes/static-plus-api/README.md +15 -0
  304. package/recipes/static-plus-api/functions/info.mjs +3 -0
  305. package/recipes/static-plus-api/public/assets/index.html +3 -0
  306. package/recipes/static-plus-api/public/assets/site.css +1 -0
  307. package/recipes/static-plus-api/public/index.html +8 -0
  308. package/recipes/static-plus-api/recipe.yaml +29 -0
  309. package/recipes/static-plus-api/tests/requests.json +56 -0
  310. package/recipes/static-plus-api/urlcode.yaml +23 -0
  311. package/recipes/typescript/README.md +7 -0
  312. package/recipes/typescript/functions/hello.ts +5 -0
  313. package/recipes/typescript/recipe.yaml +23 -0
  314. package/recipes/typescript/tests/requests.json +18 -0
  315. package/recipes/typescript/urlcode.yaml +5 -0
  316. package/recipes/webhook-receiver/README.md +16 -0
  317. package/recipes/webhook-receiver/functions/receive.mjs +16 -0
  318. package/recipes/webhook-receiver/recipe.yaml +26 -0
  319. package/recipes/webhook-receiver/tests/requests.json +59 -0
  320. package/recipes/webhook-receiver/urlcode.yaml +16 -0
  321. package/schemas/recipe.schema.json +138 -0
  322. package/schemas/urlcode.schema.json +656 -80
  323. package/skills/urlcode/SKILL.md +98 -0
  324. package/starters/default/.github/workflows/urlcode.yml +23 -0
  325. package/starters/default/.mcp.json +12 -0
  326. package/starters/default/AGENTS.md +79 -0
package/docs/EGRESS.md ADDED
@@ -0,0 +1,125 @@
1
+ # Bounded proxy and webhook transport
2
+
3
+ Outbound requests use a host-owned HTTPS transport. Guests do not receive a fetch
4
+ API. Project declarations express behavior; an external operator policy, pinned
5
+ to the complete project revision, grants exact HTTPS origins separately to proxy
6
+ and signal operations. A configured destination never grants its own authority.
7
+ Binding injection uses existing explicitly granted secrets, resolved before the
8
+ host transport receives a literal header value. Never log those values.
9
+
10
+ The transport resolves the destination for each request, refuses any DNS response
11
+ containing a nonpublic address, and pins its connection lookup to one validated
12
+ answer. HTTPS certificate and hostname verification remain enabled. There is no
13
+ connection pool, fallback address, redirect following, or automatic retry. Private,
14
+ loopback, link-local, documentation, multicast, IPv4-mapped IPv6 and transition
15
+ addresses are refused. IPv6 filtering is deliberately conservative: 2001::/16,
16
+ 2002::/16 and 3fff::/16 are refused along with addresses outside ordinary global
17
+ unicast. Network-level egress controls remain useful defense in depth.
18
+
19
+ Default limits per client are 16 simultaneous requests, five seconds including
20
+ DNS, 1 MiB request and response bodies and 16 KiB headers. Excess concurrency is
21
+ refused immediately. Requests use bounded buffering rather than streaming. Abort
22
+ and shutdown destroy active HTTP requests; unresolved system DNS operations may
23
+ finish in the background but cannot open a socket after cancellation. Those DNS
24
+ operations retain a separate concurrency slot until they settle, so repeatedly
25
+ timing out DNS cannot create an unbounded underlying lookup backlog. A shared
26
+ 64-operation DNS cap also spans all client instances and runtime reloads in the
27
+ host Node isolate; closing a client does not release an unresolved DNS slot. Closing a
28
+ client refuses future requests and drains its bounded outstanding promises.
29
+ Errors carry only a fixed category, never a destination, request, header or secret.
30
+
31
+ Proxy destinations have a literal HTTPS authority. Path placeholders are encoded
32
+ as individual components; dot-segment values are refused. Query names and incoming
33
+ and outgoing headers require explicit selection. Host, framing, hop-by-hop and
34
+ proxy-authorization headers cannot be supplied. Ambient authorization, cookies,
35
+ forwarding metadata (the entire `x-forwarded-*` family) and Set-Cookie cannot be selected. Explicit host-resolved
36
+ Authorization injection is supported. Set-Cookie forwarding is deliberately
37
+ unsupported because multiple cookie fields cannot safely be represented by the
38
+ portable scalar header contract. Upstream redirects are returned as responses;
39
+ Location is forwarded only if explicitly selected and is never fetched. Headers
40
+ nominated by an incoming or upstream Connection field are removed even if selected.
41
+ Header names are normalized to lowercase; array-valued upstream fields are omitted.
42
+ An encoded request body requires explicitly selecting its Content-Encoding
43
+ header; literal header injection cannot change or replace that coding. Bodies
44
+ remain raw bytes, including content encoding; select Content-Encoding when
45
+ forwarding compressed responses. No transparent decompression occurs.
46
+
47
+ Webhook signals are best effort. The broker schedules work after the caller's
48
+ synchronous emit operation, with eight concurrent deliveries by default and no
49
+ queue. Saturation and closed brokers drop new events. Each event contains only
50
+ version, declared route pattern, status and method: never the actual request URL,
51
+ parameters, body, client identity, incoming headers or bindings. The webhook
52
+ receives a POST with JSON. Only 2xx responses count as delivered; all other status
53
+ codes and transport failures count as failed. Delivery order is unspecified,
54
+ there are no retries or durability guarantees, and shutdown aborts outstanding
55
+ work and awaits settlement. Counter snapshots report accepted, delivered, failed
56
+ and dropped events without including destination data. Applications must not use
57
+ these signals for guaranteed billing, audit retention or job execution.
58
+
59
+ The transport is a new security-sensitive host bridge. Unit tests and CI are not
60
+ an independent assessment or proof of hostile multi-tenant readiness. Public
61
+ provider network deployments require separate operator-owned fixtures and evidence.
62
+
63
+ ## Project declarations and external grants
64
+
65
+ ```yaml
66
+ version: "1"
67
+ routes:
68
+ /items/{id}:
69
+ proxy:
70
+ url: https://api.example.com/items/{id}
71
+ query: [page]
72
+ requestHeaders: [accept]
73
+ responseHeaders: [content-type, content-encoding]
74
+ headers:
75
+ authorization: {secret: API_TOKEN}
76
+ parameters:
77
+ - {name: id, in: path, required: true, schema: {type: string}}
78
+ secrets:
79
+ API_TOKEN: {secret: UPSTREAM_AUTHORIZATION}
80
+ signals:
81
+ - url: https://hooks.example.com/events
82
+ ```
83
+
84
+ An injected secret is the complete header value (for example an operator-provided
85
+ Bearer value); interpolation and ambient credentials are not supported. Each
86
+ proxy placeholder must name a declared string path parameter. Proxy routes refuse
87
+ middleware in this initial implementation: middleware cannot safely authorize an
88
+ already materialized upstream request. Use host request policies before egress.
89
+ Proxy routes require cache disabled or `no-store`; explicit cache declarations
90
+ cannot override this. Responses always remove CDN cache directives and receive
91
+ `Cache-Control: no-store`. Compressed upstream bytes are refused if their
92
+ Content-Encoding field was not explicitly selected; it cannot be overridden by
93
+ route response headers.
94
+
95
+ The external version-1 policy has the existing `projectSha256` and route grants:
96
+
97
+ ```json
98
+ {"version":1,"projectSha256":"<reviewed revision hash>","routes":{"/items/{id}":{"secrets":["UPSTREAM_AUTHORIZATION"],"egress":{"proxy":["https://api.example.com"],"signals":["https://hooks.example.com"]}}}}
99
+ ```
100
+
101
+ Use `permissions` to generate requested grants for review. Exact origin strings
102
+ omit a trailing slash and the default 443 port. Nondefault ports are explicit.
103
+ Proxy and signal purposes are independent even when they share an origin. Every
104
+ route's grant is checked against the current revision before credentials, assets,
105
+ workers or network clients are activated. Each runtime shares one bounded proxy
106
+ client and one bounded signal client, with at most 64 origins per purpose.
107
+
108
+ Signals emit when route execution and response policies produce a result,
109
+ including guest middleware responses and returned error status codes. Thrown
110
+ handler errors and host request-policy/plugin short circuits (including cache
111
+ hits) do not emit. HEAD requests and generated readiness probes do not emit.
112
+ The response never waits for webhook delivery. Counter events contain only
113
+ accepted/delivered/failed/dropped outcomes and counts and are exposed through
114
+ runtime metrics and the Prometheus `signals_total` series. A closed runtime stops
115
+ new requests, aborts egress and drains bounded pending work. Client disconnects
116
+ are not currently propagated into the runtime transport; the five-second deadline
117
+ still applies. All non-self-hosted targets currently refuse proxy and signals.
118
+ See the [executable example](../examples/egress/README.md).
119
+
120
+ Revision hashes also include declared project policies, profiles and site
121
+ configuration. Changing an inherited pre-egress restriction invalidates grants
122
+ just like changing a route or function source. Projects with these declarations
123
+ must regenerate and review their operator policy after upgrading to this hash
124
+ coverage; an old grant is intentionally refused rather than silently retaining
125
+ network authority under changed behavior.
@@ -0,0 +1,226 @@
1
+ # Operator-installed extensions
2
+
3
+ Extensions are trusted operator modules, separate from application WASM. Auth
4
+ and admin implementations live in `urlcode-auth` and `urlcode-admin`; the runtime
5
+ supplies only the generic integration contract. No project file can import a host
6
+ extension or choose its npm package.
7
+
8
+ A project declares versioned configuration and exclusive route mounts:
9
+
10
+ ```yaml
11
+ version: "1"
12
+ extensions:
13
+ auth:
14
+ version: "1"
15
+ config: {}
16
+ routes:
17
+ /auth/*:
18
+ extension: auth
19
+ methods: [GET, HEAD, POST]
20
+ /private:
21
+ respond: {text: Private}
22
+ policies:
23
+ extensions:
24
+ auth: {signedIn: true}
25
+ ```
26
+
27
+ ## Protecting a route: the `auth` short form
28
+
29
+ When the project declares `extensions.auth`, a route may say `auth` instead of
30
+ spelling out `policies.extensions.auth`. This is the preferred way to protect a
31
+ route:
32
+
33
+ ```yaml
34
+ routes:
35
+ /account:
36
+ respond: {text: Account}
37
+ auth: {role: member} # or `auth: true` for any signed-in principal
38
+ /docs:
39
+ respond: {text: Docs}
40
+ auth: {required: false} # documents intent; emits no requirement
41
+ ```
42
+
43
+ The compiler expands the short form before anything else reads the project:
44
+ `auth: true` becomes `policies.extensions.auth: {}` and an object becomes the
45
+ same object minus `required`. The long form stays the canonical representation,
46
+ so `routes`, `audit` and `explain` show the expansion, the extension revision
47
+ hash covers it, and the installed auth extension validates the expanded
48
+ requirement with its own policy schema. The keys other than `required` are
49
+ exactly that schema's keys (`role`, `permission`, `verified`,
50
+ `freshWithinSeconds`, `onDeny`); the runtime adds nothing of its own. Loading
51
+ fails, naming the route, when `auth` appears without an `extensions.auth`
52
+ declaration, next to `policies.extensions.auth`, or next to
53
+ `policies.extensions: false`.
54
+
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)).
58
+
59
+ The configuration and requirement objects above are validated by the installed
60
+ extension's schemas. They are examples of extension-owned fields, not built-in
61
+ authentication behavior. See the executable generic fixture in
62
+ [examples/extensions](../examples/extensions). Included files can declare
63
+ extensions; duplicate names fail rather than silently override one another.
64
+
65
+ The operator passes `extensions: RuntimeExtension[]` to `createRuntime`,
66
+ `startServer`, or the AWS/Vercel adapter. Types and
67
+ `inspectExtensionRevision(project)` are exported from
68
+ `@jimhoyd/urlcode/extensions`. Inspection does not grant access: review the
69
+ project and place the exact returned SHA-256 in each registration's
70
+ `projectSha256`. YAML extension configuration, policies and routes participate
71
+ in the revision. Changing them requires an explicit operator reapproval.
72
+
73
+ Registrations provide a name, contract version, target list, JSON configuration
74
+ schema, optional policy schema and activation factory. Activation receives the
75
+ canonical operator origin, target, revision and mount bases. Its instance handles
76
+ bounded requests and, when used in policies, authorizes requests. Missing
77
+ registrations, stale grants, invalid configuration and unsupported targets fail
78
+ activation. Multiple mounts cannot overlap other declared routes.
79
+
80
+ 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.
85
+
86
+ One exception exists for content-hashed assets. A registration may declare
87
+ `immutableAssets: {prefix: '/static'}`, a normalized literal path under each
88
+ of its mounts (no `.` or `..` segments, no trailing slash). The runtime then
89
+ answers `Cache-Control: public, max-age=31536000, immutable` instead of
90
+ no-store only when every condition holds: the request path lies under
91
+ `<mount><prefix>/`, the method is GET or HEAD, the status is 200 or 304, the
92
+ response carries exactly one strong ETag, sets no Set-Cookie, and does not
93
+ vary on Cookie, Authorization or `*`. A stricter Cache-Control the extension
94
+ set (no-store, no-cache, private or a shorter max-age) is preserved; other CDN
95
+ cache headers are still stripped and compression stays disabled. Anything
96
+ that fails a condition, including a cookie added by a later response hook,
97
+ stays no-store. The extension owns the content-hashed filename: a file under
98
+ the prefix must change its name when its bytes change, because clients never
99
+ revalidate it. The prefix belongs to the operator registration, not to the
100
+ pinned project revision. The runtime withholds Cookie and Authorization plus any declared
101
+ credential headers from all application guest requests and mapped parameters.
102
+ This does not isolate browser JavaScript running on the same origin: application
103
+ HTML/JS on an authentication origin must be trusted by that site's operator.
104
+
105
+ Cloudflare refuses extensions until its artifact format supports their execution.
106
+ Node adapter conformance is not a live-provider deployment claim.
107
+
108
+ ## Discovering schemas
109
+
110
+ Each registration carries the JSON Schemas that validate its `config` block and
111
+ its per-route policy requirements. `urlcode extensions` prints them together with
112
+ the project's own declarations so an author can see what a mount accepts:
113
+
114
+ ```sh
115
+ urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--json]
116
+ ```
117
+
118
+ For every registration in the host file it reports the name, contract version,
119
+ targets, credential headers, configuration schema, policy schema (if any),
120
+ whether the project declares it, whether its `projectSha256` matches the current
121
+ revision, the routes that mount it and the routes whose policies require it.
122
+ Declared names the host does not register are listed as unregistered. The command
123
+ executes the trusted host module exactly as `validate` does, including its
124
+ absolute-path and outside-project rules, and calls `close` afterwards; it never
125
+ activates an extension and grants nothing. Without `--host-file` it lists only
126
+ the names the project declares and notes that schemas need the host file.
127
+
128
+ The same report is available as `inspectExtensions({project, hostFile?})` from
129
+ the package root and, for assistants, as the MCP tool `get_extensions`, which the
130
+ server advertises only when the operator started `urlcode mcp` with
131
+ `--host-file`. No tool argument can name a host file. See [TOOLING.md](TOOLING.md).
132
+
133
+ ## CLI host binding
134
+
135
+ Use an explicitly named operator ES module outside the application directory:
136
+
137
+ ```sh
138
+ urlcode serve --project ./site --origin https://site.example \
139
+ --host-file /absolute/operator/host.mjs
140
+ ```
141
+
142
+ The module default-exports `{extensions, plugins?, close?}`. It may import installed
143
+ operator packages, open their stores and read operator secrets. `close` releases
144
+ shared services when the CLI command finishes or the server shuts down. A runtime
145
+ reload closes extension instances but does not close caller-owned services. Host
146
+ modules are not watched or automatically rediscovered. Restart to update them.
147
+
148
+ The same explicit option is supported by dev, validate, test, routes, audit,
149
+ benchmark, extensions and mcp. These commands execute trusted host activation and may access its
150
+ store; read-only project inspection commands never implicitly load a host file.
151
+ Host-file paths must be absolute `.mjs`/`.js` files whose real path lies outside
152
+ the project, including after symlink resolution. This is an operator-code trust
153
+ boundary, not a JavaScript sandbox or an independent security review.
154
+
155
+ ## Scaffolding with `init --with`
156
+
157
+ `urlcode init <directory> --with auth,admin` produces the layered site the
158
+ [framework page](FRAMEWORK.md#the-composition-contract) describes in one
159
+ command: the starter under `<directory>/app/`, one `host.mjs`, one `README.md`,
160
+ and each extension's own operator files. Core never bundles or imports the
161
+ extension packages at build time; at run time it resolves
162
+ `@jimhoyd/urlcode-<name>` for each name with Node's package resolution from
163
+ 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`
165
+ prereleases, is the normal path and what makes `--with auth` work), imports
166
+ the package and calls its
167
+ `scaffold` export with this request:
168
+
169
+ ```ts
170
+ interface ScaffoldRequest {
171
+ directory: string; // absolute site directory; result file paths are relative to it
172
+ project: string; // absolute route project, <directory>/app (holds urlcode.yaml)
173
+ hostFile: string; // absolute combined host module, <directory>/host.mjs
174
+ names: readonly string[]; // every name in --with order, including this one
175
+ }
176
+ interface ScaffoldFile { path: string; content: string | Uint8Array; mode?: number }
177
+ interface ScaffoldResult {
178
+ name: string; // must equal the requested name
179
+ extensions: Record<string, unknown>; // merged into the project's top-level extensions
180
+ routes: Record<string, unknown>; // merged into app/routes/extensions.yaml
181
+ hostImports: string[]; hostSetup: string[]; hostEntries: string[]; hostClose?: string[];
182
+ files: ScaffoldFile[]; // written relative to directory with their modes
183
+ readme: string; nextSteps: string[]; // README section and numbered steps
184
+ env?: Record<string, string>; // environment variables the host reads
185
+ }
186
+ ```
187
+
188
+ `scaffold` writes nothing; it returns fragments and may generate key material
189
+ in memory (core zeroes `Uint8Array` contents after writing or on failure). The
190
+ types are exported from `@jimhoyd/urlcode` for packages that want to typecheck
191
+ against them.
192
+
193
+ Assembly rules, in `--with` order:
194
+
195
+ - Every package is resolved and every `scaffold` is called before anything is
196
+ written. A name that is not installed refuses with the `npm install` command;
197
+ a package without a `scaffold` export refuses and names the package; an error
198
+ thrown by a `scaffold` (for example admin without auth in the same `--with`)
199
+ is reported as that package's refusal. No directory is left behind.
200
+ - `extensions` fragments are declared in `app/urlcode.yaml`; `routes`
201
+ fragments are written to `app/routes/extensions.yaml`, appended to the
202
+ starter's `includes`, so the starter's own routes load first. A route or
203
+ extension key produced twice, or one the starter already declares, is refused
204
+ naming both sources.
205
+ - `host.mjs` is all `hostImports`, then all `hostSetup` lines, then an
206
+ `extensions` array of every `hostEntries` item, then `close()` running the
207
+ `hostClose` statements in reverse `--with` order so later entries release
208
+ before what they built on. Setup lines share one module scope: admin's entry
209
+ references the `service`, `csrfKey` and `projectSha256` identifiers that
210
+ auth's setup defines, which is why `names` carries the full list.
211
+ - `files` are created exclusively (`wx`) with their `mode` (default `0644`),
212
+ must stay inside the site directory and outside `app/`, and never pass
213
+ through a symlink. Nothing generated is ever overwritten; an existing
214
+ destination refuses like plain `init`.
215
+ - `README.md` holds the starter's README as a section, then each result's
216
+ `readme` under `## Extension: <name>`, the merged numbered `nextSteps`, the
217
+ merged `env` table and the project revision. The command prints that
218
+ revision (`inspectExtensionRevision` of `app/`) with the instruction to
219
+ review the project and pin it explicitly; the host is generated to require
220
+ the pin, never to compute it.
221
+
222
+ Serving the result is the usual explicit host binding:
223
+
224
+ ```sh
225
+ urlcode validate --project app --host-file "$PWD/host.mjs" --origin https://site.example
226
+ ```
@@ -0,0 +1,182 @@
1
+ # The URLCode framework
2
+
3
+ One page for people and AI agents. It says what the four packages are, how a
4
+ project grows from a handful of redirects into an application with accounts
5
+ and an administration console, and which facts an agent must not guess. Every
6
+ claim here is implemented in the linked repository; nothing is roadmap.
7
+
8
+ ## Four packages, one project shape
9
+
10
+ | Package | Repository | What it adds | How a project declares it |
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"` |
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
+ | `@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
+ | `@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
+
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`,
20
+ on core `0.4.0-alpha.1`). An alpha on npm is a distribution channel, not an
21
+ endorsement: the source is complete, but independent review, deployment
22
+ evidence and an accessibility assessment are still pending
23
+ ([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
24
+ files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode-auth/blob/main/IMPLEMENTATION-STATUS.md),
25
+ [admin](https://github.com/jimhoyd-com/urlcode-admin/blob/main/IMPLEMENTATION-STATUS.md),
26
+ [ui](https://github.com/jimhoyd-com/urlcode-ui/blob/main/IMPLEMENTATION-STATUS.md).
27
+
28
+ ## The ladder
29
+
30
+ A project climbs these rungs by adding YAML, never by rewriting what it has.
31
+ Each rung's YAML is valid on every rung above it.
32
+
33
+ 1. **Redirects.** A `urlcode.yaml` with `redirect` routes. No code, no database,
34
+ runs anywhere, including Vercel, AWS Lambda and Cloudflare Workers.
35
+ Thousands of rows import from CSV or provider files with `bulk-import`.
36
+ 2. **Responses, pages and files.** `respond`, `page`, `static` and `download`
37
+ handlers, `site` conventions (robots, sitemap, favicon, security.txt,
38
+ llms.txt) and `policies` (throttle, agents, security headers, compression,
39
+ cache). Still no code.
40
+ 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
47
+ page and protected routes. The operator installs it in a host file outside
48
+ the project; YAML only declares the mount and configuration.
49
+ 6. **Administration.** The `admin` extension on the same service: manage the
50
+ people who signed up, their sessions and roles, review the audit trail.
51
+ 7. **Your own look.** A shared `presentation` (catalogue and theme variables)
52
+ restyles auth and admin together; the `ui` extension adds the template kit,
53
+ project copy, template and stylesheet overrides for kit-rendered pages.
54
+
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
57
+ with a patched SQLite build; see each repository's README for the exact
58
+ requirement.
59
+
60
+ ## The composition contract
61
+
62
+ An extended project starts with the packages and one command:
63
+
64
+ ```sh
65
+ npm install @jimhoyd/urlcode @jimhoyd/urlcode-ui @jimhoyd/urlcode-auth @jimhoyd/urlcode-admin
66
+ urlcode init my-site --with auth,admin
67
+ ```
68
+
69
+ Installing from npm is the normal path; the extension repositories'
70
+ `pack-sources.mjs` still builds local tarballs from a reviewed checkout for
71
+ operators who install only source they have read. Three files make an extended
72
+ project. Nothing else is discovered by convention.
73
+
74
+ ```
75
+ site/
76
+ urlcode.yaml the project: routes, extensions, policies (Git-owned, untrusted content)
77
+ functions/, public/ guest code and assets referenced from the YAML
78
+ operator/
79
+ host.mjs trusted operator code: default-exports { extensions, plugins?, close? }
80
+ operator-service.mjs opens the auth store, keys and senders; imported by host.mjs
81
+ data/ private: auth.sqlite, encryption key, CSRF key
82
+ ```
83
+
84
+ The project declares logical extensions and exclusive mounts:
85
+
86
+ ```yaml
87
+ version: "1"
88
+ extensions:
89
+ ui: { version: "1", config: { theme: { name: Acme, colors: { primary: "24 95% 53%" } } } }
90
+ auth: { version: "1", config: { registration: "off" } }
91
+ admin: { version: "1", config: {} }
92
+ routes:
93
+ /assets/ui/*: { extension: ui, methods: [GET, HEAD] }
94
+ /account/*: { extension: auth, methods: [GET, HEAD, POST] }
95
+ /admin/*: { extension: admin, methods: [GET, HEAD, POST] }
96
+ /private:
97
+ respond: { text: Signed in }
98
+ policies:
99
+ extensions:
100
+ auth: {}
101
+ ```
102
+
103
+ The operator host wires the packages, once, in code the project cannot reach:
104
+
105
+ ```js
106
+ import { createUiExtension } from '@jimhoyd/urlcode-ui/host';
107
+ import { authExtension, createPresentation, englishCatalogue } from '@jimhoyd/urlcode-auth';
108
+ import { adminExtension } from '@jimhoyd/urlcode-admin';
109
+ import { service, csrfKey, projectSha256 } from './operator-service.mjs';
110
+
111
+ const ui = createUiExtension({ projectSha256, projectRoot: '/absolute/site', sources: [englishCatalogue] });
112
+ const presentation = createPresentation({ theme: { '--ui-accent': '#0645ad' } });
113
+ export default {
114
+ extensions: [
115
+ ui.registration,
116
+ authExtension({ service, csrfKey, projectSha256, presentation }),
117
+ adminExtension({ service, csrfKey, projectSha256, authMount: '/account', presentation }),
118
+ ],
119
+ async close() { await service.close(); },
120
+ };
121
+ ```
122
+
123
+ Auth and admin pages currently render through the shared primitives and a
124
+ `presentation` (catalogue and `--ui-*` theme variables). The `ui` extension
125
+ serves the template kit and its assets for pages an extension renders through
126
+ `ui.kit`; moving the auth and admin screens onto the kit is a listed remaining
127
+ item in the ui repository, so today one `presentation` restyles them and the
128
+ `ui` block is optional.
129
+
130
+ ```sh
131
+ urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
132
+ ```
133
+
134
+ `urlcode init <dir> --with auth,admin` writes this layout in one step: it
135
+ resolves each installed `@jimhoyd/urlcode-<name>` from the current directory,
136
+ calls its `scaffold` export and merges the fragments into `app/urlcode.yaml`,
137
+ one `host.mjs` and one `README.md`, refusing before writing anything when a
138
+ package is missing or two fragments collide (the contract is documented under
139
+ [scaffolding](EXTENSIONS.md#scaffolding-with-init---with)). `urlcode-auth init`
140
+ and `urlcode-admin init` write the same layout for a single package; `urlcode-auth bootstrap` creates the first
141
+ administrator from JSON on stdin. `inspectExtensionRevision(project)` prints
142
+ the SHA-256 that `projectSha256` must carry; changing extension YAML, policies
143
+ or mounts changes the revision and needs an explicit operator reapproval.
144
+
145
+ ## Rules an agent must follow
146
+
147
+ These are the facts that keep generated projects valid. The full matrix is in
148
+ [AI authoring](AI-AUTHORING.md); this is the short list.
149
+
150
+ - **YAML never names code, packages, databases or credentials.** Extensions are
151
+ logical names; the host file chooses the implementation. There is no
152
+ `--extension` flag, no `import` in YAML, no interpolation.
153
+ - **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
154
+ `function`, `link`, `proxy`, `conditional` or `extension`, plus optional
155
+ `middleware`. Paths are exact or single-segment `{param}`; `/*` only on
156
+ `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.
160
+ - **Authentication is host processing.** Do not build login forms, session
161
+ cookies or password checks in functions. Declare `policies.extensions.auth`
162
+ on the route; the runtime withholds `Cookie` and `Authorization` from guests.
163
+ - **Everything is validated before it runs.** `urlcode validate --local`,
164
+ `urlcode test`, `urlcode audit --expect-routes N`. Unsupported features fail
165
+ with the route named; nothing degrades silently.
166
+ - **Provider targets refuse what they cannot enforce.** Cloudflare runs
167
+ 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.
170
+ - **Report evidence, not hope.** The commands above are the evidence. Local
171
+ tests are not deployment, soak or independent security review.
172
+
173
+ ## Where to read next
174
+
175
+ | Need | Read |
176
+ |---|---|
177
+ | Write or change routes | [YAML guide](YAML-GUIDE.md), [field reference](YAML-REFERENCE.md), [cookbook](../examples/cookbook/README.md) |
178
+ | Add accounts | [auth README](https://github.com/jimhoyd-com/urlcode-auth#readme), [auth security](https://github.com/jimhoyd-com/urlcode-auth/blob/main/SECURITY.md) |
179
+ | Add administration | [admin README](https://github.com/jimhoyd-com/urlcode-admin#readme) |
180
+ | Restyle every page | [ui README](https://github.com/jimhoyd-com/urlcode-ui#readme), [ui contract](https://github.com/jimhoyd-com/urlcode-ui/blob/main/CONTRACT.md) |
181
+ | Write an extension | [extensions](EXTENSIONS.md), [extension model review](SPIKE-EXTENSION-MODEL.md) |
182
+ | Run it | [operations](OPERATIONS.md), [install](INSTALL.md), [deployment checks](DEPLOYMENT-CHECKS.md) |
package/docs/INSTALL.md CHANGED
@@ -14,26 +14,51 @@ everything except live links works on any supported build. See
14
14
  ## npm
15
15
 
16
16
  ```sh
17
- npm install --global urlcode
17
+ npm install --global @jimhoyd/urlcode
18
18
  urlcode --help
19
19
  ```
20
20
 
21
- Project-local, which is what an application repository should normally pin:
21
+ Project-local, which is what an application repository should normally pin.
22
+ Which dependency list it belongs in depends on how the project uses URLCode:
22
23
 
23
24
  ```sh
24
- npm install --save-dev urlcode
25
+ # Using URLCode as a tool: validate, test and build in CI, never imported by
26
+ # the code that serves requests.
27
+ npm install --save-dev @jimhoyd/urlcode
25
28
  npx urlcode validate
29
+
30
+ # Embedding the runtime (see TYPESCRIPT.md): the application imports
31
+ # @jimhoyd/urlcode at startup, so it must survive `npm ci --omit=dev`.
32
+ npm install --save @jimhoyd/urlcode
26
33
  ```
27
34
 
35
+ A devDependency is absent from a production install, so an application that
36
+ imports `createRuntime`, `startServer`, `prerenderPages` or any other
37
+ [embedding entry point](TYPESCRIPT.md) fails at startup on a missing module if it
38
+ is installed with `--save-dev`. An application should also pin an **exact**
39
+ version rather than a range: the compiled Cloudflare artifact format is tied to
40
+ the runtime version that reads it.
41
+
28
42
  ## Homebrew
29
43
 
30
44
  ```sh
31
45
  brew tap jimhoyd-com/urlcode
46
+ brew trust jimhoyd-com/urlcode
32
47
  brew install urlcode
33
48
  ```
34
49
 
50
+ Homebrew refuses to load a formula from a third-party tap until you trust it,
51
+ so without the middle line the install stops with `Refusing to load formula …
52
+ from untrusted tap`. Trusting a tap means agreeing to run code from this
53
+ repository, the same as with the install script; `brew trust --formula
54
+ jimhoyd-com/urlcode/urlcode` limits it to this one formula.
55
+
35
56
  The tap's formula is generated from the published tarball for each release and
36
- attached to the GitHub release as `urlcode.rb`.
57
+ attached to the GitHub release as `urlcode.rb`. Homebrew verifies the tarball's
58
+ SHA-256 against the formula before installing.
59
+
60
+ Verified on macOS (arm64) against the 0.3.0 tap: fetch verified, installed into
61
+ `/opt/homebrew/Cellar/urlcode/0.3.0`.
37
62
 
38
63
  ## Install script
39
64
 
@@ -45,7 +70,7 @@ It downloads the release tarball, verifies its SHA-256 against the release's
45
70
  `SHA256SUMS`, and installs with npm. Options:
46
71
 
47
72
  ```sh
48
- curl -fsSL .../install.sh | sh -s -- --version 0.2.0 --prefix "$HOME/.local"
73
+ curl -fsSL .../install.sh | sh -s -- --version 0.3.0 --prefix "$HOME/.local"
49
74
  ```
50
75
 
51
76
  `--prefix` avoids needing privileges for a global npm directory; add
@@ -54,8 +79,14 @@ for that moment: to inspect first, download it, read it, then run it.
54
79
 
55
80
  ## Container
56
81
 
82
+ No image is published yet: the release job's GHCR step is gated behind the
83
+ `PUBLISH_CONTAINER` repository variable and has not run, so there is nothing at
84
+ `ghcr.io/jimhoyd-com/urlcode` to pull. Build it from a release checkout:
85
+
57
86
  ```sh
58
- docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" ghcr.io/jimhoyd-com/urlcode:0.2.0 \
87
+ git clone --branch v0.3.0 https://github.com/jimhoyd-com/urlcode.git
88
+ docker build -t urlcode:0.3.0 urlcode
89
+ docker run --rm -p 127.0.0.1:3000:3000 -v "$PWD:/project:ro" urlcode:0.3.0 \
59
90
  serve --project /project --host 0.0.0.0
60
91
  ```
61
92
 
@@ -74,13 +105,20 @@ make dev
74
105
  A clone runs the TypeScript source directly (`node src/cli.ts`, Node 22.18+),
75
106
  with no build step; see [local development](LOCAL-DEVELOPMENT.md).
76
107
 
108
+ Three Node versions appear around the project, and they are not a contradiction:
109
+ the installed package runs on Node 22.13 or newer (`engines`), running the
110
+ TypeScript source from a clone needs 22.18 or newer because it relies on Node's
111
+ built-in type stripping, the release workflow's npm trusted publishing needs
112
+ 22.14 or newer, and the container image pins Node 26. Only the first number
113
+ constrains a deployment of the published tarball.
114
+
77
115
  ## Verify what you installed
78
116
 
79
117
  Releases carry Sigstore provenance signed by the release workflow. Before
80
118
  trusting a downloaded artifact:
81
119
 
82
120
  ```sh
83
- gh attestation verify urlcode-0.2.0.tgz --repo jimhoyd-com/urlcode \
121
+ gh attestation verify jimhoyd-urlcode-0.3.0.tgz --repo jimhoyd-com/urlcode \
84
122
  --signer-workflow jimhoyd-com/urlcode/.github/workflows/release.yml
85
123
  ```
86
124