@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
  2. package/.claude/skills/urlcode-operations/SKILL.md +108 -0
  3. package/.claude-plugin/marketplace.json +18 -0
  4. package/CONTRIBUTING.md +30 -2
  5. package/README.md +195 -255
  6. package/ROADMAP.md +143 -15
  7. package/SECURITY.md +31 -9
  8. package/dist/BUILD-MANIFEST.json +72 -47
  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/build-static.js +134 -0
  17. package/dist/bulk.js +37 -0
  18. package/dist/capabilities.js +262 -0
  19. package/dist/capability-query.js +71 -0
  20. package/dist/catalog.js +105 -0
  21. package/dist/cli.js +165 -34
  22. package/dist/client-address.js +1 -1
  23. package/dist/compliance-rules/baseline.js +9 -17
  24. package/dist/compliance-rules/privacy.js +7 -18
  25. package/dist/compliance-rules/shared.js +0 -2
  26. package/dist/compliance-rules/strict.js +5 -5
  27. package/dist/compliance.js +6 -8
  28. package/dist/conditions.js +88 -0
  29. package/dist/config.js +69 -6
  30. package/dist/context.js +155 -0
  31. package/dist/ecosystem-cli.js +88 -0
  32. package/dist/egress.js +98 -0
  33. package/dist/examples.js +92 -0
  34. package/dist/explain-cli.js +64 -0
  35. package/dist/explain.js +131 -0
  36. package/dist/extensions.js +231 -0
  37. package/dist/function-sources.js +49 -5
  38. package/dist/function-worker.js +3 -1
  39. package/dist/functions.js +84 -13
  40. package/dist/guest-api.js +29 -3
  41. package/dist/index.js +40 -6
  42. package/dist/init-with.js +165 -0
  43. package/dist/interchange-cli.js +42 -0
  44. package/dist/interchange.js +189 -0
  45. package/dist/manifest.js +109 -0
  46. package/dist/match.js +2 -2
  47. package/dist/mcp-authoring.js +147 -0
  48. package/dist/mcp.js +97 -0
  49. package/dist/observability.js +7 -21
  50. package/dist/operator-host.js +29 -0
  51. package/dist/plugins.js +12 -0
  52. package/dist/policies/agents.js +2 -2
  53. package/dist/policies/cache.js +8 -3
  54. package/dist/policies/compression.js +2 -1
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies.js +1 -1
  57. package/dist/policy.js +56 -15
  58. package/dist/prerender.js +100 -41
  59. package/dist/project-tests.js +3 -3
  60. package/dist/provider-verification.js +92 -0
  61. package/dist/proxy.js +44 -0
  62. package/dist/readiness.js +34 -11
  63. package/dist/recipes.js +41 -0
  64. package/dist/route-diff.js +106 -0
  65. package/dist/router.js +45 -7
  66. package/dist/runtime.js +164 -64
  67. package/dist/sandbox.js +48 -0
  68. package/dist/scaffold.js +0 -0
  69. package/dist/schema-query.js +62 -0
  70. package/dist/scripts/operational-drills.js +12 -54
  71. package/dist/server.js +3 -29
  72. package/dist/signals.js +24 -0
  73. package/dist/site.js +0 -0
  74. package/dist/tooling.js +96 -0
  75. package/dist/trusted-functions.js +210 -0
  76. package/dist/types/adapters.d.ts +7 -4
  77. package/dist/types/agent-lists.d.ts +0 -1
  78. package/dist/types/agents-guide.d.ts +17 -0
  79. package/dist/types/authoring-files.d.ts +10 -0
  80. package/dist/types/aws.d.ts +3 -1
  81. package/dist/types/build-cloudflare.d.ts +1 -0
  82. package/dist/types/build-static.d.ts +43 -0
  83. package/dist/types/bulk.d.ts +27 -0
  84. package/dist/types/capabilities.d.ts +64 -0
  85. package/dist/types/capability-query.d.ts +24 -0
  86. package/dist/types/catalog.d.ts +65 -0
  87. package/dist/types/client-address.d.ts +0 -1
  88. package/dist/types/compliance-rules/baseline.d.ts +1 -9
  89. package/dist/types/compliance-rules/privacy.d.ts +1 -4
  90. package/dist/types/compliance-rules/shared.d.ts +0 -2
  91. package/dist/types/compliance-rules/strict.d.ts +0 -5
  92. package/dist/types/compliance.d.ts +0 -3
  93. package/dist/types/conditions.d.ts +19 -0
  94. package/dist/types/config.d.ts +21 -2
  95. package/dist/types/context.d.ts +66 -0
  96. package/dist/types/ecosystem-cli.d.ts +17 -0
  97. package/dist/types/egress.d.ts +46 -0
  98. package/dist/types/examples.d.ts +50 -0
  99. package/dist/types/explain-cli.d.ts +11 -0
  100. package/dist/types/explain.d.ts +95 -0
  101. package/dist/types/extensions.d.ts +177 -0
  102. package/dist/types/function-sources.d.ts +9 -0
  103. package/dist/types/functions.d.ts +48 -5
  104. package/dist/types/guest-api.d.ts +1 -0
  105. package/dist/types/index.d.ts +36 -6
  106. package/dist/types/init-with.d.ts +30 -0
  107. package/dist/types/interchange-cli.d.ts +16 -0
  108. package/dist/types/interchange.d.ts +42 -0
  109. package/dist/types/manifest.d.ts +79 -0
  110. package/dist/types/match.d.ts +1 -0
  111. package/dist/types/mcp-authoring.d.ts +92 -0
  112. package/dist/types/mcp.d.ts +12 -0
  113. package/dist/types/observability.d.ts +3 -14
  114. package/dist/types/operator-host.d.ts +8 -0
  115. package/dist/types/plugins.d.ts +2 -0
  116. package/dist/types/policies/agents.d.ts +0 -2
  117. package/dist/types/policies/compression.d.ts +2 -0
  118. package/dist/types/policies/security.d.ts +0 -1
  119. package/dist/types/policy.d.ts +15 -4
  120. package/dist/types/project-tests.d.ts +3 -2
  121. package/dist/types/provider-verification.d.ts +53 -0
  122. package/dist/types/proxy.d.ts +21 -0
  123. package/dist/types/readiness.d.ts +10 -3
  124. package/dist/types/recipes.d.ts +30 -0
  125. package/dist/types/route-diff.d.ts +27 -0
  126. package/dist/types/router.d.ts +2 -1
  127. package/dist/types/runtime.d.ts +11 -27
  128. package/dist/types/sandbox.d.ts +12 -0
  129. package/dist/types/scaffold.d.ts +0 -2
  130. package/dist/types/schema-query.d.ts +12 -0
  131. package/dist/types/server.d.ts +1 -4
  132. package/dist/types/signals.d.ts +25 -0
  133. package/dist/types/site.d.ts +0 -1
  134. package/dist/types/tooling.d.ts +115 -0
  135. package/dist/types/trusted-functions.d.ts +29 -0
  136. package/dist/types/types.d.ts +71 -7
  137. package/dist/types/typescript-authoring.d.ts +12 -0
  138. package/dist/types/vercel.d.ts +3 -1
  139. package/dist/types/verify-deployment.d.ts +47 -0
  140. package/dist/types.js +37 -5
  141. package/dist/typescript-authoring.js +142 -0
  142. package/dist/vercel.js +4 -3
  143. package/dist/verify-deployment.js +270 -0
  144. package/docs/AI-AUTHORING.md +232 -15
  145. package/docs/AWS.md +4 -4
  146. package/docs/BEST-PRACTICES.md +3 -2
  147. package/docs/BULK.md +79 -0
  148. package/docs/CAPABILITIES.md +192 -0
  149. package/docs/CAPACITY.md +129 -32
  150. package/docs/CI.md +142 -0
  151. package/docs/CLOUDFLARE.md +1 -2
  152. package/docs/COMPLIANCE.md +6 -9
  153. package/docs/CONDITIONS.md +74 -0
  154. package/docs/DEPLOYMENT-CHECKS.md +108 -0
  155. package/docs/EGRESS.md +125 -0
  156. package/docs/EXTENSIONS.md +398 -0
  157. package/docs/FRAMEWORK.md +198 -0
  158. package/docs/FUNCTION-SECURITY.md +129 -32
  159. package/docs/INSTALL.md +45 -12
  160. package/docs/INTERCHANGE.md +134 -0
  161. package/docs/LOAD-TESTING.md +4 -4
  162. package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
  163. package/docs/MIDDLEWARE.md +29 -16
  164. package/docs/MONITORING.md +2 -19
  165. package/docs/NEXT-PHASE-PLAN.md +98 -0
  166. package/docs/NEXT-STEPS.md +634 -0
  167. package/docs/OBSERVABILITY.md +11 -18
  168. package/docs/OPEN-DECISIONS.md +212 -0
  169. package/docs/OPERATIONAL-PROOF.md +30 -31
  170. package/docs/OPERATIONS.md +29 -35
  171. package/docs/PLUGINS.md +37 -0
  172. package/docs/POLICIES.md +23 -309
  173. package/docs/PRERENDER.md +41 -1
  174. package/docs/PROJECT-DIRECTION.md +75 -8
  175. package/docs/PROVIDER-VERIFICATION.md +84 -0
  176. package/docs/READINESS.md +21 -1
  177. package/docs/README.md +87 -34
  178. package/docs/RECIPES.md +99 -0
  179. package/docs/RELEASE-READINESS.md +57 -35
  180. package/docs/RELEASE-SECURITY.md +116 -7
  181. package/docs/RESILIENCE.md +16 -15
  182. package/docs/ROUTING.md +8 -10
  183. package/docs/SANDBOX-REVIEW.md +19 -6
  184. package/docs/SCAFFOLDING.md +0 -2
  185. package/docs/SECURITY-AUDIT.md +41 -1
  186. package/docs/SPECIFICATION.md +150 -29
  187. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
  188. package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
  189. package/docs/SPIKE-CORE-LAYERING.md +337 -0
  190. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
  191. package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
  192. package/docs/SPIKE-EXTENSIONS.md +6 -0
  193. package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
  194. package/docs/SPIKE-MONOREPO.md +322 -0
  195. package/docs/STANDARDS.md +150 -142
  196. package/docs/STARTERS.md +21 -1
  197. package/docs/STATIC.md +94 -0
  198. package/docs/TOOLING.md +295 -0
  199. package/docs/TUNNELS.md +0 -3
  200. package/docs/TYPESCRIPT-AUTHORING.md +82 -0
  201. package/docs/TYPESCRIPT.md +25 -4
  202. package/docs/USABILITY-REVIEW.md +129 -0
  203. package/docs/VERCEL.md +4 -5
  204. package/docs/VERSION-ALIGNMENT.md +205 -0
  205. package/docs/YAML-GUIDE.md +15 -479
  206. package/docs/YAML-REFERENCE.md +143 -22
  207. package/docs/policies/agents.md +1 -1
  208. package/docs/policies/cache.md +13 -0
  209. package/docs/policies/contract.md +52 -0
  210. package/docs/policies/hardened.md +56 -0
  211. package/docs/policies/interoperability.md +169 -0
  212. package/docs/policies/operations.md +45 -0
  213. package/docs/yaml/assets.md +36 -0
  214. package/docs/yaml/conditions.md +20 -0
  215. package/docs/yaml/functions.md +160 -0
  216. package/docs/yaml/middleware.md +29 -0
  217. package/docs/yaml/organization.md +74 -0
  218. package/docs/yaml/policies.md +37 -0
  219. package/docs/yaml/redirects.md +64 -0
  220. package/docs/yaml/responses.md +57 -0
  221. package/docs/yaml/site.md +24 -0
  222. package/examples/assets/example.yaml +17 -0
  223. package/examples/aws/example.yaml +20 -0
  224. package/examples/cloudflare/example.yaml +19 -0
  225. package/examples/compliance/example.yaml +11 -0
  226. package/examples/conditions/README.md +12 -0
  227. package/examples/conditions/example.yaml +19 -0
  228. package/examples/conditions/tests/requests.json +13 -0
  229. package/examples/conditions/urlcode.yaml +24 -0
  230. package/examples/cookbook/README.md +8 -4
  231. package/examples/cookbook/example.yaml +17 -0
  232. package/examples/cookbook/functions/catalog.mjs +3 -0
  233. package/examples/cookbook/functions/fail.mjs +4 -0
  234. package/examples/cookbook/functions/items.mjs +3 -0
  235. package/examples/cookbook/functions/profile.mjs +3 -0
  236. package/examples/cookbook/functions/resource.mjs +3 -0
  237. package/examples/cookbook/functions/status.mjs +3 -0
  238. package/examples/cookbook/middleware/auth.mjs +48 -0
  239. package/examples/cookbook/middleware/body.mjs +15 -0
  240. package/examples/cookbook/middleware/bucket.mjs +29 -0
  241. package/examples/cookbook/middleware/cors.mjs +21 -0
  242. package/examples/cookbook/middleware/debug.mjs +13 -0
  243. package/examples/cookbook/middleware/envelope.mjs +11 -0
  244. package/examples/cookbook/middleware/errors.mjs +11 -0
  245. package/examples/cookbook/middleware/etag.mjs +18 -0
  246. package/examples/cookbook/middleware/locale.mjs +20 -0
  247. package/examples/cookbook/middleware/maintenance.mjs +10 -0
  248. package/examples/cookbook/middleware/methods.mjs +15 -0
  249. package/examples/cookbook/middleware/negotiate.mjs +20 -0
  250. package/examples/cookbook/middleware/referer.mjs +12 -0
  251. package/examples/cookbook/middleware/request-id.mjs +16 -0
  252. package/examples/cookbook/route-index.json +676 -0
  253. package/examples/cookbook/routes/middleware.yaml +126 -0
  254. package/examples/cookbook/tests/requests.json +526 -0
  255. package/examples/cookbook/urlcode.yaml +1 -0
  256. package/examples/egress/README.md +22 -0
  257. package/examples/egress/example.yaml +19 -0
  258. package/examples/egress/urlcode.yaml +19 -0
  259. package/examples/extensions/README.md +7 -0
  260. package/examples/extensions/example.yaml +21 -0
  261. package/examples/extensions/urlcode.yaml +25 -0
  262. package/examples/monitoring/example.yaml +8 -0
  263. package/examples/prerender/README.md +2 -2
  264. package/examples/prerender/example.yaml +16 -0
  265. package/examples/provider-conformance/README.md +12 -0
  266. package/examples/provider-conformance/example.yaml +14 -0
  267. package/examples/provider-conformance/urlcode.yaml +34 -0
  268. package/examples/tunnel/example.yaml +8 -0
  269. package/examples/vercel/example.yaml +19 -0
  270. package/llms-full.txt +3084 -0
  271. package/llms.txt +61 -21
  272. package/package.json +36 -7
  273. package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
  274. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
  275. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
  276. package/recipes/authenticated-json-api/README.md +51 -0
  277. package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
  278. package/recipes/authenticated-json-api/recipe.yaml +34 -0
  279. package/recipes/authenticated-json-api/tests/requests.json +39 -0
  280. package/recipes/authenticated-json-api/urlcode.yaml +12 -0
  281. package/recipes/contact-form/README.md +25 -0
  282. package/recipes/contact-form/functions/contact.mjs +17 -0
  283. package/recipes/contact-form/recipe.yaml +33 -0
  284. package/recipes/contact-form/tests/requests.json +47 -0
  285. package/recipes/contact-form/urlcode.yaml +18 -0
  286. package/recipes/cors-api/README.md +16 -0
  287. package/recipes/cors-api/functions/items.mjs +3 -0
  288. package/recipes/cors-api/middleware/cors.mjs +21 -0
  289. package/recipes/cors-api/recipe.yaml +26 -0
  290. package/recipes/cors-api/tests/requests.json +65 -0
  291. package/recipes/cors-api/urlcode.yaml +12 -0
  292. package/recipes/health-page/README.md +13 -0
  293. package/recipes/health-page/recipe.yaml +23 -0
  294. package/recipes/health-page/tests/requests.json +36 -0
  295. package/recipes/health-page/urlcode.yaml +19 -0
  296. package/recipes/json-api/README.md +6 -0
  297. package/recipes/json-api/functions/echo.mjs +3 -0
  298. package/recipes/json-api/recipe.yaml +25 -0
  299. package/recipes/json-api/tests/requests.json +34 -0
  300. package/recipes/json-api/urlcode.yaml +12 -0
  301. package/recipes/middleware/README.md +34 -0
  302. package/recipes/middleware/functions/catalog.mjs +3 -0
  303. package/recipes/middleware/functions/fail.mjs +4 -0
  304. package/recipes/middleware/functions/items.mjs +3 -0
  305. package/recipes/middleware/functions/profile.mjs +3 -0
  306. package/recipes/middleware/functions/resource.mjs +3 -0
  307. package/recipes/middleware/functions/status.mjs +3 -0
  308. package/recipes/middleware/middleware/auth.mjs +48 -0
  309. package/recipes/middleware/middleware/body.mjs +15 -0
  310. package/recipes/middleware/middleware/bucket.mjs +29 -0
  311. package/recipes/middleware/middleware/cors.mjs +21 -0
  312. package/recipes/middleware/middleware/debug.mjs +13 -0
  313. package/recipes/middleware/middleware/envelope.mjs +11 -0
  314. package/recipes/middleware/middleware/errors.mjs +11 -0
  315. package/recipes/middleware/middleware/etag.mjs +18 -0
  316. package/recipes/middleware/middleware/locale.mjs +20 -0
  317. package/recipes/middleware/middleware/maintenance.mjs +10 -0
  318. package/recipes/middleware/middleware/methods.mjs +15 -0
  319. package/recipes/middleware/middleware/negotiate.mjs +20 -0
  320. package/recipes/middleware/middleware/referer.mjs +12 -0
  321. package/recipes/middleware/middleware/request-id.mjs +16 -0
  322. package/recipes/middleware/public/guide.txt +1 -0
  323. package/recipes/middleware/recipe.yaml +50 -0
  324. package/recipes/middleware/tests/requests.json +528 -0
  325. package/recipes/middleware/urlcode.yaml +127 -0
  326. package/recipes/protected-download/README.md +22 -0
  327. package/recipes/protected-download/files/report.txt +1 -0
  328. package/recipes/protected-download/recipe.yaml +31 -0
  329. package/recipes/protected-download/tests/requests.json +32 -0
  330. package/recipes/protected-download/urlcode.yaml +15 -0
  331. package/recipes/redirect/README.md +7 -0
  332. package/recipes/redirect/recipe.yaml +25 -0
  333. package/recipes/redirect/tests/requests.json +19 -0
  334. package/recipes/redirect/urlcode.yaml +9 -0
  335. package/recipes/static-plus-api/README.md +15 -0
  336. package/recipes/static-plus-api/functions/info.mjs +3 -0
  337. package/recipes/static-plus-api/public/assets/index.html +3 -0
  338. package/recipes/static-plus-api/public/assets/site.css +1 -0
  339. package/recipes/static-plus-api/public/index.html +8 -0
  340. package/recipes/static-plus-api/recipe.yaml +29 -0
  341. package/recipes/static-plus-api/tests/requests.json +56 -0
  342. package/recipes/static-plus-api/urlcode.yaml +23 -0
  343. package/recipes/typescript/README.md +8 -0
  344. package/recipes/typescript/functions/hello.ts +5 -0
  345. package/recipes/typescript/recipe.yaml +23 -0
  346. package/recipes/typescript/tests/requests.json +18 -0
  347. package/recipes/typescript/urlcode.yaml +5 -0
  348. package/recipes/webhook-receiver/README.md +20 -0
  349. package/recipes/webhook-receiver/functions/receive.mjs +16 -0
  350. package/recipes/webhook-receiver/recipe.yaml +27 -0
  351. package/recipes/webhook-receiver/tests/requests.json +59 -0
  352. package/recipes/webhook-receiver/urlcode.yaml +23 -0
  353. package/schemas/recipe.schema.json +139 -0
  354. package/schemas/urlcode.schema.json +659 -110
  355. package/skills/urlcode/SKILL.md +119 -0
  356. package/starters/default/.github/workflows/urlcode.yml +23 -0
  357. package/starters/default/.mcp.json +12 -0
  358. package/starters/default/AGENTS.md +79 -0
  359. package/starters/default/urlcode.yaml +0 -1
  360. package/dist/link-api.js +0 -136
  361. package/dist/link-cli.js +0 -141
  362. package/dist/link-events.js +0 -76
  363. package/dist/link-records.js +0 -31
  364. package/dist/link-store-worker.js +0 -150
  365. package/dist/link-store.js +0 -250
  366. package/dist/management-policy.js +0 -41
  367. package/dist/sqlite-version.js +0 -6
  368. package/dist/types/link-api.d.ts +0 -30
  369. package/dist/types/link-cli.d.ts +0 -36
  370. package/dist/types/link-events.d.ts +0 -27
  371. package/dist/types/link-records.d.ts +0 -11
  372. package/dist/types/link-store-worker.d.ts +0 -1
  373. package/dist/types/link-store.d.ts +0 -130
  374. package/dist/types/management-policy.d.ts +0 -9
  375. package/dist/types/sqlite-version.d.ts +0 -1
  376. package/docs/DYNAMIC-LINKS.md +0 -561
  377. package/docs/MANAGEMENT-SECURITY.md +0 -82
  378. package/examples/live-links/README.md +0 -11
  379. package/examples/live-links/tests/requests.json +0 -6
  380. package/examples/live-links/urlcode.yaml +0 -16
