Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ This tranche is read-only. `hostname plan` emits proposed component operations,

## maildesk-cf lifecycle

Use `cfctl maildesk-cf verify|snapshot|diff|plan|provision --plan` with JSON specs under [state/maildesk-cf](state/maildesk-cf) when a maildesk deployment needs Email Routing aliases, Workers, D1, R2, Queues, DNS sender authentication, and outbound identity readiness checked together.
Use `cfctl maildesk-cf verify|snapshot|diff|plan|provision --plan` with JSON specs under [state/maildesk-cf](state/maildesk-cf) when a maildesk deployment needs Email Routing aliases, Workers, D1, R2, Queues, and mode-driven sender readiness checked together. The public template defaults to receive-only outbound mode; Cloudflare Email Service or Resend sender evidence is required only when the spec enables that provider.

```bash
cfctl maildesk-cf init --domain example.com
Expand All @@ -169,7 +169,7 @@ cfctl maildesk-cf diff --file state/maildesk-cf/example.json
cfctl maildesk-cf provision --file state/maildesk-cf/example.json --plan
```

`maildesk-cf provision --plan` emits a local operation id and proposed component operations. `maildesk-cf provision --ack-plan <operation-id>` is blocked until those component writes are each available through preview-gated public `cfctl` surfaces. The verifier does not perform broad live sends; sender readiness comes from DNS/authentication and provider readback evidence unless a human explicitly asks for targeted delivery proof.
`maildesk-cf provision --plan` emits a local operation id and proposed component operations. `maildesk-cf provision --ack-plan <operation-id>` is blocked until those component writes are each available through preview-gated public `cfctl` surfaces. The verifier does not perform broad live sends; enabled sender providers use DNS/authentication and provider readback evidence unless a human explicitly asks for targeted delivery proof.

Token minting:

