rcf-lite 0.24.0 → 0.25.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 (232) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/blueprints/application-account-settings/docs/topics.md +3 -0
  3. package/blueprints/application-admin-console/docs/topics.md +3 -0
  4. package/blueprints/application-api-rest/docs/topics.md +3 -0
  5. package/blueprints/application-charts/docs/topics.md +3 -0
  6. package/blueprints/application-dashboard/docs/topics.md +3 -0
  7. package/blueprints/application-datatable/docs/topics.md +3 -0
  8. package/blueprints/application-empty-error-states/docs/topics.md +3 -0
  9. package/blueprints/application-error-handling/docs/topics.md +3 -0
  10. package/blueprints/application-file-upload/docs/topics.md +3 -0
  11. package/blueprints/application-forms-wizard/docs/topics.md +3 -0
  12. package/blueprints/application-notifications-in-app/docs/topics.md +3 -0
  13. package/blueprints/application-onboarding-tour/docs/topics.md +3 -0
  14. package/blueprints/application-spa/docs/topics.md +3 -0
  15. package/blueprints/delivery-ci-workflows/docs/topics.md +3 -0
  16. package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -0
  17. package/blueprints/deploy-hetzner-server/CHANGELOG.md +34 -0
  18. package/blueprints/deploy-hetzner-server/README.md +202 -0
  19. package/blueprints/deploy-hetzner-server/blueprint.json +233 -0
  20. package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +14 -0
  21. package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +14 -0
  22. package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +20 -0
  23. package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +14 -0
  24. package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +64 -0
  25. package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +116 -0
  26. package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +180 -0
  27. package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +106 -0
  28. package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +63 -0
  29. package/blueprints/deploy-hetzner-server/contributions/probes/real-account-snapshot-on-demand.mjs +60 -0
  30. package/blueprints/deploy-hetzner-server/contributions/probes/real-account-throwaway-server-provision.mjs +58 -0
  31. package/blueprints/deploy-hetzner-server/contributions/probes/run-cloud-init-render-lint.mjs +9 -0
  32. package/blueprints/deploy-hetzner-server/contributions/probes/run-hcloud-dry-run-mock.mjs +9 -0
  33. package/blueprints/deploy-hetzner-server/contributions/probes/run-manifest-schema-validate.mjs +9 -0
  34. package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-cloud-init-hardened.mjs +9 -0
  35. package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-snapshot-on-demand.mjs +9 -0
  36. package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-throwaway-server-provision.mjs +9 -0
  37. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-001.json +18 -0
  38. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-002.json +18 -0
  39. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-003.json +18 -0
  40. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-004.json +18 -0
  41. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-005.json +18 -0
  42. package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-006.json +18 -0
  43. package/blueprints/deploy-hetzner-server/contributions/schemas/hetzner-server.schema.json +108 -0
  44. package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3801-deploy-hetzner-server-provisioner.json +27 -0
  45. package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +26 -0
  46. package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +26 -0
  47. package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +25 -0
  48. package/blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl +93 -0
  49. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37101.json +24 -0
  50. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37102.json +24 -0
  51. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37103.json +25 -0
  52. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37104.json +24 -0
  53. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37105.json +24 -0
  54. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37106.json +25 -0
  55. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37107.json +24 -0
  56. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37108.json +24 -0
  57. package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37109.json +24 -0
  58. package/blueprints/deploy-hetzner-server/docs/topics.md +48 -0
  59. package/blueprints/deploy-hetzner-server/guide/deploy-hetzner-server.md +180 -0
  60. package/blueprints/edge-cloudflare-access/docs/topics.md +3 -0
  61. package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +3 -0
  62. package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +13 -0
  63. package/blueprints/edge-cloudflare-tunnel/README.md +257 -0
  64. package/blueprints/edge-cloudflare-tunnel/blueprint.json +168 -0
  65. package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +23 -0
  66. package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +19 -0
  67. package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +19 -0
  68. package/blueprints/edge-cloudflare-tunnel/contributions/probes/aud-presence-check.mjs +114 -0
  69. package/blueprints/edge-cloudflare-tunnel/contributions/probes/cloudflared-config-lint.mjs +80 -0
  70. package/blueprints/edge-cloudflare-tunnel/contributions/probes/manifest-schema-validate.mjs +234 -0
  71. package/blueprints/edge-cloudflare-tunnel/contributions/probes/probe-utils.mjs +231 -0
  72. package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs +54 -0
  73. package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +73 -0
  74. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-aud-presence-check.mjs +4 -0
  75. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-cloudflared-config-lint.mjs +4 -0
  76. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-manifest-schema-validate.mjs +4 -0
  77. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-connector-healthy.mjs +4 -0
  78. package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-tunnel-hostname-routes.mjs +4 -0
  79. package/blueprints/edge-cloudflare-tunnel/contributions/probes/tunnel-manifest.schema.json +55 -0
  80. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-001.json +18 -0
  81. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-002.json +18 -0
  82. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-003.json +18 -0
  83. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-004.json +18 -0
  84. package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-005.json +18 -0
  85. package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +26 -0
  86. package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +26 -0
  87. package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +25 -0
  88. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39101.json +24 -0
  89. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39102.json +24 -0
  90. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39103.json +24 -0
  91. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39104.json +24 -0
  92. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39105.json +24 -0
  93. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39106.json +24 -0
  94. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39107.json +24 -0
  95. package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39108.json +24 -0
  96. package/blueprints/edge-cloudflare-tunnel/docs/topics.md +72 -0
  97. package/blueprints/edge-cloudflare-tunnel/guide/edge-cloudflare-tunnel.md +204 -0
  98. package/blueprints/edge-cloudflare-turnstile/docs/topics.md +3 -0
  99. package/blueprints/email-smtp-resend/docs/topics.md +3 -0
  100. package/blueprints/jobs-background/docs/topics.md +3 -0
  101. package/blueprints/messaging-queue-cloudflare/docs/topics.md +3 -0
  102. package/blueprints/object-storage-s3/docs/topics.md +3 -0
  103. package/blueprints/observability-essentials/docs/topics.md +3 -0
  104. package/blueprints/observability-logging/docs/topics.md +3 -0
  105. package/blueprints/observability-probe-endpoints/docs/topics.md +3 -0
  106. package/blueprints/persistence-data-d1/docs/topics.md +3 -0
  107. package/blueprints/persistence-data-postgres/docs/topics.md +3 -0
  108. package/blueprints/persistence-data-sqlite/docs/topics.md +3 -0
  109. package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +3 -0
  110. package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +3 -0
  111. package/blueprints/platform-cloudflare-kv/docs/topics.md +3 -0
  112. package/blueprints/platform-docker-compose-host/CHANGELOG.md +12 -0
  113. package/blueprints/platform-docker-compose-host/README.md +76 -0
  114. package/blueprints/platform-docker-compose-host/blueprint.json +209 -0
  115. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3901-platform-docker-compose-host-contract.json +14 -0
  116. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +14 -0
  117. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +14 -0
  118. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3904-platform-docker-compose-host-log-driver.json +14 -0
  119. package/blueprints/platform-docker-compose-host/contributions/probes/caddyfile-validate.mjs +108 -0
  120. package/blueprints/platform-docker-compose-host/contributions/probes/compose-config-lint.mjs +231 -0
  121. package/blueprints/platform-docker-compose-host/contributions/probes/probe-utils.mjs +185 -0
  122. package/blueprints/platform-docker-compose-host/contributions/probes/real-account-minimal-stack-up.mjs +41 -0
  123. package/blueprints/platform-docker-compose-host/contributions/probes/real-account-reload-burst.mjs +36 -0
  124. package/blueprints/platform-docker-compose-host/contributions/probes/run-caddyfile-validate.mjs +5 -0
  125. package/blueprints/platform-docker-compose-host/contributions/probes/run-compose-config-lint.mjs +5 -0
  126. package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-minimal-stack-up.mjs +5 -0
  127. package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-reload-burst.mjs +5 -0
  128. package/blueprints/platform-docker-compose-host/contributions/probes/run-secrets-as-files-scan.mjs +5 -0
  129. package/blueprints/platform-docker-compose-host/contributions/probes/secrets-as-files-scan.mjs +116 -0
  130. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-001.json +18 -0
  131. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-002.json +18 -0
  132. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-003.json +18 -0
  133. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-004.json +18 -0
  134. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-005.json +18 -0
  135. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-006.json +18 -0
  136. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +26 -0
  137. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +26 -0
  138. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +26 -0
  139. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +26 -0
  140. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38101.json +24 -0
  141. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38102.json +24 -0
  142. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38103.json +24 -0
  143. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38104.json +24 -0
  144. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38105.json +25 -0
  145. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38106.json +24 -0
  146. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38107.json +24 -0
  147. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38108.json +24 -0
  148. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38109.json +24 -0
  149. package/blueprints/platform-docker-compose-host/docs/topics.md +71 -0
  150. package/blueprints/platform-docker-compose-host/guide/platform-docker-compose-host.md +94 -0
  151. package/blueprints/security-auth-clerk/docs/topics.md +3 -0
  152. package/blueprints/security-auth-keycloak/docs/topics.md +3 -0
  153. package/blueprints/security-auth-magic-link/docs/topics.md +3 -0
  154. package/blueprints/security-auth-oauth2/docs/topics.md +3 -0
  155. package/blueprints/security-secrets-management/docs/topics.md +3 -0
  156. package/fixtures/canary-manifest.json +9 -9
  157. package/package.json +1 -1
  158. package/rcf/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +13 -0
  159. package/rcf/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +13 -0
  160. package/rcf/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +13 -0
  161. package/rcf/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +13 -0
  162. package/rcf/adrs/adr-3901-platform-docker-compose-host-contract.json +13 -0
  163. package/rcf/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +13 -0
  164. package/rcf/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +13 -0
  165. package/rcf/adrs/adr-3904-platform-docker-compose-host-log-driver.json +13 -0
  166. package/rcf/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +13 -0
  167. package/rcf/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +13 -0
  168. package/rcf/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +13 -0
  169. package/rcf/code-nodes/cn-410.json +20 -0
  170. package/rcf/code-nodes/cn-440.json +20 -0
  171. package/rcf/code-nodes/cn-470.json +19 -0
  172. package/rcf/fbs/fbs-130.json +23 -0
  173. package/rcf/fbs/fbs-140.json +23 -0
  174. package/rcf/fbs/fbs-150.json +22 -0
  175. package/rcf/requirements/req-110.json +58 -0
  176. package/rcf/requirements/req-111.json +32 -0
  177. package/rcf/requirements/req-112.json +42 -0
  178. package/rcf/requirements/req-113.json +33 -0
  179. package/rcf/requirements/req-114.json +20 -0
  180. package/rcf/requirements/req-115.json +38 -0
  181. package/rcf/requirements/req-120.json +20 -0
  182. package/rcf/requirements/req-121.json +20 -0
  183. package/rcf/requirements/req-122.json +20 -0
  184. package/rcf/requirements/req-123.json +42 -0
  185. package/rcf/requirements/req-124.json +27 -0
  186. package/rcf/requirements/req-125.json +20 -0
  187. package/rcf/requirements/req-130.json +32 -0
  188. package/rcf/requirements/req-131.json +54 -0
  189. package/rcf/requirements/req-132.json +27 -0
  190. package/rcf/requirements/req-133.json +20 -0
  191. package/rcf/requirements/req-134.json +42 -0
  192. package/rcf/tacs/tac-3801-deploy-hetzner-server-provisioner.json +17 -0
  193. package/rcf/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +17 -0
  194. package/rcf/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +17 -0
  195. package/rcf/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +16 -0
  196. package/rcf/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +17 -0
  197. package/rcf/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +17 -0
  198. package/rcf/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +17 -0
  199. package/rcf/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +17 -0
  200. package/rcf/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +17 -0
  201. package/rcf/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +17 -0
  202. package/rcf/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +16 -0
  203. package/rcf/test-suites/ts-140.json +86 -0
  204. package/rcf/test-suites/ts-150.json +86 -0
  205. package/rcf/test-suites/ts-160.json +78 -0
  206. package/rcf/user-stories/us-11001.json +23 -0
  207. package/rcf/user-stories/us-11101.json +23 -0
  208. package/rcf/user-stories/us-11102.json +24 -0
  209. package/rcf/user-stories/us-11201.json +23 -0
  210. package/rcf/user-stories/us-11202.json +23 -0
  211. package/rcf/user-stories/us-11301.json +24 -0
  212. package/rcf/user-stories/us-11401.json +23 -0
  213. package/rcf/user-stories/us-11402.json +23 -0
  214. package/rcf/user-stories/us-11501.json +23 -0
  215. package/rcf/user-stories/us-12001.json +23 -0
  216. package/rcf/user-stories/us-12101.json +23 -0
  217. package/rcf/user-stories/us-12102.json +23 -0
  218. package/rcf/user-stories/us-12201.json +23 -0
  219. package/rcf/user-stories/us-12202.json +24 -0
  220. package/rcf/user-stories/us-12301.json +23 -0
  221. package/rcf/user-stories/us-12401.json +23 -0
  222. package/rcf/user-stories/us-12402.json +23 -0
  223. package/rcf/user-stories/us-12501.json +23 -0
  224. package/rcf/user-stories/us-13001.json +23 -0
  225. package/rcf/user-stories/us-13002.json +23 -0
  226. package/rcf/user-stories/us-13101.json +23 -0
  227. package/rcf/user-stories/us-13102.json +23 -0
  228. package/rcf/user-stories/us-13201.json +23 -0
  229. package/rcf/user-stories/us-13301.json +23 -0
  230. package/rcf/user-stories/us-13302.json +23 -0
  231. package/rcf/user-stories/us-13401.json +23 -0
  232. package/releases/releases.yaml +11 -1