package/dist/cli.js CHANGED
@@ -1,23 +1,34 @@
1
1
  #!/usr/bin/env node
2
+ import { getCapabilities, formatCapabilities } from './capabilities.js';
3
+ import { getCapability, formatCapability } from './capability-query.js';
4
+ import { getSchemaFragment } from './schema-query.js';
5
+ import { stringify as stringifyYaml } from 'yaml';
2
6
  import { auditProject, benchmarkProject } from './readiness.js';
3
7
 
4
8
  import { parseArgs } from 'node:util';
5
9
  import { createRuntime } from './runtime.js';
10
+ import { loadOperatorHost } from './operator-host.js';
11
+
6
12
  import { startServer } from './server.js';
7
13
 
8
14
  import {scaffoldProject} from './scaffold.js';
9
15
  import { initProject, addRedirect } from './authoring.js';
16
+ import { initProjectWith, parseWithNames } from './init-with.js';
10
17
  import { runProjectTests } from './project-tests.js';
18
+ import { verifyDeployment, failLevels } from './verify-deployment.js';
19
+
11
20
  import { loadOperatorPolicy, prepareFunctionSnapshot, requestedPermissions } from './policy.js';
12
21
  import { loadDocument } from './config.js';
13
- import {parseLinkBinding,runLinkCommand,linkPoolOptions} from './link-cli.js';
22
+ import { describeExtensions } from './tooling.js';
23
+
14
24
  import { ConfigError, HttpError } from './errors.js';
