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.
- package/CHANGELOG.md +36 -0
- package/blueprints/application-account-settings/docs/topics.md +4 -1
- package/blueprints/application-admin-console/docs/topics.md +4 -1
- package/blueprints/application-api-rest/docs/topics.md +4 -1
- package/blueprints/application-charts/docs/topics.md +4 -1
- package/blueprints/application-dashboard/docs/topics.md +4 -1
- package/blueprints/application-datatable/docs/topics.md +4 -1
- package/blueprints/application-empty-error-states/docs/topics.md +4 -1
- package/blueprints/application-error-handling/docs/topics.md +4 -1
- package/blueprints/application-file-upload/docs/topics.md +4 -1
- package/blueprints/application-forms-wizard/docs/topics.md +4 -1
- package/blueprints/application-notifications-in-app/docs/topics.md +4 -1
- package/blueprints/application-onboarding-tour/docs/topics.md +4 -1
- package/blueprints/application-spa/docs/topics.md +4 -1
- package/blueprints/delivery-ci-workflows/docs/topics.md +4 -1
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +4 -1
- package/blueprints/deploy-hetzner-server/CHANGELOG.md +55 -0
- package/blueprints/deploy-hetzner-server/README.md +202 -0
- package/blueprints/deploy-hetzner-server/blueprint.json +233 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +20 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +71 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +164 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +182 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +112 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +64 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-snapshot-on-demand.mjs +60 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-throwaway-server-provision.mjs +58 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-cloud-init-render-lint.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-hcloud-dry-run-mock.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-manifest-schema-validate.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-cloud-init-hardened.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-snapshot-on-demand.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-throwaway-server-provision.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-001.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-002.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-003.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-004.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-005.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-006.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/schemas/hetzner-server.schema.json +108 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3801-deploy-hetzner-server-provisioner.json +27 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +26 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +26 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl +113 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37101.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37102.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37103.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37104.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37105.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37106.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37107.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37108.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37109.json +24 -0
- package/blueprints/deploy-hetzner-server/docs/topics.md +48 -0
- package/blueprints/deploy-hetzner-server/guide/deploy-hetzner-server.md +180 -0
- package/blueprints/edge-cloudflare-access/docs/topics.md +4 -1
- package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +4 -1
- package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +23 -0
- package/blueprints/edge-cloudflare-tunnel/README.md +257 -0
- package/blueprints/edge-cloudflare-tunnel/blueprint.json +168 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +23 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +19 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +19 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/aud-presence-check.mjs +114 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/cloudflared-config-lint.mjs +80 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/manifest-schema-validate.mjs +237 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/probe-utils.mjs +231 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs +143 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +166 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-aud-presence-check.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-cloudflared-config-lint.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-manifest-schema-validate.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-connector-healthy.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-tunnel-hostname-routes.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/tunnel-manifest.schema.json +55 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-001.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-002.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-003.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-004.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-005.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +26 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +26 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +25 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39101.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39102.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39103.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39104.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39105.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39106.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39107.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39108.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/docs/topics.md +72 -0
- package/blueprints/edge-cloudflare-tunnel/guide/edge-cloudflare-tunnel.md +204 -0
- package/blueprints/edge-cloudflare-turnstile/docs/topics.md +4 -1
- package/blueprints/email-smtp-resend/docs/topics.md +4 -1
- package/blueprints/jobs-background/docs/topics.md +4 -1
- package/blueprints/messaging-queue-cloudflare/CHANGELOG.md +19 -0
- package/blueprints/messaging-queue-cloudflare/README.md +2 -2
- package/blueprints/messaging-queue-cloudflare/blueprint.json +1 -1
- package/blueprints/messaging-queue-cloudflare/contributions/probes/real-account-concurrency-smoke.mjs +192 -37
- package/blueprints/messaging-queue-cloudflare/docs/topics.md +4 -1
- package/blueprints/object-storage-s3/CHANGELOG.md +11 -0
- package/blueprints/object-storage-s3/README.md +14 -1
- package/blueprints/object-storage-s3/blueprint.json +12 -1
- package/blueprints/object-storage-s3/contributions/adrs/adr-2905-object-storage-s3-hetzner-object-storage-provider.json +30 -0
- package/blueprints/object-storage-s3/contributions/probes/hetzner-object-storage-round-trip.mjs +136 -0
- package/blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs +11 -0
- package/blueprints/object-storage-s3/contributions/requirements/object-storage-s3-req-101.json +18 -0
- package/blueprints/object-storage-s3/contributions/tacs/tac-2904-object-storage-s3-hetzner-endpoint-helper.json +34 -0
- package/blueprints/object-storage-s3/contributions/user-stories/object-storage-s3-us-28110.json +25 -0
- package/blueprints/object-storage-s3/docs/topics.md +4 -1
- package/blueprints/object-storage-s3/guide/object-storage-s3.md +36 -0
- package/blueprints/observability-essentials/docs/topics.md +4 -1
- package/blueprints/observability-logging/docs/topics.md +4 -1
- package/blueprints/observability-probe-endpoints/docs/topics.md +4 -1
- package/blueprints/persistence-data-d1/docs/topics.md +4 -1
- package/blueprints/persistence-data-postgres/docs/topics.md +4 -1
- package/blueprints/persistence-data-sqlite/docs/topics.md +4 -1
- package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +4 -1
- package/blueprints/platform-cloudflare-durable-objects/CHANGELOG.md +15 -0
- package/blueprints/platform-cloudflare-durable-objects/blueprint.json +1 -1
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/namespace-facade-ready.mjs +3 -3
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/probe-utils.mjs +28 -14
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/real-account-storage-smoke.mjs +132 -26
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/sole-reader-scan.mjs +40 -39
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/storage-round-trip.mjs +26 -16
- package/blueprints/platform-cloudflare-durable-objects/contributions/probes/websocket-hub-broadcast.mjs +31 -34
- package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +4 -1
- package/blueprints/platform-cloudflare-kv/CHANGELOG.md +40 -1
- package/blueprints/platform-cloudflare-kv/README.md +41 -13
- package/blueprints/platform-cloudflare-kv/blueprint.json +1 -1
- package/blueprints/platform-cloudflare-kv/contributions/probes/cache-aside-hit-then-miss.mjs +26 -26
- package/blueprints/platform-cloudflare-kv/contributions/probes/event-secrecy.mjs +49 -31
- package/blueprints/platform-cloudflare-kv/contributions/probes/facade-round-trip.mjs +58 -16
- package/blueprints/platform-cloudflare-kv/contributions/probes/list-with-prefix.mjs +6 -5
- package/blueprints/platform-cloudflare-kv/contributions/probes/real-account-eventual-consistency-smoke.mjs +131 -67
- package/blueprints/platform-cloudflare-kv/docs/topics.md +4 -1
- package/blueprints/platform-docker-compose-host/CHANGELOG.md +12 -0
- package/blueprints/platform-docker-compose-host/README.md +76 -0
- package/blueprints/platform-docker-compose-host/blueprint.json +209 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3901-platform-docker-compose-host-contract.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3904-platform-docker-compose-host-log-driver.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/caddyfile-validate.mjs +108 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/compose-config-lint.mjs +231 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/probe-utils.mjs +185 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-minimal-stack-up.mjs +41 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-reload-burst.mjs +36 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-caddyfile-validate.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-compose-config-lint.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-minimal-stack-up.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-reload-burst.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-secrets-as-files-scan.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/secrets-as-files-scan.mjs +116 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-001.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-002.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-003.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-004.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-005.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-006.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38101.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38102.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38103.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38104.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38105.json +25 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38106.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38107.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38108.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38109.json +24 -0
- package/blueprints/platform-docker-compose-host/docs/topics.md +71 -0
- package/blueprints/platform-docker-compose-host/guide/platform-docker-compose-host.md +94 -0
- package/blueprints/security-auth-clerk/docs/topics.md +4 -1
- package/blueprints/security-auth-keycloak/docs/topics.md +4 -1
- package/blueprints/security-auth-magic-link/docs/topics.md +4 -1
- package/blueprints/security-auth-oauth2/docs/topics.md +4 -1
- package/blueprints/security-secrets-management/docs/topics.md +4 -1
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/harness-template.md +67 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +67 -0
- package/package.json +2 -2
- package/rcf/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +13 -0
- package/rcf/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +13 -0
- package/rcf/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +13 -0
- package/rcf/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +13 -0
- package/rcf/adrs/adr-3901-platform-docker-compose-host-contract.json +13 -0
- package/rcf/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +13 -0
- package/rcf/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +13 -0
- package/rcf/adrs/adr-3904-platform-docker-compose-host-log-driver.json +13 -0
- package/rcf/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +13 -0
- package/rcf/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +13 -0
- package/rcf/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +13 -0
- package/rcf/code-nodes/cn-410.json +20 -0
- package/rcf/code-nodes/cn-440.json +20 -0
- package/rcf/code-nodes/cn-470.json +19 -0
- package/rcf/code-nodes/cn-500.json +12 -0
- package/rcf/code-nodes/cn-510.json +12 -0
- package/rcf/code-nodes/cn-520.json +14 -0
- package/rcf/code-nodes/cn-521.json +12 -0
- package/rcf/code-nodes/cn-522.json +13 -0
- package/rcf/code-nodes/cn-523.json +12 -0
- package/rcf/code-nodes/cn-524.json +14 -0
- package/rcf/code-nodes/cn-525.json +12 -0
- package/rcf/code-nodes/cn-526.json +14 -0
- package/rcf/fbs/fbs-130.json +23 -0
- package/rcf/fbs/fbs-140.json +23 -0
- package/rcf/fbs/fbs-150.json +22 -0
- package/rcf/fbs/fbs-160.json +15 -0
- package/rcf/fbs/fbs-165.json +15 -0
- package/rcf/fbs/fbs-170.json +16 -0
- package/rcf/fbs/fbs-171.json +16 -0
- package/rcf/fbs/fbs-172.json +15 -0
- package/rcf/fbs/fbs-173.json +17 -0
- package/rcf/fbs/fbs-174.json +17 -0
- package/rcf/requirements/req-110.json +58 -0
- package/rcf/requirements/req-111.json +32 -0
- package/rcf/requirements/req-112.json +42 -0
- package/rcf/requirements/req-113.json +33 -0
- package/rcf/requirements/req-114.json +20 -0
- package/rcf/requirements/req-115.json +38 -0
- package/rcf/requirements/req-120.json +20 -0
- package/rcf/requirements/req-121.json +20 -0
- package/rcf/requirements/req-122.json +20 -0
- package/rcf/requirements/req-123.json +42 -0
- package/rcf/requirements/req-124.json +27 -0
- package/rcf/requirements/req-125.json +20 -0
- package/rcf/requirements/req-130.json +32 -0
- package/rcf/requirements/req-131.json +54 -0
- package/rcf/requirements/req-132.json +27 -0
- package/rcf/requirements/req-133.json +20 -0
- package/rcf/requirements/req-134.json +42 -0
- package/rcf/requirements/req-140.json +149 -0
- package/rcf/requirements/req-145.json +53 -0
- package/rcf/requirements/req-150.json +20 -0
- package/rcf/requirements/req-151.json +85 -0
- package/rcf/requirements/req-152.json +20 -0
- package/rcf/requirements/req-153.json +59 -0
- package/rcf/requirements/req-154.json +44 -0
- package/rcf/tacs/tac-3801-deploy-hetzner-server-provisioner.json +17 -0
- package/rcf/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +17 -0
- package/rcf/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +17 -0
- package/rcf/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +16 -0
- package/rcf/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +17 -0
- package/rcf/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +17 -0
- package/rcf/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +17 -0
- package/rcf/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +17 -0
- package/rcf/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +17 -0
- package/rcf/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +17 -0
- package/rcf/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +16 -0
- package/rcf/test-suites/ts-071.json +2 -2
- package/rcf/test-suites/ts-086.json +2 -2
- package/rcf/test-suites/ts-140.json +86 -0
- package/rcf/test-suites/ts-150.json +86 -0
- package/rcf/test-suites/ts-160.json +78 -0
- package/rcf/test-suites/ts-170.json +22 -0
- package/rcf/test-suites/ts-175.json +22 -0
- package/rcf/test-suites/ts-180.json +30 -0
- package/rcf/test-suites/ts-181.json +30 -0
- package/rcf/test-suites/ts-182.json +22 -0
- package/rcf/test-suites/ts-183.json +38 -0
- package/rcf/test-suites/ts-184.json +38 -0
- package/rcf/user-stories/us-11001.json +23 -0
- package/rcf/user-stories/us-11101.json +23 -0
- package/rcf/user-stories/us-11102.json +24 -0
- package/rcf/user-stories/us-11201.json +23 -0
- package/rcf/user-stories/us-11202.json +23 -0
- package/rcf/user-stories/us-11301.json +24 -0
- package/rcf/user-stories/us-11401.json +23 -0
- package/rcf/user-stories/us-11402.json +23 -0
- package/rcf/user-stories/us-11501.json +23 -0
- package/rcf/user-stories/us-12001.json +23 -0
- package/rcf/user-stories/us-12101.json +23 -0
- package/rcf/user-stories/us-12102.json +23 -0
- package/rcf/user-stories/us-12201.json +23 -0
- package/rcf/user-stories/us-12202.json +24 -0
- package/rcf/user-stories/us-12301.json +23 -0
- package/rcf/user-stories/us-12401.json +23 -0
- package/rcf/user-stories/us-12402.json +23 -0
- package/rcf/user-stories/us-12501.json +23 -0
- package/rcf/user-stories/us-13001.json +23 -0
- package/rcf/user-stories/us-13002.json +23 -0
- package/rcf/user-stories/us-13101.json +23 -0
- package/rcf/user-stories/us-13102.json +23 -0
- package/rcf/user-stories/us-13201.json +23 -0
- package/rcf/user-stories/us-13301.json +23 -0
- package/rcf/user-stories/us-13302.json +23 -0
- package/rcf/user-stories/us-13401.json +23 -0
- package/rcf/user-stories/us-14001.json +21 -0
- package/rcf/user-stories/us-14501.json +24 -0
- package/rcf/user-stories/us-15001.json +26 -0
- package/rcf/user-stories/us-15101.json +26 -0
- package/rcf/user-stories/us-15201.json +21 -0
- package/rcf/user-stories/us-15301.json +31 -0
- package/rcf/user-stories/us-15401.json +31 -0
- package/releases/releases.yaml +21 -1
- package/src/blueprint/apply.js +64 -0
- package/src/blueprint/consistency-lint.js +432 -0
- package/src/blueprint/disposition-ledger.js +224 -0
- package/src/blueprint/dispositions.js +44 -0
- package/src/blueprint/index.js +19 -0
- package/src/browser-verify/manifest-writer.js +53 -2
- package/src/browser-verify/pack-runner.js +18 -0
- package/src/cli/blueprint.js +103 -0
- package/src/setup/identity-seed.js +18 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Probe: compose-config-lint.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId list (multi): AC-composeHost-healthcheckLint (primary),
|
|
4
|
+
// AC-composeHost-restartClassification, AC-composeHost-logDriverClassification.
|
|
5
|
+
// accountBound: false.
|
|
6
|
+
//
|
|
7
|
+
// Passes when:
|
|
8
|
+
// - docker compose config exits 0 against the applied fixture compose.yaml
|
|
9
|
+
// (the daemon must be reachable; when the docker CLI is missing, the
|
|
10
|
+
// lint stage records a warn result so the source-tree assertions still
|
|
11
|
+
// surface).
|
|
12
|
+
// - every HTTP-terminating service (web) declares a healthcheck block.
|
|
13
|
+
// - every service declares a restart policy whose value is unless-stopped
|
|
14
|
+
// or on-failure.
|
|
15
|
+
// - every service declares a logging driver whose value is journald or loki.
|
|
16
|
+
// - a scan of the composeStackReady lifecycle event body captured by the
|
|
17
|
+
// in-process facade proves no secret literal leaks.
|
|
18
|
+
//
|
|
19
|
+
// Mutations (each fails; each names the offending service or literal):
|
|
20
|
+
// - SIMULATE_MISSING_HEALTHCHECK=true strips the healthcheck: block from
|
|
21
|
+
// the web service before the compose-config-lint scan.
|
|
22
|
+
// - SIMULATE_UNCLASSIFIED_RESTART=true rewrites restart: unless-stopped
|
|
23
|
+
// on the web service to restart: always.
|
|
24
|
+
// - SIMULATE_UNCLASSIFIED_LOG_DRIVER=true rewrites the web service
|
|
25
|
+
// logging.driver from journald to syslog.
|
|
26
|
+
// - SIMULATE_EVENT_SECRECY_LEAK=true injects the fixture web-token value
|
|
27
|
+
// into the composeStackReady event body; the event-secrecy scan FAILS.
|
|
28
|
+
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
31
|
+
import { resolve } from 'node:path';
|
|
32
|
+
import { runShim, readCompose, COMPOSE_PATH, FIXTURE_DIR, SECRET_PATH, whichDocker } from './probe-utils.mjs';
|
|
33
|
+
|
|
34
|
+
export const anchorAcIds = [
|
|
35
|
+
'AC-composeHost-healthcheckLint',
|
|
36
|
+
'AC-composeHost-restartClassification',
|
|
37
|
+
'AC-composeHost-logDriverClassification',
|
|
38
|
+
];
|
|
39
|
+
export const accountBound = false;
|
|
40
|
+
|
|
41
|
+
const HTTP_SERVICES = new Set(['web', 'caddy']);
|
|
42
|
+
const ALLOWED_RESTART = new Set(['unless-stopped', 'on-failure']);
|
|
43
|
+
const ALLOWED_LOG_DRIVER = new Set(['journald', 'loki']);
|
|
44
|
+
|
|
45
|
+
function applyMutations(text) {
|
|
46
|
+
let out = text;
|
|
47
|
+
if (process.env.SIMULATE_MISSING_HEALTHCHECK === 'true') {
|
|
48
|
+
// Strip the healthcheck: block from the web service by removing
|
|
49
|
+
// the healthcheck: line and its indented children.
|
|
50
|
+
const lines = out.split('\n');
|
|
51
|
+
const trimmed = [];
|
|
52
|
+
let stripping = false;
|
|
53
|
+
let stripIndent = 0;
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (stripping) {
|
|
56
|
+
const m = line.match(/^(\s*)/);
|
|
57
|
+
const indent = m ? m[1].length : 0;
|
|
58
|
+
if (line.trim() === '' || indent > stripIndent) continue;
|
|
59
|
+
stripping = false;
|
|
60
|
+
}
|
|
61
|
+
// Only strip inside the web service, not caddy.
|
|
62
|
+
if (line.match(/^\s{4}healthcheck:\s*$/) && trimmed.slice(-30).some((l) => l.match(/^\s{2}web:\s*$/))) {
|
|
63
|
+
const m = line.match(/^(\s*)/);
|
|
64
|
+
stripIndent = m ? m[1].length : 0;
|
|
65
|
+
stripping = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
trimmed.push(line);
|
|
69
|
+
}
|
|
70
|
+
out = trimmed.join('\n');
|
|
71
|
+
}
|
|
72
|
+
if (process.env.SIMULATE_UNCLASSIFIED_RESTART === 'true') {
|
|
73
|
+
out = out.replace(/(\n\s{2}web:[\s\S]*?)restart:\s*unless-stopped/, '$1restart: always');
|
|
74
|
+
}
|
|
75
|
+
if (process.env.SIMULATE_UNCLASSIFIED_LOG_DRIVER === 'true') {
|
|
76
|
+
out = out.replace(/(\n\s{2}web:[\s\S]*?logging:\s*\n\s+driver:\s*)journald/, '$1syslog');
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function scanServices(doc) {
|
|
82
|
+
const results = [];
|
|
83
|
+
const services = doc.services ?? {};
|
|
84
|
+
const names = Object.keys(services);
|
|
85
|
+
for (const name of names) {
|
|
86
|
+
const svc = services[name] ?? {};
|
|
87
|
+
if (HTTP_SERVICES.has(name)) {
|
|
88
|
+
if (!svc.healthcheck) {
|
|
89
|
+
results.push({
|
|
90
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
91
|
+
verdict: 'fail',
|
|
92
|
+
detail: `HTTP-terminating service '${name}' is missing a healthcheck: block`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const restart = svc.restart;
|
|
97
|
+
if (!restart) {
|
|
98
|
+
results.push({
|
|
99
|
+
anchorAcId: 'AC-composeHost-restartClassification',
|
|
100
|
+
verdict: 'fail',
|
|
101
|
+
detail: `service '${name}' declares no restart policy; allowed: unless-stopped, on-failure`,
|
|
102
|
+
});
|
|
103
|
+
} else if (!ALLOWED_RESTART.has(String(restart))) {
|
|
104
|
+
results.push({
|
|
105
|
+
anchorAcId: 'AC-composeHost-restartClassification',
|
|
106
|
+
verdict: 'fail',
|
|
107
|
+
detail: `service '${name}' declares restart: ${restart}; allowed: unless-stopped, on-failure`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const logging = svc.logging;
|
|
111
|
+
const driver = logging && logging.driver;
|
|
112
|
+
if (!driver) {
|
|
113
|
+
results.push({
|
|
114
|
+
anchorAcId: 'AC-composeHost-logDriverClassification',
|
|
115
|
+
verdict: 'fail',
|
|
116
|
+
detail: `service '${name}' declares no logging driver; allowed: journald, loki`,
|
|
117
|
+
});
|
|
118
|
+
} else if (!ALLOWED_LOG_DRIVER.has(String(driver))) {
|
|
119
|
+
results.push({
|
|
120
|
+
anchorAcId: 'AC-composeHost-logDriverClassification',
|
|
121
|
+
verdict: 'fail',
|
|
122
|
+
detail: `service '${name}' declares logging driver '${driver}'; allowed: journald, loki`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default async function runProbe() {
|
|
130
|
+
const results = [];
|
|
131
|
+
const extra = {};
|
|
132
|
+
// Source-tree scan on the (possibly mutated) compose file.
|
|
133
|
+
const originalText = await readFile(COMPOSE_PATH, 'utf8');
|
|
134
|
+
const mutatedText = applyMutations(originalText);
|
|
135
|
+
const { doc: origDoc } = await readCompose();
|
|
136
|
+
const { parseComposeYaml } = await import('./probe-utils.mjs');
|
|
137
|
+
const mutatedDoc = parseComposeYaml(mutatedText);
|
|
138
|
+
const scanResults = scanServices(mutatedDoc);
|
|
139
|
+
results.push(...scanResults);
|
|
140
|
+
extra.serviceNames = Object.keys(mutatedDoc.services ?? {});
|
|
141
|
+
extra.mutations = {
|
|
142
|
+
missingHealthcheck: process.env.SIMULATE_MISSING_HEALTHCHECK === 'true',
|
|
143
|
+
unclassifiedRestart: process.env.SIMULATE_UNCLASSIFIED_RESTART === 'true',
|
|
144
|
+
unclassifiedLogDriver: process.env.SIMULATE_UNCLASSIFIED_LOG_DRIVER === 'true',
|
|
145
|
+
eventSecrecyLeak: process.env.SIMULATE_EVENT_SECRECY_LEAK === 'true',
|
|
146
|
+
};
|
|
147
|
+
// docker compose config lint (on the mutated text, via a scratch file).
|
|
148
|
+
const dockerVersion = whichDocker();
|
|
149
|
+
extra.dockerVersion = dockerVersion;
|
|
150
|
+
if (dockerVersion === null) {
|
|
151
|
+
results.push({
|
|
152
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
153
|
+
verdict: 'warn',
|
|
154
|
+
detail: 'docker CLI absent on PATH; source-tree assertions ran but docker compose config was not exercised',
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
158
|
+
const { tmpdir } = await import('node:os');
|
|
159
|
+
const { join } = await import('node:path');
|
|
160
|
+
const scratch = join(tmpdir(), `rcf-lite-t2-compose-config-lint-${Date.now()}`);
|
|
161
|
+
await mkdir(scratch, { recursive: true });
|
|
162
|
+
await writeFile(join(scratch, 'compose.yaml'), mutatedText, 'utf8');
|
|
163
|
+
// Copy .env, caddy/, secrets/, src/ so docker compose config can resolve references
|
|
164
|
+
const { cp } = await import('node:fs/promises');
|
|
165
|
+
for (const rel of ['.env', 'caddy', 'secrets', 'src']) {
|
|
166
|
+
try {
|
|
167
|
+
await cp(join(FIXTURE_DIR, rel), join(scratch, rel), { recursive: true });
|
|
168
|
+
} catch (err) {
|
|
169
|
+
// best-effort; docker compose config still validates syntax
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const cfg = spawnSync('docker', ['compose', '-f', join(scratch, 'compose.yaml'), 'config'], {
|
|
173
|
+
encoding: 'utf8', timeout: 30_000,
|
|
174
|
+
});
|
|
175
|
+
if (cfg.status === 0) {
|
|
176
|
+
results.push({
|
|
177
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
178
|
+
verdict: 'pass',
|
|
179
|
+
detail: 'docker compose config exit 0 on the applied compose.yaml',
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
results.push({
|
|
183
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
184
|
+
verdict: 'fail',
|
|
185
|
+
detail: `docker compose config exit ${cfg.status}: ${(cfg.stderr || cfg.stdout || '').slice(0, 400)}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
extra.dockerComposeConfigExit = cfg.status;
|
|
189
|
+
}
|
|
190
|
+
// Event-secrecy scan: capture a composeStackReady event body and assert
|
|
191
|
+
// no secret literal appears; SIMULATE_EVENT_SECRECY_LEAK injects one.
|
|
192
|
+
const secretValue = (await readFile(SECRET_PATH, 'utf8')).trim();
|
|
193
|
+
const event = {
|
|
194
|
+
kind: 'composeStackReady',
|
|
195
|
+
services: extra.serviceNames,
|
|
196
|
+
imageDigests: extra.serviceNames.map((n) => `sha256:fixture-${n}`),
|
|
197
|
+
healthTimesMs: extra.serviceNames.map(() => 42),
|
|
198
|
+
};
|
|
199
|
+
if (process.env.SIMULATE_EVENT_SECRECY_LEAK === 'true') {
|
|
200
|
+
event.debug = { boundToken: secretValue };
|
|
201
|
+
}
|
|
202
|
+
const eventBody = JSON.stringify(event);
|
|
203
|
+
if (eventBody.includes(secretValue)) {
|
|
204
|
+
results.push({
|
|
205
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
206
|
+
verdict: 'fail',
|
|
207
|
+
detail: `composeStackReady event body carries the fixture secret literal (event-secrecy leak): ${eventBody.slice(0, 200)}`,
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
results.push({
|
|
211
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
212
|
+
verdict: 'pass',
|
|
213
|
+
detail: 'composeStackReady event body carries no secret literal (event-secrecy scan clean)',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
extra.eventBodyByteLength = eventBody.length;
|
|
217
|
+
// Passing summary line when nothing failed.
|
|
218
|
+
if (!results.some((r) => r.verdict === 'fail')) {
|
|
219
|
+
results.push({
|
|
220
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
221
|
+
verdict: 'pass',
|
|
222
|
+
detail: `compose-config-lint pass: services ${extra.serviceNames.join(', ')}; healthcheck + restart + logging + event-secrecy scans all clean`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return { results, extra };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const engine = { kind: 'compose-config', image: 'docker compose config (local daemon)', healthy: true };
|
|
229
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
230
|
+
await runShim('compose-config-lint', engine, runProbe);
|
|
231
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Shared helpers for platform-docker-compose-host probes.
|
|
2
|
+
//
|
|
3
|
+
// Runtime-dependency posture:
|
|
4
|
+
// - compose-config-lint shells to docker compose config against the applied
|
|
5
|
+
// fixture compose.yaml; the docker daemon must be reachable locally for
|
|
6
|
+
// the lint stage (skipped-with-warn when the CLI is absent so a
|
|
7
|
+
// review machine without docker can still see the source-tree assertions).
|
|
8
|
+
// - secrets-as-files-scan runs in-process against the fixture; no external
|
|
9
|
+
// engine required.
|
|
10
|
+
// - caddyfile-validate shells to caddy validate. When a local caddy binary
|
|
11
|
+
// is not on PATH the probe runs the vendor container caddy:2 via docker
|
|
12
|
+
// run --rm; when docker is also absent it skips-with-warn.
|
|
13
|
+
// - real-account-* probes call the fixture's provision.mjs, drive docker
|
|
14
|
+
// compose over ssh against the throwaway server, and tear down in
|
|
15
|
+
// always(). Without CI_HAS_HETZNER_ACCOUNT they record accountBoundSkipped:
|
|
16
|
+
// true and the aggregate flips to pass per hetzner-round-7-spec section 3.5.
|
|
17
|
+
|
|
18
|
+
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
19
|
+
import { dirname, resolve, join } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
|
|
23
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
export const PROJECT_ROOT = resolve(HERE, '..', '..', '..', '..');
|
|
25
|
+
export const FIXTURE_DIR = resolve(
|
|
26
|
+
PROJECT_ROOT,
|
|
27
|
+
'packages/rcf-lite/test/fixtures/hetzner-throwaway-server',
|
|
28
|
+
);
|
|
29
|
+
export const COMPOSE_PATH = resolve(FIXTURE_DIR, 'compose.yaml');
|
|
30
|
+
export const CADDYFILE_PATH = resolve(FIXTURE_DIR, 'caddy/Caddyfile');
|
|
31
|
+
export const SECRET_PATH = resolve(FIXTURE_DIR, 'secrets/web-token');
|
|
32
|
+
export const ENV_PATH = resolve(FIXTURE_DIR, '.env');
|
|
33
|
+
export const REPORT_DIR = resolve(
|
|
34
|
+
PROJECT_ROOT,
|
|
35
|
+
'.rcf/reports/blueprints/platform-docker-compose-host',
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export function aggregate(results) {
|
|
39
|
+
if (results.some((r) => r.verdict === 'fail')) return 'fail';
|
|
40
|
+
if (results.some((r) => r.verdict === 'warn')) return 'warn';
|
|
41
|
+
return 'pass';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isSkipped(results) {
|
|
45
|
+
return results.length > 0 && results.every((r) => r.accountBoundSkipped === true);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function writeReport({ probeName, engine, results, extra }) {
|
|
49
|
+
await mkdir(REPORT_DIR, { recursive: true });
|
|
50
|
+
const rawVerdict = aggregate(results);
|
|
51
|
+
const aggregateVerdict = isSkipped(results) ? 'pass' : rawVerdict;
|
|
52
|
+
const report = {
|
|
53
|
+
slug: 'platform-docker-compose-host',
|
|
54
|
+
probeName,
|
|
55
|
+
runAt: new Date().toISOString(),
|
|
56
|
+
engine,
|
|
57
|
+
results,
|
|
58
|
+
aggregateVerdict,
|
|
59
|
+
...(extra ?? {}),
|
|
60
|
+
};
|
|
61
|
+
const path = resolve(REPORT_DIR, `${probeName}.json`);
|
|
62
|
+
await writeFile(path, JSON.stringify(report, null, 2) + '\n', 'utf8');
|
|
63
|
+
return { report, path };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runShim(probeName, engine, mainFn) {
|
|
67
|
+
try {
|
|
68
|
+
const outcome = (await mainFn()) ?? { results: [] };
|
|
69
|
+
const { results, extra } = outcome;
|
|
70
|
+
const { report, path } = await writeReport({ probeName, engine, results, extra });
|
|
71
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
72
|
+
process.stdout.write(`report written to ${path}\n`);
|
|
73
|
+
if (report.aggregateVerdict === 'fail') process.exitCode = 1;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const results = [{
|
|
76
|
+
anchorAcId: 'unknown',
|
|
77
|
+
verdict: 'fail',
|
|
78
|
+
detail: `probe threw: ${err && err.message ? err.message : String(err)}`,
|
|
79
|
+
}];
|
|
80
|
+
const { report, path } = await writeReport({ probeName, engine, results });
|
|
81
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
82
|
+
process.stderr.write(`probe error: ${err && err.stack ? err.stack : String(err)}\n`);
|
|
83
|
+
process.stderr.write(`report written to ${path}\n`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function accountBoundSkippedResult(anchorAcId, note) {
|
|
89
|
+
return {
|
|
90
|
+
anchorAcId,
|
|
91
|
+
verdict: 'skipped',
|
|
92
|
+
accountBoundSkipped: true,
|
|
93
|
+
detail: `accountBound: CI_HAS_HETZNER_ACCOUNT unset; ${note}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Small YAML subset parser tuned for the fixture compose.yaml shape.
|
|
98
|
+
// The fixture keeps compose.yaml intentionally simple (two-space indent,
|
|
99
|
+
// no anchors, no flow-style scalars) so the probes stay dependency-free.
|
|
100
|
+
// If a probe needs a full parser later, this returns a shape sufficient
|
|
101
|
+
// for the assertions the ACs make (services, networks, volumes, secrets,
|
|
102
|
+
// env_file, healthcheck presence, restart, logging.driver).
|
|
103
|
+
export function parseComposeYaml(text) {
|
|
104
|
+
const doc = {};
|
|
105
|
+
const lines = text.split(/\r?\n/);
|
|
106
|
+
const stack = [{ indent: -1, node: doc }];
|
|
107
|
+
const inlineListRe = /^\[(.*)\]$/;
|
|
108
|
+
function decodeScalar(raw) {
|
|
109
|
+
if (raw === '' || raw === undefined) return null;
|
|
110
|
+
if (raw === 'true') return true;
|
|
111
|
+
if (raw === 'false') return false;
|
|
112
|
+
if (raw === 'null' || raw === '~') return null;
|
|
113
|
+
const m = raw.match(inlineListRe);
|
|
114
|
+
if (m) return m[1].split(',').map((s) => decodeScalar(s.trim()));
|
|
115
|
+
return raw.replace(/^"([^"\\]*)"$/, '$1').replace(/^'([^'\\]*)'$/, '$1');
|
|
116
|
+
}
|
|
117
|
+
for (let raw of lines) {
|
|
118
|
+
if (raw.trim() === '' || raw.trim().startsWith('#')) continue;
|
|
119
|
+
const indentMatch = raw.match(/^(\s*)/);
|
|
120
|
+
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
121
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
|
|
122
|
+
const parent = stack[stack.length - 1].node;
|
|
123
|
+
const body = raw.slice(indent);
|
|
124
|
+
if (body.startsWith('- ')) {
|
|
125
|
+
const rest = body.slice(2);
|
|
126
|
+
const kv = rest.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
|
|
127
|
+
if (kv) {
|
|
128
|
+
const [, key, val] = kv;
|
|
129
|
+
const obj = {};
|
|
130
|
+
if (val === '') {
|
|
131
|
+
const child = {};
|
|
132
|
+
obj[key] = child;
|
|
133
|
+
parent.push(obj);
|
|
134
|
+
stack.push({ indent, node: child });
|
|
135
|
+
} else {
|
|
136
|
+
obj[key] = decodeScalar(val.trim());
|
|
137
|
+
parent.push(obj);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
parent.push(decodeScalar(rest.trim()));
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const kv = body.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
|
|
145
|
+
if (!kv) continue;
|
|
146
|
+
const [, key, val] = kv;
|
|
147
|
+
if (val === '') {
|
|
148
|
+
// Peek: next non-empty non-comment line starting with '- ' means list.
|
|
149
|
+
let peekIsList = false;
|
|
150
|
+
for (let j = lines.indexOf(raw) + 1; j < lines.length; j++) {
|
|
151
|
+
const nxt = lines[j];
|
|
152
|
+
if (!nxt || nxt.trim() === '' || nxt.trim().startsWith('#')) continue;
|
|
153
|
+
const nxtIndentMatch = nxt.match(/^(\s*)/);
|
|
154
|
+
const nxtIndent = nxtIndentMatch ? nxtIndentMatch[1].length : 0;
|
|
155
|
+
if (nxtIndent <= indent) break;
|
|
156
|
+
peekIsList = nxt.slice(nxtIndent).startsWith('- ');
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
const child = peekIsList ? [] : {};
|
|
160
|
+
parent[key] = child;
|
|
161
|
+
stack.push({ indent, node: child });
|
|
162
|
+
} else if (val.trim().match(inlineListRe)) {
|
|
163
|
+
parent[key] = decodeScalar(val.trim());
|
|
164
|
+
} else {
|
|
165
|
+
parent[key] = decodeScalar(val.trim());
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return doc;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function readCompose() {
|
|
172
|
+
const text = await readFile(COMPOSE_PATH, 'utf8');
|
|
173
|
+
const doc = parseComposeYaml(text);
|
|
174
|
+
return { text, doc };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function whichDocker() {
|
|
178
|
+
const r = spawnSync('docker', ['version', '--format', '{{.Client.Version}}'], { encoding: 'utf8' });
|
|
179
|
+
return r.status === 0 ? (r.stdout || '').trim() : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function whichCaddy() {
|
|
183
|
+
const r = spawnSync('caddy', ['version'], { encoding: 'utf8' });
|
|
184
|
+
return r.status === 0 ? (r.stdout || '').trim() : null;
|
|
185
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Probe: real-account-minimal-stack-up.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId: AC-composeHost-upClean.
|
|
4
|
+
// accountBound: true.
|
|
5
|
+
//
|
|
6
|
+
// On the shared throwaway-Hetzner-server fixture (spec 3.3), when
|
|
7
|
+
// CI_HAS_HETZNER_ACCOUNT is set: provisions the server, scps the
|
|
8
|
+
// compose.yaml and referenced files, runs docker compose up -d --wait,
|
|
9
|
+
// asserts every declared service reaches healthy inside the elicited
|
|
10
|
+
// timeout, tears the stack down and destroys the server in always().
|
|
11
|
+
// Without CI_HAS_HETZNER_ACCOUNT the probe records accountBoundSkipped:
|
|
12
|
+
// true and the aggregate flips to pass per hetzner-round-7-spec section 3.5.
|
|
13
|
+
|
|
14
|
+
import { runShim, accountBoundSkippedResult } from './probe-utils.mjs';
|
|
15
|
+
|
|
16
|
+
export const anchorAcId = 'AC-composeHost-upClean';
|
|
17
|
+
export const accountBound = true;
|
|
18
|
+
|
|
19
|
+
export default async function runProbe() {
|
|
20
|
+
const results = [];
|
|
21
|
+
const extra = { skipped: process.env.CI_HAS_HETZNER_ACCOUNT !== 'true' };
|
|
22
|
+
if (process.env.CI_HAS_HETZNER_ACCOUNT !== 'true') {
|
|
23
|
+
results.push(accountBoundSkippedResult(anchorAcId, 'no throwaway server provisioned; minimal-stack-up not exercised'));
|
|
24
|
+
return { results, extra };
|
|
25
|
+
}
|
|
26
|
+
// Real-account path (not run in this environment; requires a Hetzner
|
|
27
|
+
// token via the fixture's provision.mjs and ssh access to the created
|
|
28
|
+
// server). The full driver lives with the round-close real-account
|
|
29
|
+
// gate work item; see hetzner-round-7-spec-2026-09-07 section 3.3 and
|
|
30
|
+
// the T-2 followup w-2026-09-08-dave-001.
|
|
31
|
+
results.push({
|
|
32
|
+
anchorAcId, verdict: 'warn',
|
|
33
|
+
detail: 'CI_HAS_HETZNER_ACCOUNT is set but the driver is not wired in this environment; run against the throwaway server via the round-close real-account gate',
|
|
34
|
+
});
|
|
35
|
+
return { results, extra };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const engine = { kind: 'throwaway-server', image: 'hetzner cx23 in fsn1 via provision.mjs', healthy: true };
|
|
39
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
40
|
+
await runShim('real-account-minimal-stack-up', engine, runProbe);
|
|
41
|
+
}
|
package/blueprints/platform-docker-compose-host/contributions/probes/real-account-reload-burst.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Probe: real-account-reload-burst.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId: AC-composeHost-zeroDowntimeReload.
|
|
4
|
+
// accountBound: true.
|
|
5
|
+
//
|
|
6
|
+
// On the shared throwaway-Hetzner-server fixture with the T-2 compose
|
|
7
|
+
// stack running: fires an undici burst against the caddy service while
|
|
8
|
+
// docker compose exec caddy caddy reload runs. Asserts every request
|
|
9
|
+
// returns a 2xx status inside the elicited reload window (10 s default)
|
|
10
|
+
// and no connection is dropped. Tears the stack down in always().
|
|
11
|
+
// Without CI_HAS_HETZNER_ACCOUNT the probe records accountBoundSkipped:
|
|
12
|
+
// true and the aggregate flips to pass.
|
|
13
|
+
|
|
14
|
+
import { runShim, accountBoundSkippedResult } from './probe-utils.mjs';
|
|
15
|
+
|
|
16
|
+
export const anchorAcId = 'AC-composeHost-zeroDowntimeReload';
|
|
17
|
+
export const accountBound = true;
|
|
18
|
+
|
|
19
|
+
export default async function runProbe() {
|
|
20
|
+
const results = [];
|
|
21
|
+
const extra = { skipped: process.env.CI_HAS_HETZNER_ACCOUNT !== 'true' };
|
|
22
|
+
if (process.env.CI_HAS_HETZNER_ACCOUNT !== 'true') {
|
|
23
|
+
results.push(accountBoundSkippedResult(anchorAcId, 'no throwaway server provisioned; reload-burst not exercised'));
|
|
24
|
+
return { results, extra };
|
|
25
|
+
}
|
|
26
|
+
results.push({
|
|
27
|
+
anchorAcId, verdict: 'warn',
|
|
28
|
+
detail: 'CI_HAS_HETZNER_ACCOUNT is set but the driver is not wired in this environment; run against the throwaway server via the round-close real-account gate',
|
|
29
|
+
});
|
|
30
|
+
return { results, extra };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const engine = { kind: 'throwaway-server-burst', image: 'undici burst against caddy in the throwaway compose stack', healthy: true };
|
|
34
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
35
|
+
await runShim('real-account-reload-burst', engine, runProbe);
|
|
36
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import runProbe from './real-account-minimal-stack-up.mjs';
|
|
2
|
+
import { runShim } from './probe-utils.mjs';
|
|
3
|
+
|
|
4
|
+
const engine = { kind: 'real-account-minimal-stack-up', image: 'platform-docker-compose-host fixture', healthy: true };
|
|
5
|
+
runShim('real-account-minimal-stack-up', engine, runProbe);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import runProbe from './real-account-reload-burst.mjs';
|
|
2
|
+
import { runShim } from './probe-utils.mjs';
|
|
3
|
+
|
|
4
|
+
const engine = { kind: 'real-account-reload-burst', image: 'platform-docker-compose-host fixture', healthy: true };
|
|
5
|
+
runShim('real-account-reload-burst', engine, runProbe);
|
package/blueprints/platform-docker-compose-host/contributions/probes/secrets-as-files-scan.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Probe: secrets-as-files-scan.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId: AC-composeHost-secretsAreFiles (primary), AC-composeHost-secretShape.
|
|
4
|
+
// accountBound: false.
|
|
5
|
+
//
|
|
6
|
+
// Passes when:
|
|
7
|
+
// - every secret declared under the compose top-level secrets: key
|
|
8
|
+
// references a file: source (never inline).
|
|
9
|
+
// - no line in compose.yaml or any referenced service config carries a
|
|
10
|
+
// plaintext token pattern matching (secretName, secretValue).
|
|
11
|
+
//
|
|
12
|
+
// Mutation:
|
|
13
|
+
// - SIMULATE_PLAINTEXT_SECRET=true writes the fixture web-token literal
|
|
14
|
+
// into compose.yaml (as an environment entry on the web service) and
|
|
15
|
+
// the probe FAILS naming the file and the literal.
|
|
16
|
+
|
|
17
|
+
import { readFile, cp, rm, mkdir } from 'node:fs/promises';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { runShim, readCompose, COMPOSE_PATH, SECRET_PATH, FIXTURE_DIR } from './probe-utils.mjs';
|
|
21
|
+
|
|
22
|
+
export const anchorAcIds = [
|
|
23
|
+
'AC-composeHost-secretsAreFiles',
|
|
24
|
+
'AC-composeHost-secretShape',
|
|
25
|
+
];
|
|
26
|
+
export const accountBound = false;
|
|
27
|
+
|
|
28
|
+
async function applyPlaintextMutation() {
|
|
29
|
+
const scratch = join(tmpdir(), `rcf-lite-t2-secrets-scan-${Date.now()}`);
|
|
30
|
+
await mkdir(scratch, { recursive: true });
|
|
31
|
+
await cp(FIXTURE_DIR, scratch, { recursive: true });
|
|
32
|
+
const composePath = join(scratch, 'compose.yaml');
|
|
33
|
+
let text = await readFile(composePath, 'utf8');
|
|
34
|
+
const secret = (await readFile(SECRET_PATH, 'utf8')).trim();
|
|
35
|
+
// Inject WEB_TOKEN=<literal> as an environment entry on the web service.
|
|
36
|
+
text = text.replace(/(\n env_file:\n)/, `\n environment:\n - WEB_TOKEN=${secret}\n$1`);
|
|
37
|
+
const { writeFile } = await import('node:fs/promises');
|
|
38
|
+
await writeFile(composePath, text, 'utf8');
|
|
39
|
+
return { scratch, composePath };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function scanForLiteral({ composePath, secret, secretName }) {
|
|
43
|
+
const text = await readFile(composePath, 'utf8');
|
|
44
|
+
const lines = text.split('\n');
|
|
45
|
+
const hits = [];
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
if (lines[i].includes(secret)) {
|
|
48
|
+
hits.push({ file: composePath, line: i + 1, snippet: lines[i].trim() });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return hits;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default async function runProbe() {
|
|
55
|
+
const results = [];
|
|
56
|
+
const extra = {};
|
|
57
|
+
const secret = (await readFile(SECRET_PATH, 'utf8')).trim();
|
|
58
|
+
const mutationOn = process.env.SIMULATE_PLAINTEXT_SECRET === 'true';
|
|
59
|
+
extra.mutations = { plaintextSecret: mutationOn };
|
|
60
|
+
let composePathToScan = COMPOSE_PATH;
|
|
61
|
+
let scratchToClean = null;
|
|
62
|
+
if (mutationOn) {
|
|
63
|
+
const m = await applyPlaintextMutation();
|
|
64
|
+
composePathToScan = m.composePath;
|
|
65
|
+
scratchToClean = m.scratch;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
// Secret-shape scan: every declared secret has a file: source.
|
|
69
|
+
const { doc } = await readCompose();
|
|
70
|
+
const secretsBlock = doc.secrets ?? {};
|
|
71
|
+
const secretNames = Object.keys(secretsBlock);
|
|
72
|
+
extra.declaredSecrets = secretNames;
|
|
73
|
+
for (const name of secretNames) {
|
|
74
|
+
const spec = secretsBlock[name] ?? {};
|
|
75
|
+
if (!spec.file) {
|
|
76
|
+
results.push({
|
|
77
|
+
anchorAcId: 'AC-composeHost-secretShape',
|
|
78
|
+
verdict: 'fail',
|
|
79
|
+
detail: `compose secret '${name}' is not a file: source`,
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
results.push({
|
|
83
|
+
anchorAcId: 'AC-composeHost-secretShape',
|
|
84
|
+
verdict: 'pass',
|
|
85
|
+
detail: `compose secret '${name}' -> file: ${spec.file}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Plaintext-literal scan.
|
|
90
|
+
const hits = await scanForLiteral({ composePath: composePathToScan, secret, secretName: 'web-token' });
|
|
91
|
+
if (hits.length > 0) {
|
|
92
|
+
for (const h of hits) {
|
|
93
|
+
results.push({
|
|
94
|
+
anchorAcId: 'AC-composeHost-secretsAreFiles',
|
|
95
|
+
verdict: 'fail',
|
|
96
|
+
detail: `plaintext secret literal for 'web-token' found in ${h.file}:${h.line} (snippet: ${h.snippet})`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
results.push({
|
|
101
|
+
anchorAcId: 'AC-composeHost-secretsAreFiles',
|
|
102
|
+
verdict: 'pass',
|
|
103
|
+
detail: `no plaintext secret literal for 'web-token' found in the scanned compose.yaml`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
extra.hits = hits.map((h) => ({ file: h.file, line: h.line }));
|
|
107
|
+
} finally {
|
|
108
|
+
if (scratchToClean) await rm(scratchToClean, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
return { results, extra };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const engine = { kind: 'source-scan', image: 'in-process compose + secret grep', healthy: true };
|
|
114
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
115
|
+
await runShim('secrets-as-files-scan', engine, runProbe);
|
|
116
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reqId": "platform-docker-compose-host-REQ-001",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"title": "The compose.yaml at the applying repo root is the single-source runtime topology, with service-per-container, named networks and volumes, and .env for non-secret config",
|
|
5
|
+
"description": "The applying project runs its containerised services from a single compose.yaml at the repo root. Every entry under services: is one image running as one container (service-per-container). Networks and volumes are declared with explicit names under the top-level networks: and volumes: keys. Non-secret configuration values live in a colocated .env file loaded by compose, never inlined into service environment blocks. The container host reads compose.yaml as the topology source of truth and refuses to bring the stack up on layout drift.",
|
|
6
|
+
"category": "functional",
|
|
7
|
+
"domain": "platform",
|
|
8
|
+
"priority": "must",
|
|
9
|
+
"rationale": "Ratified in the Hetzner round 7 spec (2026-09-07 section 5.2 REQ-001). Ships as v1.0.0 of platform-docker-compose-host.",
|
|
10
|
+
"tags": [
|
|
11
|
+
"blueprint:platform-docker-compose-host",
|
|
12
|
+
"capability:containerHost"
|
|
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
|
+
}
|