rcf-lite 0.24.0 → 0.26.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 (313) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/blueprints/application-account-settings/docs/topics.md +4 -1
  3. package/blueprints/application-admin-console/docs/topics.md +4 -1
  4. package/blueprints/application-api-rest/docs/topics.md +4 -1
  5. package/blueprints/application-charts/docs/topics.md +4 -1
  6. package/blueprints/application-dashboard/docs/topics.md +4 -1
  7. package/blueprints/application-datatable/docs/topics.md +4 -1
  8. package/blueprints/application-empty-error-states/docs/topics.md +4 -1
  9. package/blueprints/application-error-handling/docs/topics.md +4 -1
  10. package/blueprints/application-file-upload/docs/topics.md +4 -1
  11. package/blueprints/application-forms-wizard/docs/topics.md +4 -1
  12. package/blueprints/application-notifications-in-app/docs/topics.md +4 -1
  13. package/blueprints/application-onboarding-tour/docs/topics.md +4 -1
  14. package/blueprints/application-spa/docs/topics.md +4 -1
  15. package/blueprints/delivery-ci-workflows/docs/topics.md +4 -1
  16. package/blueprints/deploy-cloudflare-workers/docs/topics.md +4 -1
  17. package/blueprints/deploy-hetzner-server/CHANGELOG.md +55 -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 +71 -0
  25. package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +164 -0
  26. package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +182 -0
  27. package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +112 -0
  28. package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +64 -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 +113 -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 +4 -1
  61. package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +4 -1
  62. package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +23 -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 +237 -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 +143 -0
  73. package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +166 -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 +4 -1
  99. package/blueprints/email-smtp-resend/docs/topics.md +4 -1
  100. package/blueprints/jobs-background/docs/topics.md +4 -1
  101. package/blueprints/messaging-queue-cloudflare/CHANGELOG.md +19 -0
  102. package/blueprints/messaging-queue-cloudflare/README.md +2 -2
  103. package/blueprints/messaging-queue-cloudflare/blueprint.json +1 -1
  104. package/blueprints/messaging-queue-cloudflare/contributions/probes/real-account-concurrency-smoke.mjs +192 -37
  105. package/blueprints/messaging-queue-cloudflare/docs/topics.md +4 -1
  106. package/blueprints/object-storage-s3/CHANGELOG.md +11 -0
  107. package/blueprints/object-storage-s3/README.md +14 -1
  108. package/blueprints/object-storage-s3/blueprint.json +12 -1
  109. package/blueprints/object-storage-s3/contributions/adrs/adr-2905-object-storage-s3-hetzner-object-storage-provider.json +30 -0
  110. package/blueprints/object-storage-s3/contributions/probes/hetzner-object-storage-round-trip.mjs +136 -0
  111. package/blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs +11 -0
  112. package/blueprints/object-storage-s3/contributions/requirements/object-storage-s3-req-101.json +18 -0
  113. package/blueprints/object-storage-s3/contributions/tacs/tac-2904-object-storage-s3-hetzner-endpoint-helper.json +34 -0
  114. package/blueprints/object-storage-s3/contributions/user-stories/object-storage-s3-us-28110.json +25 -0
  115. package/blueprints/object-storage-s3/docs/topics.md +4 -1
  116. package/blueprints/object-storage-s3/guide/object-storage-s3.md +36 -0
  117. package/blueprints/observability-essentials/docs/topics.md +4 -1
  118. package/blueprints/observability-logging/docs/topics.md +4 -1
  119. package/blueprints/observability-probe-endpoints/docs/topics.md +4 -1
  120. package/blueprints/persistence-data-d1/docs/topics.md +4 -1
  121. package/blueprints/persistence-data-postgres/docs/topics.md +4 -1
  122. package/blueprints/persistence-data-sqlite/docs/topics.md +4 -1
  123. package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +4 -1
  124. package/blueprints/platform-cloudflare-durable-objects/CHANGELOG.md +15 -0
  125. package/blueprints/platform-cloudflare-durable-objects/blueprint.json +1 -1
  126. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/namespace-facade-ready.mjs +3 -3
  127. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/probe-utils.mjs +28 -14
  128. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/real-account-storage-smoke.mjs +132 -26
  129. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/sole-reader-scan.mjs +40 -39
  130. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/storage-round-trip.mjs +26 -16
  131. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/websocket-hub-broadcast.mjs +31 -34
  132. package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +4 -1
  133. package/blueprints/platform-cloudflare-kv/CHANGELOG.md +40 -1
  134. package/blueprints/platform-cloudflare-kv/README.md +41 -13
  135. package/blueprints/platform-cloudflare-kv/blueprint.json +1 -1
  136. package/blueprints/platform-cloudflare-kv/contributions/probes/cache-aside-hit-then-miss.mjs +26 -26
  137. package/blueprints/platform-cloudflare-kv/contributions/probes/event-secrecy.mjs +49 -31
  138. package/blueprints/platform-cloudflare-kv/contributions/probes/facade-round-trip.mjs +58 -16
  139. package/blueprints/platform-cloudflare-kv/contributions/probes/list-with-prefix.mjs +6 -5
  140. package/blueprints/platform-cloudflare-kv/contributions/probes/real-account-eventual-consistency-smoke.mjs +131 -67
  141. package/blueprints/platform-cloudflare-kv/docs/topics.md +4 -1
  142. package/blueprints/platform-docker-compose-host/CHANGELOG.md +12 -0
  143. package/blueprints/platform-docker-compose-host/README.md +76 -0
  144. package/blueprints/platform-docker-compose-host/blueprint.json +209 -0
  145. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3901-platform-docker-compose-host-contract.json +14 -0
  146. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +14 -0
  147. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +14 -0
  148. package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3904-platform-docker-compose-host-log-driver.json +14 -0
  149. package/blueprints/platform-docker-compose-host/contributions/probes/caddyfile-validate.mjs +108 -0
  150. package/blueprints/platform-docker-compose-host/contributions/probes/compose-config-lint.mjs +231 -0
  151. package/blueprints/platform-docker-compose-host/contributions/probes/probe-utils.mjs +185 -0
  152. package/blueprints/platform-docker-compose-host/contributions/probes/real-account-minimal-stack-up.mjs +41 -0
  153. package/blueprints/platform-docker-compose-host/contributions/probes/real-account-reload-burst.mjs +36 -0
  154. package/blueprints/platform-docker-compose-host/contributions/probes/run-caddyfile-validate.mjs +5 -0
  155. package/blueprints/platform-docker-compose-host/contributions/probes/run-compose-config-lint.mjs +5 -0
  156. package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-minimal-stack-up.mjs +5 -0
  157. package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-reload-burst.mjs +5 -0
  158. package/blueprints/platform-docker-compose-host/contributions/probes/run-secrets-as-files-scan.mjs +5 -0
  159. package/blueprints/platform-docker-compose-host/contributions/probes/secrets-as-files-scan.mjs +116 -0
  160. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-001.json +18 -0
  161. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-002.json +18 -0
  162. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-003.json +18 -0
  163. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-004.json +18 -0
  164. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-005.json +18 -0
  165. package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-006.json +18 -0
  166. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +26 -0
  167. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +26 -0
  168. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +26 -0
  169. package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +26 -0
  170. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38101.json +24 -0
  171. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38102.json +24 -0
  172. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38103.json +24 -0
  173. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38104.json +24 -0
  174. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38105.json +25 -0
  175. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38106.json +24 -0
  176. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38107.json +24 -0
  177. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38108.json +24 -0
  178. package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38109.json +24 -0
  179. package/blueprints/platform-docker-compose-host/docs/topics.md +71 -0
  180. package/blueprints/platform-docker-compose-host/guide/platform-docker-compose-host.md +94 -0
  181. package/blueprints/security-auth-clerk/docs/topics.md +4 -1
  182. package/blueprints/security-auth-keycloak/docs/topics.md +4 -1
  183. package/blueprints/security-auth-magic-link/docs/topics.md +4 -1
  184. package/blueprints/security-auth-oauth2/docs/topics.md +4 -1
  185. package/blueprints/security-secrets-management/docs/topics.md +4 -1
  186. package/fixtures/canary-manifest.json +9 -9
  187. package/guidance/harness-template.md +67 -0
  188. package/guidance/managed/agent-instructions-block.hash +1 -1
  189. package/guidance/managed/agent-instructions-block.md +67 -0
  190. package/package.json +2 -2
  191. package/rcf/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +13 -0
  192. package/rcf/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +13 -0
  193. package/rcf/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +13 -0
  194. package/rcf/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +13 -0
  195. package/rcf/adrs/adr-3901-platform-docker-compose-host-contract.json +13 -0
  196. package/rcf/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +13 -0
  197. package/rcf/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +13 -0
  198. package/rcf/adrs/adr-3904-platform-docker-compose-host-log-driver.json +13 -0
  199. package/rcf/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +13 -0
  200. package/rcf/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +13 -0
  201. package/rcf/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +13 -0
  202. package/rcf/code-nodes/cn-410.json +20 -0
  203. package/rcf/code-nodes/cn-440.json +20 -0
  204. package/rcf/code-nodes/cn-470.json +19 -0
  205. package/rcf/code-nodes/cn-500.json +12 -0
  206. package/rcf/code-nodes/cn-510.json +12 -0
  207. package/rcf/code-nodes/cn-520.json +14 -0
  208. package/rcf/code-nodes/cn-521.json +12 -0
  209. package/rcf/code-nodes/cn-522.json +13 -0
  210. package/rcf/code-nodes/cn-523.json +12 -0
  211. package/rcf/code-nodes/cn-524.json +14 -0
  212. package/rcf/code-nodes/cn-525.json +12 -0
  213. package/rcf/code-nodes/cn-526.json +14 -0
  214. package/rcf/fbs/fbs-130.json +23 -0
  215. package/rcf/fbs/fbs-140.json +23 -0
  216. package/rcf/fbs/fbs-150.json +22 -0
  217. package/rcf/fbs/fbs-160.json +15 -0
  218. package/rcf/fbs/fbs-165.json +15 -0
  219. package/rcf/fbs/fbs-170.json +16 -0
  220. package/rcf/fbs/fbs-171.json +16 -0
  221. package/rcf/fbs/fbs-172.json +15 -0
  222. package/rcf/fbs/fbs-173.json +17 -0
  223. package/rcf/fbs/fbs-174.json +17 -0
  224. package/rcf/requirements/req-110.json +58 -0
  225. package/rcf/requirements/req-111.json +32 -0
  226. package/rcf/requirements/req-112.json +42 -0
  227. package/rcf/requirements/req-113.json +33 -0
  228. package/rcf/requirements/req-114.json +20 -0
  229. package/rcf/requirements/req-115.json +38 -0
  230. package/rcf/requirements/req-120.json +20 -0
  231. package/rcf/requirements/req-121.json +20 -0
  232. package/rcf/requirements/req-122.json +20 -0
  233. package/rcf/requirements/req-123.json +42 -0
  234. package/rcf/requirements/req-124.json +27 -0
  235. package/rcf/requirements/req-125.json +20 -0
  236. package/rcf/requirements/req-130.json +32 -0
  237. package/rcf/requirements/req-131.json +54 -0
  238. package/rcf/requirements/req-132.json +27 -0
  239. package/rcf/requirements/req-133.json +20 -0
  240. package/rcf/requirements/req-134.json +42 -0
  241. package/rcf/requirements/req-140.json +149 -0
  242. package/rcf/requirements/req-145.json +53 -0
  243. package/rcf/requirements/req-150.json +20 -0
  244. package/rcf/requirements/req-151.json +85 -0
  245. package/rcf/requirements/req-152.json +20 -0
  246. package/rcf/requirements/req-153.json +59 -0
  247. package/rcf/requirements/req-154.json +44 -0
  248. package/rcf/tacs/tac-3801-deploy-hetzner-server-provisioner.json +17 -0
  249. package/rcf/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +17 -0
  250. package/rcf/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +17 -0
  251. package/rcf/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +16 -0
  252. package/rcf/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +17 -0
  253. package/rcf/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +17 -0
  254. package/rcf/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +17 -0
  255. package/rcf/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +17 -0
  256. package/rcf/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +17 -0
  257. package/rcf/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +17 -0
  258. package/rcf/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +16 -0
  259. package/rcf/test-suites/ts-071.json +2 -2
  260. package/rcf/test-suites/ts-086.json +2 -2
  261. package/rcf/test-suites/ts-140.json +86 -0
  262. package/rcf/test-suites/ts-150.json +86 -0
  263. package/rcf/test-suites/ts-160.json +78 -0
  264. package/rcf/test-suites/ts-170.json +22 -0
  265. package/rcf/test-suites/ts-175.json +22 -0
  266. package/rcf/test-suites/ts-180.json +30 -0
  267. package/rcf/test-suites/ts-181.json +30 -0
  268. package/rcf/test-suites/ts-182.json +22 -0
  269. package/rcf/test-suites/ts-183.json +38 -0
  270. package/rcf/test-suites/ts-184.json +38 -0
  271. package/rcf/user-stories/us-11001.json +23 -0
  272. package/rcf/user-stories/us-11101.json +23 -0
  273. package/rcf/user-stories/us-11102.json +24 -0
  274. package/rcf/user-stories/us-11201.json +23 -0
  275. package/rcf/user-stories/us-11202.json +23 -0
  276. package/rcf/user-stories/us-11301.json +24 -0
  277. package/rcf/user-stories/us-11401.json +23 -0
  278. package/rcf/user-stories/us-11402.json +23 -0
  279. package/rcf/user-stories/us-11501.json +23 -0
  280. package/rcf/user-stories/us-12001.json +23 -0
  281. package/rcf/user-stories/us-12101.json +23 -0
  282. package/rcf/user-stories/us-12102.json +23 -0
  283. package/rcf/user-stories/us-12201.json +23 -0
  284. package/rcf/user-stories/us-12202.json +24 -0
  285. package/rcf/user-stories/us-12301.json +23 -0
  286. package/rcf/user-stories/us-12401.json +23 -0
  287. package/rcf/user-stories/us-12402.json +23 -0
  288. package/rcf/user-stories/us-12501.json +23 -0
  289. package/rcf/user-stories/us-13001.json +23 -0
  290. package/rcf/user-stories/us-13002.json +23 -0
  291. package/rcf/user-stories/us-13101.json +23 -0
  292. package/rcf/user-stories/us-13102.json +23 -0
  293. package/rcf/user-stories/us-13201.json +23 -0
  294. package/rcf/user-stories/us-13301.json +23 -0
  295. package/rcf/user-stories/us-13302.json +23 -0
  296. package/rcf/user-stories/us-13401.json +23 -0
  297. package/rcf/user-stories/us-14001.json +21 -0
  298. package/rcf/user-stories/us-14501.json +24 -0
  299. package/rcf/user-stories/us-15001.json +26 -0
  300. package/rcf/user-stories/us-15101.json +26 -0
  301. package/rcf/user-stories/us-15201.json +21 -0
  302. package/rcf/user-stories/us-15301.json +31 -0
  303. package/rcf/user-stories/us-15401.json +31 -0
  304. package/releases/releases.yaml +21 -1
  305. package/src/blueprint/apply.js +64 -0
  306. package/src/blueprint/consistency-lint.js +432 -0
  307. package/src/blueprint/disposition-ledger.js +224 -0
  308. package/src/blueprint/dispositions.js +44 -0
  309. package/src/blueprint/index.js +19 -0
  310. package/src/browser-verify/manifest-writer.js +53 -2
  311. package/src/browser-verify/pack-runner.js +18 -0
  312. package/src/cli/blueprint.js +103 -0
  313. package/src/setup/identity-seed.js +18 -0