15
- import {supportsConcurrentWal} from './sqlite-version.js';
16
25
  import { registry as policyRegistry } from './policies.js';
17
26
  import { loadComplianceRules, profileNames as complianceProfiles } from './compliance.js';
27
+ import { parseRouteSnapshot, diffRoutes, renderRouteDiff } from './route-diff.js';
28
+ import { readFile } from 'node:fs/promises';
18
29
 
19
- const usage = `URLCode 0.3.0 — local/self-hosted runtime
20
- urlcode init <directory>
30
+ const usage = `URLCode 0.4.0-alpha.2 — local/self-hosted runtime
31
+ urlcode init <directory> [--with auth,admin] # --with: layered site from installed @jimhoyd/urlcode-<name> packages
21
32
  urlcode scaffold [--project directory] [--dry-run]
22
33
  urlcode validate [--project directory] [--local] [--origin https://links.example] # origin: absolute URLs in site.* files
23
34
  urlcode dev [--project directory] [--port 3000] [--host 127.0.0.1]
@@ -28,35 +39,53 @@ const usage = `URLCode 0.3.0 — local/self-hosted runtime
28
39
  policies: [--trusted-proxies 10.0.0.0/8,fd00::/8] # peers allowed to set X-Forwarded-For for client policies
29
40
  urlcode add <destination-url> [--alias short-code] [--project directory]
30
41
  urlcode test [--project directory] [--origin https://links.example]
31
- urlcode build --target cloudflare [--project directory] [--out dist/cloudflare] [--origin https://links.example]
42
+ urlcode build --target cloudflare|static [--project directory] [--out dist/cloudflare|dist/static] [--origin https://links.example]
43
+ # static: redirect/respond/page/static/download only, compiled for S3 + CloudFront; no server, see docs/STATIC.md
32
44
  urlcode routes [--project directory] [--origin https://links.example]
45
+ diff: [--compare previous-routes.json] [--format json|markdown] # added/removed/changed routes against an earlier report; always exits 0
33
46
  urlcode audit [--project directory] [--expect-routes 2]
34
47
  compliance: [--compliance baseline|strict|privacy|none] [--compliance-rules /absolute/rules.mjs] [--compliance-ignore id,id]
35
48
  [--compliance-warn] [--origin https://links.example] [--request-log minimal|detailed] # declare the deployment under review
36
49
  urlcode benchmark [--project directory] [--requests 1000] [--concurrency 2] [--seconds 30] [--max-p95-ms 50]
37
50
  [--warmup 50] [--target https://links.example] # target measures a running deployment, not a local snapshot
38
- urlcode permissions [--project directory] # inspect requested bindings; grants nothing
39
- urlcode links init|create|get|list|update|delete|export|import|api --store /absolute/links.sqlite [--collection links]
40
- create/update: --destination https://example.com [--code abc] [--status 302] [--enabled true] [--expires UTC]
41
- update/delete: --code abc --if-version N (update replaces all mutable fields)
42
- export: consistent NDJSON snapshot to stdout [--collection links] [--page-size 100]
43
- import: --input /absolute/export.ndjson restores into empty collections (versions are reassigned)
44
- api: --auth-file /operator/management.json (or legacy --token-file /operator/token) --port 3001 (separate authenticated server)
51
+ urlcode verify-deployment --target https://links.example [--project directory] [--origin https://links.example]
52
+ [--expect-routes 2] [--expect-metrics] [--timeout-ms 10000] [--fail-on high|medium|low|info|none]
53
+ [--compliance baseline|strict|privacy|none] [--compliance-rules ...] [--compliance-ignore id,id] [--compliance-warn]
54
+ # compares the running deployment's responses with what this project declares; never follows redirects, no --insecure
55
+ urlcode permissions [--project directory] # inspect requested bindings and egress origins; grants nothing
56
+ urlcode explain [/route] [--project directory] [--target self-hosted|cloudflare|aws|vercel|static] [--host-file ...] [--json]
57
+ # effective methods, handler, middleware, inputs, policies, cache outcome, bindings and target support from the compiled configuration
58
+ urlcode manifest [--project directory] [--json] # generated semantic manifest; build writes the same file as manifest.json
59
+ urlcode extensions [--project directory] [--host-file /absolute/operator/host.mjs] [--json] # registered contracts and schemas; executes trusted host code, activates nothing
60
+ urlcode import [netlify|cloudflare|vercel|netlify-toml] <file> [--format csv|json|yaml] [--out new-file] [--dry-run] [--report json]
61
+ urlcode export --target netlify|cloudflare|vercel|netlify-toml|csv|json|yaml [--project directory] [--out new-file] [--report json]
62
+ conversion: [--accept-provider-differences] # explicit non-lossless migration candidate; exact behavior requires runtime
63
+ urlcode recipes [list|search <text>|show <name>|add <name> --out new-directory] [--dry-run] [--json]
64
+ urlcode examples [list|search <text>] [--json] # bundled runnable examples and the cookbook route index
65
+ urlcode build-typescript [--project directory] --out new-directory [--dry-run]
66
+ urlcode bulk-import csv|json|yaml <file> --out new-directory [--dry-run]
67
+ urlcode verify-provider --target self-hosted|aws|vercel|cloudflare --origin https://owned-fixture.example
68
+ [--timeout-ms 3000] [--release label] [--git-commit sha] # explicitly invokes synthetic deployment probes
69
+ urlcode mcp [--project directory] [--allow-authoring] [--host-file ...] # bounded stdio tooling; --allow-authoring adds project-confined authoring tools, host file adds get_extensions
70
+ urlcode capabilities [--target self-hosted|cloudflare|aws|vercel|static] [--json]
71
+ urlcode capabilities <name> [--json] # one catalog entry: schema fragment, constraints, grants, targets, bundled uses
72
+ urlcode schema <path> [--json|--yaml] # schema fragment for route, redirect, policies.cache, site.sitemap, ...
73
+ urlcode context [--project directory] [--target self-hosted|cloudflare|aws|vercel|static] [--budget 500] [--json] [--stats]
74
+ # compact facts for an authoring agent from the compiled project; --stats compares estimated tokens with the docs
45
75
  urlcode doctor
46
- serve/dev/validate/test/routes/audit/benchmark: --link-store links=/absolute/links.sqlite
47
- Store pool controls: --link-readers 2 (1–8), --link-read-limit 32, --link-write-limit 32 (1–32 each)
76
+ serve/dev/validate/test/routes/audit/benchmark/explain/context/extensions/mcp: --host-file /absolute/operator/host.mjs (trusted code outside project)
48
77
  Dev loads .env.local and watches; serve does neither. Functions run in WASM isolation; external bindings require --policy outside the project.
49
78
  `;
50
79
  const print = (value ) => process.stdout.write(typeof value === 'string' ? value : JSON.stringify(value) + '\n');
51
80
  const options = {
52
- project:{ type:'string', default:'.' },
81
+ json:{ type:'boolean' }, yaml:{ type:'boolean' }, report:{type:'string'}, 'accept-provider-differences':{type:'boolean'},
82
+ project:{ type:'string', default:'.' }, 'host-file':{type:'string'}, with:{type:'string'},
53
83
  port:{ type:'string' }, host:{ type:'string', default:'127.0.0.1' },
54
84
  'expect-routes':{type:'string'}, requests:{type:'string'}, concurrency:{type:'string'}, seconds:{type:'string'}, 'max-p95-ms':{type:'string'}, warmup:{type:'string'}, target:{type:'string'},
55
- 'link-readers':{type:'string'}, 'link-read-limit':{type:'string'}, 'link-write-limit':{type:'string'},
56
85
  workers:{type:'string'}, 'function-timeout-ms':{type:'string'}, 'max-response-bytes':{type:'string'}, 'max-body-bytes':{type:'string'},
57
86
  'max-in-flight':{type:'string'}, 'max-in-flight-health':{type:'string'}, 'request-log':{type:'string'}, 'trust-request-id':{type:'boolean'}, 'trusted-proxies':{type:'string'}, metrics:{type:'boolean'},
58
- 'link-store':{type:'string'}, store:{type:'string'}, collection:{type:'string'}, code:{type:'string'}, destination:{type:'string'}, status:{type:'string'}, enabled:{type:'string'}, expires:{type:'string'}, 'if-version':{type:'string'}, limit:{type:'string'}, after:{type:'string'}, 'token-file':{type:'string'}, 'auth-file':{type:'string'}, input:{type:'string'}, 'page-size':{type:'string'},
59
- out:{type:'string'}, 'dry-run':{type:'boolean'}, compliance:{type:'string'}, 'compliance-rules':{type:'string'}, 'compliance-ignore':{type:'string'}, 'compliance-warn':{type:'boolean'}, policy:{ type:'string' }, origin:{ type:'string' }, alias:{ type:'string' }, local:{ type:'boolean' }, help:{ type:'boolean', short:'h' },
87
+ release:{type:'string'}, 'git-commit':{type:'string'}, 'timeout-ms':{type:'string'}, 'fail-on':{type:'string'}, 'expect-metrics':{type:'boolean'},
88
+ budget:{type:'string'}, stats:{type:'boolean'}, out:{type:'string'}, 'dry-run':{type:'boolean'}, compare:{type:'string'}, format:{type:'string'}, compliance:{type:'string'}, 'compliance-rules':{type:'string'}, 'compliance-ignore':{type:'string'}, 'compliance-warn':{type:'boolean'}, policy:{ type:'string' }, origin:{ type:'string' }, alias:{ type:'string' }, local:{ type:'boolean' }, 'allow-authoring':{ type:'boolean' }, help:{ type:'boolean', short:'h' },
60
89
  } ;
61
90
 
62
91
 
@@ -91,21 +120,67 @@ async function complianceOptions(values )
91
120
  if(!complianceProfiles.includes(profile))throw new ConfigError(`Use --compliance ${complianceProfiles.join('|')}`);
92
121
  const ignore=(values['compliance-ignore'] ?? '').split(',').map(id=>id.trim()).filter(Boolean);
93
122
  const operator=await loadComplianceRules(values['compliance-rules'],values.project);
94
- const host={requestLog:values['request-log'] ?? 'minimal',linkEvents:false};
123
+ const host={requestLog:values['request-log'] ?? 'minimal'};
95
124
  if(!['minimal','detailed'].includes(host.requestLog))throw new ConfigError('Use --request-log minimal or detailed');
96
125
  return {profile,ignore,origin:values.origin,host,rules:operator?.rules ?? [],disable:operator?.disable ?? [],override:operator?.override ?? {}};
97
126
  }
