@jimhoyd/urlcode 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CONTRIBUTING.md +80 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +20 -0
  4. package/README.md +342 -0
  5. package/ROADMAP.md +248 -0
  6. package/SECURITY.md +40 -0
  7. package/data/agents/LICENSES/ai-robots-txt.txt +24 -0
  8. package/data/agents/LICENSES/crawler-user-agents.txt +23 -0
  9. package/data/agents/ai-crawlers.json +1240 -0
  10. package/data/agents/crawlers.json +10515 -0
  11. package/data/agents/index.d.ts +3 -0
  12. package/data/agents/index.js +2453 -0
  13. package/data/agents/monitoring.json +1520 -0
  14. package/data/agents/seo.json +3865 -0
  15. package/dist/BUILD-MANIFEST.json +60 -0
  16. package/dist/adapters.js +59 -0
  17. package/dist/agent-lists.js +59 -0
  18. package/dist/assets.js +129 -0
  19. package/dist/authoring.js +68 -0
  20. package/dist/aws.js +110 -0
  21. package/dist/build-cloudflare.js +164 -0
  22. package/dist/cli.js +185 -0
  23. package/dist/client-address.js +85 -0
  24. package/dist/cloudflare.js +165 -0
  25. package/dist/compliance-rules/baseline.js +98 -0
  26. package/dist/compliance-rules/privacy.js +38 -0
  27. package/dist/compliance-rules/shared.js +62 -0
  28. package/dist/compliance-rules/strict.js +61 -0
  29. package/dist/compliance.js +219 -0
  30. package/dist/config-worker.js +9 -0
  31. package/dist/config.js +172 -0
  32. package/dist/errors.js +8 -0
  33. package/dist/function-sources.js +54 -0
  34. package/dist/function-worker.js +105 -0
  35. package/dist/functions.js +174 -0
  36. package/dist/guest-api.js +114 -0
  37. package/dist/header-validation.js +18 -0
  38. package/dist/http-policy.js +74 -0
  39. package/dist/http-response.js +84 -0
  40. package/dist/index.js +11 -0
  41. package/dist/link-api.js +136 -0
  42. package/dist/link-cli.js +141 -0
  43. package/dist/link-events.js +76 -0
  44. package/dist/link-records.js +31 -0
  45. package/dist/link-store-worker.js +150 -0
  46. package/dist/link-store.js +250 -0
  47. package/dist/logging.js +22 -0
  48. package/dist/management-policy.js +41 -0
  49. package/dist/match.js +124 -0
  50. package/dist/observability.js +242 -0
  51. package/dist/plugins.js +74 -0
  52. package/dist/policies/agents.js +248 -0
  53. package/dist/policies/cache.js +297 -0
  54. package/dist/policies/compression.js +187 -0
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies/throttle.js +131 -0
  57. package/dist/policies.js +142 -0
  58. package/dist/policy.js +55 -0
  59. package/dist/prerender.js +165 -0
  60. package/dist/project-tests.js +23 -0
  61. package/dist/readiness.js +216 -0
  62. package/dist/router.js +166 -0
  63. package/dist/runtime.js +244 -0
  64. package/dist/scaffold.js +0 -0
  65. package/dist/scripts/operational-drills.js +75 -0
  66. package/dist/server.js +270 -0
  67. package/dist/site.js +0 -0
  68. package/dist/sqlite-version.js +6 -0
  69. package/dist/types/adapters.d.ts +12 -0
  70. package/dist/types/agent-lists.d.ts +5 -0
  71. package/dist/types/assets.d.ts +13 -0
  72. package/dist/types/authoring.d.ts +2 -0
  73. package/dist/types/aws.d.ts +35 -0
  74. package/dist/types/build-cloudflare.d.ts +14 -0
  75. package/dist/types/cli.d.ts +2 -0
  76. package/dist/types/client-address.d.ts +9 -0
  77. package/dist/types/cloudflare.d.ts +75 -0
  78. package/dist/types/compliance-rules/baseline.d.ts +12 -0
  79. package/dist/types/compliance-rules/privacy.d.ts +6 -0
  80. package/dist/types/compliance-rules/shared.d.ts +24 -0
  81. package/dist/types/compliance-rules/strict.d.ts +9 -0
  82. package/dist/types/compliance.d.ts +119 -0
  83. package/dist/types/config-worker.d.ts +1 -0
  84. package/dist/types/config.d.ts +21 -0
  85. package/dist/types/errors.d.ts +7 -0
  86. package/dist/types/function-sources.d.ts +21 -0
  87. package/dist/types/function-worker.d.ts +1 -0
  88. package/dist/types/functions.d.ts +92 -0
  89. package/dist/types/guest-api.d.ts +16 -0
  90. package/dist/types/header-validation.d.ts +2 -0
  91. package/dist/types/http-policy.d.ts +39 -0
  92. package/dist/types/http-response.d.ts +43 -0
  93. package/dist/types/index.d.ts +11 -0
  94. package/dist/types/link-api.d.ts +30 -0
  95. package/dist/types/link-cli.d.ts +36 -0
  96. package/dist/types/link-events.d.ts +27 -0
  97. package/dist/types/link-records.d.ts +11 -0
  98. package/dist/types/link-store-worker.d.ts +1 -0
  99. package/dist/types/link-store.d.ts +130 -0
  100. package/dist/types/logging.d.ts +9 -0
  101. package/dist/types/management-policy.d.ts +9 -0
  102. package/dist/types/match.d.ts +81 -0
  103. package/dist/types/observability.d.ts +104 -0
  104. package/dist/types/plugins.d.ts +27 -0
  105. package/dist/types/policies/agents.d.ts +86 -0
  106. package/dist/types/policies/cache.d.ts +82 -0
  107. package/dist/types/policies/compression.d.ts +41 -0
  108. package/dist/types/policies/security.d.ts +37 -0
  109. package/dist/types/policies/throttle.d.ts +55 -0
  110. package/dist/types/policies.d.ts +36 -0
  111. package/dist/types/policy.d.ts +21 -0
  112. package/dist/types/prerender.d.ts +31 -0
  113. package/dist/types/project-tests.d.ts +13 -0
  114. package/dist/types/readiness.d.ts +121 -0
  115. package/dist/types/router.d.ts +11 -0
  116. package/dist/types/runtime.d.ts +91 -0
  117. package/dist/types/scaffold.d.ts +17 -0
  118. package/dist/types/server.d.ts +37 -0
  119. package/dist/types/site.d.ts +12 -0
  120. package/dist/types/sqlite-version.d.ts +1 -0
  121. package/dist/types/types.d.ts +332 -0
  122. package/dist/types/vercel.d.ts +12 -0
  123. package/dist/types.js +177 -0
  124. package/dist/vercel.js +65 -0
  125. package/docs/AI-AUTHORING.md +113 -0
  126. package/docs/ASSETS.md +106 -0
  127. package/docs/AWS.md +77 -0
  128. package/docs/BEST-PRACTICES.md +267 -0
  129. package/docs/CAPACITY.md +208 -0
  130. package/docs/CLOUDFLARE.md +110 -0
  131. package/docs/COMPLIANCE.md +242 -0
  132. package/docs/DYNAMIC-LINKS.md +561 -0
  133. package/docs/FUNCTION-SECURITY.md +113 -0
  134. package/docs/HTTP.md +129 -0
  135. package/docs/INSTALL.md +90 -0
  136. package/docs/LOAD-TESTING.md +91 -0
  137. package/docs/LOCAL-DEVELOPMENT.md +99 -0
  138. package/docs/MANAGEMENT-SECURITY.md +82 -0
  139. package/docs/MIDDLEWARE.md +83 -0
  140. package/docs/MONITORING.md +132 -0
  141. package/docs/OBSERVABILITY.md +229 -0
  142. package/docs/OPERATIONAL-PROOF.md +42 -0
  143. package/docs/OPERATIONS.md +207 -0
  144. package/docs/ORGANIZATION.md +135 -0
  145. package/docs/PERFORMANCE.md +72 -0
  146. package/docs/PLUGINS.md +234 -0
  147. package/docs/POLICIES.md +489 -0
  148. package/docs/PRERENDER.md +193 -0
  149. package/docs/PROJECT-DIRECTION.md +51 -0
  150. package/docs/READINESS.md +125 -0
  151. package/docs/README.md +58 -0
  152. package/docs/RELEASE-READINESS.md +95 -0
  153. package/docs/RELEASE-SECURITY.md +72 -0
  154. package/docs/RESILIENCE.md +160 -0
  155. package/docs/ROUTING.md +95 -0
  156. package/docs/SANDBOX-REVIEW.md +59 -0
  157. package/docs/SCAFFOLDING.md +72 -0
  158. package/docs/SECURITY-AUDIT.md +124 -0
  159. package/docs/SITE.md +150 -0
  160. package/docs/SPECIFICATION.md +232 -0
  161. package/docs/SPIKE-EXTENSIONS.md +475 -0
  162. package/docs/STANDARDS.md +303 -0
  163. package/docs/STARTERS.md +51 -0
  164. package/docs/TUNNELS.md +75 -0
  165. package/docs/TYPESCRIPT.md +102 -0
  166. package/docs/VERCEL.md +107 -0
  167. package/docs/YAML-GUIDE.md +521 -0
  168. package/docs/YAML-REFERENCE.md +328 -0
  169. package/docs/policies/agents.md +182 -0
  170. package/docs/policies/cache.md +139 -0
  171. package/docs/policies/compression.md +168 -0
  172. package/docs/policies/security.md +160 -0
  173. package/docs/policies/throttle.md +103 -0
  174. package/examples/assets/.env.example +1 -0
  175. package/examples/assets/.gitattributes +2 -0
  176. package/examples/assets/Makefile +30 -0
  177. package/examples/assets/README.md +18 -0
  178. package/examples/assets/functions/hello.mjs +3 -0
  179. package/examples/assets/gitignore.template +7 -0
  180. package/examples/assets/public/about.html +2 -0
  181. package/examples/assets/public/assets/example.txt +1 -0
  182. package/examples/assets/public/guide.txt +1 -0
  183. package/examples/assets/tests/requests.json +44 -0
  184. package/examples/assets/urlcode.yaml +27 -0
  185. package/examples/aws/README.md +13 -0
  186. package/examples/aws/handler.mjs +5 -0
  187. package/examples/aws/public/index.html +3 -0
  188. package/examples/aws/public/notes.txt +1 -0
  189. package/examples/aws/template.yaml +33 -0
  190. package/examples/aws/tests/requests.json +9 -0
  191. package/examples/aws/urlcode.yaml +19 -0
  192. package/examples/cloudflare/README.md +18 -0
  193. package/examples/cloudflare/package.json +14 -0
  194. package/examples/cloudflare/tests/requests.json +9 -0
  195. package/examples/cloudflare/urlcode.yaml +24 -0
  196. package/examples/cloudflare/wrangler.toml +5 -0
  197. package/examples/compliance/README.md +20 -0
  198. package/examples/compliance/rules.mjs +65 -0
  199. package/examples/cookbook/README.md +21 -0
  200. package/examples/cookbook/functions/choice.mjs +4 -0
  201. package/examples/cookbook/functions/echo.mjs +3 -0
  202. package/examples/cookbook/functions/hello.mjs +3 -0
  203. package/examples/cookbook/functions/text.mjs +3 -0
  204. package/examples/cookbook/middleware/headers.mjs +6 -0
  205. package/examples/cookbook/public/about.html +1 -0
  206. package/examples/cookbook/public/assets/index.html +1 -0
  207. package/examples/cookbook/public/assets/site.css +1 -0
  208. package/examples/cookbook/public/favicon.svg +1 -0
  209. package/examples/cookbook/public/guide.txt +1 -0
  210. package/examples/cookbook/public/llms.txt +6 -0
  211. package/examples/cookbook/routes/code.yaml +55 -0
  212. package/examples/cookbook/routes/files.yaml +17 -0
  213. package/examples/cookbook/routes/policies.yaml +28 -0
  214. package/examples/cookbook/routes/redirects.yaml +38 -0
  215. package/examples/cookbook/routes/responses.yaml +26 -0
  216. package/examples/cookbook/tests/requests.json +254 -0
  217. package/examples/cookbook/urlcode.yaml +22 -0
  218. package/examples/live-links/README.md +11 -0
  219. package/examples/live-links/tests/requests.json +6 -0
  220. package/examples/live-links/urlcode.yaml +16 -0
  221. package/examples/monitoring/blackbox-jobs.yaml +37 -0
  222. package/examples/monitoring/prometheus-rules.yaml +88 -0
  223. package/examples/monitoring/prometheus-scrape.yaml +20 -0
  224. package/examples/monitoring/vector.toml +65 -0
  225. package/examples/prerender/README.md +48 -0
  226. package/examples/prerender/functions/page.mjs +6 -0
  227. package/examples/prerender/middleware/template.mjs +29 -0
  228. package/examples/prerender/prerender.d.mts +4 -0
  229. package/examples/prerender/prerender.mjs +63 -0
  230. package/examples/prerender/tests/requests.json +39 -0
  231. package/examples/prerender/urlcode.yaml +50 -0
  232. package/examples/tunnel/dev-with-ngrok.sh +61 -0
  233. package/examples/vercel/README.md +13 -0
  234. package/examples/vercel/api/index.js +5 -0
  235. package/examples/vercel/package.json +8 -0
  236. package/examples/vercel/public/index.html +3 -0
  237. package/examples/vercel/public/notes.txt +1 -0
  238. package/examples/vercel/tests/requests.json +48 -0
  239. package/examples/vercel/urlcode.yaml +19 -0
  240. package/examples/vercel/vercel.json +10 -0
  241. package/llms.txt +46 -0
  242. package/package.json +123 -0
  243. package/schemas/urlcode.schema.json +1230 -0
  244. package/starters/default/.gitattributes +1 -0
  245. package/starters/default/Makefile +30 -0
  246. package/starters/default/README.md +34 -0
  247. package/starters/default/functions/hello.mjs +3 -0
  248. package/starters/default/gitignore.template +8 -0
  249. package/starters/default/middleware/headers.mjs +6 -0
  250. package/starters/default/routes/functions.yaml +20 -0
  251. package/starters/default/routes/marketing/links.yaml +7 -0
  252. package/starters/default/starter.json +5 -0
  253. package/starters/default/tests/requests.json +56 -0
  254. package/starters/default/urlcode.yaml +8 -0