Expand Down
6 changes: 3 additions & 3 deletions catalog/standards.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,19 +235,19 @@
"id": "maildesk-cf.state-owns-complete-path",
"level": "required",
"title": "One spec owns the maildesk path",
"summary": "A maildesk lifecycle spec should declare domains, role aliases, personal aliases, Workers, D1, R2, Queues, sender authentication, and outbound policy together."
"summary": "A maildesk lifecycle spec should declare domains, role aliases, personal aliases, Workers, D1, R2, Queues, sender mode, and outbound policy together."
},
{
"id": "maildesk-cf.verify-before-provision",
"level": "required",
"title": "Verify every dependency before planning provisioning",
"summary": "Maildesk readiness must prove template validation, instance policy, Worker/storage bindings, Email Routing aliases, sender authentication, provider status, and outbound proof posture with evidence artifacts."
"summary": "Maildesk readiness must prove template validation, instance policy, Worker/storage bindings, Email Routing aliases, sender mode, and any enabled provider status with evidence artifacts."
},
{
"id": "maildesk-cf.component-writes-preview-gated",
"level": "required",
"title": "Do not composite-apply before component writes are gated",
"summary": "Composite provisioning must remain plan-only until Worker, D1, R2, Queue, Email Routing, DNS authentication, and sender-provider operations are each available through preview-gated public cfctl surfaces."
"summary": "Composite provisioning must remain plan-only until Worker, D1, R2, Queue, Email Routing, and enabled sender-provider operations are each available through preview-gated public cfctl surfaces."
},
{
"id": "maildesk-cf.targeted-mail-proof-only",
Expand Down
2 changes: 1 addition & 1 deletion catalog/surfaces.json
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@
]
},
"maildesk-cf": {
"description": "Composite maildesk-cf lifecycle readiness across Email Routing aliases, Workers, D1, R2, Queues, sender authentication, and outbound identity evidence.",
"description": "Composite maildesk-cf lifecycle readiness across Email Routing aliases, Workers, D1, R2, Queues, and mode-driven sender evidence.",
"standards_ref": "maildesk-cf",
"docs_topics": ["email-routing", "email-workers", "d1", "r2", "queues", "api-auth"],
"backend": "maildesk_cf_lifecycle",
Expand Down
5 changes: 2 additions & 3 deletions commands/cfctl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3488,8 +3488,7 @@ cfctl_find_maildesk_cf_plan_receipt_path() {
return 1
fi

for candidate in "${runtime_dir}"/*.json; do
[[ -f "${candidate}" ]] || continue
while IFS= read -r candidate; do
if jq -e \
--arg ack_plan "${ack_plan}" \
'
Expand All @@ -3503,7 +3502,7 @@ cfctl_find_maildesk_cf_plan_receipt_path() {
printf '%s\n' "${candidate}"
return 0
fi
done
done < <(find "${runtime_dir}" -maxdepth 1 -type f -name 'maildesk-cf-maildesk-cf-*.json' -print | sort -r)

return 1
}
Expand Down
9 changes: 5 additions & 4 deletions docs/runbooks/cfctl.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ CF_TOKEN_LANE=global cfctl apply edge.certificate order --zone example.com --hos
- `apply <surface> sync` performs selective desired-state reconciliation on supported surfaces
- `hostname verify|diff|plan` checks one YAML hostname lifecycle spec across DNS, TLS, Worker route, Access, Worker script, HTTP response, D1, and R2
- `hostname apply` is blocked until composite mutation is backed by preview-gated component surfaces
- `maildesk-cf init|verify|snapshot|diff|plan|provision --plan` checks one JSON maildesk spec across Email Routing aliases, Workers, D1, R2, Queues, DNS sender authentication, and outbound identity readback
- `maildesk-cf init|verify|snapshot|diff|plan|provision --plan` checks one JSON maildesk spec across Email Routing aliases, Workers, D1, R2, Queues, sender mode, and enabled provider readback
- `maildesk-cf provision --ack-plan <operation-id>` is blocked until composite mutation is backed by preview-gated component surfaces
- `maildesk-cf` does not perform broad live sends; targeted delivery proof must be requested explicitly
- destructive operations require explicit confirmation such as `--confirm delete`
Expand Down Expand Up @@ -202,7 +202,7 @@ The current implementation is read-only. It emits evidence for each component su

## maildesk-cf Lifecycle

Use `maildesk-cf` when the question is whether a maildesk deployment is ready across inbound routing, storage, Workers, sender authentication, and outbound identity, not whether one isolated Cloudflare resource exists.
Use `maildesk-cf` when the question is whether a maildesk deployment is ready across inbound routing, storage, Workers, and mode-driven sender posture, not whether one isolated Cloudflare resource exists.

```bash
cfctl maildesk-cf init --domain example.com
Expand All @@ -215,8 +215,9 @@ cfctl maildesk-cf provision --file state/maildesk-cf/example.json --plan
The current implementation is read-only. `provision --plan` emits a preview
operation id plus proposed component operations; `provision --ack-plan` is
blocked until each component mutation is present as a public preview-gated
surface. Sender readiness uses DNS/authentication and provider readback
evidence. Broad live sends are never attempted by default.
surface. The public template defaults to receive-only outbound mode; enabled
sender providers use DNS/authentication and provider readback evidence. Broad
live sends are never attempted by default.

## Result Envelope

Expand Down
159 changes: 74 additions & 85 deletions scripts/cf_maildesk_cf_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def init_spec(domain: str) -> dict[str, Any]:
"queue": "maildesk-cf-jobs",
},
"sender": {
"mode": "cloudflare_first",
"authenticated_domains": [domain],
"mode": "disabled",
"authenticated_domains": [],
},
"verification": {
"allow_broad_live_sends": False,
Expand Down Expand Up @@ -694,95 +694,84 @@ def append_sender_checks(
)
return
if mode in {"disabled", "receive_only"}:
drifts.append(
drift(
"sender_adapter_receive_only",
"error",
"sender.mode",
"Sender adapter is configured without outbound sending",
"outbound sender provider",
sender_spec.get("mode"),
checks["sender"]["ready"] = True
else:
for domain in [str(value) for value in authenticated if value]:
domain_evidence = (evidence.get("domains") or {}).get(domain) or {}
dns_items = items(domain_evidence.get("dns.record"))
spf_ok = has_txt(dns_items, domain, "v=spf1")
dmarc_ok = has_txt(dns_items, f"_dmarc.{domain}", "v=DMARC1")
domain_status = sender_domain_status(sender_evidence, domain)
provider_status = domain_status or {}
provider_verified = bool(
provider_status.get("verified")
or str(provider_status.get("status") or "").lower() in {"verified", "active", "ready"}
)
)
checks["sender"]["ready"] = False
return

for domain in [str(value) for value in authenticated if value]:
domain_evidence = (evidence.get("domains") or {}).get(domain) or {}
dns_items = items(domain_evidence.get("dns.record"))
spf_ok = has_txt(dns_items, domain, "v=spf1")
dmarc_ok = has_txt(dns_items, f"_dmarc.{domain}", "v=DMARC1")
domain_status = sender_domain_status(sender_evidence, domain)
provider_status = domain_status or {}
provider_verified = bool(
provider_status.get("verified")
or str(provider_status.get("status") or "").lower() in {"verified", "active", "ready"}
)
domain_check = {
"provider": mode,
"provider_readback": provider_status if domain_status else None,
"spf_dns": spf_ok,
"dmarc_dns": dmarc_ok,
"provider_verified": provider_verified,
}
checks["sender"][domain] = domain_check
if not spf_ok:
drifts.append(
drift(
"dns_authentication_drift",
"error",
f"dns.record:{domain}:SPF",
"SPF TXT record is missing for sender authentication",
"TXT v=spf1",
[dns_name(record) for record in dns_items],
domain_check = {
"provider": mode,
"provider_readback": provider_status if domain_status else None,
"spf_dns": spf_ok,
"dmarc_dns": dmarc_ok,
"provider_verified": provider_verified,
}
checks["sender"][domain] = domain_check
if not spf_ok:
drifts.append(
drift(
"dns_authentication_drift",
"error",
f"dns.record:{domain}:SPF",
"SPF TXT record is missing for sender authentication",
"TXT v=spf1",
[dns_name(record) for record in dns_items],
)
)
)
if not dmarc_ok:
drifts.append(
drift(
"dns_authentication_drift",
"error",
f"dns.record:_dmarc.{domain}",
"DMARC TXT record is missing for sender authentication",
"TXT v=DMARC1",
[dns_name(record) for record in dns_items],
if not dmarc_ok:
drifts.append(
drift(
"dns_authentication_drift",
"error",
f"dns.record:_dmarc.{domain}",
"DMARC TXT record is missing for sender authentication",
"TXT v=DMARC1",
[dns_name(record) for record in dns_items],
)
)
)
if domain_status is None and not sender_readback_available(sender_evidence):
drifts.append(
drift(
"provider_status_unavailable",
"error",
f"sender_domain:{domain}",
"Sender-provider domain status readback is not available",
{"provider": mode, "domain": domain},
None,
if domain_status is None and not sender_readback_available(sender_evidence):
drifts.append(
drift(
"provider_status_unavailable",
"error",
f"sender_domain:{domain}",
"Sender-provider domain status readback is not available",
{"provider": mode, "domain": domain},
None,
)
)
)
elif domain_status is None:
drifts.append(
drift(
"sender_domain_drift",
"error",
f"sender_domain:{domain}",
"Sender-provider domain is not present in readback",
"verified",
{"provider": mode, "domains": sender_evidence.get("domains") or []},
sender_domain_enable_plan_command(domain),
elif domain_status is None:
drifts.append(
drift(
"sender_domain_drift",
"error",
f"sender_domain:{domain}",
"Sender-provider domain is not present in readback",
"verified",
{"provider": mode, "domains": sender_evidence.get("domains") or []},
sender_domain_enable_plan_command(domain),
)
)
)
elif not provider_verified:
drifts.append(
drift(
"sender_domain_drift",
"error",
f"sender_domain:{domain}",
"Sender-provider domain is not verified",
"verified",
provider_status,
sender_domain_enable_plan_command(domain),
elif not provider_verified:
drifts.append(
drift(
"sender_domain_drift",
"error",
f"sender_domain:{domain}",
"Sender-provider domain is not verified",
"verified",
provider_status,
sender_domain_enable_plan_command(domain),
)
)
)

verification_spec = spec.get("verification") or {}
if verification_spec.get("targeted_send_required"):
Expand Down
20 changes: 17 additions & 3 deletions scripts/verify_maildesk_cf_contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ require_source_line() {
fixture_file="$(mktemp "${TMPDIR:-/tmp}/maildesk-cf-fixture.XXXXXX")"
missing_fixture_file="$(mktemp "${TMPDIR:-/tmp}/maildesk-cf-missing-fixture.XXXXXX")"
unverified_sender_fixture_file="$(mktemp "${TMPDIR:-/tmp}/maildesk-cf-unverified-sender.XXXXXX")"
cloudflare_sender_spec_file="$(mktemp "${TMPDIR:-/tmp}/maildesk-cf-cloudflare-sender-spec.XXXXXX")"
caller_spec_dir="$(mktemp -d "${TMPDIR:-/tmp}/maildesk-cf-caller-spec.XXXXXX")"
trap 'rm -f "${fixture_file}" "${missing_fixture_file}" "${unverified_sender_fixture_file}"; rm -rf "${caller_spec_dir}"' EXIT
trap 'rm -f "${fixture_file}" "${missing_fixture_file}" "${unverified_sender_fixture_file}" "${cloudflare_sender_spec_file}"; rm -rf "${caller_spec_dir}"' EXIT

require_source_line "worker evidence lane" '"worker.script": run_cfctl(["list", "worker.script"], lane="global"),' "${ROOT_DIR}/scripts/cf_maildesk_cf_lifecycle.py"
require_source_line "d1 evidence lane" '"d1.database": run_cfctl(["list", "d1.database"], lane="global"),' "${ROOT_DIR}/scripts/cf_maildesk_cf_lifecycle.py"
Expand Down Expand Up @@ -140,6 +141,14 @@ cat >"${unverified_sender_fixture_file}" <<'JSON'
}
JSON

jq '.sender = {"mode": "cloudflare_email_service", "authenticated_domains": ["example.com"]}' \
"${ROOT_DIR}/state/maildesk-cf/example.json" >"${cloudflare_sender_spec_file}"

jq -e '
.sender.mode == "disabled"
and (.sender.authenticated_domains | length) == 0
' "${ROOT_DIR}/state/maildesk-cf/example.json" >/dev/null || die "checked-in maildesk-cf example must default to disabled outbound sender mode"

output="$(
MAILDESK_CF_EVIDENCE_FILE="${fixture_file}" \
MAILDESK_CF_ACTION=verify \
Expand All @@ -154,7 +163,11 @@ jq -e '
and .readiness.instance_ready == true
and .readiness.edge_ready == true
and .readiness.mail_ready == true
and .checks.sender.mode.normalized == "disabled"
and .checks.sender.ready == true
and (.drift_classes | index("provider_status_unavailable")) == null
and (.drift_classes | index("sender_adapter_receive_only")) == null
and (.drift_classes | index("sender_domain_drift")) == null
and (.drift_classes | index("optional_live_send_not_requested")) != null
and (.plan.operations | length) == 0
' "${artifact_path}" >/dev/null || die "fixture readiness contract did not match"
Expand All @@ -172,7 +185,8 @@ jq -e '
.readiness.edge_ready == false
and (.drift_classes | index("missing_resource")) != null
and (.drift_classes | index("email_routing_alias_drift")) != null
and (.drift_classes | index("dns_authentication_drift")) != null
and (.drift_classes | index("dns_authentication_drift")) == null
and (.drift_classes | index("sender_adapter_receive_only")) == null
and any(.plan.operations[]; .surface == "d1.database" and .preview_command == "cfctl wrangler d1 create maildesk-cf-db --plan" and .blocked == null)
and any(.plan.operations[]; .surface == "d1.database" and .preview_command == "cfctl wrangler d1 create maildesk-cf-preview-db --plan" and .blocked == null)
and any(.plan.operations[]; .surface == "r2.bucket" and .preview_command == "cfctl wrangler r2 bucket create maildesk-cf-raw-mail --plan" and .blocked == null)
Expand All @@ -183,7 +197,7 @@ jq -e '
unverified_sender_output="$(
MAILDESK_CF_EVIDENCE_FILE="${unverified_sender_fixture_file}" \
MAILDESK_CF_ACTION=verify \
SPEC_FILE="${ROOT_DIR}/state/maildesk-cf/example.json" \
SPEC_FILE="${cloudflare_sender_spec_file}" \
python3 "${ROOT_DIR}/scripts/cf_maildesk_cf_lifecycle.py"
)"
unverified_sender_artifact_path="$(printf '%s\n' "${unverified_sender_output}" | tail -n 1)"
Expand Down
8 changes: 5 additions & 3 deletions state/maildesk-cf/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# maildesk-cf State

`maildesk-cf` specs describe the Cloudflare account resources needed for one
maildesk deployment: Email Routing aliases, Workers, D1, R2, Queues, sender
authentication, and outbound identity readiness.
maildesk deployment: Email Routing aliases, Workers, D1, R2, Queues, and
mode-driven sender readiness. The public example defaults to receive-only
outbound mode; sender authentication is required only when a provider is
enabled in the spec.

Use the composite command when the question is deployment readiness rather than
one isolated Cloudflare resource:
Expand All @@ -19,6 +21,6 @@ operation id. `maildesk-cf provision --ack-plan <operation-id>` is intentionally
blocked until the component write paths are each preview-gated through public
`cfctl` surfaces.

The verifier does not perform broad live sends. Sender readiness is based on
The verifier does not perform broad live sends. Enabled sender providers use
DNS/authentication and provider readback evidence; targeted send proof remains
an explicit human-requested check.
6 changes: 2 additions & 4 deletions state/maildesk-cf/example.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,8 @@
"queue": "maildesk-cf-jobs"
},
"sender": {
"mode": "cloudflare_first",
"authenticated_domains": [
"example.com"
]
"mode": "disabled",
"authenticated_domains": []
},
"verification": {
"allow_broad_live_sends": false,
Expand Down
Loading