127
+ function formatExtensions(report ) {
128
+ const lines = [`Project revision: ${report.projectSha256}`];
129
+ for (const item of report.declared) lines.push(`Declared: ${item.name} (contract ${item.version}) ${item.registered ? 'registered' : report.hostLoaded ? 'NOT registered by the host file' : 'schemas need --host-file'}`, ` mounts: ${item.mounts.join(', ') || '(none)'}`, ` policy routes: ${item.policyRoutes.join(', ') || '(none)'}`);
130
+ if (!report.declared.length) lines.push('Declared: (none)');
131
+ for (const item of report.extensions) lines.push(`Registered: ${item.name} (contract ${item.version}; targets ${item.targets.join(', ') || '(none)'}; ${item.declared ? 'declared' : 'not declared'}; revision ${item.revisionPinned ? 'pinned' : 'NOT pinned'})`,
132
+ ` mounts: ${item.mounts.join(', ') || '(none)'}`, ` policy routes: ${item.policyRoutes.join(', ') || '(none)'}`, ` credential headers: ${item.credentialHeaders.join(', ') || '(none)'}`,
133
+ ` configuration schema: ${JSON.stringify(item.schema)}`, ` policy schema: ${item.policySchema ? JSON.stringify(item.policySchema) : '(none)'}`);
134
+ lines.push(report.note);
135
+ return lines.join('\n') + '\n';
136
+ }
98
137
  const errorMessages = { ERR_PARSE_ARGS_UNKNOWN_OPTION:'Unknown option; use --help', EEXIST:'Destination or edit lock already exists', ENOENT:'Required file or directory not found', EADDRINUSE:'Port is already in use', EACCES:'Permission denied' };
