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,237 @@
|
|
|
1
|
+
// Probe: manifest-schema-validate.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId list (multi):
|
|
4
|
+
// - AC-tunnel-manifestSchema (primary)
|
|
5
|
+
// - AC-tunnel-noPublicOrigin (refuses ingress rules whose service URL
|
|
6
|
+
// binds to a host public interface)
|
|
7
|
+
// - AC-tunnel-credentialsDiscipline (refuses when credentials are inlined
|
|
8
|
+
// instead of via secretRef; runs an event-secrecy scan on the
|
|
9
|
+
// fixture credentials placeholder body)
|
|
10
|
+
// accountBound: false.
|
|
11
|
+
//
|
|
12
|
+
// Verdict is a pure function of the files at fixtureRoot() (default: the
|
|
13
|
+
// canonical fixture; a fixture-side delegate points FIXTURE_ROOT at a
|
|
14
|
+
// scratch dir with a mutation applied). The probe module reads no
|
|
15
|
+
// fixture-side mutation env var and does not branch on being under mutation.
|
|
16
|
+
|
|
17
|
+
import { readFile } from 'node:fs/promises';
|
|
18
|
+
import { dirname, resolve } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { runShim, fixtureRoot, parseTunnelYaml, pairs } from './probe-utils.mjs';
|
|
21
|
+
|
|
22
|
+
export const anchorAcIds = [
|
|
23
|
+
'AC-tunnel-manifestSchema',
|
|
24
|
+
'AC-tunnel-noPublicOrigin',
|
|
25
|
+
'AC-tunnel-credentialsDiscipline',
|
|
26
|
+
];
|
|
27
|
+
export const accountBound = false;
|
|
28
|
+
|
|
29
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const SCHEMA_PATH = resolve(HERE, 'tunnel-manifest.schema.json');
|
|
31
|
+
|
|
32
|
+
function validateAgainstSchema(doc, schema) {
|
|
33
|
+
// Minimal Ajv-free walker sufficient for this schema. Returns an array
|
|
34
|
+
// of violation strings.
|
|
35
|
+
const errors = [];
|
|
36
|
+
function walk(node, sub, pathHint) {
|
|
37
|
+
if (sub.type === 'object' && sub.properties) {
|
|
38
|
+
if (typeof node !== 'object' || node === null || Array.isArray(node)) {
|
|
39
|
+
errors.push(`${pathHint}: expected object, got ${node === null ? 'null' : Array.isArray(node) ? 'array' : typeof node}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const key of sub.required ?? []) {
|
|
43
|
+
if (!(key in node)) errors.push(`${pathHint}: missing required field '${key}'`);
|
|
44
|
+
}
|
|
45
|
+
if (sub.additionalProperties === false) {
|
|
46
|
+
for (const key of Object.keys(node)) {
|
|
47
|
+
if (!(key in (sub.properties ?? {}))) errors.push(`${pathHint}: unknown field '${key}'`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const [key, subsub] of Object.entries(sub.properties)) {
|
|
51
|
+
if (key in node) walk(node[key], subsub, `${pathHint}.${key}`);
|
|
52
|
+
}
|
|
53
|
+
} else if (sub.type === 'array') {
|
|
54
|
+
if (!Array.isArray(node)) {
|
|
55
|
+
errors.push(`${pathHint}: expected array, got ${typeof node}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (typeof sub.minItems === 'number' && node.length < sub.minItems) {
|
|
59
|
+
errors.push(`${pathHint}: expected at least ${sub.minItems} item(s), got ${node.length}`);
|
|
60
|
+
}
|
|
61
|
+
if (sub.items) {
|
|
62
|
+
node.forEach((item, i) => walk(item, sub.items, `${pathHint}[${i}]`));
|
|
63
|
+
}
|
|
64
|
+
} else if (sub.type === 'string') {
|
|
65
|
+
if (typeof node !== 'string') {
|
|
66
|
+
errors.push(`${pathHint}: expected string, got ${typeof node}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (typeof sub.minLength === 'number' && node.length < sub.minLength) {
|
|
70
|
+
errors.push(`${pathHint}: expected minLength ${sub.minLength}, got ${node.length}`);
|
|
71
|
+
}
|
|
72
|
+
if (sub.pattern && !(new RegExp(sub.pattern).test(node))) {
|
|
73
|
+
errors.push(`${pathHint}: value '${node}' does not match pattern ${sub.pattern}`);
|
|
74
|
+
}
|
|
75
|
+
} else if (sub.type === 'boolean') {
|
|
76
|
+
if (typeof node !== 'boolean') errors.push(`${pathHint}: expected boolean`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
walk(doc, schema, 'manifest');
|
|
80
|
+
return errors;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function refuseOriginPortRules(doc) {
|
|
84
|
+
// A service URL that binds to a host public interface is a policy
|
|
85
|
+
// violation for a T-3 apply (no reverse proxy). The catch-all
|
|
86
|
+
// http_status:N is exempt.
|
|
87
|
+
const violations = [];
|
|
88
|
+
const rules = Array.isArray(doc.ingress) ? doc.ingress : [];
|
|
89
|
+
for (let i = 0; i < rules.length; i += 1) {
|
|
90
|
+
const rule = rules[i] ?? {};
|
|
91
|
+
const svc = String(rule.service ?? '');
|
|
92
|
+
if (svc.startsWith('http_status:')) continue;
|
|
93
|
+
const m = svc.match(/^https?:\/\/([^\/:]+)(:\d+)?(\/.*)?$/);
|
|
94
|
+
if (!m) {
|
|
95
|
+
violations.push(`ingress[${i}] service '${svc}' does not match the internal-address shape (expected http(s)://<internal-name>[:port]/...)`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const host = m[1];
|
|
99
|
+
if (host === '0.0.0.0' || host === '') {
|
|
100
|
+
violations.push(`ingress[${i}] service '${svc}' binds to a host public interface (${host || 'empty'}); T-3 requires internal addresses only`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(host) && host !== '127.0.0.1') {
|
|
104
|
+
violations.push(`ingress[${i}] service '${svc}' binds to a numeric IP that is not loopback; T-3 requires internal names or loopback`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return violations;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function eventSecrecyScan(credentialsPath) {
|
|
111
|
+
let creds;
|
|
112
|
+
try {
|
|
113
|
+
creds = JSON.parse(await readFile(credentialsPath, 'utf8'));
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return {
|
|
116
|
+
verdict: 'fail',
|
|
117
|
+
detail: `credentials placeholder read failed at ${credentialsPath}: ${err.message}. Fix the fixture setup step (packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared/*/credentials/probe.json.example must exist and parse as JSON) before re-running the probe. A missing placeholder is a fixture-setup defect, not an event-secrecy warning.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const eventBody = {
|
|
121
|
+
kind: 'cloudflaredReady',
|
|
122
|
+
connectorId: 'sha256:fixture-connector',
|
|
123
|
+
region: 'fixture-region',
|
|
124
|
+
version: '2026.8.3',
|
|
125
|
+
// A fixture-side mutation may seed a _leakedEvent object which the
|
|
126
|
+
// probe surfaces by including it in the synthetic event body.
|
|
127
|
+
...(creds._leakedEvent ?? {}),
|
|
128
|
+
};
|
|
129
|
+
const body = JSON.stringify(eventBody);
|
|
130
|
+
const leaks = [];
|
|
131
|
+
for (const [k, v] of Object.entries(creds)) {
|
|
132
|
+
if (k === '_comment' || k === '_leakedEvent') continue;
|
|
133
|
+
if (typeof v !== 'string' || v.length < 8) continue;
|
|
134
|
+
if (body.includes(v)) leaks.push(`event body carries credentials.${k} literal`);
|
|
135
|
+
}
|
|
136
|
+
if (leaks.length) {
|
|
137
|
+
return { verdict: 'fail', detail: `event-secrecy scan FAILED: ${leaks.join('; ')}` };
|
|
138
|
+
}
|
|
139
|
+
return { verdict: 'pass', detail: 'event-secrecy scan clean; cloudflaredReady body carries metadata only' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default async function runProbe() {
|
|
143
|
+
const results = [];
|
|
144
|
+
const extra = {};
|
|
145
|
+
const root = fixtureRoot();
|
|
146
|
+
extra.fixtureRoot = root;
|
|
147
|
+
const schema = JSON.parse(await readFile(SCHEMA_PATH, 'utf8'));
|
|
148
|
+
extra.schemaId = schema.$id;
|
|
149
|
+
for (const { runtime, mode } of pairs()) {
|
|
150
|
+
const manifestPath = resolve(root, runtime, 'cloudflare/tunnels', `${mode}.yaml`);
|
|
151
|
+
let text;
|
|
152
|
+
try {
|
|
153
|
+
text = await readFile(manifestPath, 'utf8');
|
|
154
|
+
} catch (err) {
|
|
155
|
+
results.push({
|
|
156
|
+
anchorAcId: 'AC-tunnel-manifestSchema',
|
|
157
|
+
verdict: 'fail',
|
|
158
|
+
detail: `${runtime}/${mode}: manifest read failed at ${manifestPath}: ${err.message}`,
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const doc = parseTunnelYaml(text);
|
|
163
|
+
const schemaErrors = validateAgainstSchema(doc, schema);
|
|
164
|
+
if (schemaErrors.length) {
|
|
165
|
+
results.push({
|
|
166
|
+
anchorAcId: 'AC-tunnel-manifestSchema',
|
|
167
|
+
verdict: 'fail',
|
|
168
|
+
detail: `${runtime}/${mode}: schema violation(s): ${schemaErrors.slice(0, 5).join('; ')}${schemaErrors.length > 5 ? ` (+${schemaErrors.length - 5} more)` : ''}`,
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
results.push({
|
|
172
|
+
anchorAcId: 'AC-tunnel-manifestSchema',
|
|
173
|
+
verdict: 'pass',
|
|
174
|
+
detail: `${runtime}/${mode}: manifest at ${manifestPath} validates against tunnel-manifest.schema.json`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const rulesForCatchall = Array.isArray(doc.ingress) ? doc.ingress : [];
|
|
178
|
+
const lastRule = rulesForCatchall[rulesForCatchall.length - 1];
|
|
179
|
+
if (!lastRule || typeof lastRule.service !== 'string' || !lastRule.service.startsWith('http_status:')) {
|
|
180
|
+
results.push({
|
|
181
|
+
anchorAcId: 'AC-tunnel-manifestSchema',
|
|
182
|
+
verdict: 'fail',
|
|
183
|
+
detail: `${runtime}/${mode}: last ingress rule must be a catch-all http_status:N (found: ${lastRule ? JSON.stringify(lastRule).slice(0, 120) : '(no ingress rules)'})`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const originViolations = refuseOriginPortRules(doc);
|
|
187
|
+
if (originViolations.length) {
|
|
188
|
+
results.push({
|
|
189
|
+
anchorAcId: 'AC-tunnel-noPublicOrigin',
|
|
190
|
+
verdict: 'fail',
|
|
191
|
+
detail: `${runtime}/${mode}: ${originViolations.join('; ')}`,
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
results.push({
|
|
195
|
+
anchorAcId: 'AC-tunnel-noPublicOrigin',
|
|
196
|
+
verdict: 'pass',
|
|
197
|
+
detail: `${runtime}/${mode}: every ingress service URL binds to an internal address; no host public port exposed`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const cred = doc.credentialsFile;
|
|
201
|
+
if (!cred || typeof cred !== 'object' || Array.isArray(cred)) {
|
|
202
|
+
results.push({
|
|
203
|
+
anchorAcId: 'AC-tunnel-credentialsDiscipline',
|
|
204
|
+
verdict: 'fail',
|
|
205
|
+
detail: `${runtime}/${mode}: credentialsFile must be an object with a secretRef`,
|
|
206
|
+
});
|
|
207
|
+
} else if (!('secretRef' in cred) || typeof cred.secretRef !== 'string' || !cred.secretRef.length) {
|
|
208
|
+
results.push({
|
|
209
|
+
anchorAcId: 'AC-tunnel-credentialsDiscipline',
|
|
210
|
+
verdict: 'fail',
|
|
211
|
+
detail: `${runtime}/${mode}: credentialsFile.secretRef is required (found keys: ${Object.keys(cred).join(', ') || '(none)'})`,
|
|
212
|
+
});
|
|
213
|
+
} else if ('AccountTag' in cred || 'TunnelSecret' in cred || 'TunnelID' in cred) {
|
|
214
|
+
results.push({
|
|
215
|
+
anchorAcId: 'AC-tunnel-credentialsDiscipline',
|
|
216
|
+
verdict: 'fail',
|
|
217
|
+
detail: `${runtime}/${mode}: credentialsFile carries an inline JSON body (bypasses secretRef)`,
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
results.push({
|
|
221
|
+
anchorAcId: 'AC-tunnel-credentialsDiscipline',
|
|
222
|
+
verdict: 'pass',
|
|
223
|
+
detail: `${runtime}/${mode}: credentialsFile.secretRef = ${cred.secretRef}`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const credPath = resolve(root, runtime, 'credentials/probe.json.example');
|
|
227
|
+
const scan = await eventSecrecyScan(credPath);
|
|
228
|
+
results.push({ anchorAcId: 'AC-tunnel-credentialsDiscipline', ...scan });
|
|
229
|
+
extra[`${runtime}/${mode}`] = { tunnel: doc.tunnel, ingressCount: Array.isArray(doc.ingress) ? doc.ingress.length : 0 };
|
|
230
|
+
}
|
|
231
|
+
return { results, extra };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const engine = { kind: 'schema-validate', image: 'edge-cloudflare-tunnel manifest schema (Ajv-free walker)', healthy: true };
|
|
235
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
236
|
+
await runShim('manifest-schema-validate', engine, runProbe);
|
|
237
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Shared helpers for edge-cloudflare-tunnel probes (round-7 T-3).
|
|
2
|
+
//
|
|
3
|
+
// Runtime-dependency posture:
|
|
4
|
+
// - manifest-schema-validate is dependency-free: it reads yaml text and
|
|
5
|
+
// walks the parsed document. The tunnel manifest schema ships at
|
|
6
|
+
// ./tunnel-manifest.schema.json.
|
|
7
|
+
// - cloudflared-config-lint shells to cloudflared tunnel ingress validate.
|
|
8
|
+
// When a local cloudflared binary is on PATH the probe uses it; when it
|
|
9
|
+
// is not, the probe runs the vendor container image
|
|
10
|
+
// cloudflare/cloudflared:2026.8.3 via docker run --rm -v <dir>:/etc/cloudflared:ro
|
|
11
|
+
// per the vendor downloads page. When neither is available the probe
|
|
12
|
+
// records warn (source-tree assertions still run).
|
|
13
|
+
// - aud-presence-check reads a sidecar JSON and the paired tunnel yaml;
|
|
14
|
+
// dependency-free.
|
|
15
|
+
// - real-account-* probes shell to cloudflared tunnel info / undici curl
|
|
16
|
+
// against a scratch subdomain on a real Cloudflare account; without
|
|
17
|
+
// CI_HAS_CLOUDFLARE_ACCOUNT AND CI_HAS_HETZNER_ACCOUNT they record
|
|
18
|
+
// accountBoundSkipped: true and the aggregate flips to pass per
|
|
19
|
+
// hetzner-round-7-spec section 3.5.
|
|
20
|
+
//
|
|
21
|
+
// FIXTURE_ROOT posture:
|
|
22
|
+
// - The fixture-side delegate shims under
|
|
23
|
+
// packages/rcf-lite/test/fixtures/hetzner-throwaway-server/run-<probe>.mjs
|
|
24
|
+
// set RCF_LITE_T3_FIXTURE_ROOT to a scratch directory when a mutation
|
|
25
|
+
// switch is on. The probe modules NEVER read a fixture-side mutation env var and
|
|
26
|
+
// NEVER branch on being under mutation; their verdict is a pure
|
|
27
|
+
// function of the files at FIXTURE_ROOT.
|
|
28
|
+
|
|
29
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
30
|
+
import { dirname, resolve } from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { spawnSync } from 'node:child_process';
|
|
33
|
+
|
|
34
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
export const PROJECT_ROOT = resolve(HERE, '..', '..', '..', '..');
|
|
36
|
+
|
|
37
|
+
// Fixture root defaults to the shared throwaway-server fixture cloudflared/
|
|
38
|
+
// sub-directory. A fixture-side delegate that needs to inject a mutation
|
|
39
|
+
// copies the sub-tree to a scratch dir and points FIXTURE_ROOT at it.
|
|
40
|
+
export const CANONICAL_FIXTURE_ROOT = resolve(
|
|
41
|
+
PROJECT_ROOT,
|
|
42
|
+
'packages/rcf-lite/test/fixtures/hetzner-throwaway-server/cloudflared',
|
|
43
|
+
);
|
|
44
|
+
export function fixtureRoot() {
|
|
45
|
+
return process.env.RCF_LITE_T3_FIXTURE_ROOT
|
|
46
|
+
? resolve(process.env.RCF_LITE_T3_FIXTURE_ROOT)
|
|
47
|
+
: CANONICAL_FIXTURE_ROOT;
|
|
48
|
+
}
|
|
49
|
+
export const REPORT_DIR = resolve(
|
|
50
|
+
PROJECT_ROOT,
|
|
51
|
+
'.rcf/reports/blueprints/edge-cloudflare-tunnel',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export function aggregate(results) {
|
|
55
|
+
if (results.some((r) => r.verdict === 'fail')) return 'fail';
|
|
56
|
+
if (results.some((r) => r.verdict === 'warn')) return 'warn';
|
|
57
|
+
return 'pass';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isSkipped(results) {
|
|
61
|
+
return results.length > 0 && results.every((r) => r.accountBoundSkipped === true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function writeReport({ probeName, engine, results, extra }) {
|
|
65
|
+
await mkdir(REPORT_DIR, { recursive: true });
|
|
66
|
+
const rawVerdict = aggregate(results);
|
|
67
|
+
const aggregateVerdict = isSkipped(results) ? 'pass' : rawVerdict;
|
|
68
|
+
const report = {
|
|
69
|
+
slug: 'edge-cloudflare-tunnel',
|
|
70
|
+
probeName,
|
|
71
|
+
runAt: new Date().toISOString(),
|
|
72
|
+
engine,
|
|
73
|
+
results,
|
|
74
|
+
aggregateVerdict,
|
|
75
|
+
...(extra ?? {}),
|
|
76
|
+
};
|
|
77
|
+
// Strip absolute builder scratchpad paths from persisted reports so
|
|
78
|
+
// the committed envelopes stay repo-relative and diff cleanly. Walks
|
|
79
|
+
// every string in the report and rewrites PROJECT_ROOT-prefixed paths
|
|
80
|
+
// to their repo-relative form (covers report.fixtureRoot, results[].detail,
|
|
81
|
+
// extra.* and any nested string value future probes surface).
|
|
82
|
+
const prefix = PROJECT_ROOT + '/';
|
|
83
|
+
function stripPaths(node) {
|
|
84
|
+
if (typeof node === 'string') {
|
|
85
|
+
if (node === PROJECT_ROOT) return '.';
|
|
86
|
+
return node.split(prefix).join('');
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(node)) return node.map(stripPaths);
|
|
89
|
+
if (node && typeof node === 'object') {
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [k, v] of Object.entries(node)) out[k] = stripPaths(v);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
return node;
|
|
95
|
+
}
|
|
96
|
+
Object.assign(report, stripPaths(report));
|
|
97
|
+
const path = resolve(REPORT_DIR, `${probeName}.json`);
|
|
98
|
+
await writeFile(path, JSON.stringify(report, null, 2) + '\n', 'utf8');
|
|
99
|
+
return { report, path };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runShim(probeName, engine, mainFn) {
|
|
103
|
+
try {
|
|
104
|
+
const outcome = (await mainFn()) ?? { results: [] };
|
|
105
|
+
const { results, extra } = outcome;
|
|
106
|
+
const { report, path } = await writeReport({ probeName, engine, results, extra });
|
|
107
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
108
|
+
process.stdout.write(`report written to ${path}\n`);
|
|
109
|
+
if (report.aggregateVerdict === 'fail') process.exitCode = 1;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const results = [{
|
|
112
|
+
anchorAcId: 'unknown',
|
|
113
|
+
verdict: 'fail',
|
|
114
|
+
detail: `probe threw: ${err && err.message ? err.message : String(err)}`,
|
|
115
|
+
}];
|
|
116
|
+
const { report, path } = await writeReport({ probeName, engine, results });
|
|
117
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
118
|
+
process.stderr.write(`probe error: ${err && err.stack ? err.stack : String(err)}\n`);
|
|
119
|
+
process.stderr.write(`report written to ${path}\n`);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function accountBoundSkippedResult(anchorAcId, envList, note) {
|
|
125
|
+
return {
|
|
126
|
+
anchorAcId,
|
|
127
|
+
verdict: 'skipped',
|
|
128
|
+
accountBoundSkipped: true,
|
|
129
|
+
detail: `accountBound: ${envList.join(' or ')} unset; ${note}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Minimal yaml subset parser tuned for the tunnel manifest shape.
|
|
134
|
+
// The fixture manifests are intentionally simple: no anchors, no flow-style
|
|
135
|
+
// scalars, two-space indent. The parser produces a shape sufficient for
|
|
136
|
+
// the ACs (tunnel id string, credentialsFile.secretRef, ingress array of
|
|
137
|
+
// {hostname?, service, originRequest?.access.aud?}).
|
|
138
|
+
export function parseTunnelYaml(text) {
|
|
139
|
+
const doc = {};
|
|
140
|
+
const lines = text.split(/\r?\n/);
|
|
141
|
+
const stack = [{ indent: -1, node: doc }];
|
|
142
|
+
const kvRe = /^([A-Za-z0-9_.-]+):\s*(.*)$/;
|
|
143
|
+
function decode(raw) {
|
|
144
|
+
if (raw === '' || raw === undefined) return null;
|
|
145
|
+
if (raw === 'true') return true;
|
|
146
|
+
if (raw === 'false') return false;
|
|
147
|
+
if (raw === 'null' || raw === '~') return null;
|
|
148
|
+
return raw.replace(/^"([^"\\]*)"$/, '$1').replace(/^'([^'\\]*)'$/, '$1');
|
|
149
|
+
}
|
|
150
|
+
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
151
|
+
const raw = lines[idx];
|
|
152
|
+
if (raw.trim() === '' || raw.trim().startsWith('#')) continue;
|
|
153
|
+
const indentM = raw.match(/^(\s*)/);
|
|
154
|
+
const indent = indentM ? indentM[1].length : 0;
|
|
155
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) stack.pop();
|
|
156
|
+
const parent = stack[stack.length - 1].node;
|
|
157
|
+
const body = raw.slice(indent);
|
|
158
|
+
if (body.startsWith('- ')) {
|
|
159
|
+
const rest = body.slice(2);
|
|
160
|
+
const kv = rest.match(kvRe);
|
|
161
|
+
if (kv) {
|
|
162
|
+
const [, key, val] = kv;
|
|
163
|
+
const obj = {};
|
|
164
|
+
if (val === '') {
|
|
165
|
+
const child = {};
|
|
166
|
+
obj[key] = child;
|
|
167
|
+
parent.push(obj);
|
|
168
|
+
stack.push({ indent, node: obj });
|
|
169
|
+
stack.push({ indent: indent + 2, node: child });
|
|
170
|
+
} else {
|
|
171
|
+
obj[key] = decode(val.trim());
|
|
172
|
+
parent.push(obj);
|
|
173
|
+
stack.push({ indent, node: obj });
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
parent.push(decode(rest.trim()));
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const kv = body.match(kvRe);
|
|
181
|
+
if (!kv) continue;
|
|
182
|
+
const [, key, val] = kv;
|
|
183
|
+
if (val === '') {
|
|
184
|
+
let peekIsList = false;
|
|
185
|
+
for (let j = idx + 1; j < lines.length; j += 1) {
|
|
186
|
+
const nxt = lines[j];
|
|
187
|
+
if (!nxt || nxt.trim() === '' || nxt.trim().startsWith('#')) continue;
|
|
188
|
+
const nxtIndentM = nxt.match(/^(\s*)/);
|
|
189
|
+
const nxtIndent = nxtIndentM ? nxtIndentM[1].length : 0;
|
|
190
|
+
if (nxtIndent <= indent) break;
|
|
191
|
+
peekIsList = nxt.slice(nxtIndent).startsWith('- ');
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
const child = peekIsList ? [] : {};
|
|
195
|
+
parent[key] = child;
|
|
196
|
+
stack.push({ indent, node: child });
|
|
197
|
+
} else {
|
|
198
|
+
parent[key] = decode(val.trim());
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return doc;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function whichDocker() {
|
|
205
|
+
const r = spawnSync('docker', ['version', '--format', '{{.Client.Version}}'], { encoding: 'utf8' });
|
|
206
|
+
return r.status === 0 ? (r.stdout || '').trim() : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function whichCloudflared() {
|
|
210
|
+
const r = spawnSync('cloudflared', ['--version'], { encoding: 'utf8' });
|
|
211
|
+
return r.status === 0 ? (r.stdout || '').trim() : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const CLOUDFLARED_IMAGE = 'cloudflare/cloudflared:2026.8.3';
|
|
215
|
+
|
|
216
|
+
// Two dimensions: connector runtime (compose-service or systemd-unit) and
|
|
217
|
+
// hostname mode (public-hostname or access-gated). Every combination
|
|
218
|
+
// ships one manifest under cloudflared/<runtime>/cloudflare/tunnels/<mode>.yaml.
|
|
219
|
+
export const RUNTIMES = ['compose-service', 'systemd-unit'];
|
|
220
|
+
export const HOSTNAME_MODES = ['public-hostname', 'access-gated'];
|
|
221
|
+
export function pairs() {
|
|
222
|
+
const list = [];
|
|
223
|
+
for (const runtime of RUNTIMES) {
|
|
224
|
+
for (const mode of HOSTNAME_MODES) list.push({ runtime, mode });
|
|
225
|
+
}
|
|
226
|
+
return list;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Backwards compat name used by the manifest-schema-validate probe: iterate over
|
|
230
|
+
// runtime variants for schema-shape checks (both modes are shape-checked).
|
|
231
|
+
export const VARIANTS = RUNTIMES;
|
package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Probe: real-account-connector-healthy.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId: AC-tunnel-connectorHealthy (US-39102 contribution;
|
|
4
|
+
// real-account-connector-healthy declared with tunnelConnectorUp).
|
|
5
|
+
// C4 ruling: the AC-tunnel-* symbolic anchors on the tunnel blueprint
|
|
6
|
+
// ARE the AC ids in the shipped US-39101..US-39108 contribution band
|
|
7
|
+
// (see blueprints/edge-cloudflare-tunnel/contributions/user-stories/
|
|
8
|
+
// edge-cloudflare-tunnel-us-3910N.json); the anchors already resolve
|
|
9
|
+
// to the shipped band, so no re-anchor is performed by H-2. The kv
|
|
10
|
+
// treatment differed because those probes anchored AC-5xxx ids that
|
|
11
|
+
// existed nowhere on the shelf; here the ids exist and match by
|
|
12
|
+
// design.
|
|
13
|
+
//
|
|
14
|
+
// accountBound: true (gates on CI_HAS_CLOUDFLARE_ACCOUNT AND
|
|
15
|
+
// CI_HAS_HETZNER_ACCOUNT; the connector runs on a throwaway Hetzner
|
|
16
|
+
// server).
|
|
17
|
+
//
|
|
18
|
+
// Env-absent branch: pass-with-skip (spec section 3.5).
|
|
19
|
+
// Env-present branch: provisions a throwaway Hetzner server through
|
|
20
|
+
// the shared T-1 fixture provisionThrowawayServer, drives the
|
|
21
|
+
// cloudflared control surface through the cf-edge shim's
|
|
22
|
+
// cloudflaredTunnelInfo({ tunnelName }) call, asserts at least one
|
|
23
|
+
// HEALTHY connector, and captures a tunnelConnectorUp event
|
|
24
|
+
// (metadata-only: connector id, region, version). If the fixture
|
|
25
|
+
// provisioning throws (missing HCLOUD_TOKEN, quota, etc.), the
|
|
26
|
+
// probe FAILS with a pointer naming the fixture step: the tunnel
|
|
27
|
+
// connector cannot report healthy on a server that never provisioned.
|
|
28
|
+
//
|
|
29
|
+
// The shim seams (cloudflaredTunnelInfo, event capture) allow the
|
|
30
|
+
// driver path and mutation switches to be exercised locally without
|
|
31
|
+
// a real Cloudflare account; the real-account run happens at HQ
|
|
32
|
+
// gate time.
|
|
33
|
+
|
|
34
|
+
import { runShim, accountBoundSkippedResult } from './probe-utils.mjs';
|
|
35
|
+
|
|
36
|
+
export const anchorAcIds = ['AC-tunnel-connectorHealthy'];
|
|
37
|
+
export const accountBound = true;
|
|
38
|
+
|
|
39
|
+
const TUNNEL_NAME_ENV = 'CF_TUNNEL_NAME';
|
|
40
|
+
const DEFAULT_TUNNEL_NAME = 'h2-cf-probe-integrity-scratch';
|
|
41
|
+
|
|
42
|
+
export default async function runProbe() {
|
|
43
|
+
const results = [];
|
|
44
|
+
const extra = {
|
|
45
|
+
envRequired: ['CI_HAS_CLOUDFLARE_ACCOUNT', 'CI_HAS_HETZNER_ACCOUNT'],
|
|
46
|
+
envPresent: {
|
|
47
|
+
CI_HAS_CLOUDFLARE_ACCOUNT: process.env.CI_HAS_CLOUDFLARE_ACCOUNT === 'true',
|
|
48
|
+
CI_HAS_HETZNER_ACCOUNT: process.env.CI_HAS_HETZNER_ACCOUNT === 'true',
|
|
49
|
+
},
|
|
50
|
+
events: [],
|
|
51
|
+
};
|
|
52
|
+
const hasAll = extra.envPresent.CI_HAS_CLOUDFLARE_ACCOUNT && extra.envPresent.CI_HAS_HETZNER_ACCOUNT;
|
|
53
|
+
if (!hasAll) {
|
|
54
|
+
results.push(accountBoundSkippedResult(
|
|
55
|
+
'AC-tunnel-connectorHealthy',
|
|
56
|
+
['CI_HAS_CLOUDFLARE_ACCOUNT', 'CI_HAS_HETZNER_ACCOUNT'],
|
|
57
|
+
'real-account cloudflared tunnel info + tunnelConnectorUp capture not exercised. Set CI_HAS_CLOUDFLARE_ACCOUNT=true AND CI_HAS_HETZNER_ACCOUNT=true (with HCLOUD_TOKEN and CLOUDFLARE_API_TOKEN in the environment via security-secrets-management) to run the real-account path.',
|
|
58
|
+
));
|
|
59
|
+
return { results, extra };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Env-present path: real driver. All account-bound work runs through
|
|
63
|
+
// the cf-edge shim, which itself delegates to the hetzner fixture in
|
|
64
|
+
// production and returns synthetic responses under the fixture-side
|
|
65
|
+
// H2_CF_TUNNEL_SHIM_MODE token. Probe body reads no mutation-switch
|
|
66
|
+
// env var and imports no fixture module directly.
|
|
67
|
+
const {
|
|
68
|
+
provisionScratchServer, destroyScratchServer, cloudflaredTunnelInfo,
|
|
69
|
+
} = await import(
|
|
70
|
+
'../../../../packages/rcf-lite/test/fixtures/cf-edge/h2-cf-tunnel-shim.mjs'
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const runId = process.env.GITHUB_RUN_ID || `local-${Date.now()}`;
|
|
74
|
+
let provisioned = null;
|
|
75
|
+
try {
|
|
76
|
+
provisioned = await provisionScratchServer({ runId });
|
|
77
|
+
extra.provisioned = {
|
|
78
|
+
id: provisioned.id, name: provisioned.name, location: provisioned.location, shimMode: provisioned.shimMode,
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
results.push({
|
|
82
|
+
anchorAcId: 'AC-tunnel-connectorHealthy',
|
|
83
|
+
verdict: 'fail',
|
|
84
|
+
detail: `fixture provisionScratchServer failed: ${err.message}. The tunnel connector cannot report healthy on a server that never provisioned. Fix the fixture step (packages/rcf-lite/test/fixtures/hetzner-throwaway-server/provision.mjs) or the account tokens (HCLOUD_TOKEN) before re-running.`,
|
|
85
|
+
});
|
|
86
|
+
return { results, extra };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const tunnelName = process.env[TUNNEL_NAME_ENV] || DEFAULT_TUNNEL_NAME;
|
|
91
|
+
extra.tunnelName = tunnelName;
|
|
92
|
+
let info;
|
|
93
|
+
try {
|
|
94
|
+
info = await cloudflaredTunnelInfo({ tunnelName });
|
|
95
|
+
} catch (err) {
|
|
96
|
+
results.push({
|
|
97
|
+
anchorAcId: 'AC-tunnel-connectorHealthy',
|
|
98
|
+
verdict: 'fail',
|
|
99
|
+
detail: `cloudflared tunnel info shim call failed: ${err.message}. Fix the cf-edge shim (packages/rcf-lite/test/fixtures/cf-edge/h2-cf-tunnel-shim.mjs) or the cloudflared control surface before re-running.`,
|
|
100
|
+
});
|
|
101
|
+
return { results, extra };
|
|
102
|
+
}
|
|
103
|
+
extra.shimMode = info.shimMode;
|
|
104
|
+
extra.connectorCount = info.connectors.length;
|
|
105
|
+
const healthy = info.connectors.filter((c) => String(c.status || '').toUpperCase() === 'HEALTHY');
|
|
106
|
+
if (healthy.length === 0) {
|
|
107
|
+
results.push({
|
|
108
|
+
anchorAcId: 'AC-tunnel-connectorHealthy',
|
|
109
|
+
verdict: 'fail',
|
|
110
|
+
detail: `cloudflared tunnel info returned ${info.connectors.length} connector(s), none HEALTHY. Statuses observed: ${info.connectors.map((c) => String(c.status || 'MISSING')).join(', ') || '(no connectors)'}.`,
|
|
111
|
+
});
|
|
112
|
+
return { results, extra };
|
|
113
|
+
}
|
|
114
|
+
// Capture tunnelConnectorUp event, metadata only (id, region, version).
|
|
115
|
+
for (const c of healthy) {
|
|
116
|
+
extra.events.push({
|
|
117
|
+
kind: 'tunnelConnectorUp',
|
|
118
|
+
connectorId: String(c.id ?? ''),
|
|
119
|
+
region: String(c.region ?? ''),
|
|
120
|
+
version: String(c.version ?? ''),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
results.push({
|
|
124
|
+
anchorAcId: 'AC-tunnel-connectorHealthy',
|
|
125
|
+
verdict: 'pass',
|
|
126
|
+
detail: `cloudflared tunnel info returned ${info.connectors.length} connector(s), ${healthy.length} HEALTHY (shim mode ${info.shimMode}); tunnelConnectorUp event captured for each healthy connector with metadata-only payload (connectorId, region, version).`,
|
|
127
|
+
});
|
|
128
|
+
return { results, extra };
|
|
129
|
+
} finally {
|
|
130
|
+
// Always attempt teardown; a failed teardown is a fixture concern
|
|
131
|
+
// and rides sweep-orphans, not this probe.
|
|
132
|
+
try {
|
|
133
|
+
await destroyScratchServer(provisioned);
|
|
134
|
+
} catch (_err) {
|
|
135
|
+
// Best-effort; the nightly sweep-orphans job collects any leak.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const engine = { kind: 'real-account-cloudflared-info', image: 'cloudflared + hcloud (throwaway server) via cf-edge shim', healthy: true };
|
|
141
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
142
|
+
await runShim('real-account-connector-healthy', engine, runProbe);
|
|
143
|
+
}
|