@@ -0,0 +1,13 @@
1
+ # edge-cloudflare-tunnel CHANGELOG
2
+
3
+ ## 1.0.0 - 2026-09-08
4
+
5
+ Initial release. Round-7 T-3 of the Hetzner spec at `projects/blueprint-library/specs/hetzner-round-7-spec-2026-09-07.md`.
6
+
7
+ - Mints capability `tunnelBridge` and global topic `edgeIngressBridge`.
8
+ - Five REQs, eight USs, three TACs, three ADRs; five Node-only probes.
9
+ - Extends the shared throwaway-Hetzner-server fixture with a cloudflared connector in both runtime shapes (compose-service alongside the T-2 web and caddy services when `containerHost` is applied; systemd-unit for a bare `cloudHost`) and both hostname modes (access-gated when `zeroTrustGate` is applied; public-hostname when absent) plus five `run-<probe>.mjs` delegate shims.
10
+ - manifest-schema-validate carries five fixture-side mutation switches (`SIMULATE_MANIFEST_INVALID_TUNNEL_ID`, `SIMULATE_MANIFEST_CREDENTIALS_INLINE`, `SIMULATE_MANIFEST_MISSING_CATCHALL`, `SIMULATE_ORIGIN_PORT_OPEN`, `SIMULATE_EVENT_SECRECY_LEAK`); cloudflared-config-lint carries `SIMULATE_INGRESS_INVALID` and runs `cloudflared tunnel ingress validate` via the `cloudflare/cloudflared:2026.8.3` container when a local binary is not on PATH; aud-presence-check carries `SIMULATE_AUD_DROP`.
11
+ - Mutation-discipline (T-2 gate ruling carried forward): every `SIMULATE_*` switch lives in the fixture-side delegate shim; the probe modules read only `RCF_LITE_T3_FIXTURE_ROOT` and never branch on being under mutation.
12
+ - Connector-runtime choice elicited via `connector-runtime` (compose-service default when `containerHost` applied; systemd-unit alternative for bare `cloudHost`, per the vendor cloudflared as-a-service local-configuration-file docs); hostname-mode discovered (not elicited) from `appliedCapabilities` via the sidecar per ADR-4003.
13
+ - Consumers: `w-2026-09-06-dave-010` follow-on (make the librarian API and workspace viewer reachable without Mullvad) is the estate consumer; blueprint acceptance never depends on that consumer, and the ops-01 migration mints as its own work item.
@@ -0,0 +1,257 @@
1
+ # edge-cloudflare-tunnel
2
+
3
+ Round-7 T-3 core blueprint on the rcf-lite shelf. Bridges a service on a
4
+ `cloudHost` (bare, systemd shape) or a `containerHost` (compose-service
5
+ shape) to the Cloudflare edge through a Cloudflare Tunnel, with no
6
+ public origin ports. When applied alongside `edge-cloudflare-access`
7
+ (via the `zeroTrustGate` capability) the tunnel binds an Access AUD;
8
+ when applied without it the tunnel serves in public-hostname mode.
9
+
10
+ - Slug: `edge-cloudflare-tunnel`
11
+ - Version: `1.0.0`
12
+ - Category: `edge`
13
+ - Capabilities: `["tunnelBridge"]`
14
+ - Mints global topic: `edgeIngressBridge`
15
+ - Contribution count: 5 REQs, 8 USs, 3 TACs, 3 ADRs, 5 probes
16
+ - Consumers: ops-01 has no consumer today (SSH is the only inbound path). Estate consumer is the follow-on make-the-librarian-API-and-workspace-viewer-reachable-without-Mullvad work that `w-2026-09-06-dave-010` is expected to green-light. Blueprint acceptance never depends on that consumer; the ops-01 migration mints as a separate work item at that point (`w-2026-09-07-dave-004`).
17
+
18
+ ## Five REQs
19
+
20
+ | Id | Contract |
21
+ |---|---|
22
+ | edge-cloudflare-tunnel-REQ-001 | Connector install shape switches on applied `containerHost` (compose-service default; systemd-unit for bare `cloudHost`); `cloudflaredReady` fires on boot with connector id, region and version metadata. |
23
+ | edge-cloudflare-tunnel-REQ-002 | Tunnel manifest schema at `cloudflare/tunnels/<name>.yaml` (uuid tunnel id, `credentialsFile.secretRef` reference, ingress rules with catch-all `http_status:404`). |
24
+ | edge-cloudflare-tunnel-REQ-003 | No public origin ports on the host; T-1 firewall stays closed on 80/443 unless a reverse proxy is applied; manifest refuses ingress rules whose service URL binds to a host public interface. |
25
+ | edge-cloudflare-tunnel-REQ-004 | Access-gated composition: `zeroTrustGate` applied attaches `originRequest.access.aud`; absent omits the block and serves in public-hostname mode. |
26
+ | edge-cloudflare-tunnel-REQ-005 | Credentials-file discipline (host mode 0o400, referenced via `secretRef`, not committed, grep-refuse, no leak in event bodies). |
27
+
28
+ ## Connector runtime shapes
29
+
30
+ The connector runtime is elicited via `connector-runtime` (ADR-4002) with
31
+ `recommendedDefault: compose-service`.
32
+
33
+ - **compose-service** (default when `containerHost` is applied). The
34
+ cloudflared container ships alongside the applying project services on
35
+ the same web-net network. No host ports; restart `unless-stopped`;
36
+ logging driver aligned with the T-2 compose-host log-driver elicit.
37
+ Fixture: `packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared/compose-service/`.
38
+ - **systemd-unit** (alternative for a bare `cloudHost`). The cloudflared
39
+ binary is installed via the vendor package per the cloudflared
40
+ as-a-service documented systemd unit and runs as a systemd service
41
+ reading the local configuration file. Fixture:
42
+ `packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared/systemd-unit/`.
43
+
44
+ When to reach for which: pick compose-service when the project already
45
+ runs docker compose on the host (natural home, same log driver, same
46
+ restart discipline); pick systemd-unit when the host runs bare and there
47
+ is no compose stack to add a service to (or when the project deliberately
48
+ keeps cloudflared outside the compose lifecycle for boot ordering).
49
+
50
+ ## Tunnel manifest shape
51
+
52
+ The manifest lives at `cloudflare/tunnels/<name>.yaml` under the
53
+ applying project. The shipped JSON schema
54
+ (`contributions/probes/tunnel-manifest.schema.json`) requires:
55
+
56
+ - `tunnel`: uuid-shaped Cloudflare Tunnel id.
57
+ - `credentialsFile.secretRef`: reference into `security-secrets-management`
58
+ (never a literal path or JSON body inline).
59
+ - `ingress`: array of `{ hostname?, service, originRequest? }`. The last
60
+ entry MUST be a catch-all `service: http_status:404`. When the
61
+ Access-gated shape is applied every non-catch-all rule attaches
62
+ `originRequest.access.aud` with the AUD from the sidecar.
63
+
64
+ Example (compose-service, public-hostname mode):
65
+
66
+ ```yaml
67
+ tunnel: 00000000-0000-4000-8000-0000000012ab
68
+ credentialsFile:
69
+ secretRef: hq-estate/CLOUDFLARE_TUNNEL_CREDENTIALS_MY_PROJECT
70
+ ingress:
71
+ - hostname: my-app.example.com
72
+ service: http://web:8080
73
+ - service: http_status:404
74
+ ```
75
+
76
+ Example (Access-gated shape adds one block per non-catch-all rule):
77
+
78
+ ```yaml
79
+ ingress:
80
+ - hostname: my-app.example.com
81
+ service: http://web:8080
82
+ originRequest:
83
+ access:
84
+ aud: AUD-example-cloudflare-team
85
+ teamName: my-team
86
+ required: true
87
+ - service: http_status:404
88
+ ```
89
+
90
+ ## Credentials-file discipline
91
+
92
+ The tunnel credentials JSON file lands on the host at mode 0o400 (owner
93
+ read only). The manifest references it as a `credentialsFile.secretRef`;
94
+ the fixture placeholder at
95
+ `packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared/*/credentials/probe.json.example`
96
+ documents the pattern. The credentials file MUST NOT be committed to
97
+ the applying project tree; a grep across the working tree for the
98
+ tunnel id and account tag literals refuses on any tracked file. Every
99
+ `cloudflaredReady`, `tunnelConnectorUp` and `ingressRuleReloaded` event
100
+ body carries metadata only; the event-secrecy scan refuses on a body
101
+ that mentions the credentials literal.
102
+
103
+ Rotate credentials by rewriting the secret pointed to by `secretRef`
104
+ and re-applying; the connector reloads on the next boot cycle.
105
+
106
+ ## Composition point (Access-gated vs public-hostname)
107
+
108
+ The hostname mode is discovered from the applied capability set (not
109
+ elicited; see ADR-4003). The blueprint reads the sidecar at
110
+ `rcf/blueprints/edge-cloudflare-tunnel.applied.json` and inspects
111
+ `appliedCapabilities`.
112
+
113
+ - When `zeroTrustGate` is present (i.e. `edge-cloudflare-access` v1.0.0
114
+ is applied on the same project), every ingress rule attaches
115
+ `originRequest.access.aud` and cloudflared refuses at the edge any
116
+ request without a valid Access JWT for that AUD.
117
+ - When `zeroTrustGate` is absent, the block is omitted and the tunnel
118
+ serves in public-hostname mode (open ingress on the public hostname;
119
+ authorization is whatever the origin service itself carries).
120
+
121
+ The `aud-presence-check` probe refuses when the gated variant drifts to
122
+ omit the AUD block (defensive against silent degradation).
123
+
124
+ Graceful degrade: when Access is not applied (T-4 absent from
125
+ `origin/main` or the applying project simply does not want the gate),
126
+ the blueprint still ships in public-hostname mode. The probe assertion
127
+ runs on both fixture variants so the shape flip is proven on every
128
+ reviewer boot.
129
+
130
+ ## No-public-ports rule
131
+
132
+ The T-1 firewall on a `cloudHost` allows 22 unconditionally and 80/443
133
+ only when a reverse proxy is applied. A project applying only T-1 plus
134
+ T-3 (no reverse proxy) declares zero 80/443 rules; the tunnel is the
135
+ sole path from the edge to the origin. The manifest schema refuses
136
+ ingress rules whose service URL binds to `0.0.0.0`, a non-loopback
137
+ numeric IP, or an unresolvable shape.
138
+
139
+ ## Elicited parameters
140
+
141
+ - `tunnel-name` (string; default `probe`): the Cloudflare Tunnel name
142
+ and the manifest filename under `cloudflare/tunnels/<name>.yaml`.
143
+ - `cloudflare-zone-secret` (string; default `hq-estate/CLOUDFLARE_ZONE_ID`):
144
+ `security-secrets-management` reference to the Cloudflare zone the
145
+ tunnel serves under.
146
+ - `public-hostname-template` (string; default `<service>.<zone>`):
147
+ template for the public hostname, expanded per ingress rule.
148
+ - `ingress-rules` (string; default `[]`): ingress rules array (hostname
149
+ + service URL pairs).
150
+ - `access-aud` (string; default empty): Access AUD tag for the gated
151
+ shape. Presence toggles the gated shape when `zeroTrustGate` is
152
+ applied.
153
+ - `connector-runtime` (enum: `compose-service`, `systemd-unit`; default
154
+ `compose-service`): the connector runtime per ADR-4002.
155
+
156
+ ## Companions
157
+
158
+ - `suggestedCompanions[logging]`: every `cloudflaredReady`,
159
+ `tunnelConnectorUp`, `tunnelConnectorDown`, `ingressRuleReloaded` and
160
+ `accessGateRefused` event writes through the applied logger.
161
+ - `suggestedCompanions[errorHandling]`: a cloudflared exit, a JWT-check
162
+ failure, an ingress-rule schema violation, a hostname-DNS-route API
163
+ refusal constructs an internal error record.
164
+
165
+ `providesRoles: []`.
166
+
167
+ ## Five probes
168
+
169
+ Every probe writes its report envelope to
170
+ `.rcf/reports/blueprints/edge-cloudflare-tunnel/<probe-name>.json`
171
+ under the repo root.
172
+
173
+ - **manifest-schema-validate** (local). Anchors AC-tunnel-manifestSchema,
174
+ AC-tunnel-noPublicOrigin, AC-tunnel-credentialsDiscipline. `accountBound: false`.
175
+ Ajv-free walker over the shipped JSON schema; runs on all four
176
+ variants; refuses on `SIMULATE_MANIFEST_INVALID_TUNNEL_ID`,
177
+ `SIMULATE_MANIFEST_CREDENTIALS_INLINE`, `SIMULATE_MANIFEST_MISSING_CATCHALL`,
178
+ `SIMULATE_ORIGIN_PORT_OPEN`, `SIMULATE_EVENT_SECRECY_LEAK`.
179
+ - **cloudflared-config-lint** (local). Anchors AC-tunnel-hostnameRoutes.
180
+ `accountBound: false`. Runs `cloudflared tunnel --config <path> ingress validate`
181
+ via a local binary or the vendor container image
182
+ `cloudflare/cloudflared:2026.8.3`; refuses on `SIMULATE_INGRESS_INVALID`.
183
+ - **aud-presence-check** (local). Anchors AC-tunnel-accessGated,
184
+ AC-tunnel-accessGatedRefuse. `accountBound: false`. Reads the sidecar
185
+ and the paired manifests; asserts the shape flips correctly; refuses
186
+ on `SIMULATE_AUD_DROP` when the gated fixture drifts to open ingress.
187
+ - **real-account-connector-healthy** (account-bound). Anchors
188
+ AC-tunnel-connectorHealthy. `accountBound: true` on
189
+ `CI_HAS_CLOUDFLARE_ACCOUNT` AND `CI_HAS_HETZNER_ACCOUNT`. Without
190
+ both, records `accountBoundSkipped: true` and the aggregate flips to
191
+ `pass`.
192
+ - **real-account-tunnel-hostname-routes** (account-bound). Anchors
193
+ AC-tunnel-hostnameRoutes (public sub-case) and AC-tunnel-accessGated
194
+ (gated sub-case). `accountBound: true`. The public-hostname sub-case
195
+ runs on `CI_HAS_CLOUDFLARE_ACCOUNT` + `CI_HAS_HETZNER_ACCOUNT`; the
196
+ access-gated sub-case adds `CI_HAS_CLOUDFLARE_ACCESS` per the round-7
197
+ Q4 ratification. Sub-cases skip independently.
198
+
199
+ The delegate shims at
200
+ `packages/rcf-lite/test/fixtures/hetzner-throwaway-server/run-<probe>.mjs`
201
+ read the `SIMULATE_*` env vars and prepare mutated inputs in a scratch
202
+ dir; the probe modules never read a `SIMULATE_` env var and never
203
+ branch on being under mutation.
204
+
205
+ ## Two fixture variants times two hostname modes
206
+
207
+ The fixture lives under
208
+ `packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared/`:
209
+
210
+ - `compose-service/compose-fragment.yaml`: the cloudflared compose
211
+ service alongside the T-2 web and caddy services.
212
+ - `systemd-unit/cloudflared.service`: the systemd unit per the vendor
213
+ as-a-service docs.
214
+ - `<runtime>/cloudflare/tunnels/public-hostname.yaml`: manifest without
215
+ the AUD block.
216
+ - `<runtime>/cloudflare/tunnels/access-gated.yaml`: manifest with the
217
+ AUD block on every non-catch-all rule.
218
+ - `<runtime>/credentials/probe.json.example`: placeholder credentials
219
+ documenting the 0o400 mode discipline.
220
+ - `sidecars/access-gated.applied.json`: `appliedCapabilities` includes
221
+ `zeroTrustGate`.
222
+ - `sidecars/public-hostname.applied.json`: `appliedCapabilities`
223
+ excludes `zeroTrustGate`.
224
+
225
+ Reviewer boot (mocked, no account) from the fixture directory:
226
+
227
+ ```
228
+ cd packages/rcf-lite/test/fixtures/hetzner-throwaway-server
229
+ node ./run-tunnel-manifest-schema-validate.mjs && node ./run-cloudflared-config-lint.mjs && node ./run-aud-presence-check.mjs
230
+ ```
231
+
232
+ Reviewer boot (real account, throwaway server): set
233
+ `CI_HAS_CLOUDFLARE_ACCOUNT=true` and `CI_HAS_HETZNER_ACCOUNT=true`
234
+ (and `CI_HAS_CLOUDFLARE_ACCESS=true` for the gated sub-case) with the
235
+ account tokens loaded via `security-secrets-management`, then invoke the
236
+ two `run-real-account-*.mjs` scripts.
237
+
238
+ ## Composition on the shelf
239
+
240
+ Consumes:
241
+ - `platform-docker-compose-host` v1.0.0 (`containerHost` capability) for
242
+ the compose-service runtime path.
243
+ - `deploy-hetzner-server` v1.0.0 (`cloudHost` capability) for the bare
244
+ systemd-unit runtime path.
245
+ - `security-secrets-management` v1.0.1 (`secretsProvider` capability) for
246
+ the tunnel credentials `secretRef`.
247
+ - Optionally `edge-cloudflare-access` v1.0.0 (`zeroTrustGate` capability)
248
+ for the Access-gated shape.
249
+
250
+ Provides:
251
+ - `tunnelBridge` (declared under `capabilities` on `blueprint.json`).
252
+ - New global topic `edgeIngressBridge` on ADR-4001 (scope global).
253
+
254
+ Conflict-by-design with future non-Cloudflare siblings on the
255
+ `edgeIngressBridge` topic: Tailscale Funnel, Fly.io proxy, etc.
256
+ would contribute the same topic string with a different mechanism.
257
+ Round 7 ships only the Cloudflare Tunnel provider.
@@ -0,0 +1,168 @@
1
+ {
2
+ "slug": "edge-cloudflare-tunnel",
3
+ "version": "1.0.0",
4
+ "category": "edge",
5
+ "capabilities": [
6
+ "tunnelBridge"
7
+ ],
8
+ "suggestedCompanions": [
9
+ {
10
+ "role": "logging",
11
+ "reason": "Every cloudflaredReady, tunnelConnectorUp, tunnelConnectorDown, ingressRuleReloaded and accessGateRefused event writes through the applied logger; a logging companion supplies the factory."
12
+ },
13
+ {
14
+ "role": "errorHandling",
15
+ "reason": "A cloudflared exit, a JWT-check failure, an ingress-rule schema violation, a hostname-DNS-route API refusal constructs an internal error record; an error-handling companion supplies the record factory and the boundary."
16
+ }
17
+ ],
18
+ "elicits": [
19
+ {
20
+ "id": "tunnel-name",
21
+ "prompt": "The Cloudflare Tunnel name (also the manifest filename under cloudflare/tunnels/<name>.yaml). Names the connector in cloudflared tunnel list and in the routing DNS entries.",
22
+ "kind": "string",
23
+ "default": "probe"
24
+ },
25
+ {
26
+ "id": "cloudflare-zone-secret",
27
+ "prompt": "security-secrets-management reference to the Cloudflare zone the tunnel serves under. Read at apply time to create the DNS route; never inlined into the manifest.",
28
+ "kind": "string",
29
+ "default": "hq-estate/CLOUDFLARE_ZONE_ID"
30
+ },
31
+ {
32
+ "id": "public-hostname-template",
33
+ "prompt": "Template for the public hostname (e.g. <service>.<zone>). The template is expanded per ingress rule at apply time.",
34
+ "kind": "string",
35
+ "default": "<service>.<zone>"
36
+ },
37
+ {
38
+ "id": "ingress-rules",
39
+ "prompt": "Ingress rules array: each entry names a public hostname and the internal service URL to route to. The last entry is a mandatory catch-all http_status 404 (added by the blueprint if omitted).",
40
+ "kind": "string",
41
+ "default": "[]"
42
+ },
43
+ {
44
+ "id": "access-aud",
45
+ "prompt": "Optional Access AUD tag for the Access-gated shape. When present AND edge-cloudflare-access is applied (zeroTrustGate on appliedCapabilities), every ingress rule attaches originRequest.access.aud with this value. When absent OR edge-cloudflare-access is not applied, the block is omitted and the tunnel serves in public-hostname mode.",
46
+ "kind": "string",
47
+ "default": ""
48
+ },
49
+ {
50
+ "id": "connector-runtime",
51
+ "prompt": "Connector runtime per ADR-4002. compose-service is the shipped default when containerHost is applied (T-2 platform-docker-compose-host on top of T-1 deploy-hetzner-server); systemd-unit is the alternative for a bare cloudHost per the cloudflared as-a-service local-configuration-file docs.",
52
+ "kind": "enum",
53
+ "options": [
54
+ "compose-service",
55
+ "systemd-unit"
56
+ ],
57
+ "default": "compose-service"
58
+ }
59
+ ],
60
+ "contributions": [
61
+ {
62
+ "id": "edge-cloudflare-tunnel-REQ-001",
63
+ "kind": "req",
64
+ "path": "requirements/edge-cloudflare-tunnel-req-001.json"
65
+ },
66
+ {
67
+ "id": "edge-cloudflare-tunnel-REQ-002",
68
+ "kind": "req",
69
+ "path": "requirements/edge-cloudflare-tunnel-req-002.json"
70
+ },
71
+ {
72
+ "id": "edge-cloudflare-tunnel-REQ-003",
73
+ "kind": "req",
74
+ "path": "requirements/edge-cloudflare-tunnel-req-003.json"
75
+ },
76
+ {
77
+ "id": "edge-cloudflare-tunnel-REQ-004",
78
+ "kind": "req",
79
+ "path": "requirements/edge-cloudflare-tunnel-req-004.json"
80
+ },
81
+ {
82
+ "id": "edge-cloudflare-tunnel-REQ-005",
83
+ "kind": "req",
84
+ "path": "requirements/edge-cloudflare-tunnel-req-005.json"
85
+ },
86
+ {
87
+ "id": "edge-cloudflare-tunnel-US-39101",
88
+ "kind": "us",
89
+ "path": "user-stories/edge-cloudflare-tunnel-us-39101.json"
90
+ },
91
+ {
92
+ "id": "edge-cloudflare-tunnel-US-39102",
93
+ "kind": "us",
94
+ "path": "user-stories/edge-cloudflare-tunnel-us-39102.json"
95
+ },
96
+ {
97
+ "id": "edge-cloudflare-tunnel-US-39103",
98
+ "kind": "us",
99
+ "path": "user-stories/edge-cloudflare-tunnel-us-39103.json"
100
+ },
101
+ {
102
+ "id": "edge-cloudflare-tunnel-US-39104",
103
+ "kind": "us",
104
+ "path": "user-stories/edge-cloudflare-tunnel-us-39104.json"
105
+ },
106
+ {
107
+ "id": "edge-cloudflare-tunnel-US-39105",
108
+ "kind": "us",
109
+ "path": "user-stories/edge-cloudflare-tunnel-us-39105.json"
110
+ },
111
+ {
112
+ "id": "edge-cloudflare-tunnel-US-39106",
113
+ "kind": "us",
114
+ "path": "user-stories/edge-cloudflare-tunnel-us-39106.json"
115
+ },
116
+ {
117
+ "id": "edge-cloudflare-tunnel-US-39107",
118
+ "kind": "us",
119
+ "path": "user-stories/edge-cloudflare-tunnel-us-39107.json"
120
+ },
121
+ {
122
+ "id": "edge-cloudflare-tunnel-US-39108",
123
+ "kind": "us",
124
+ "path": "user-stories/edge-cloudflare-tunnel-us-39108.json"
125
+ },
126
+ {
127
+ "id": "TAC-4001-edge-cloudflare-tunnel-connector-shape",
128
+ "kind": "tac",
129
+ "path": "tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json"
130
+ },
131
+ {
132
+ "id": "TAC-4002-edge-cloudflare-tunnel-manifest-schema",
133
+ "kind": "tac",
134
+ "path": "tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json"
135
+ },
136
+ {
137
+ "id": "TAC-4003-edge-cloudflare-tunnel-access-binding",
138
+ "kind": "tac",
139
+ "path": "tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json"
140
+ },
141
+ {
142
+ "id": "ADR-4001-edge-cloudflare-tunnel-ingress-bridge-contract",
143
+ "kind": "adr",
144
+ "path": "adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json",
145
+ "scope": "global",
146
+ "topic": "edgeIngressBridge",
147
+ "recommendedDefault": true,
148
+ "elicited": false,
149
+ "standardsTraceClause": "Cloudflare Tunnel overview and get-started documented mechanics per hetzner-round-7-spec-2026-09-07.md"
150
+ },
151
+ {
152
+ "id": "ADR-4002-edge-cloudflare-tunnel-connector-runtime",
153
+ "kind": "adr",
154
+ "path": "adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json",
155
+ "recommendedDefault": true,
156
+ "elicited": true,
157
+ "standardsTraceClause": "Cloudflare cloudflared as-a-service documented systemd unit per hetzner-round-7-spec-2026-09-07.md"
158
+ },
159
+ {
160
+ "id": "ADR-4003-edge-cloudflare-tunnel-hostname-mode",
161
+ "kind": "adr",
162
+ "path": "adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json",
163
+ "recommendedDefault": true,
164
+ "elicited": false,
165
+ "standardsTraceClause": "Cloudflare Access self-hosted-public-app documented shape per hetzner-round-7-spec-2026-09-07.md"
166
+ }
167
+ ]
168
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "adrId": "ADR-4001-edge-cloudflare-tunnel-ingress-bridge-contract",
3
+ "prdId": "PRD-001",
4
+ "tadId": "TAD-001",
5
+ "version": "1.0.0",
6
+ "status": "accepted",
7
+ "title": "Edge ingress bridge contract: Cloudflare Tunnel as the shipped ingress-bridge shape from a cloudHost service to the CF edge (scope global, new topic edgeIngressBridge)",
8
+ "context": "An applying project running on a Linux cloudHost needs one shipped answer to the question of how a service on that host reaches the public internet without opening origin ports. Round-7 T-3 mints the edgeIngressBridge topic on this ADR. Cloudflare Tunnel is the shipped v1.0.0 mechanism (cloudflared connector out to the CF edge; DNS route on a Cloudflare zone; ingress rules mapping public hostname to internal service URL). Every applied ingress bridge needs one project-wide answer to the edgeIngressBridge topic; a future non-Cloudflare sibling (Tailscale Funnel, Fly.io proxy) would contribute the same topic string with a different mechanism, forcing a DELIBERATE conflict the operator resolves per the round-2 conflict pattern. Standards trace clause: Cloudflare Tunnel overview and get-started documented mechanics per hetzner-round-7-spec-2026-09-07.md.",
9
+ "decision": "The shipped v1.0.0 default is a Cloudflare Tunnel connector as the edgeIngressBridge provider. A cloudflared connector on the applying host (compose service by default when containerHost is applied per ADR-4002; systemd unit on a bare cloudHost) reads a tunnel manifest at cloudflare/tunnels/<name>.yaml and terminates ingress at the CF edge. The edgeIngressBridge topic scope is global on this blueprint; the topic string is edgeIngressBridge (lower camelCase, one concept per topic, no version suffix, per the round-2 rules-for-new-topics clause on the shelf registry). Standards trace clause: Cloudflare Tunnel overview and get-started documented mechanics per hetzner-round-7-spec-2026-09-07.md.",
10
+ "consequences": "Consumer blueprints that expose a service publicly declare capabilities: [tunnelBridge] on their applied contract to know a bridge is in place. Composition with zeroTrustGate (edge-cloudflare-access) toggles the Access-gated ingress shape per ADR-4003. A T-3-only project (no reverse proxy) declares no 80/443 firewall rule; the tunnel is the sole path from the edge to the origin. Rebuild-from-manifest is always possible: the manifest is the source of truth, the running connector is derivative. Folding this shape into edge-cloudflare-access was rejected: Access is an authorisation gate, Tunnel is an ingress bridge; the responsibilities are distinct and one applies without the other.",
11
+ "alternativesConsidered": [
12
+ {
13
+ "name": "Fold into edge-cloudflare-access as an adapter",
14
+ "reason": "Access is an authorisation gate on top of an ingress; Tunnel is an ingress bridge. One applies without the other. Folding conflated two decisions the operator ships separately. Rejected per Baz ruling decision 21."
15
+ },
16
+ {
17
+ "name": "Ship a Tailscale Funnel or Fly.io proxy sibling in the same PR",
18
+ "reason": "Out of scope for round 7. Siblings mint on operator demand into their own reserved slots and conflict-by-design with this blueprint on the edgeIngressBridge topic."
19
+ }
20
+ ],
21
+ "createdAt": "2026-09-08T00:00:00.000Z",
22
+ "updatedAt": "2026-09-08T00:00:00.000Z"
23
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "adrId": "ADR-4002-edge-cloudflare-tunnel-connector-runtime",
3
+ "prdId": "PRD-001",
4
+ "tadId": "TAD-001",
5
+ "version": "1.0.0",
6
+ "status": "accepted",
7
+ "title": "Connector runtime: compose-service default when containerHost applied; systemd-unit alternative on bare cloudHost",
8
+ "context": "The cloudflared connector runs in one of two supported vendor shapes: as a container image published on the cloudflared downloads page, or as a systemd unit installed via the vendor package per the cloudflared as-a-service local-configuration-file docs. An applying project running its stack under docker compose has a natural home for the connector alongside its other services (same network, same logging driver, same restart policy). An applying project running bare on the host has no compose stack and needs a systemd unit for boot ordering and journald logging. Standards trace clause: Cloudflare cloudflared as-a-service documented systemd unit per hetzner-round-7-spec-2026-09-07.md.",
9
+ "decision": "The connector runtime is elicited via connector-runtime with enum {compose-service, systemd-unit} and recommendedDefault compose-service. When the containerHost capability is applied (T-2 platform-docker-compose-host) the shipped default is compose-service; when the host runs bare (only T-1 applied) the operator picks systemd-unit at apply time. Standards trace clause: Cloudflare cloudflared as-a-service documented systemd unit per hetzner-round-7-spec-2026-09-07.md.",
10
+ "consequences": "The fixture ships both variants and both pass the local probes (manifest-schema-validate, cloudflared-config-lint, aud-presence-check). A future non-Cloudflare tunnel sibling (Tailscale Funnel, Fly.io proxy) reuses the runtime enum shape but the elicit lives on that sibling own ADR (conflict-by-design on edgeIngressBridge per ADR-4001). The systemd variant carries an install caveat in the guide: the vendor package landed via apt or curl per the as-a-service docs, which is out of scope for the blueprint itself.",
11
+ "alternativesConsidered": [
12
+ {
13
+ "name": "Ship only the compose-service variant",
14
+ "reason": "A bare cloudHost is a valid apply shape; forcing docker compose onto a host that does not want it is a hard cost the elicit avoids."
15
+ }
16
+ ],
17
+ "createdAt": "2026-09-08T00:00:00.000Z",
18
+ "updatedAt": "2026-09-08T00:00:00.000Z"
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "adrId": "ADR-4003-edge-cloudflare-tunnel-hostname-mode",
3
+ "prdId": "PRD-001",
4
+ "tadId": "TAD-001",
5
+ "version": "1.0.0",
6
+ "status": "accepted",
7
+ "title": "Hostname mode: access-gated when zeroTrustGate applied (ingress attaches AUD); public-hostname when absent (block omitted)",
8
+ "context": "A tunnel can serve a public hostname openly (any request reaches the origin, protected only by whatever authentication the origin itself carries) or Access-gated (cloudflared refuses at the edge any request without a valid Access JWT for a named AUD, per the vendor self-hosted-public-app docs). The hostname mode is not elicited: it is DISCOVERED from the applied capability set. When zeroTrustGate is applied (edge-cloudflare-access v1.0.0), the tunnel binds the Access AUD; when it is not, the tunnel serves in public-hostname mode. Standards trace clause: Cloudflare Access self-hosted-public-app documented shape per hetzner-round-7-spec-2026-09-07.md.",
9
+ "decision": "ADR-4003 records the hostname mode as elicited: false and discovered from applied capabilities via the sidecar rcf/blueprints/edge-cloudflare-tunnel.applied.json. When appliedCapabilities includes zeroTrustGate, every ingress rule under the tunnel manifest attaches an originRequest.access block naming the AUD; when it does not, the block is omitted. The aud-presence-check probe runs on both fixture variants and asserts the shape flips correctly. Standards trace clause: Cloudflare Access self-hosted-public-app documented shape per hetzner-round-7-spec-2026-09-07.md.",
10
+ "consequences": "A project that applies zeroTrustGate later inherits the Access-gated shape on the next reapply of edge-cloudflare-tunnel without a manual manifest edit; a project that removes zeroTrustGate drops to public-hostname mode explicitly. A silent drift (the sidecar keeps zeroTrustGate but the ingress loses the AUD block) is caught by aud-presence-check refusing before the shape ships. Fixture variants live under cloudflared/sidecars/{access-gated,public-hostname}.applied.json; the SIMULATE_AUD_DROP mutation on the gated fixture proves the refuse path.",
11
+ "alternativesConsidered": [
12
+ {
13
+ "name": "Elicit the hostname mode as a separate parameter",
14
+ "reason": "The applied capability set is already the source of truth; a separate elicit would drift from that set. Discovery keeps the shape aligned by construction."
15
+ }
16
+ ],
17
+ "createdAt": "2026-09-08T00:00:00.000Z",
18
+ "updatedAt": "2026-09-08T00:00:00.000Z"
19
+ }
@@ -0,0 +1,114 @@
1
+ // Probe: aud-presence-check.
2
+ //
3
+ // anchorAcId list:
4
+ // - AC-tunnel-accessGated (primary)
5
+ // - AC-tunnel-accessGatedRefuse (drift refuse)
6
+ // accountBound: false.
7
+ //
8
+ // The probe reads the two sidecars under fixtureRoot()/sidecars and the
9
+ // two manifests per runtime under fixtureRoot()/<runtime>/cloudflare/tunnels/<mode>.yaml.
10
+ // A fixture-side delegate points FIXTURE_ROOT at a scratch copy to drive
11
+ // the fixture-side aud-drop mutation. The probe module itself is a pure
12
+ // function of the files it sees.
13
+ //
14
+ // Passes when:
15
+ // - Each access-gated manifest carries originRequest.access.aud equal to
16
+ // the gated sidecar accessAud on every non-catch-all ingress rule.
17
+ // - Each public-hostname manifest carries no originRequest.access block.
18
+ //
19
+ // Fails when the gated variant drifts to omit the AUD block (the drift-
20
+ // refuse case). Also fails when the public-hostname variant carries an
21
+ // AUD block (that shape has no source of authority).
22
+
23
+ import { readFile } from 'node:fs/promises';
24
+ import { resolve } from 'node:path';
25
+ import { runShim, fixtureRoot, parseTunnelYaml, RUNTIMES } from './probe-utils.mjs';
26
+
27
+ export const anchorAcIds = ['AC-tunnel-accessGated', 'AC-tunnel-accessGatedRefuse'];
28
+ export const accountBound = false;
29
+
30
+ async function loadSidecar(root, name) {
31
+ const path = resolve(root, 'sidecars', `${name}.applied.json`);
32
+ const text = await readFile(path, 'utf8');
33
+ return JSON.parse(text);
34
+ }
35
+
36
+ async function loadManifest(root, runtime, mode) {
37
+ const path = resolve(root, runtime, 'cloudflare/tunnels', `${mode}.yaml`);
38
+ const text = await readFile(path, 'utf8');
39
+ return parseTunnelYaml(text);
40
+ }
41
+
42
+ export default async function runProbe() {
43
+ const results = [];
44
+ const extra = { fixtureRoot: fixtureRoot() };
45
+ const root = fixtureRoot();
46
+ const gatedSidecar = await loadSidecar(root, 'access-gated');
47
+ const publicSidecar = await loadSidecar(root, 'public-hostname');
48
+ const gatedHasZtg = (gatedSidecar.appliedCapabilities ?? []).includes('zeroTrustGate');
49
+ const publicHasZtg = (publicSidecar.appliedCapabilities ?? []).includes('zeroTrustGate');
50
+ extra.gatedSidecarZtg = gatedHasZtg;
51
+ extra.publicSidecarZtg = publicHasZtg;
52
+ if (!gatedHasZtg) {
53
+ results.push({
54
+ anchorAcId: 'AC-tunnel-accessGated',
55
+ verdict: 'fail',
56
+ detail: `access-gated sidecar is missing zeroTrustGate in appliedCapabilities (found: ${(gatedSidecar.appliedCapabilities ?? []).join(', ')})`,
57
+ });
58
+ }
59
+ if (publicHasZtg) {
60
+ results.push({
61
+ anchorAcId: 'AC-tunnel-accessGated',
62
+ verdict: 'fail',
63
+ detail: `public-hostname sidecar unexpectedly names zeroTrustGate in appliedCapabilities (found: ${(publicSidecar.appliedCapabilities ?? []).join(', ')})`,
64
+ });
65
+ }
66
+ for (const runtime of RUNTIMES) {
67
+ // access-gated manifest: every non-catch-all rule attaches the AUD.
68
+ const gDoc = await loadManifest(root, runtime, 'access-gated');
69
+ const gRules = Array.isArray(gDoc.ingress) ? gDoc.ingress : [];
70
+ const gNonCatch = gRules.filter((r) => !String(r.service ?? '').startsWith('http_status:'));
71
+ const gAttached = gNonCatch.filter((r) => r?.originRequest?.access?.aud === gatedSidecar.accessAud);
72
+ if (gAttached.length === gNonCatch.length && gNonCatch.length > 0) {
73
+ results.push({
74
+ anchorAcId: 'AC-tunnel-accessGated',
75
+ verdict: 'pass',
76
+ detail: `${runtime}/access-gated: all ${gNonCatch.length} non-catch-all rule(s) attach originRequest.access.aud=${gatedSidecar.accessAud}`,
77
+ });
78
+ } else {
79
+ const missing = gNonCatch
80
+ .filter((r) => r?.originRequest?.access?.aud !== gatedSidecar.accessAud)
81
+ .map((r, i) => `service=${r.service} aud=${r?.originRequest?.access?.aud ?? '(none)'}`);
82
+ results.push({
83
+ anchorAcId: 'AC-tunnel-accessGatedRefuse',
84
+ verdict: 'fail',
85
+ detail: `${runtime}/access-gated: sidecar declares zeroTrustGate + accessAud=${gatedSidecar.accessAud} but ${gNonCatch.length - gAttached.length}/${gNonCatch.length} rule(s) drift (missing or wrong AUD). Drifted: ${missing.slice(0, 3).join(' | ')}`,
86
+ });
87
+ }
88
+ // public-hostname manifest: no rule carries originRequest.access.
89
+ const pDoc = await loadManifest(root, runtime, 'public-hostname');
90
+ const pRules = Array.isArray(pDoc.ingress) ? pDoc.ingress : [];
91
+ const carriers = pRules
92
+ .map((r, i) => ({ i, r }))
93
+ .filter(({ r }) => r?.originRequest?.access);
94
+ if (carriers.length === 0) {
95
+ results.push({
96
+ anchorAcId: 'AC-tunnel-accessGated',
97
+ verdict: 'pass',
98
+ detail: `${runtime}/public-hostname: manifest omits originRequest.access on every ingress rule (open ingress by design)`,
99
+ });
100
+ } else {
101
+ results.push({
102
+ anchorAcId: 'AC-tunnel-accessGated',
103
+ verdict: 'fail',
104
+ detail: `${runtime}/public-hostname: sidecar declares no zeroTrustGate but ${carriers.length} ingress rule(s) carry originRequest.access; that shape has no source of authority`,
105
+ });
106
+ }
107
+ }
108
+ return { results, extra };
109
+ }
110
+
111
+ const engine = { kind: 'sidecar-read', image: 'edge-cloudflare-tunnel aud-presence facade', healthy: true };
112
+ if (import.meta.url === `file://${process.argv[1]}`) {
113
+ await runShim('aud-presence-check', engine, runProbe);
114
+ }