138
+ let operatorHost = {};
139
+ let serving = false;
99
140
  try {
100
141
  const { values, positionals } = parseArgs({ allowPositionals:true, options });
101
142
  const [command, arg, ...extra] = positionals;
102
- values.port ??= command==='links' && arg==='api' ? '3001' : '3000';
143
+ values.port ??= '3000';
103
144
  if (values.help || !command) print(usage);
104
145
  else {
105
- if (extra.length || (!['init','add','links'].includes(command) && arg)) throw new ConfigError('Unexpected positional arguments');
106
- if(command==='links'){await runLinkCommand(arg,values,print);}else{
146
+ if (values['host-file'] !== undefined) {
147
+ if (!['serve','dev','validate','test','routes','audit','benchmark','explain','context','extensions','mcp'].includes(command)) throw new ConfigError('--host-file is only supported by serve/dev/validate/test/routes/audit/benchmark/explain/context/extensions/mcp');
148
+ // The MCP server and context command load and release the host themselves.
149
+ if (command !== 'mcp' && command !== 'context') operatorHost = await loadOperatorHost(values['host-file'], values.project);
150
+ }
151
+ if (values.with !== undefined && command !== 'init') throw new ConfigError('--with is only supported by init');
152
+ if (values['allow-authoring'] && command !== 'mcp') throw new ConfigError('--allow-authoring is only supported by mcp');
153
+ const hostOptions = { extensions: operatorHost.extensions, plugins: operatorHost.plugins };
154
+ if ((!['import','recipes','recipe','examples','example','bulk-import'].includes(command) && extra.length) || (!['init','add','import','recipes','recipe','examples','example','bulk-import','explain','capabilities','schema'].includes(command) && arg)) throw new ConfigError('Unexpected positional arguments');
155
+
156
+ if(command==='import'||command==='export'){
157
+ const { runInterchange } = await import('./interchange-cli.js');
158
+ const converted = await runInterchange(command,positionals.slice(1),{project:values.project,target:values.target,format:values.format,out:values.out,report:values.report,dryRun:values['dry-run'],acceptProviderDifferences:values['accept-provider-differences']});
159
+ print(converted.text); if(!converted.report.ok)process.exitCode=1;
160
+ }else if(['recipes','recipe','examples','example','build-typescript','bulk-import','verify-provider','mcp'].includes(command)){
161
+ const {runEcosystemCommand}=await import('./ecosystem-cli.js');
162
+ await runEcosystemCommand(command,positionals.slice(1),values,print);
163
+ }else if(command==='explain'||command==='manifest'){
164
+ const {runExplainCommand}=await import('./explain-cli.js');
165
+ const exitCode=await runExplainCommand(command,arg,{project:values.project,target:values.target,origin:values.origin,json:values.json,extensions:operatorHost.extensions},print);
166
+ if(exitCode)process.exitCode=exitCode;
167
+ }else if(command==='capabilities'){
168
+ if(arg!==undefined){ if(values.target!==undefined)throw new ConfigError('--target applies to the full catalog, not one entry'); const entry=getCapability(arg); print(values.json ? entry : formatCapability(entry)); }
169
+ else { const catalog = getCapabilities(values.target); print(values.json ? catalog : formatCapabilities(catalog)); }
170
+ }else if(command==='schema'){
171
+ if(arg===undefined)throw new ConfigError('Use urlcode schema <path>');
172
+ const fragment=getSchemaFragment(arg);
173
+ print(values.yaml ? stringifyYaml(fragment.schema) : JSON.stringify(fragment.schema,null,2)+'\n');
174
+ }else if(command==='context'){
175
+ if (values.budget !== undefined && !/^\d{1,9}$/.test(values.budget)) throw new ConfigError('Invalid --budget');
176
+ const { buildContext, renderContext, estimateTokens, documentationTokens } = await import('./context.js');
177
+ const context = await buildContext(values.project, { target:values.target, hostFile:values['host-file'], ...(values.budget === undefined ? {} : { budget:Number(values.budget) }) });
178
+ const text = values.json ? JSON.stringify(context) + '\n' : renderContext(context);
179
+ print(text);
180
+ // Estimates only (characters / 4); a tokenizer is not a dependency. Stats go to stderr so stdout stays parseable.
181
+ if (values.stats) process.stderr.write(JSON.stringify({ event:'stats', estimate:'characters/4', documentationTokens:await documentationTokens(), contextTokens:estimateTokens(text) }) + '\n');
182
+ }else{
107
183
  const permissions = await loadOperatorPolicy(values.policy,values.project);
108
- const linkStore=parseLinkBinding(values['link-store'],linkPoolOptions(values));
109
184
  switch (command) {
110
185
  case 'routes': case 'audit': case 'benchmark': {
111
186
  const number = (key ,fallback ) => {
@@ -117,12 +192,22 @@ try {
117
192
  const expected=number('expect-routes');
118
193
  if(expected!==undefined && !Number.isSafeInteger(expected))throw new ConfigError('Expected route count must be an integer');
119
194
  const compliance=command==='audit'?await complianceOptions(values):undefined;
195
+ const format=values.format ?? 'json';
196
+ if(!['json','markdown'].includes(format))throw new ConfigError('Use --format json or markdown');
197
+ if(values.format!==undefined && values.compare===undefined)throw new ConfigError('--format applies to routes --compare');
120
198
  const started=performance.now();
121
- const app=await startServer({project:values.project,port:0,local:true,permissions,linkStore,origin:values.origin,log:()=>{}});
199
+ const app=await startServer({...hostOptions,project:values.project,port:0,local:true,permissions,origin:values.origin,log:()=>{}});
122
200
  const startupMs=performance.now()-started;
123
201
  try {
124
202
  if(command==='routes') {
125
- const plan=app.testPlan(); print({routes:plan.inventory.length,dynamicLinks:plan.dynamicLinks,inventory:plan.inventory,policies:plan.policies});
203
+ const plan=app.testPlan();
204
+ if(values.compare===undefined) print({routes:plan.inventory.length,inventory:plan.inventory,policies:plan.policies});
205
+ else {
206
+ // The diff reports; it never judges, so the exit code stays 0.
207
+ const before=parseRouteSnapshot(JSON.parse(await readFile(values.compare,'utf8'))); // file boundary: an earlier `routes` report
208
+ const diff=diffRoutes(before,{inventory:plan.inventory,policies:plan.policies});
209
+ print(format==='markdown'?renderRouteDiff(diff):diff);
210
+ }
126
211
  } else if(command==='audit') {
127
212
  const report=await auditProject(app,{expectRoutes:expected,log:print,compliance});print(report);if(!report.ready)process.exitCode=1;
128
213
  if(report.compliance && !report.compliance.pass && !values['compliance-warn'])process.exitCode=1;
@@ -134,43 +219,85 @@ try {
134
219
  } finally {await app.close();}
135
220
  break;
136
221
  }
222
+ case 'verify-deployment': {
223
+ if (!values.target) throw new ConfigError('Provide --target https://host');
224
+ const integer = (key ) => {
225
+ const value = values[key];
226
+ if (value === undefined) return undefined;
227
+ if (!/^\d{1,9}$/.test(value)) throw new ConfigError(`Invalid --${key}`);
228
+ return Number(value);
229
+ };
230
+ const failOn = values['fail-on'] ?? 'high';
231
+ const isFailOn = (value ) => (failLevels ).includes(value);
232
+ if (!isFailOn(failOn)) throw new ConfigError(`Use --fail-on ${failLevels.join('|')}`);
233
+ const report = await verifyDeployment(values.project, { target: values.target, origin: values.origin, expectRoutes: integer('expect-routes'), timeoutMs: integer('timeout-ms'),
234
+ expectMetrics: values['expect-metrics'], failOn, compliance: await complianceOptions(values), complianceWarn: values['compliance-warn'], permissions, log: print });
235
+ print(report); if (!report.pass) process.exitCode = 1; break;
236
+ }
237
+ case 'verify-provider': {
238
+ const target=values.target;
239
+ if(target!=='self-hosted'&&target!=='aws'&&target!=='vercel'&&target!=='cloudflare')throw new ConfigError('Provide --target self-hosted|aws|vercel|cloudflare');
240
+ if(!values.origin)throw new ConfigError('Provide --origin https://owned-fixture.example');
241
+ if(values['timeout-ms']!==undefined&&!/^\d{1,5}$/.test(values['timeout-ms']))throw new ConfigError('Invalid --timeout-ms');
242
+ const {verifyProviderDeployment}=await import('./provider-verification.js');
243
+ const report=await verifyProviderDeployment(target,values.origin,{
244
+ ...(values['timeout-ms']===undefined?{}:{timeoutMs:Number(values['timeout-ms'])}),
245
+ ...(values.release===undefined?{}:{release:values.release}),
246
+ ...(values['git-commit']===undefined?{}:{gitCommit:values['git-commit']}),
247
+ });
248
+ print(report);if(!report.pass)process.exitCode=1;break;
249
+ }
137
250
  case 'build': {
138
- if (values.target !== 'cloudflare') throw new ConfigError('Use --target cloudflare');
251
+ if (values.target === 'static') {
252
+ const { buildStatic } = await import('./build-static.js');
253
+ print({ event:'built', ...await buildStatic(values.project,{ out:values.out, origin:values.origin }) }); break;
254
+ }
255
+ if (values.target !== 'cloudflare') throw new ConfigError('Use --target cloudflare or static');
139
256
  const { buildCloudflare } = await import('./build-cloudflare.js');
140
257
  print({ event:'built', ...await buildCloudflare(values.project,{ out:values.out, origin:values.origin }) }); break;
141
258
  }
142
259
  case 'scaffold':
143
260
  print(await scaffoldProject(values.project,{dryRun:values['dry-run']}));break;
261
+ case 'extensions': {
262
+ const report = await describeExtensions(values.project, values['host-file'] === undefined ? undefined : operatorHost.extensions ?? []);
263
+ print(values.json ? report : formatExtensions(report)); break;
264
+ }
144
265
  case 'permissions': {
145
266
  const loaded = await loadDocument(values.project);
146
267
  print(requestedPermissions(loaded,await prepareFunctionSnapshot(loaded))); break;
147
268
  }
148
269
  case 'init':
149
270
  if (!arg) throw new ConfigError('Provide a new project directory');
150
- await initProject(arg); print({ event:'created' }); break;
271
+ if (values.with === undefined) { await initProject(arg); print({ event:'created' }); break; }
272
+ {
273
+ const created = await initProjectWith(arg, parseWithNames(values.with));
274
+ print({ event:'created', ...created, review:`Review ${created.project}/urlcode.yaml and pin its revision explicitly (for example PROJECT_SHA256=${created.projectSha256}); re-review after any project change` });
275
+ }
276
+ break;
151
277
  case 'validate': {
152
- const runtime = await createRuntime(values.project, { local:values.local, permissions, linkStore, origin:values.origin });
153
- print({ event:'valid', dynamicLinks:runtime.testPlan().dynamicLinks, routes:runtime.count, version:runtime.version }); await runtime.close(); break;
278
+ const runtime = await createRuntime(values.project, { ...hostOptions, local:values.local, permissions, origin:values.origin });
279
+ print({ event:'valid', routes:runtime.count, version:runtime.version }); await runtime.close(); break;
154
280
  }
155
281
  case 'add':
156
282
  if (!arg) throw new ConfigError('Provide an HTTP(S) destination URL');
157
283
  print({ event:'added', path:await addRedirect(values.project,arg,values.alias) }); break;
158
284
  case 'test': {
159
- const result = await runProjectTests(values.project, { log:print, permissions, linkStore, origin:values.origin });
285
+ const result = await runProjectTests(values.project, { ...hostOptions, log:print, permissions, origin:values.origin });
160
286
  print(result); if (result.failed) process.exitCode = 1; break;
161
287
  }
162
288
  case 'doctor':
163
- print({ node:process.version, sqlite:process.versions.sqlite, liveLinks:supportsConcurrentWal(process.versions.sqlite), platform:process.platform, architecture:process.arch, runtime:'node-process', functionSandbox:'quickjs-wasm', network:false, filesystem:false, providers:[], policies:Object.keys(policyRegistry), license:'Apache-2.0' }); break;
289
+ print({ node:process.version, platform:process.platform, architecture:process.arch, runtime:'node-process', functionSandbox:'quickjs-wasm', network:false, filesystem:false, guestNetwork:false, hostEgress:'revision-pinned-origin-grants', tooling:['recipes','examples','bulk-import','build-typescript','mcp','verify-provider'], providers:[], capabilityTargets:getCapabilities().targets, policies:Object.keys(policyRegistry), license:'Apache-2.0' }); break;
164
290
  case 'dev': case 'serve': {
165
291
  const port = Number(values.port);
166
292
  if (!/^\d+$/.test(values.port) || !Number.isInteger(port) || port < 0 || port > 65535) throw new ConfigError('Invalid port');
167
293
  if (command === 'serve' && values.local) throw new ConfigError('serve never reads local dotenv files');
168
- const app = await startServer({ project:values.project, host:values.host, port,
169
- local:command === 'dev', watch:command === 'dev', origin:values.origin, permissions, linkStore,
294
+ const app = await startServer({ ...hostOptions, project:values.project, host:values.host, port,
295
+ local:command === 'dev', watch:command === 'dev', origin:values.origin, permissions,
170
296
  ...serverCapacity(values) });
171
297
  print({ event:'listening', address:app.address.address, port:app.address.port, mode:command, origin:app.origin });
298
+ serving = true;
172
299
  let stopping = false;
173
- const stop = async () => { if (stopping) return; stopping = true; await app.close(); };
300
+ const stop = async () => { if (stopping) return; stopping = true; try { await app.close(); } finally { await operatorHost.close?.(); } };
174
301
  process.once('SIGINT',stop); process.once('SIGTERM',stop);
175
302
  break;
176
303
  }
@@ -182,4 +309,8 @@ try {
182
309
  const code = typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code : undefined;
183
310
  const message = (error instanceof ConfigError || error instanceof HttpError) ? error.message : ((code !== undefined ? errorMessages[code] : undefined) || 'Operation failed; check project files, module dependencies and command options');
184
311
  process.stderr.write(JSON.stringify({ event:'error', message }) + '\n'); process.exitCode = 1;
312
+ } finally {
313
+ if (!serving) {
314
+ try { await operatorHost.close?.(); } catch { process.stderr.write('Operator host cleanup failed\n'); process.exitCode = 1; }
315
+ }
185
316
  }
@@ -57,7 +57,7 @@ function within(address , { bytes, prefix } ) {
57
57
  return true;
58
58
  }
59
59
 
60
- export function isTrustedProxy(address , trusted ) {
60
+ function isTrustedProxy(address , trusted ) {
61
61
  return trusted.some(range => within(address, range));
62
62
  }
63
63
 
@@ -1,9 +1,9 @@
1
- import { oshp, rfc9111, rfc9110, rfc6585, rfc9309, breach, management, active, functionLike, hasSecrets, yamlHeader, handlerCacheControl, directive, emittedSecurityHeaders } from './shared.js';
1
+ import { oshp, rfc9111, rfc9110, rfc6585, rfc9309, breach, active, functionLike, hasSecrets, yamlHeader, handlerCacheControl, directive, emittedSecurityHeaders } from './shared.js';
2
2
 
3
3
 
4
4
  export const profile = 'baseline';
5
5
 
6
- export const securityHeaders = {
6
+ const securityHeaders = {
7
7
  id: 'oshp/security-headers', title: 'Every active route carries a security headers profile', standard: oshp, severity: 'medium', appliesTo: 'route',
8
8
  check({ route, effective }) {
9
9
  if (!active(route) || effective.security) return [];
@@ -11,7 +11,7 @@ export const securityHeaders = {
11
11
  },
12
12
  };
13
13
 
14
- export const hstsOrigin = {
14
+ const hstsOrigin = {
15
15
  id: 'oshp/hsts-origin', title: 'HSTS is only reachable on an https origin', standard: { ...oshp, section: 'Strict-Transport-Security' }, severity: 'low', appliesTo: 'project',
16
16
  check({ plan, policies, origin }) {
17
17
  const emitting = plan.inventory.filter(route => active(route) && emittedSecurityHeaders(policies[route.path], plan.policies?.[route.path]).has('strict-transport-security'));
@@ -23,7 +23,7 @@ export const hstsOrigin = {
23
23
  },
24
24
  };
25
25
 
26
- export const secretsCompression = {
26
+ const secretsCompression = {
27
27
  id: 'breach/secrets-compression', title: 'Routes bound to secrets are never compressed', standard: breach, severity: 'high', appliesTo: 'route',
28
28
  check({ route, config, effective }) {
29
29
  if (!hasSecrets(config) || effective.compression?.allowWithSecrets !== true) return [];
@@ -31,7 +31,7 @@ export const secretsCompression = {
31
31
  },
32
32
  };
33
33
 
34
- export const secretsNoStore = {
34
+ const secretsNoStore = {
35
35
  id: 'rfc9111/secrets-no-store', title: 'Routes bound to secrets declare no-store or private caching', standard: { ...rfc9111, section: '5.2.2.5 no-store, 5.2.2.7 private' }, severity: 'medium', appliesTo: 'route',
36
36
  check({ route, config, effective }) {
37
37
  if (!hasSecrets(config)) return [];
@@ -47,7 +47,7 @@ export const secretsNoStore = {
47
47
  },
48
48
  };
49
49
 
50
- export const cacheControlDeclared = {
50
+ const cacheControlDeclared = {
51
51
  id: 'rfc9111/cache-control-declared', title: 'Every declared response states its Cache-Control', standard: rfc9111, severity: 'low', appliesTo: 'route',
52
52
  check({ route, config, effective }) {
53
53
  if (!active(route) || effective.cache) return [];
@@ -60,7 +60,7 @@ export const cacheControlDeclared = {
60
60
  },
61
61
  };
62
62
 
63
- export const throttleFunctions = {
63
+ const throttleFunctions = {
64
64
  id: 'rfc6585/throttle-functions', title: 'Function and middleware routes declare a request budget', standard: rfc6585, severity: 'medium', appliesTo: 'route',
65
65
  check({ route, effective }) {
66
66
  if (!active(route) || !functionLike(route) || effective.throttle) return [];
@@ -78,7 +78,7 @@ export const robots = {
78
78
  },
79
79
  };
80
80
 
81
- export const expiredRoutes = {
81
+ const expiredRoutes = {
82
82
  id: 'rfc9110/expired-routes', title: 'Expired routes still present in YAML', standard: { ...rfc9110, section: '15.5.11 410 Gone' }, severity: 'info', appliesTo: 'project',
83
83
  check({ plan }) {
84
84
  const expired = plan.inventory.filter(route => route.state === 'expired').map(route => route.path);
@@ -87,12 +87,4 @@ export const expiredRoutes = {
87
87
  },
88
88
  };
89
89
 
90
- export const managementPrivate = {
91
- id: 'ops/management-private', title: 'Management and probe listeners stay private', standard: management, severity: 'info', appliesTo: 'project',
92
- check({ document }) {
93
- if (document.dynamicLinks !== true) return [];
94
- return [{ message: 'dynamicLinks is enabled; the link management API and the /_urlcode probes are meant for a private bind', remediation: 'Run `urlcode links api` on a private interface with an operator auth file, and keep /_urlcode/health and /_urlcode/ready internal' }];
95
- },
96
- };
97
-
98
- export const rules = Object.freeze([securityHeaders, hstsOrigin, secretsCompression, secretsNoStore, cacheControlDeclared, throttleFunctions, robots, expiredRoutes, managementPrivate]);
90
+ export const rules = Object.freeze([securityHeaders, hstsOrigin, secretsCompression, secretsNoStore, cacheControlDeclared, throttleFunctions, robots, expiredRoutes]);
@@ -1,14 +1,13 @@
1
- import { monitoring, linkChannel, active } from './shared.js';
1
+ import { monitoring, active } from './shared.js';
2
2
 
3
3
 
4
4
  export const profile = 'privacy';
5
5
 
6
6
  // These rules check the host settings the operator declares for the
7
- // deployment under review (`host.requestLog`, `host.linkEvents`,
8
- // `host.includeCode`), because the runtime's log guarantees are settings of
9
- // the serving process, not of the project. An undeclared setting is reported
10
- // as unknown rather than assumed.
11
- export const requestLogMinimal = {
7
+ // deployment under review (`host.requestLog`), because the runtime's log
8
+ // guarantees are settings of the serving process, not of the project. An
9
+ // undeclared setting is reported as unknown rather than assumed.
10
+ const requestLogMinimal = {
12
11
  id: 'privacy/request-log-minimal', title: 'Request log records stay minimal', standard: monitoring, severity: 'medium', appliesTo: 'project',
13
12
  check({ host }) {
14
13
  if (host.requestLog === 'minimal') return [];
@@ -17,17 +16,7 @@ export const requestLogMinimal = {
17
16
  },
18
17
  };
19
18
 
20
- export const linkEventsOff = {
21
- id: 'privacy/link-events-off', title: 'Link events stay off unless declared', standard: linkChannel, severity: 'medium', appliesTo: 'project',
22
- check({ document, host }) {
23
- if (document.dynamicLinks !== true || host.linkEvents === false) return [];
24
- if (host.linkEvents === null) return [{ severity: 'info', message: 'dynamicLinks is enabled and the deployment did not declare whether the link event channel is on', remediation: 'Declare host.linkEvents (and host.includeCode) for the deployment under review' }];
25
- if (host.includeCode === true) return [{ severity: 'high', message: 'The link event channel is on with includeCode, so every event names the short code a person followed', remediation: 'Leave includeCode off; the channel then redacts the code and reports only route, outcome and timing' }];
26
- return [{ message: 'The link event channel is on, so per-click records leave the process', remediation: 'Enable linkEvents only for a declared click-collection purpose and keep the collector bounded' }];
27
- },
28
- };
29
-
30
- export const detailedParameters = {
19
+ const detailedParameters = {
31
20
  id: 'privacy/detailed-log-parameters', title: 'No detailed logging on parameterised routes', standard: monitoring, severity: 'low', appliesTo: 'route',
32
21
  check({ route, config, host }) {
33
22
  if (host.requestLog !== 'detailed' || !active(route) || !((config.parameters?.length ?? 0) > 0)) return [];
@@ -35,4 +24,4 @@ export const detailedParameters = {
35
24
  },
36
25
  };
37
26
 
38
- export const rules = Object.freeze([requestLogMinimal, linkEventsOff, detailedParameters]);
27
+ export const rules = Object.freeze([requestLogMinimal, detailedParameters]);
@@ -12,8 +12,6 @@ export const rfc6585 = Object.freeze({ name: 'RFC 6585 Additional HTTP
12
12
  export const rfc9309 = Object.freeze({ name: 'RFC 9309 Robots Exclusion Protocol', reference: 'https://www.rfc-editor.org/rfc/rfc9309' });
13
13
  export const breach = Object.freeze({ name: 'BREACH (compression side channel)', reference: 'https://www.breachattack.com/', section: 'Mitigations' });
14
14
  export const monitoring = Object.freeze({ name: 'URLCode logging guarantees', reference: 'docs/MONITORING.md', section: 'Log records' });
15
- export const linkChannel = Object.freeze({ name: 'URLCode link event channel', reference: 'docs/MONITORING.md', section: 'The link event channel' });
16
- export const management = Object.freeze({ name: 'URLCode management security', reference: 'docs/MANAGEMENT-SECURITY.md' });
17
15
  export const agentLists = Object.freeze({ name: 'URLCode bundled agent lists', reference: 'docs/policies/agents.md', section: 'Bundled lists' });
18
16
 
19
17
  export const active = (route ) => route.state === 'active';
@@ -6,7 +6,7 @@ export const profile = 'strict';
6
6
 
7
7
  // The runtime caps a response at 16 KiB of headers; the security policy
8
8
  // bounds its own static headers at half of that so the handler keeps room.
9
- export const headerBudgetBytes = 8192;
9
+ const headerBudgetBytes = 8192;
10
10
 
11
11
  export const csp = {
12
12
  id: 'oshp/csp', title: 'Every active route emits a Content-Security-Policy', standard: { ...oshp, section: 'Content-Security-Policy' }, severity: 'medium', appliesTo: 'route',
@@ -16,7 +16,7 @@ export const csp = {
16
16
  },
17
17
  };
18
18
 
19
- export const throttleAll = {
19
+ const throttleAll = {
20
20
  id: 'rfc6585/throttle-all', title: 'Every active route declares a request budget', standard: rfc6585, severity: 'medium', appliesTo: 'route',
21
21
  check({ route, effective }) {
22
22
  if (!active(route) || functionLike(route) || effective.throttle) return [];
@@ -24,7 +24,7 @@ export const throttleAll = {
24
24
  },
25
25
  };
26
26
 
27
- export const listsPinned = {
27
+ const listsPinned = {
28
28
  id: 'agents/lists-pinned', title: 'Agent deny and allow lists are pinned to a revision', standard: agentLists, severity: 'low', appliesTo: 'route',
29
29
  check({ route, effective, policy }) {
30
30
  if (!active(route) || !effective.agents) return [];
@@ -39,7 +39,7 @@ export const listsPinned = {
39
39
  },
40
40
  };
41
41
 
42
- export const redirectHttps = {
42
+ const redirectHttps = {
43
43
  id: 'rfc9110/redirect-https', title: 'Redirect targets are https', standard: { ...rfc9110, section: '15.4 Redirection 3xx' }, severity: 'medium', appliesTo: 'route',
44
44
  check({ route, config }) {
45
45
  const url = config.redirect?.url;
@@ -48,7 +48,7 @@ export const redirectHttps = {
48
48
  },
49
49
  };
50
50
 
51
- export const headerBudget = {
51
+ const headerBudget = {
52
52
  id: 'http/header-budget', title: 'Declared response headers stay under the static budget', standard: { ...rfc9110, section: '5.4 Field Limits' }, severity: 'low', appliesTo: 'route',
53
53
  check({ route, config, effective, policy }) {
54
54
  if (!active(route)) return [];
@@ -37,7 +37,7 @@ import * as privacy from './compliance-rules/privacy.js';
37
37
 
38
38
 
39
39
  /** The host settings under review, null where the operator declared nothing. */
40
-
40
+
41
41
 
42
42
 
43
43
 
@@ -58,14 +58,14 @@ import * as privacy from './compliance-rules/privacy.js';
58
58
 
59
59
 
60
60
 
61
-
61
+
62
62
 
63
63
 
64
64
 
65
65
  export const severities = Object.freeze(['high','medium','low','info']);
66
66
  export const idPattern = /^[a-z][a-z0-9-]{0,31}\/[a-z][a-z0-9-]{0,63}$/;
67
67
  const referencePattern = /^(?:https?:\/\/\S+|RFC ?\d{3,5}|docs\/[A-Za-z0-9./-]+\.md)$/;
68
- const hostKeys = ['requestLog','linkEvents','includeCode'];
68
+ const hostKeys = ['requestLog'];
69
69
  const isSeverity = (value ) => (severities ).includes(value);
70
70
 
71
71
 
@@ -147,11 +147,9 @@ export async function loadComplianceRules(file , project
147
147
 
148
148
  function validateHost(host = {}) {
149
149
  assert(host && typeof host === 'object' && !Array.isArray(host) && Object.keys(host).every(key => hostKeys.includes(key)), `Compliance host settings accept ${hostKeys.join(', ')}`);
150
- const { requestLog, linkEvents, includeCode } = host ;
150
+ const { requestLog } = host ;
151
151
  assert(requestLog === undefined || requestLog === 'minimal' || requestLog === 'detailed', 'Compliance host.requestLog must be minimal or detailed');
152
- assert(linkEvents === undefined || typeof linkEvents === 'boolean', 'Compliance host.linkEvents must be a boolean');
153
- assert(includeCode === undefined || typeof includeCode === 'boolean', 'Compliance host.includeCode must be a boolean');
154
- return { requestLog: requestLog ?? null, linkEvents: linkEvents ?? null, includeCode: includeCode ?? null };
152
+ return { requestLog: requestLog ?? null };
155
153
  }
156
154
 
157
155
  function finding(rule , raw , route ) {
@@ -209,7 +207,7 @@ export async function runCompliance(app , { rules = [], profile =
209
207
  return {
210
208
  profile, rules: set.length, ruleIds: set.map(rule => rule.id), ignored: [...ignore], findings, counts, pass: counts.high === 0,
211
209
  evidence: {
212
- routes: plan.inventory.length, active, dynamicLinks: plan.dynamicLinks === true,
210
+ routes: plan.inventory.length, active,
213
211
  policies: [...new Set(Object.values(policies).flatMap(Object.keys))].sort(),
214
212
  files: loaded.files.map(file => relative(loaded.root, file)),
215
213
  origin: origin ?? null, target, host: hostSettings,