@@ -0,0 +1,242 @@
1
+ # Compliance rules
2
+
3
+ A compliance rule is a standards-referenced check over what a project declares
4
+ and what the runtime knows about it: the parsed YAML, the route inventory and
5
+ policy table from `testPlan()`, the effective policy per route, the public
6
+ origin and the logging settings the operator states for the deployment. The
7
+ runtime ships three built-in profiles; an operator adds, overrides or disables
8
+ rules with code kept outside the project, the same trust boundary as
9
+ [plugins](PLUGINS.md) and the `--policy` binding grant; `urlcode audit` prints
10
+ every finding with the reference it rests on and sets the exit code.
11
+
12
+ ## What it is and is not
13
+
14
+ - It checks **declared configuration and runtime facts**. No rule sends a
15
+ request, reads a binding, runs guest code or inspects a running deployment.
16
+ `oshp/hsts-origin` cannot know your TLS terminator; it knows the origin you
17
+ declared with `--origin`, so declare it truthfully.
18
+ - A passing run is **not a certification**, an audit opinion or evidence that
19
+ a deployment is secure or lawful. It says the declared configuration matches
20
+ a rule set whose references are listed beside each finding. Deployment,
21
+ soak and recovery proof live in [release readiness](RELEASE-READINESS.md).
22
+ - Rules never change behavior. The runtime serves the same responses whether a
23
+ finding exists or not; a rule set is an operator's review checklist in code.
24
+ - The built-in rules only check what the runtime can see. Anything a rule
25
+ cannot verify (an undeclared log level, an unknown origin) is reported as an
26
+ `info` finding saying so, never assumed to pass.
27
+
28
+ ## The rule contract
29
+
30
+ ```js
31
+ export const rules = [{
32
+ id: 'acme/redirect-hosts', // ^[a-z][a-z0-9-]{0,31}/[a-z][a-z0-9-]{0,63}$, unique per run
33
+ title: 'Redirects only leave for approved hosts',
34
+ standard: { name: 'ACME link policy', reference: 'https://example.com/policies/links', section: 'Outbound' },
35
+ severity: 'high', // high | medium | low | info
36
+ appliesTo: 'route', // project | route
37
+ check(context) { return []; }, // findings[]; may be async
38
+ }];
39
+ ```
40
+
41
+ `standard.reference` is a URL, an RFC number (`RFC 9110`) or a path under
42
+ `docs/`. `check` returns an array of findings (an empty array, `undefined`,
43
+ a single finding or an array); a thrown error fails the run with the rule
44
+ named, so a rule cannot silently pass by crashing.
45
+
46
+ A **project** rule runs once with:
47
+
48
+ | Field | Value |
49
+ |---|---|
50
+ | `document` | The parsed and validated `urlcode.yaml` (includes are merged into `routes`) |
51
+ | `routes` | Route configuration by pattern, as written in YAML |
52
+ | `plan` | `testPlan()`: `inventory[]` (`path`, `handler`, `methods`, `middleware`, `policies`, `state`), `policies` (the per-route describe map), `dynamicLinks` |
53
+ | `policies` | `effectivePolicies(document, route)` by pattern: the merged configuration of every policy on each route |
54
+ | `origin` | The declared public origin, or `null` |
55
+ | `target` | `node` unless the caller states another |
56
+ | `host` | `{ requestLog, linkEvents, includeCode }` as declared for the deployment; `null` where undeclared |
57
+
58
+ A **route** rule runs once per inventory entry and additionally receives
59
+ `route` (the inventory entry), `config` (that route's YAML), `policy` (the
60
+ runtime's describe map for the route: `security.emits`, `agents.deny[].revision`,
61
+ `cache.cacheControl`, …) and `effective` (the merged policy configuration).
62
+
63
+ A **finding** is `{ rule, severity, route?, message, remediation, standard }`.
64
+ `severity` defaults to the rule's own; a check may lower or raise it for one
65
+ finding (the privacy rules report an undeclared setting as `info`). The
66
+ runtime fills `rule` and `standard` and the route pattern for route rules.
67
+
68
+ ## Built-in profiles
69
+
70
+ `strict` contains every `baseline` rule plus its own; `privacy` stands alone;
71
+ `none` runs only operator rules. Check derivations reference the runtime's
72
+ own code: the security profile tables in `src/policies/security.ts`, the
73
+ cache and compression secrets handling in `src/policies/cache.ts` and
74
+ `src/policies/compression.ts`, the `no-store` default in
75
+ `src/http-response.ts`, the 16 KiB header cap in `src/http-policy.ts`.
76
+
77
+ ### `baseline`
78
+
79
+ | Rule | Standard | Severity | Checks | Remediation |
80
+ |---|---|---|---|---|
81
+ | `oshp/security-headers` | [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/) | medium | Every active route has an effective `policies.security` | Declare `policies.security: { headers: oshp }` on the project or the route |
82
+ | `oshp/hsts-origin` | OWASP Secure Headers Project, Strict-Transport-Security | low | When any active route's security profile emits HSTS, the declared origin is `https:`; the runtime emits HSTS only then | Serve behind TLS and declare `--origin https://…` |
83
+ | `breach/secrets-compression` | [BREACH](https://www.breachattack.com/) | high | No route binding `secrets` sets `policies.compression.allowWithSecrets` | Remove `allowWithSecrets` (the runtime then skips compression on secret routes) |
84
+ | `rfc9111/secrets-no-store` | [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111) §5.2.2.5, §5.2.2.7 | medium | A route binding `secrets` declares no cache strategy, Cache-Control or handler `cacheControl` other than `no-store`/`private` | Use `no-store` or `private` on secret routes |
85
+ | `rfc9111/cache-control-declared` | RFC 9111 §5.2 | low | `respond` and `redirect` routes declare a `Cache-Control` header or a cache policy; asset handlers declare `cacheControl` (otherwise the runtime defaults `no-store` / assets `no-cache`) | State the intent in `response.headers`, the handler or `policies.cache` |
86
+ | `rfc6585/throttle-functions` | [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585) §4 | medium | Every active function or middleware route has an effective `policies.throttle` | Declare a throttle on the route or the project |
87
+ | `rfc9309/robots` | [RFC 9309](https://www.rfc-editor.org/rfc/rfc9309) | low | Some active route declares `policies.agents`, or an active `/robots.txt` `respond` route exists | Add a `/robots.txt` route or an agents deny list |
88
+ | `rfc9110/expired-routes` | [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110) §15.5.11 | info | Lists routes past `expires` that still answer 410 | Remove them once the 410 window has served its purpose |
89
+ | `ops/management-private` | [Management security](MANAGEMENT-SECURITY.md) | info | Reminder when `dynamicLinks: true`: the management API and `/_urlcode` probes belong on a private bind | Run `links api` privately with an auth file; keep probes internal |
90
+
91
+ ### `strict` (baseline plus)
92
+
93
+ | Rule | Standard | Severity | Checks | Remediation |
94
+ |---|---|---|---|---|
95
+ | `oshp/csp` | OWASP Secure Headers Project, Content-Security-Policy | medium | Every active route emits `Content-Security-Policy` (security profile minus `unset`, plus `set`, or a YAML response header) | Use the `oshp` profile or set the header |
96
+ | `rfc6585/throttle-all` | RFC 6585 §4 | medium | Every active non-function route has an effective throttle (function routes are covered by `throttle-functions`) | Declare `policies.throttle` at the project level |
97
+ | `agents/lists-pinned` | [Bundled agent lists](policies/agents.md) | low | Every agents deny/allow list is a bundled list that carries an upstream revision; project `.json` list files report `revision: project` | Prefer bundled lists or record the revision a list was built from |
98
+ | `rfc9110/redirect-https` | RFC 9110 §15.4 | medium | No active `redirect.url` starts with `http:` | Redirect to https |
99
+ | `http/header-budget` | RFC 9110 §5.4 | low | YAML `response.headers` bytes plus the security profile's static bytes stay at or under 8192, half the runtime's 16 KiB response header cap | Trim headers or the security set |
100
+
101
+ ### `privacy`
102
+
103
+ These rules check deployment settings, so `audit` takes `--request-log` to
104
+ declare the level the deployment uses (the audit process itself always logs
105
+ nothing). The CLI cannot enable link events, so it declares `linkEvents:
106
+ false`; embedders pass `host` themselves. The references are the runtime's
107
+ own [logging guarantees](MONITORING.md): records carry no URL, query, header,
108
+ body or binding, and `detailed` adds only the method and route pattern.
109
+
110
+ | Rule | Standard | Severity | Checks | Remediation |
111
+ |---|---|---|---|---|
112
+ | `privacy/request-log-minimal` | [Monitoring](MONITORING.md), Log records | medium (`info` when undeclared) | `host.requestLog` is `minimal` | Use the default log unless per-route rates are required |
113
+ | `privacy/link-events-off` | [Monitoring](MONITORING.md), The link event channel | medium; `high` with `includeCode` (`info` when undeclared and `dynamicLinks` is on) | The link event channel is off, or on without `includeCode` | Enable it only for a declared purpose; never disclose codes |
114
+ | `privacy/detailed-log-parameters` | [Monitoring](MONITORING.md), Log records | low | With `detailed` logging, no active route takes parameters (records name the pattern and method, never values) | Keep `minimal` on parameterised deployments |
115
+
116
+ ## Writing custom rules
117
+
118
+ [`examples/compliance/rules.mjs`](../examples/compliance/rules.mjs) is a
119
+ complete operator module; it runs against the cookbook:
120
+
121
+ ```sh
122
+ node src/cli.ts audit --project examples/cookbook \
123
+ --compliance baseline --compliance-rules "$PWD/examples/compliance/rules.mjs" --compliance-warn
124
+ ```
125
+
126
+ A module exports up to three names:
127
+
128
+ ```js
129
+ export const rules = [ /* rules to add; an id already in the profile is an error */ ];
130
+ export const disable = ['rfc9110/expired-routes']; // remove built-in or added rules by id
131
+ export const override = { 'oshp/security-headers': { severity: 'high' } }; // shallow-merge fields of an existing rule
132
+ ```
133
+
134
+ Order is fixed: profile rules, then `rules` added, then `override` applied,
135
+ then `disable` removed, then `--compliance-ignore`. The result is validated
136
+ as one set, so an override cannot produce an invalid rule.
137
+
138
+ The module must be an absolute path to an `.mjs`/`.js` file **outside** the
139
+ audited project, checked the way `--policy` is (`realpath` of both, the file
140
+ may not resolve inside the project root). It is imported as trusted operator
141
+ code with the host's privileges, the same standing as a plugin: a project
142
+ cannot ship its own rule set and grade itself. Rules read the context they
143
+ are given; a rule that reaches for the filesystem or the network is a plugin
144
+ in the wrong place.
145
+
146
+ ## CLI
147
+
148
+ ```
149
+ urlcode audit [--project dir] [--compliance baseline|strict|privacy|none]
150
+ [--compliance-rules /absolute/rules.mjs] [--compliance-ignore id,id]
151
+ [--compliance-warn] [--origin https://links.example] [--request-log minimal|detailed]
152
+ ```
153
+
154
+ Without any compliance flag the audit report is unchanged apart from
155
+ `compliance: null`. With one, the report gains a `compliance` section:
156
+
157
+ ```json
158
+ { "profile": "baseline", "rules": 9, "ruleIds": ["oshp/security-headers", "…"], "ignored": [],
159
+ "findings": [{ "rule": "rfc6585/throttle-functions", "severity": "medium", "route": "/hello/{name}",
160
+ "message": "…", "remediation": "…", "standard": { "name": "RFC 6585 …", "reference": "…", "section": "…" } }],
161
+ "counts": { "high": 0, "medium": 4, "low": 11, "info": 1 }, "pass": true,
162
+ "evidence": { "routes": 21, "active": 19, "dynamicLinks": false, "policies": ["agents", "cache", "security", "throttle"],
163
+ "files": ["urlcode.yaml", "routes/code.yaml"], "origin": null, "target": "node",
164
+ "host": { "requestLog": "minimal", "linkEvents": false, "includeCode": null },
165
+ "scope": "declared configuration and runtime facts; not a deployment or certification" } }
166
+ ```
167
+
168
+ `--compliance-rules` alone implies `--compliance baseline`. `--origin` and
169
+ `--request-log` describe the deployment under review and are echoed in
170
+ `evidence`. Findings are sorted by severity, rule and route.
171
+
172
+ ### Exit codes
173
+
174
+ | Condition | Exit |
175
+ |---|---|
176
+ | Readiness failed (`ready: false`) | 1, as before |
177
+ | A `high` finding and no `--compliance-warn` | 1 |
178
+ | `--compliance-warn`: findings are printed, `pass` is still `false` | 0 unless readiness failed |
179
+ | Unknown profile, rules file inside the project, malformed rule or ignore id | 1 with an `error` event on stderr |
180
+
181
+ ## Programmatic API
182
+
183
+ ```js
184
+ import { runCompliance, builtinProfiles, validateRules, resolveRules, loadComplianceRules } from '@jimhoyd/urlcode/compliance';
185
+ import { createRuntime } from '@jimhoyd/urlcode';
186
+
187
+ const runtime = await createRuntime('./site');
188
+ const report = await runCompliance(runtime, {
189
+ profile: 'strict', // baseline | strict | privacy | none
190
+ rules: [], override: {}, disable: [], // as a rules module would export them
191
+ ignore: ['rfc9110/expired-routes'],
192
+ origin: 'https://links.example',
193
+ host: { requestLog: 'minimal', linkEvents: false }, // what the deployment is configured with
194
+ });
195
+ await runtime.close();
196
+ ```
197
+
198
+ The declarations ship with the package: `ComplianceRule` (with `ProjectRule`
199
+ and `RouteRule`, and `ProjectContext`/`RouteContext` for what `check`
200
+ receives), `RawFinding` and `Finding`, `ComplianceOptions`, `ComplianceReport`
201
+ and `ComplianceProfileName` are all exported from `@jimhoyd/urlcode/compliance`, so a
202
+ rules module written in TypeScript is checked against the same contract the
203
+ runtime validates at load time:
204
+
205
+ ```ts
206
+ import type { ComplianceRule, ComplianceReport } from '@jimhoyd/urlcode/compliance';
207
+ import { runCompliance } from '@jimhoyd/urlcode/compliance';
208
+
209
+ export const rules: ComplianceRule[] = [{
210
+ id: 'acme/redirect-hosts',
211
+ title: 'Redirects only leave for approved hosts',
212
+ standard: { name: 'ACME link policy', reference: 'https://example.com/policies/links', section: 'Outbound' },
213
+ severity: 'high',
214
+ appliesTo: 'route',
215
+ check(context) {
216
+ if (context.config.redirect?.url.startsWith('https://acme.example/')) return [];
217
+ return [{ message: `${context.route.path} redirects outside the approved hosts`, remediation: 'Point the redirect at an approved host' }];
218
+ },
219
+ }];
220
+ const report: ComplianceReport = await runCompliance(runtime, { profile: 'strict', rules });
221
+ ```
222
+
223
+ `runCompliance` accepts a started server from `startServer` or a runtime from
224
+ `createRuntime`; it re-reads the YAML from the runtime's `root` and takes the
225
+ plan from `testPlan()`, so rules see what the runtime compiled.
226
+ `auditProject(app, { compliance })` runs the same and attaches the report
227
+ under `compliance`. `builtinProfiles` maps profile names to their frozen rule
228
+ arrays for reuse or inspection; `validateRules` checks a rule array;
229
+ `resolveRules` builds the final set from a profile and operator additions.
230
+
231
+ ## Extending with a plugin-style workflow
232
+
233
+ Keep rule modules where you keep plugins: in the operator application, under
234
+ version control, reviewed like code, outside every audited project. A shared
235
+ module can export rules that read the same policy tables a plugin's
236
+ `onActivate(runtime)` sees through `testPlan()`, so one review of "what does
237
+ this runtime enforce" serves both. Compose organisation rules on top of a
238
+ built-in profile with `override` for stricter severities and `disable` for
239
+ rules that a documented decision replaces, and run `urlcode audit
240
+ --compliance strict --compliance-rules …` in CI with the exit code as the
241
+ gate. Record the report beside the readiness and benchmark evidence for the
242
+ revision; a report proves what was declared at that commit, nothing more.