@@ -0,0 +1,182 @@
1
+ // Probe: manifest schema validate.
2
+ //
3
+ // anchorAcId: AC-37102-1 (also covers AC-37106-1 firewall shape and
4
+ // AC-37107-1 snapshot cadence enum via the same schema). accountBound: false.
5
+ //
6
+ // Validates every hetzner/servers/*.json under the shared throwaway-server
7
+ // fixture against blueprints/deploy-hetzner-server/contributions/schemas/
8
+ // hetzner-server.schema.json. A lightweight in-process draft-07 subset
9
+ // validator ships with the probe so the shelf gains no runtime dependency
10
+ // on ajv or similar. See validate() at the bottom.
11
+ //
12
+ // Purity: the probe reads only the manifest dir and the shipped schema;
13
+ // no process.env.SIMULATE_ switch is read here. The fixture-side shim
14
+ // packages/rcf-lite/test/fixtures/hetzner-throwaway-server/
15
+ // run-manifest-schema-validate.mjs is the sole reader of
16
+ // SIMULATE_MANIFEST_INVALID and it mutates INPUT (a temp manifest dir
17
+ // pointed at via RCF_FIXTURE_MANIFEST_DIR) only; the probe then FAILS
18
+ // naming the offending field per hetzner-round-7-spec-2026-09-07.md
19
+ // section 3.4 lesson 4. H-1 (2026-09-08).
20
+
21
+ import { readFile } from 'node:fs/promises';
22
+ import { readManifestFiles, SCHEMA_PATH } from './probe-utils.mjs';
23
+
24
+ export const anchorAcId = 'AC-37102-1';
25
+ export const accountBound = false;
26
+
27
+ export default async function runProbe() {
28
+ const schema = JSON.parse(await readFile(SCHEMA_PATH, 'utf8'));
29
+ const { present, files } = await readManifestFiles();
30
+ if (!present) {
31
+ return {
32
+ results: [{
33
+ anchorAcId,
34
+ verdict: 'fail',
35
+ detail: 'fixture hetzner/servers/ holds zero JSON files; ship at least ci-throwaway.json.',
36
+ }],
37
+ };
38
+ }
39
+ const results = [];
40
+ for (const f of files) {
41
+ let doc;
42
+ try {
43
+ doc = JSON.parse(f.text);
44
+ } catch (err) {
45
+ results.push({
46
+ anchorAcId,
47
+ verdict: 'fail',
48
+ detail: `manifest ${f.name} does not parse: ${err.message}`,
49
+ });
50
+ continue;
51
+ }
52
+ const errors = validate(schema, doc, `#/${f.name}`);
53
+ if (errors.length > 0) {
54
+ for (const e of errors) {
55
+ const anchor = anchorForError(e);
56
+ results.push({
57
+ anchorAcId: anchor,
58
+ verdict: 'fail',
59
+ detail: `manifest ${f.name} schema violation at ${e.path}: ${e.message}`,
60
+ });
61
+ }
62
+ } else {
63
+ results.push({
64
+ anchorAcId,
65
+ verdict: 'pass',
66
+ detail: `manifest ${f.name} validates against hetzner-server.schema.json (nine required fields, firewall rule shape, snapshotCadence enum all satisfied).`,
67
+ });
68
+ }
69
+ }
70
+ return {
71
+ results,
72
+ extra: {
73
+ manifestCount: files.length,
74
+ manifestNames: files.map((f) => f.name),
75
+ },
76
+ };
77
+ }
78
+
79
+ function anchorForError(err) {
80
+ if (err.field === 'firewallRules' || (err.path || '').includes('firewallRules')) {
81
+ return 'AC-37106-1';
82
+ }
83
+ if (err.field === 'snapshotCadence' || (err.path || '').includes('snapshotCadence')) {
84
+ return 'AC-37107-1';
85
+ }
86
+ return 'AC-37102-1';
87
+ }
88
+
89
+ // Draft-07 subset validator: required, type, enum, minLength, minItems,
90
+ // additionalProperties, items, properties, required-on-array-items.
91
+ // Extra rule: on firewallRules the ssh rule sourceIps must NOT include
92
+ // 0.0.0.0/0 (per TAC-3804); handled after schema validation.
93
+ export function validate(schema, doc, pathPrefix = '#') {
94
+ const errors = [];
95
+ walk(schema, doc, pathPrefix, errors);
96
+ if (Array.isArray(doc.firewallRules)) {
97
+ const ssh = doc.firewallRules.find((r) => r && r.name === 'ssh');
98
+ if (ssh && Array.isArray(ssh.sourceIps) && ssh.sourceIps.some((s) => s === '0.0.0.0/0' || s === '::/0')) {
99
+ errors.push({
100
+ path: `${pathPrefix}/firewallRules/ssh/sourceIps`,
101
+ field: 'firewallRules',
102
+ message: 'ssh rule sourceIps must not include 0.0.0.0/0 or ::/0; use an operator-nominated set per TAC-3804.',
103
+ });
104
+ }
105
+ const need = ['ssh', 'http', 'https'];
106
+ for (const n of need) {
107
+ if (!doc.firewallRules.some((r) => r && r.name === n)) {
108
+ errors.push({
109
+ path: `${pathPrefix}/firewallRules`,
110
+ field: 'firewallRules',
111
+ message: `firewallRules is missing the required ${n} rule per TAC-3804.`,
112
+ });
113
+ }
114
+ }
115
+ }
116
+ return errors;
117
+ }
118
+
119
+ function walk(schema, value, path, errors) {
120
+ if (!schema || typeof schema !== 'object') return;
121
+ if (Array.isArray(schema.required) && typeof value === 'object' && value !== null && !Array.isArray(value)) {
122
+ for (const k of schema.required) {
123
+ if (!(k in value)) {
124
+ errors.push({ path: `${path}/${k}`, field: k, message: `missing required field ${k}` });
125
+ }
126
+ }
127
+ }
128
+ if (schema.type) {
129
+ const typeOk = checkType(schema.type, value);
130
+ if (!typeOk) {
131
+ errors.push({ path, message: `expected type ${JSON.stringify(schema.type)}, got ${jsType(value)}` });
132
+ return;
133
+ }
134
+ }
135
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
136
+ errors.push({ path, message: `value ${JSON.stringify(value)} is not in enum ${JSON.stringify(schema.enum)}` });
137
+ }
138
+ if (typeof schema.minLength === 'number' && typeof value === 'string' && value.length < schema.minLength) {
139
+ errors.push({ path, message: `string shorter than minLength ${schema.minLength}` });
140
+ }
141
+ if (typeof schema.minItems === 'number' && Array.isArray(value) && value.length < schema.minItems) {
142
+ errors.push({ path, message: `array shorter than minItems ${schema.minItems}` });
143
+ }
144
+ if (schema.type === 'object' && schema.additionalProperties === false && value && typeof value === 'object') {
145
+ const known = new Set(Object.keys(schema.properties || {}));
146
+ for (const k of Object.keys(value)) {
147
+ if (!known.has(k)) {
148
+ errors.push({ path: `${path}/${k}`, field: k, message: `unknown field ${k} (additionalProperties false)` });
149
+ }
150
+ }
151
+ }
152
+ if (schema.properties && value && typeof value === 'object' && !Array.isArray(value)) {
153
+ for (const [k, sub] of Object.entries(schema.properties)) {
154
+ if (k in value) walk(sub, value[k], `${path}/${k}`, errors);
155
+ }
156
+ }
157
+ if (schema.items && Array.isArray(value)) {
158
+ value.forEach((v, i) => walk(schema.items, v, `${path}/${i}`, errors));
159
+ }
160
+ }
161
+
162
+ function checkType(type, value) {
163
+ const types = Array.isArray(type) ? type : [type];
164
+ return types.some((t) => matchType(t, value));
165
+ }
166
+
167
+ function matchType(t, value) {
168
+ if (t === 'null') return value === null;
169
+ if (t === 'array') return Array.isArray(value);
170
+ if (t === 'object') return typeof value === 'object' && value !== null && !Array.isArray(value);
171
+ if (t === 'integer') return Number.isInteger(value);
172
+ if (t === 'number') return typeof value === 'number';
173
+ if (t === 'string') return typeof value === 'string';
174
+ if (t === 'boolean') return typeof value === 'boolean';
175
+ return false;
176
+ }
177
+
178
+ function jsType(v) {
179
+ if (v === null) return 'null';
180
+ if (Array.isArray(v)) return 'array';
181
+ return typeof v;
182
+ }
@@ -0,0 +1,112 @@
1
+ // Shared helpers for deploy-hetzner-server probes.
2
+ //
3
+ // Runtime-dependency posture:
4
+ // - cloud-init-render-lint, manifest-schema-validate and
5
+ // hcloud-dry-run-mock run in-process against the shared throwaway-
6
+ // server fixture at packages/rcf-lite/test/fixtures/hetzner-throwaway-
7
+ // server/. No real API call fires; hcloud is mocked via
8
+ // src/hcloud-mock.mjs so the shim never crosses the process boundary.
9
+ // - real-account-* probes call the fixture's provision.mjs / destroy.mjs
10
+ // / snapshot verbs. Without CI_HAS_HETZNER_ACCOUNT they record
11
+ // accountBoundSkipped: true and the aggregate flips to pass per
12
+ // hetzner-round-7-spec-2026-09-07.md section 3.5.
13
+
14
+ import { mkdir, writeFile, readFile, readdir } from 'node:fs/promises';
15
+ import { dirname, resolve, join } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const HERE = dirname(fileURLToPath(import.meta.url));
19
+ export const PROJECT_ROOT = resolve(HERE, '..', '..', '..', '..');
20
+ export const FIXTURE_DIR = resolve(
21
+ PROJECT_ROOT,
22
+ 'packages/rcf-lite/test/fixtures/hetzner-throwaway-server',
23
+ );
24
+ // Config override, not a mutation switch: the fixture-side shim for
25
+ // manifest-schema-validate may write a mutated copy to a scratch dir
26
+ // and point MANIFEST_DIR at it via RCF_FIXTURE_MANIFEST_DIR so the
27
+ // probe stays SIMULATE-free (H-1 mutation-purity gate row, 2026-09-08).
28
+ export const MANIFEST_DIR = process.env.RCF_FIXTURE_MANIFEST_DIR
29
+ ? resolve(process.env.RCF_FIXTURE_MANIFEST_DIR)
30
+ : resolve(FIXTURE_DIR, 'hetzner/servers');
31
+ export const SCHEMA_PATH = resolve(HERE, '..', 'schemas', 'hetzner-server.schema.json');
32
+ export const REPORT_DIR = resolve(
33
+ PROJECT_ROOT,
34
+ '.rcf/reports/blueprints/deploy-hetzner-server',
35
+ );
36
+
37
+ export function aggregate(results) {
38
+ if (results.some((r) => r.verdict === 'fail')) return 'fail';
39
+ if (results.some((r) => r.verdict === 'warn')) return 'warn';
40
+ return 'pass';
41
+ }
42
+
43
+ export function isSkipped(results) {
44
+ return results.length > 0 && results.every((r) => r.accountBoundSkipped === true);
45
+ }
46
+
47
+ export async function writeReport({ probeName, engine, results, extra }) {
48
+ await mkdir(REPORT_DIR, { recursive: true });
49
+ const rawVerdict = aggregate(results);
50
+ const aggregateVerdict = isSkipped(results) ? 'pass' : rawVerdict;
51
+ const report = {
52
+ slug: 'deploy-hetzner-server',
53
+ probeName,
54
+ runAt: new Date().toISOString(),
55
+ engine,
56
+ results,
57
+ aggregateVerdict,
58
+ ...(extra ?? {}),
59
+ };
60
+ const path = resolve(REPORT_DIR, `${probeName}.json`);
61
+ await writeFile(path, JSON.stringify(report, null, 2) + '\n', 'utf8');
62
+ return { report, path };
63
+ }
64
+
65
+ export async function runShim(probeName, engine, mainFn) {
66
+ try {
67
+ const outcome = (await mainFn()) ?? { results: [] };
68
+ const { results, extra } = outcome;
69
+ const { report, path } = await writeReport({ probeName, engine, results, extra });
70
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
71
+ process.stdout.write(`report written to ${path}\n`);
72
+ if (report.aggregateVerdict === 'fail') process.exitCode = 1;
73
+ } catch (err) {
74
+ const results = [{
75
+ anchorAcId: 'unknown',
76
+ verdict: 'fail',
77
+ detail: `probe threw: ${err && err.message ? err.message : String(err)}`,
78
+ }];
79
+ const { report, path } = await writeReport({ probeName, engine, results });
80
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
81
+ process.stderr.write(`probe error: ${err && err.stack ? err.stack : String(err)}\n`);
82
+ process.stderr.write(`report written to ${path}\n`);
83
+ process.exitCode = 1;
84
+ }
85
+ }
86
+
87
+ export async function readManifestFiles(dir = MANIFEST_DIR) {
88
+ let entries;
89
+ try {
90
+ entries = (await readdir(dir))
91
+ .filter((f) => f.endsWith('.json'))
92
+ .sort();
93
+ } catch (err) {
94
+ return { present: false, entries: [], files: [], error: err.message };
95
+ }
96
+ const files = [];
97
+ for (const name of entries) {
98
+ const p = join(dir, name);
99
+ const text = await readFile(p, 'utf8');
100
+ files.push({ name, path: p, text });
101
+ }
102
+ return { present: entries.length > 0, entries, files };
103
+ }
104
+
105
+ export function accountBoundSkippedResult(anchorAcId, note) {
106
+ return {
107
+ anchorAcId,
108
+ verdict: 'skipped',
109
+ accountBoundSkipped: true,
110
+ detail: `accountBound: CI_HAS_HETZNER_ACCOUNT unset; ${note}`,
111
+ };
112
+ }
@@ -0,0 +1,64 @@
1
+ // Probe: real-account cloud-init hardened.
2
+ //
3
+ // anchorAcId: AC-37105-1. accountBound: true.
4
+ //
5
+ // Without CI_HAS_HETZNER_ACCOUNT the probe records
6
+ // accountBoundSkipped: true and the aggregate flips to pass.
7
+ //
8
+ // With the env var set the probe:
9
+ // - provisions the throwaway server via the fixture provision.mjs;
10
+ // - shells cloud-init status --wait over ssh to the throwaway server;
11
+ // - runs the six baseline checks over ssh (grep for the marker per
12
+ // baseline block); FAILS naming the first missing block on any
13
+ // defect.
14
+ // - always() calls destroy.mjs regardless of verdict.
15
+
16
+ import { accountBoundSkippedResult, FIXTURE_DIR } from './probe-utils.mjs';
17
+ import { resolve } from 'node:path';
18
+
19
+ export const anchorAcId = 'AC-37105-1';
20
+ export const accountBound = true;
21
+
22
+ export default async function runProbe() {
23
+ if (process.env.CI_HAS_HETZNER_ACCOUNT !== 'true') {
24
+ return {
25
+ results: [accountBoundSkippedResult(anchorAcId, 'real-account cloud-init hardened check skipped; run with CI_HAS_HETZNER_ACCOUNT=true to exercise the six ssh baseline checks against a throwaway cx23.')],
26
+ };
27
+ }
28
+ const provisionPath = resolve(FIXTURE_DIR, 'provision.mjs');
29
+ const destroyPath = resolve(FIXTURE_DIR, 'destroy.mjs');
30
+ const sshCheckPath = resolve(FIXTURE_DIR, 'src/ssh-baseline-check.mjs');
31
+ const { provisionThrowawayServer } = await import(provisionPath);
32
+ const { destroyThrowawayServer } = await import(destroyPath);
33
+ const { runSshBaselineChecks } = await import(sshCheckPath);
34
+ let provisioned;
35
+ try {
36
+ provisioned = await provisionThrowawayServer({ runId: process.env.GITHUB_RUN_ID ?? 'local' });
37
+ const report = await runSshBaselineChecks(provisioned);
38
+ const failed = report.checks.filter((c) => c.verdict === 'fail');
39
+ if (failed.length > 0) {
40
+ return {
41
+ results: failed.map((c) => ({
42
+ anchorAcId,
43
+ verdict: 'fail',
44
+ detail: `cloud-init baseline block "${c.label}" failed on server ${provisioned.id}: ${c.detail}`,
45
+ })),
46
+ };
47
+ }
48
+ const blocks = report.checks.filter((c) => c.verdict === 'pass').map((c) => c.id);
49
+ return {
50
+ results: [{
51
+ anchorAcId,
52
+ verdict: 'pass',
53
+ detail: `cloud-init reached terminal state on server ${provisioned.id} (cloud-init status --wait exit ${report.cloudInit ? report.cloudInit.code : 'skipped'}); six baseline hardening blocks observed: ${blocks.join(', ')}.`,
54
+ }],
55
+ extra: report,
56
+ };
57
+ } catch (err) {
58
+ return { results: [{ anchorAcId, verdict: 'fail', detail: `check failed: ${err.message}` }] };
59
+ } finally {
60
+ if (provisioned && provisioned.id) {
61
+ try { await destroyThrowawayServer(provisioned); } catch (_) { /* swept by orphan cron */ }
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,60 @@
1
+ // Probe: real-account snapshot on demand.
2
+ //
3
+ // anchorAcId: AC-37108-1. accountBound: true.
4
+ //
5
+ // Without CI_HAS_HETZNER_ACCOUNT the probe records
6
+ // accountBoundSkipped: true and the aggregate flips to pass.
7
+ //
8
+ // With the env var set the probe:
9
+ // - provisions the throwaway server via provision.mjs;
10
+ // - fires the snapshot verb on the provisioner facade;
11
+ // - asserts hcloud image list --type=snapshot --output json shows a
12
+ // snapshot with the server name and current wall-clock time in the
13
+ // labels;
14
+ // - always() calls destroy.mjs regardless of verdict; the snapshot is
15
+ // deleted as part of destroy.mjs so the run leaves no orphaned
16
+ // image behind.
17
+
18
+ import { accountBoundSkippedResult, FIXTURE_DIR } from './probe-utils.mjs';
19
+ import { resolve } from 'node:path';
20
+
21
+ export const anchorAcId = 'AC-37108-1';
22
+ export const accountBound = true;
23
+
24
+ export default async function runProbe() {
25
+ if (process.env.CI_HAS_HETZNER_ACCOUNT !== 'true') {
26
+ return {
27
+ results: [accountBoundSkippedResult(anchorAcId, 'real-account snapshot on-demand skipped; run with CI_HAS_HETZNER_ACCOUNT=true to exercise the hcloud image create-image path.')],
28
+ };
29
+ }
30
+ const provisionPath = resolve(FIXTURE_DIR, 'provision.mjs');
31
+ const destroyPath = resolve(FIXTURE_DIR, 'destroy.mjs');
32
+ const snapshotPath = resolve(FIXTURE_DIR, 'src/snapshot-verb.mjs');
33
+ const { provisionThrowawayServer } = await import(provisionPath);
34
+ const { destroyThrowawayServer } = await import(destroyPath);
35
+ const { takeAndVerifySnapshot } = await import(snapshotPath);
36
+ let provisioned;
37
+ try {
38
+ provisioned = await provisionThrowawayServer({ runId: process.env.GITHUB_RUN_ID ?? 'local' });
39
+ const outcome = await takeAndVerifySnapshot(provisioned);
40
+ if (!outcome.snapshotId) {
41
+ return {
42
+ results: [{ anchorAcId, verdict: 'fail', detail: `snapshot verb ran but no matching snapshot appeared in hcloud image list.` }],
43
+ };
44
+ }
45
+ return {
46
+ results: [{
47
+ anchorAcId,
48
+ verdict: 'pass',
49
+ detail: `snapshot ${outcome.snapshotId} tagged with server name ${provisioned.name} and time ${outcome.wallClockTime} on server ${provisioned.id}.`,
50
+ }],
51
+ extra: outcome,
52
+ };
53
+ } catch (err) {
54
+ return { results: [{ anchorAcId, verdict: 'fail', detail: `snapshot verb threw: ${err.message}` }] };
55
+ } finally {
56
+ if (provisioned && provisioned.id) {
57
+ try { await destroyThrowawayServer(provisioned); } catch (_) { /* swept by orphan cron */ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,58 @@
1
+ // Probe: real-account throwaway-server provision.
2
+ //
3
+ // anchorAcId: AC-37103-1. accountBound: true.
4
+ //
5
+ // Without CI_HAS_HETZNER_ACCOUNT the probe records
6
+ // accountBoundSkipped: true and the aggregate flips to pass per
7
+ // hetzner-round-7-spec-2026-09-07.md section 3.5.
8
+ //
9
+ // With the env var set the probe:
10
+ // - runs the fixture's provision.mjs against the ci-throwaway manifest;
11
+ // - asserts a new server appears in `hcloud server list --output json`
12
+ // whose id matches the scratch-file-written id;
13
+ // - asserts the fixture's captured hetznerServerProvisioned event
14
+ // carries id, primaryIpv4, location=fsn1, serverType=cx23;
15
+ // - always() calls destroy.mjs so the throwaway server is torn down
16
+ // regardless of verdict.
17
+
18
+ import { accountBoundSkippedResult, FIXTURE_DIR } from './probe-utils.mjs';
19
+ import { resolve } from 'node:path';
20
+
21
+ export const anchorAcId = 'AC-37103-1';
22
+ export const accountBound = true;
23
+
24
+ export default async function runProbe() {
25
+ if (process.env.CI_HAS_HETZNER_ACCOUNT !== 'true') {
26
+ return {
27
+ results: [accountBoundSkippedResult(anchorAcId, 'real-account throwaway-server provision skipped; run with CI_HAS_HETZNER_ACCOUNT=true to exercise.')],
28
+ };
29
+ }
30
+ const provisionPath = resolve(FIXTURE_DIR, 'provision.mjs');
31
+ const destroyPath = resolve(FIXTURE_DIR, 'destroy.mjs');
32
+ const { provisionThrowawayServer } = await import(provisionPath);
33
+ const { destroyThrowawayServer } = await import(destroyPath);
34
+ let provisioned;
35
+ try {
36
+ provisioned = await provisionThrowawayServer({ runId: process.env.GITHUB_RUN_ID ?? 'local' });
37
+ return {
38
+ results: [{
39
+ anchorAcId,
40
+ verdict: 'pass',
41
+ detail: `throwaway server ${provisioned.id} provisioned; hetznerServerProvisioned event captured; primaryIpv4=${provisioned.primaryIpv4} location=${provisioned.location} serverType=${provisioned.serverType}.`,
42
+ }],
43
+ extra: { serverId: provisioned.id, primaryIpv4: provisioned.primaryIpv4 },
44
+ };
45
+ } catch (err) {
46
+ return {
47
+ results: [{
48
+ anchorAcId,
49
+ verdict: 'fail',
50
+ detail: `provision failed: ${err.message}`,
51
+ }],
52
+ };
53
+ } finally {
54
+ if (provisioned && provisioned.id) {
55
+ try { await destroyThrowawayServer(provisioned); } catch (_) { /* swept by orphan cron */ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,9 @@
1
+ import runProbe from './cloud-init-render-lint.mjs';
2
+ import { runShim } from './probe-utils.mjs';
3
+
4
+ const engine = {
5
+ kind: 'template-render',
6
+ image: 'hetzner-throwaway-server fixture (in-process template render)',
7
+ healthy: true,
8
+ };
9
+ runShim('cloud-init-render-lint', engine, runProbe);
@@ -0,0 +1,9 @@
1
+ import runProbe from './hcloud-dry-run-mock.mjs';
2
+ import { runShim } from './probe-utils.mjs';
3
+
4
+ const engine = {
5
+ kind: 'facade-round-trip',
6
+ image: 'hetzner-throwaway-server fixture provisioner facade against a mocked hcloud shim',
7
+ healthy: true,
8
+ };
9
+ runShim('hcloud-dry-run-mock', engine, runProbe);
@@ -0,0 +1,9 @@
1
+ import runProbe from './manifest-schema-validate.mjs';
2
+ import { runShim } from './probe-utils.mjs';
3
+
4
+ const engine = {
5
+ kind: 'schema-validate',
6
+ image: 'hetzner-throwaway-server fixture manifest directory (draft-07 subset in-process)',
7
+ healthy: true,
8
+ };
9
+ runShim('manifest-schema-validate', engine, runProbe);
@@ -0,0 +1,9 @@
1
+ import runProbe from './real-account-cloud-init-hardened.mjs';
2
+ import { runShim } from './probe-utils.mjs';
3
+
4
+ const engine = {
5
+ kind: 'real-account-ssh',
6
+ image: 'Hetzner Cloud throwaway cx23 with cloud-init user-data (ssh baseline checks)',
7
+ healthy: true,
8
+ };
9
+ runShim('real-account-cloud-init-hardened', engine, runProbe);
@@ -0,0 +1,9 @@
1
+ import runProbe from './real-account-snapshot-on-demand.mjs';
2
+ import { runShim } from './probe-utils.mjs';
3
+
4
+ const engine = {
5
+ kind: 'real-account-snapshot',
6
+ image: 'Hetzner Cloud throwaway cx23 with snapshot verb via hcloud image create-image',
7
+ healthy: true,
8
+ };
9
+ runShim('real-account-snapshot-on-demand', engine, runProbe);
@@ -0,0 +1,9 @@
1
+ import runProbe from './real-account-throwaway-server-provision.mjs';
2
+ import { runShim } from './probe-utils.mjs';
3
+
4
+ const engine = {
5
+ kind: 'real-account-throwaway-server',
6
+ image: 'Hetzner Cloud API via hcloud CLI (provision + list + destroy)',
7
+ healthy: true,
8
+ };
9
+ runShim('real-account-throwaway-server-provision', engine, runProbe);
@@ -0,0 +1,18 @@
1
+ {
2
+ "reqId": "deploy-hetzner-server-REQ-001",
3
+ "prdId": "PRD-001",
4
+ "title": "The provisioner facade is the sole reader of the Hetzner API token and opens on boot with provisionerReady",
5
+ "description": "The applying project reaches the Hetzner Cloud API through a single provisioner facade module that is the ONLY module holding a reference to the HETZNER_ACCOUNT_API_KEY secret (or the elicited project-scoped alternative). The facade wraps typed verbs (createServer, applyFirewall, takeSnapshot, listSnapshots, destroyServer) and, when provisioningTool=hcloud (default), shells to the hcloud CLI with `--output json`; when provisioningTool=raw-api it fetches the equivalent Hetzner Cloud REST endpoints per https://docs.hetzner.cloud/. On process boot the facade opens against the token via a bounded ready-check and emits a provisionerReady lifecycle event on the injected event sink with a metadata-only payload {tool, apiHost}. No other module reads the token; the injected event sink never carries the token or user-data bodies.",
6
+ "category": "functional",
7
+ "domain": "platform",
8
+ "priority": "must",
9
+ "rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-001). Ships as v1.0.0 of deploy-hetzner-server.",
10
+ "tags": [
11
+ "blueprint:deploy-hetzner-server",
12
+ "capability:cloudHost"
13
+ ],
14
+ "version": "1.0.0",
15
+ "status": "approved",
16
+ "createdAt": "2026-09-08T00:00:00.000Z",
17
+ "updatedAt": "2026-09-08T00:00:00.000Z"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "reqId": "deploy-hetzner-server-REQ-002",
3
+ "prdId": "PRD-001",
4
+ "title": "The applying project holds server declarations at hetzner/servers/name.json against a shipped JSON Schema",
5
+ "description": "The applying project holds every server declaration as one JSON document under hetzner/servers/<name>.json, matching a shipped draft-07 JSON Schema at blueprints/deploy-hetzner-server/contributions/schemas/hetzner-server.schema.json. Required fields: name, serverType (cx23, cx33, and other Hetzner shared-cpu shapes), location (fsn1 default per Baz ruling 2026-09-07 09:50Z; also nbg1, hel1, ash, hil), image (ubuntu-24.04 default), sshKeyIds (nonempty array of Hetzner ssh-key references delegated to security-secrets-management), firewallId (an existing firewall or auto-create), networkId (optional private network), cloudInitPath (relative path to the rendered cloud-init user-data), labels (Hetzner tag map that must include role and blueprint), firewallRules (see REQ-004), snapshotCadence (see REQ-005). The schema refuses on missing required fields and on rule-shape violations (see REQ-004).",
6
+ "category": "functional",
7
+ "domain": "platform",
8
+ "priority": "must",
9
+ "rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-002).",
10
+ "tags": [
11
+ "blueprint:deploy-hetzner-server",
12
+ "capability:cloudHost"
13
+ ],
14
+ "version": "1.0.0",
15
+ "status": "approved",
16
+ "createdAt": "2026-09-08T00:00:00.000Z",
17
+ "updatedAt": "2026-09-08T00:00:00.000Z"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "reqId": "deploy-hetzner-server-REQ-003",
3
+ "prdId": "PRD-001",
4
+ "title": "Cloud-init template renders YAML that lands the six hardening baseline blocks on every provisioned server",
5
+ "description": "The blueprint ships a cloud-init user-data template at blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl. Rendering the template against a manifest lands the six baseline blocks named by the estate baseline at operator/knowledge/systems/environments/hetzner-cloud/overview.md Security posture baseline and by the Debian unattended-upgrades wiki: (1) SSH key-only (PasswordAuthentication no via write_files on /etc/ssh/sshd_config.d/hardening.conf), (2) root disabled (PermitRootLogin no via the same file), (3) UFW default-deny incoming with 22, 80, 443 allowed per the manifest firewall shape, (4) DOCKER-USER iptables chain wired to drop non-conforming egress, (5) fail2ban SSH jail, (6) unattended-upgrades on for security updates. A render-lint asserts every block appears in the rendered YAML; a mutation SIMULATE_HARDENING_DRIFT=true removes the unattended-upgrades block and the lint FAILS naming the missing line.",
6
+ "category": "functional",
7
+ "domain": "platform",
8
+ "priority": "must",
9
+ "rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-003).",
10
+ "tags": [
11
+ "blueprint:deploy-hetzner-server",
12
+ "capability:cloudHost"
13
+ ],
14
+ "version": "1.0.0",
15
+ "status": "approved",
16
+ "createdAt": "2026-09-08T00:00:00.000Z",
17
+ "updatedAt": "2026-09-08T00:00:00.000Z"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "reqId": "deploy-hetzner-server-REQ-004",
3
+ "prdId": "PRD-001",
4
+ "title": "Firewall shape: 22 from operator-nominated prefixes, 80/443 open, all other inbound denied, applied at provision time",
5
+ "description": "Every hetzner/servers/name.json manifest carries a firewallRules array with the three-rule minimum shape: (a) an ssh rule permitting inbound TCP on 22 from a nonempty list of source-prefix ranges (the operator-nominated set; the schema refuses on 0.0.0.0/0 in this list), (b) an http rule permitting inbound TCP on 80 from any source, (c) an https rule permitting inbound TCP on 443 from any source, (d) an implicit deny for every other inbound rule. The provisioner applies the rule set to the server at provision time via hcloud firewall create (or reuse) plus hcloud firewall apply-to-resource. A raw-api provisioner posts the equivalent create-firewall and apply-to-resources bodies per https://docs.hetzner.cloud/#firewalls. Manifest schema validation refuses on any rule-set shape that violates (a) through (c).",
6
+ "category": "functional",
7
+ "domain": "platform",
8
+ "priority": "must",
9
+ "rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-004).",
10
+ "tags": [
11
+ "blueprint:deploy-hetzner-server",
12
+ "capability:cloudHost"
13
+ ],
14
+ "version": "1.0.0",
15
+ "status": "approved",
16
+ "createdAt": "2026-09-08T00:00:00.000Z",
17
+ "updatedAt": "2026-09-08T00:00:00.000Z"
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "reqId": "deploy-hetzner-server-REQ-005",
3
+ "prdId": "PRD-001",
4
+ "title": "Snapshot cadence elicited (weekly default, daily and off alternatives), snapshot-on-demand fires a tagged snapshot",
5
+ "description": "The applying project elicits a snapshot cadence per ADR-3802 with an enum of weekly (recommended default), daily and off. The value lives on hetzner/servers/name.json as snapshotCadence. When the cadence is non-off, a scheduled snapshot runner in the applying project fires the snapshot verb on the provisioner facade against every server whose cadence matches, and tags each snapshot with the server name and wall-clock time. A snapshot-on-demand verb on the provisioner fires the same shape without a schedule. Under provisioningTool=hcloud the facade shells to hcloud image create-image and tags via hcloud image update; under raw-api it posts to POST /servers/{id}/actions/create_image and PATCH /images/{id}. The hetznerSnapshotTaken lifecycle event carries snapshot id and wall-clock time. Hetzner-managed backups are a separately elicited paid opt-in (approximately 20 percent of monthly server price per https://docs.hetzner.com/cloud/servers/backups-snapshots/overview) and compose with the scheduled snapshot runner.",
6
+ "category": "functional",
7
+ "domain": "platform",
8
+ "priority": "must",
9
+ "rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.1 REQ-005).",
10
+ "tags": [
11
+ "blueprint:deploy-hetzner-server",
12
+ "capability:cloudHost"
13
+ ],
14
+ "version": "1.0.0",
15
+ "status": "approved",
16
+ "createdAt": "2026-09-08T00:00:00.000Z",
17
+ "updatedAt": "2026-09-08T00:00:00.000Z"
18
+ }